面试专题--JVM体系
2025-02-21 10:50:54 0 举报
在面试中探讨JVM体系时,核心内容通常包括JVM的基本概念、内存模型、垃圾回收机制、执行引擎、类加载机制以及性能监控与调优等方面。考生应熟悉JVM的主要组成部分,如堆(Heap)、栈(Stack)、方法区(Method Area)以及程序计数器(Program Counter)。理解垃圾回收算法,如标记-清除(Mark-Sweep)、复制(Copying)、标记-整理(Mark-Compact)以及分代收集(Generational Collection),并能够解释各种垃圾回收器(如Serial、Parallel、CMS、G1等)的特点和适用场景。此外,熟悉类加载过程(加载、验证、准备、解析、初始化),以及不同类加载器(引导类加载器、扩展类加载器、系统类加载器等)的职责也是关键。性能调优问题可能涉及JVM参数调优、内存泄露诊断、分析堆转储文件(heap dump)等高级主题。准备面试时,确保能够结合实例,如实际项目中的性能问题和解决方案,清晰阐述个人理解。
作者其他创作
大纲/内容
对于 新生代(Young Generation),尽管它的总大小是 2048 KB,但实际上,S0 和 S1 两个 Survivor 区域 不能同时 被使用。Eden 区 和 一个 Survivor 区(即 S0 或 S1)是活跃的,而另一个 Survivor 区则用于交换(用作 下一次垃圾回收时的目标区)。因此,可用空间 会有所不同。新生代总大小是 2048 KB。可用空间包括:Eden 区:1024 KB。一个活跃的 Survivor 区:512 KB。因此,新生代的可用空间是 1024 KB + 512 KB = 1536 KB。
局部变量表
单线程
GC (垃圾回收)
程序计数器
活跃的线程
线程栈N
方法返回信息
CMSCMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。整个过程分为以下几个步骤,包括:(1)初始标记:只标记根节点直接关联的引用对象,需要暂停用户线程(时间短);(2)并发标记:标记其他引用对象,可以跟用户线程并发同时执行;(3)重新标记:暂停用户线程,对并发标记期间新增加的引用关系变化再次标记(时间短);(4)并发清除:跟用户线程并发进行。(5)并发重置:回收完成后,重置GC数据结构,为下一轮GC做准备。其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,并发标记阶段就是进行 GC Roots Tracing 的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发地执行。CMS 收集器已经在很大程度上减少了用户线程的停顿时间CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候回产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。
JVM体系结构
存活对象
句柄如果使用句柄访问的话,那么Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。使用句柄来访问的最大好处就是 reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference 本身不需要修改直接指针如果使用直接指针访问,reference 中存储的直接就是对象地址,这两种对象访问方式各有优势,使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在 Java 中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。对SunHotSpot而言,它是使用直接指针访问方式进行对象访问的
可达性分析
Object
1:安全点(Safe Point)和安全区域(Safe Region)JVM的GC不是在任何时刻都会发生的,只有在特定的位置才能停顿下来(stw)开始GC, 也就是安全点和安全区域。2. GC时线程的中断策略抢先式中断: 首先中断所有线程。如果还有线程不在安全点, 就恢复线程, 让线程跑到安全点。 主动式中断(一般是这种): 设置一个中断标志, 各个线程运行到Safe Point的时候主动轮询这个标志,如果中断标志为真, 则将自己进行中断挂起。 3.安全点(Safe Point) 安全点的选择很重要, 如果太少可能导致GC 等待的时间太长, 如果太频繁可能导致运行时的性 能问题。一般选择如下位置作为安全点,如:循环的末尾、方法临返回前、调用方法之后、抛异常的位置 4.安全区域(Safe Region)安全区域(Safe Region)用来保证一些无法主动响应JVM中断请求的线程。例如线程处于Sleep状态或Blocked状态,无法主动走到安全点去中断挂起,JVM也不太可能等待线程被唤醒时再进行。 JVM认为在安全区域代码片段中,对象的引用关系不会发生变化(都睡眠了肯定不会变化), 在这个区域中的任何位置开始GC都是安全的。但是注意的是安全区域中的代码,醒来的时候需要判断是否GC完毕,如果此时还在GC中,那么是无法醒来执行逻辑的,必须等待GC后才能执行后面的逻辑
Eden-----8/10内存
老年代
线程1
线程公有内存区域
STW
线程私有内存区域
Parallel Old
操作数栈
动态链接
在运行时生成的代理类和匿名类的元数据也存储在元空间中,这些类不在磁盘上存在,而是动态创建的类。
生产相关JVM配置
JIT 编译信息
初始标记(单线程)
判断对象存活
执行引擎(Execution Engine)是 JVM 的核心组件之一,负责解释和执行 Java 字节码,将其转换成机器代码,以便在底层硬件上运行。执行引擎的主要职责是将字节码转换为机器指令,并有效执行。结合解释器、 JIT 编译器的优势,能够在提供跨平台支持的同时保障较高的性能。通过与内存管理(垃圾回收)和类加载机制的配合,执行引擎是 Java 的跨平台性和高效执行的核心支柱。
多线程
重新标记(多线程)
font color=\"#262626\
帧栈1
安全点
GC(垃圾回收)
1:每个类加载器加载的类的信息。2:用于 JVM 管理类和类加载器关系的相关数据结构
线程2
类加载器
对象头(Header)类型指针:指向该对象所属类的元数据的指针,知道是属于哪个类。MarkWord标记字:例如哈希码、锁状态、GC 信息span style=\"font-size:inherit;\
方法元数据
并发标记(多线程)
实例数据
JNI 引用(Java Native Interface)
符号信息
Interpreter(解释器)解释执行是一种直接的执行方式,不会生成机器码,它较为简单,但速度较慢。Java 程序在启动时通常会使用解释执行方式。JIT(即时编译器)为了提高性能,JVM 会使用 JIT 编译器来处理被频繁调用的“热点代码”,将其编译为机器码并缓存,避免多次解释,从而显著提升性能。GC(垃圾回收器)执行引擎会和垃圾回收器一起工作,管理对象生命周期并释放内存,防止内存泄漏和溢出。
1:方法的字节码和方法的调用信息。2:JVM 需要的相关信息,比如方法表,用于动态绑定方法调用
Serial Old
线程N
栈增长方向
类名、字段名、方法名的符号引用等信息,用于方法的动态绑定和查找。
GC Root
本地方法栈
1 大对象直接进入老年代2 长期存活的对象age到MaxTenuringThreshold(可置最大为15)进入老年代
JVM 的结构确实包含多个核心部分,通常分为 两个主要子系统 和 两个主要组件。JVM 的核心子系统:Class Loader(类加载器)和 Execution Engine(执行引擎),JVM 的主要组件:Runtime Data Area(运行时数据区) 和 Native Interface(本地接口)
SurvivorFrom-----1/10内存
Serial
筛选回收(多线程)
RDA(运行时数据区)
新生代(Yong Generation)-----1/3内存
Interpreter(解释器)
标记-清除(Mark-Sweep
并发清理(多线程)
句柄池
并发重置(单线程)
1:类的名称、修饰符(如 public、final)、父类和实现的接口信息。2:类的变量的定义,包括字段名称、类型、访问修饰符等。
标记-整理(Mark-Compact)
数据长度(数组对象)
Parallel Scavenge
垃圾回收算法
收集器
回收对象和算法
收集类型
备注
新生代、复制
新生代单线程收集器,优点是简单高效
ParNew
新生代并行收集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现
新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景
老年代、标记整理
老年代单线程收集器,Serial收集器的老年代版本
CMS
老年代、标记清除
多线程并于用户线程并发
老年代并发收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本
G1
全部、标记整理
java堆并发收集器,G1收集器是JDK1.7提供的一个新收集器,JDK1.9已经设为默认的垃圾收集器
类型指针
线程栈1
分代收集(Generational Collection)
本地方法栈中变量
对象头
类加载器信息
对象
G1G1不区分物理上的新生代和老年代,而是逻辑上区分新生代和老年代,回收的范围是整个Java堆(包括新生代,老年代)。G1把连续的 Java 堆划分为多个大小相等的独立区域region,每个 region 都可以根据需要,扮演新生代的 Eden 区,Survivor 空间或者老年代空间,可看做俄罗斯方块动态移动,压缩空闲内存使之足够紧凑减少内存碎片的产生。region区域大小1-32M为2的幂,最多2048个region区域,可以通过参数-XX:G1HeapRegionSize设定。整个过程分为以下几个步骤,包括: (1)初始标记:只标记GC Roots能直接关联到的对象 (2)并发标记:进行GC RootsTracing的过程 (3)最终标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象 (4)筛选回收:根据时间来进行价值最大化的回收其中初始标记、最终标记这两个步骤仍然需要“Stop The World”。与CMS相比的特点: 1.标记整理不会出现内存碎片。 2;用户可指定期望的stw时间,可以通过参数-XX:MaxGCPauseMillis=n设定。 3.不区分物理上的内存,逻辑上区分新生代和老年代。 4.专用大对象region。
JIT(即时编译器)
类元数据
方法区(Method Area)是 JVM 内存结构的一部分,用于存储类的元数据(如类的结构、方法、字段、常量池等)。它是所有线程共享的内存区域,负责存储与类相关的信息。方法区在不同版本的 JVM 中有不同的实现方式。在 JDK 1.7 之前,方法区通常指的是 永久代(PermGen)。从 JDK 1.8 开始,永久代被移除,取而代之的是 元空间(Metaspace)元空间是 JVM 中的一块内存区域,用来存储类的元数据。与永久代不同,元空间不再使用堆内存,而是使用 本地内存(native memory)。元空间的大小不再受到固定限制,它可以根据需要动态扩展,只有操作系统内存不足时,才会抛出内存溢出的异常。元空间的大小可以通过 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 来设置。
标记-清除(Mark-Sweep)过程:标记阶段,遍历所有对象,标记可达对象;清除阶段,清理所有未标记的对象。优点:实现简单,适用于小规模的内存管理。缺点:存在“内存碎片”问题,因为清除阶段可能导致内存不连续,影响分配效率。标记-整理(Mark-Compact)过程:类似标记-清除,但清理阶段除了清除不可达对象外,还会压缩存活对象,将其移动到内存的一端,避免碎片。优点:消除了内存碎片问题,适合长时间运行的应用。缺点:对象需要移动,可能会消耗更多的 CPU 资源。复制算法(Copying)过程:将内存分成两个区域,活跃对象从一个区域复制到另一个区域,剩余的部分被清空。优点:通过将对象分配到连续的内存区域,避免了内存碎片问题,提升了分配效率。缺点:需要额外的内存空间,通常会把内存一分为二,这对于内存较小的系统可能不合适。分代收集(Generational Collection)过程:JVM 将堆内存分为多个区域,如新生代、老年代等。新生代使用复制算法,老年代则使用标记-清除或标记-压缩算法。优点:针对不同生命周期的对象采用不同的回收策略,提高效率。新生代对象回收频繁,适合使用复制算法;老年代对象回收较少,适合使用标记-清除或标记-压缩。缺点:需要维护多个区域,内存管理更复杂。
调用方帧栈
最终标记(多线程)
Class Loader(类加载器)
类加载器(Class Loader) 是一个负责将类文件加载到 JVM 的组件。它将 .class 文件加载到内存中,并将其转换成 JVM 能够理解的 Class 对象,以便在运行时使用,并不会一次性将所有的 .class 文件加载到内存中。Java 的类加载是**惰性加载(Lazy Loading)**的,即只有当类被实际需要时,才会加载该类。遵循双亲委派模型。类加载的三个步骤1:加载(Loading):类加载器读取 .class 文件,并将其字节码内容加载到 JVM 中,形成一个 Class 对象。2:链接(Linking):将类的符号引用解析为实际引用,这一步包括验证、准备和解析。 ①验证:检查加载的 class 文件的正确性 ②准备: 给类中的静态变量分配内存空间 ③解析:虚拟机将常量池中的符号引用替换成直接引用的过程。在编译的时候一个每个java类都会被编译成一个class文件,但在编译的时候 虚拟机并不知道所引用类的地址,所以就用符号引用来代替,而在解析阶段就是为了把这个符号引用转化成真正的地址的阶段。3:初始化(Initialization):执行类的静态初始化块和静态变量赋值在 Java 项目启动后,类加载器会默认加载以下类:启动类加载器(Bootstrap ClassLoader) 加载核心类库(如 java.lang.*)。扩展类加载器(Extension ClassLoader) 加载 Java 扩展库(如 javax.servlet.*)。应用类加载器(AppClassLoader) 加载项目中的自定义类及其依赖的库。双亲委派的过程如下:1:类加载请求由应用程序类加载器接收。2:应用程序类加载器先将请求交给其父类加载器(扩展类加载器)。3:扩展类加载器再将请求交给其父类(启动类加载器)。4:如果启动类加载器无法找到该类,加载权会逐层传回,直到当前类加载器尝试加载。Class Loader(类加载器)和Runtime Data Area(运行时数据区)的关联1. 类加载过程和方法区的关联类加载器将 .class 文件加载到内存,并将类的静态数据结构存放到方法区(Method Area)。每次加载新类时,类加载器会检查方法区是否已经有该类的定义,从而避免重复加载。2. 对象实例化和堆(Heap)区的关联当类加载器将类加载到方法区后,程序可以开始创建该类的对象实例。这些对象会存储在运行时数据区的堆(Heap)中。类加载器在加载类文件的过程中,为对象的创建和运行时数据提供了类型信息和结构。3. 执行环境与线程栈的关联类加载器还与Java 栈(Java Stack) 和本地方法栈(Native Method Stack)有关。每当 JVM 创建一个新线程时,会为其分配一个 Java 栈,包含该线程的方法调用栈帧。当线程调用方法时,类加载器通过加载的类定义将方法信息、局部变量和字节码加载到线程栈中进行执行。这一机制确保每个线程都可以基于加载的类来执行独立的操作。4. 双亲委派模型和类加载的隔离类加载器采用双亲委派模型,确保不同类加载器之间的隔离性。运行时数据区中的方法区和堆都依据类加载器隔离加载的类,防止类定义的冲突。例如,Web 容器(如 Tomcat)在 JVM 内为不同应用创建隔离的类加载器,将各个应用程序的类分别加载到独立的命名空间中,避免数据区的相互干扰。
字符串常量池
font color=\"#e74f4c\
age=15
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
老年代(Yong Generation)-----2/3内存
每个栈帧代表一个方法的调用。在方法调用时,JVM 为该方法分配一个栈帧,栈帧包含了该方法的局部变量、操作数栈、返回地址等信息。每个方法执行时都会有一个栈帧。帧栈存在嵌套情况(比如方法内部调用其他方法,method调用methodA,methodA调用methodAB)流程:1:当 method() 方法执行时,JVM 会为 method() 方法分配一个栈帧。2:method() 方法调用 methodA(),此时 methodA() 的栈帧会被压入虚拟机栈,methodA() 执行。3:methodA() 调用 methodB(),此时 methodB() 的栈帧会被压入虚拟机栈,methodB() 执行。4:执行完 methodB() 后,它的栈帧被销毁,控制权返回到 methodA()。5: methodA() 执行完毕,栈帧被销毁,控制权返回到 method()。6:method() 方法执行完毕,栈帧被销毁,线程执行完毕。局部变量表存储方法参数和局部变量。方法返回信息正常返回:方法执行完毕后,会通过返回地址跳转到调用方法的下一条指令。异常返回:当方法抛出异常时,栈帧的返回地址被跳过,异常的处理流程开始。异常发生时,会“跳出”当前方法,并跳转到一个与异常类型匹配的异常处理代码块。如果没有找到对应的 catch 块,异常会继续传播,最终导致程序崩溃或终止。操作数栈(Operand Stack)是每个方法的栈帧中的一个重要部分,主要用于存放操作数和方法执行过程中的临时计算结果。它是栈帧的一部分,随方法调用的执行而动态变化。遵循FILO原则,比如int a = 5;int b = 3;int c = a + b;在执行 a + b 时,操作数栈会如下操作:1:将 a(5)压入栈中。2:将 b(3)压入栈中。3:执行加法操作,栈顶两个数(5 和 3)被弹出并进行加法运算,结果 8 被压入栈中。4:将 8 赋值给 c。动态链接是 JVM 中将方法调用与实际方法实现关联的过程。它允许 JVM 在运行时将符号引用(如方法或字段的名字)解析成实际的内存地址。这一过程是为了支持方法调用和其他程序结构的动态解析。
虚拟机栈( Stack)
栈帧中的局部变量
可回收对象
引用计数器
Native Interface(本地接口)
每个类的运行时常量池,包括静态字段和方法引用,存放在元空间内。这些是 JVM 运行时所需的字节码指令执行的引用信息
复制(Copying)
栈帧重叠优化的技术用于优化方法调用时的内存使用和参数传递效率。在传统的栈帧模型中,每个方法调用都会创建一个新的栈帧,这些栈帧的内存区域通常是独立的,包含自己的局部变量表和操作数栈。然而在一些JVM的优化实现中,为了避免冗余的数据拷贝和存储,栈帧重叠允许不同栈帧共享内存区域。具体来说,在方法调用过程中,如果被调用方法需要使用上层方法的一些局部变量作为参数传递,这部分局部变量可以和当前栈帧的操作数栈部分重叠。这样,被调用方法就可以直接访问这些参数,而不必再将参数从调用者的栈帧复制到被调用者的栈帧中优势1:减少内存消耗:栈帧的重叠减少了重复存储的需求。对于频繁调用的方法,这种优化节省的内存空间更加明显,特别是在局部变量和参数较多的场景下。2:提高调用效率:在方法调用时,参数传递不需要额外的复制操作,降低了内存带宽的使用和调用开销。这种优化在高频率方法调用的情况下效果尤其显著。3:简化参数传递:通过重叠实现的参数共享,能够简化JVM的参数传递机制,并减少栈操作,间接优化执行效率。实现方式:1:静态分析:在编译期分析方法间的参数传递关系,判断哪些参数可以在栈帧重叠时直接访问,并在生成字节码时做好相应布局。2:动态链接:JVM在执行字节码时,通过动态链接找到需要共享的局部变量或操作数栈区域,将其指向调用者的栈帧内存地址。
一些元空间会保存 JIT 编译器的元信息,用于动态优化代码执行,如已编译的机器代码地址和优化信息。
动态代理类和匿名类
引用类型引用
Metaspace(元空间)----方法区
被调用方帧栈
引用不可达可回收
MarkWord
SurvivorTo-----1/10内存
栈帧N
Execution Engine(执行引擎)
与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。
堆(Heap)
方法区静态变量
对齐填充(8的倍数)
运行时常量池
安全点和安全区域
基本类型
Native Library(本地方法库)
垃圾回收器
新生代
0 条评论
下一页
为你推荐
查看更多