JVM -- 内存模型(运行时数据区,垃圾回收机制)

📅 2026/7/27 9:09:18
JVM -- 内存模型(运行时数据区,垃圾回收机制)
一. 运行时数据区Java虚拟机在运行Java程序过程中管理的内存区域称之为运行时数据区。运行时数据区主要分为线程共享区和线程不共享区。1.1 程序计数器程序计数器Program Counter Register也叫PC寄存器每个线程会通过程序计数器记录当前要执行的的字节码指令的地址。在代码执行过程中程序计数器会记录下一行字节码指令的地址。执行完当前指令之后虚拟机的执行引擎根据程序计数器执行下一行指令。程序计数器会出现内存溢出吗内存溢出指的是程序在使用某一块内存区域时存放的数据需要占用的内存大小超过了虚拟机能提供的内存上限。由于每个线程只存储一个固定长度的内存地址程序计数器是不会发生内存溢出的。程序员无需对程序计数器做任何处理。1.2 Java虚拟机栈Java虚拟机栈Java Virtual Machine Stack采用栈的数据结构来管理方法调用中的基本数据先进后出First In Last Out,每一个方法的调用使用一个栈帧Stack Frame来保存。Java虚拟机栈的栈帧中主要包含三方面的内容局部变量表局部变量表的作用是在运行过程中存放所有的局部变量操作数栈操作数栈是栈帧中虚拟机在执行指令过程中用来存放临时数据的一块区域帧数据帧数据主要包含动态链接、方法出口、异常表的引用1.2.1 局部变量表局部变量表的作用是在方法执行过程中存放所有的局部变量。局部变量表分为两种一种是字节码文件中的另外一种是栈帧中的也就是保存在内存中。栈帧中的局部变量表是根据字节码文件中的内容生成的。字节码文件中的局部变量表比如i这个变量它的起始PC是2代表从lconst_1这句指令开始才能使用i长度为3也就是2-4这三句指令都可以使用i。为什么从2才能使用因为0和1这两句字节码指令还在处理int i 0这句赋值语句。j这个变量只有等3指令执行完之后也就是long j 1代码执行完之后才能使用所以起始PC为4只能在4这行字节码指令中使用。栈帧中的局部变量表栈帧中的局部变量表是一个数组数组中每一个位置称之为槽(slot) long和double类型占用两个槽其他类型占用一个槽。方法参数也会保存在局部变量表中其顺序与方法中参数定义的顺序一致。局部变量表保存的内容有实例方法的this对象方法的参数方法体中声明的局部变量。为了节省空间局部变量表中的槽是可以复用的一旦某个局部变量不再生效当前槽就可以再次被使用。1.2.2 操作数栈操作数栈是栈帧中虚拟机在执行指令过程中用来存放中间数据的一块区域。他是一种栈式的数据结构如果一条指令将一个值压入操作数栈则后面的指令可以弹出并使用该值。在编译期就可以确定操作数栈的最大深度从而在执行时正确的分配内存大小。1.2.3 帧数据帧数据主要包含动态链接、方法出口、异常表的引用。动态链接当前类的字节码指令引用了其他类的属性或者方法时需要将符号引用编号转换成对应的运行时常量池中的内存地址。动态链接就保存了编号到运行时常量池的内存地址的映射关系。方法出口方法出口指的是方法在正确或者异常结束时当前栈帧会被弹出同时程序计数器应该指向上一个栈帧中的下一条指令的地址。所以在当前栈帧中需要存储此方法出口的地址。异常表异常表存放的是代码中异常的处理信息包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置。Java虚拟机栈如果栈帧过多占用内存超过栈内存可以分配的最大大小就会出现内存溢出。Java虚拟机栈内存溢出时会出现StackOverflowError的错误。要修改Java虚拟机栈的大小可以使用虚拟机参数 -Xss 。语法-Xss栈大小单位字节默认必须是 1024 的倍数、k或者K(KB)、m或者M(MB)、g或者G(GB) 例如-Xss1048576-Xss1024K-Xss1m-Xss1g1.3 本地方法栈本地方法栈Native Method Stack是 Java 虚拟机JVM运行时数据区的一个重要组成部分。简单来说它是 JVM 为了支持 Java 代码调用非 Java 代码如 C/C 编写的本地方法而专门开辟的一块内存区域。1.4 方法区方法区是存放基础信息的位置线程共享主要包含三部分内容类的元信息保存了所有类的基本信息运行时常量池保存了字节码文件中的常量池内容字符串常量池保存了字符串常量1.4.1 类的元信息方法区是用来存储每个类的基本信息元信息一般称之为InstanceKlass对象。在类的加载阶段完成。其中就包含了类的字段、方法等字节码文件中的内容同时还保存了运行过程中需要使用的虚方法表实现多态的基础等信息。1.4.2 运行时常量池方法区除了存储类的元信息之外还存放了运行时常量池。常量池中存放的是字节码中的常量池内容。字节码文件中通过编号查表的方式找到常量这种常量池称为静态常量池。当常量池加载到内存中之后可以通过内存地址快速的定位到常量池中的内容这种常量池称为运行时常量池。1.4.3 字符串常量池jdk8之后放在堆中方法区中除了类的元信息、运行时常量池之外还有一块区域叫字符串常量池(StringTable)。字符串常量池存储在代码中定义的常量字符串内容。比如“123” 这个123就会被放入字符串常量池。如下代码执行时代码中包含abc字符串就会被直接放入字符串常量池。在堆上创建String对象并通过局部变量s1引用堆上的对象。所以s1和s2指向的不是同一个对象打印出false。注JDK7及之前的版本将方法区存放在堆区域中的永久代空间堆的大小由虚拟机参数来控制。JDK8及之后的版本将方法区存放在元空间中元空间位于操作系统维护的直接内存中默认情况下只要不超过操作系统承受的上限可以一直分配。早期设计时字符串常量池是属于运行时常量池的一部分他们存储的位置也是一致的。后续做出了调整将字符串常量池和运行时常量池做了拆分。JDK7将方法区存放在堆区域中的永久代空间堆的大小由虚拟机参数**-XX:MaxPermSize**值来控制。JDK8将方法区存放在元空间中元空间位于操作系统维护的直接内存中默认情况下只要不超过操作系统承受的上限可以一直分配。可以使用**-XX:MaxMetaspaceSize**值将元空间最大大小进行限制。1.5 堆区创建出来的对象都存在于堆上。栈上的局部变量表中可以存放堆上对象的引用。静态变量也可以存放堆对象的引用通过静态变量就可以实现对象在线程之间共享。堆空间有三个需要关注的值used、total、max。used指的是当前已使用的堆内存total是java虚拟机已经分配的可用堆内存max是java虚拟机可以分配的最大堆内存。设置堆的大小 要修改堆的大小可以使用虚拟机参数 –Xmxmax最大值和-Xms (初始的total)。 语法-Xmx值 -Xms值建议将-Xmx和-Xms设置为相同的值单位字节默认必须是 1024 的倍数、k或者K(KB)、m或者M(MB)、g或者G(GB) 限制Xmx必须大于 2MBXms必须大于1MB-Xms6291456-Xms6144k-Xms6m-Xmx83886080-Xmx81920k-Xmx80m二. 垃圾回收机制Java虚拟机时如何判断这个内存是垃圾的Java使用的是可达性分析算法来判断对象是否可以被回收。可达性分析将对象分为两类垃圾回收的根对象GC Root和普通对象对象与对象之间存在引用关系。哪些对象被称之为GC Root对象呢线程Thread对象引用线程栈帧中的方法参数、局部变量等。系统类加载器加载的java.lang.Class对象引用类中的静态变量。监视器对象用来保存同步锁synchronized关键字持有的对象。本地方法调用时使用的全局对象。2.1 参见的引用对象软引用相对于强引用是一种比较弱的引用关系如果一个对象只有软引用关联到它当程序内存不足时就会将软引用中的数据进行回收。在JDK 1.2版之后提供了SoftReference类来实现软引用软引用常用于缓存中。软引用的执行过程如下1.将对象使用软引用包装起来new SoftReference对象类型(对象)。2.内存不足时虚拟机尝试进行垃圾回收。3.如果垃圾回收仍不能解决内存不足的问题回收软引用中的对象。4.如果依然内存不足抛出OutOfMemory异常。如果软引用对象里边包含的数据已经被回收了那么软引用对象本身其实也可以被回收了。SoftReference提供了一套队列机制1、软引用创建时通过构造器传入引用队列2、在软引用中包含的对象被回收时该软引用对象会被放入引用队列3、通过代码遍历引用队列将SoftReference的强引用删除弱引用整体机制和软引用基本一致区别在于弱引用包含的对象在垃圾回收时不管内存够不够都会直接被回收。在JDK 1.2版之后提供了WeakReference类来实现弱引用弱引用主要在ThreadLocal中使用。虚引用也叫幽灵引用/幻影引用不能通过虚引用对象获取到包含的对象。虚引用唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知。Java中使用PhantomReference实现了虚引用直接内存中为了及时知道直接内存对象不再使用从而回收内存使用了虚引用来实现。终结器引用指的是在对象需要被回收时终结器引用会关联对象并放置在Finalizer类中的引用队列中在稍后由一条由FinalizerThread线程从队列中获取对象然后执行对象的finalize方法在对象第二次被回收时该对象才真正的被回收。在这个过程中可以在finalize方法中再将自身对象使用强引用关联上但是不建议这样做。2.2 垃圾回收算法Java垃圾回收过程会通过单独的GC线程来完成但是不管使用哪一种GC算法都会有部分阶段需要停止所有的用户线程。这个过程被称之为Stop The World简称STW如果STW时间过长则会影响用户的使用。所以判断GC算法是否优秀可以从三个方面来考虑1.吞吐量吞吐量指的是 CPU 用于执行用户代码的时间与 CPU 总执行时间的比值即吞吐量 执行用户代码时间 /执行用户代码时间 GC时间。吞吐量数值越高垃圾回收的效率就越高。2.最大暂停时间最大暂停时间指的是所有在垃圾回收过程中的STW时间最大值。比如如下的图中黄色部分的STW就是最大暂停时间显而易见上面的图比下面的图拥有更少的最大暂停时间。最大暂停时间越短用户使用系统时受到的影响就越短。3.堆使用效率不同垃圾回收算法对堆内存的使用方式是不同的。比如标记清除算法可以使用完整的堆内存。而复制算法会将堆内存一分为二每次只能使用一半内存。从堆使用效率上来说标记清除算法要优于复制算法。2.2.1 标记清除算法1.标记阶段将所有存活的对象进行标记。Java中使用可达性分析算法从GC Root开始通过引用链遍历出所有存活对象。2.清除阶段从内存中删除没有被标记也就是非存活对象。优点实现简单只需要在第一阶段给每个对象维护标志位第二阶段删除对象即可。缺点1.碎片化问题由于内存是连续的所以在对象被删除之后内存中会出现很多细小的可用内存单元。如果我们需要的是一个比较大的空间很有可能这些内存单元的大小过小无法进行分配。2.分配速度慢。由于内存碎片的存在需要维护一个空闲链表极有可能发生每次需要遍历到链表的最后才能获得合适的内存空间。2.2.2 复制算法复制算法Copying Algorithm是 Java 垃圾回收GC中的一种核心算法它通过“空间换时间”的策略高效地解决了内存碎片问题。1.准备两块空间From空间和To空间每次在对象分配阶段只能使用其中一块空间From空间。2.在垃圾回收GC阶段将From中存活对象复制到To空间。3.将两块空间的From和To名字互换。优点吞吐量高复制算法只需要遍历一次存活对象复制到To空间即可比标记-整理算法少了一次遍历的过程因而性能较好但是不如标记-清除算法因为标记清除算法不需要进行对象的移动不会发生碎片化复制算法在复制之后就会将对象按顺序放入To空间中所以对象以外的区域都是可用空间不存在碎片化内存空间。缺点 内存使用效率低每次只能让一半的内存空间来为创建对象使用。2.2.3 标记整理算法标记-整理算法Mark-Compact Algorithm是 Java 垃圾回收中一种非常重要的算法它巧妙地结合了“标记-清除”和“复制”算法的优点旨在解决内存碎片和空间利用率低的问题。1.标记阶段将所有存活的对象进行标记。Java中使用可达性分析算法从GC Root开始通过引用链遍历出所有存活对象。2.整理阶段将存活对象移动到堆的一端。清理掉存活对象的内存空间。优点内存使用效率高整个堆内存都可以使用不会像复制算法只能使用半个堆内存不会发生碎片化在整理阶段可以将对象往内存的一侧进行移动剩下的空间都是可以分配对象的有效空间缺点整理阶段的效率不高整理算法有很多种比如Lisp2整理算法需要对整个堆中的对象搜索3次整体性能不佳。可以通过Two-Finger、表格算法、ImmixGC等高效的整理算法优化此阶段的性能。2.2.4 分代垃圾回收算法分代垃圾回收Generational Garbage Collection是现代 Java 虚拟机JVM中应用最广泛的垃圾回收策略。它并不是一种具体的微观算法而是一种基于经验观察的宏观设计思想其核心在于根据对象生命周期的不同将堆内存划分为不同的区域并针对不同区域的特点采用最合适的回收算法。1、分代回收时创建出来的对象首先会被放入Eden伊甸园区。2、随着对象在Eden区越来越多如果Eden区满新创建的对象已经无法放入就会触发年轻代的GC称为Minor GC或者Young GC。3、接下来S0会变成To区S1变成From区。当eden区满时再往里放入对象依然会发生Minor GC。此时会回收eden区和S1(from)中的对象并把eden和from区中剩余的对象放入S0。4、如果Minor GC后对象的年龄达到阈值最大15默认值和垃圾回收器有关对象就会被晋升至老年代。当老年代中空间不足无法放入新的对象时先尝试minor gc如果还是不足就会触发Full GCFull GC会对整个堆进行垃圾回收。如果Full GC依然无法回收掉老年代的对象那么当对象继续放入老年代时就会抛出Out Of Memory异常。2.3 垃圾回收器分代GC算法将堆分成年轻代和老年代主要原因有1、可以通过调整年轻代和老年代的比例来适应不同类型的应用程序提高内存的利用率和性能。2、新生代和老年代使用不同的垃圾回收算法新生代一般选择复制算法老年代可以选择标记-清除和标记-整理算法由程序员来选择灵活度较高。3、分代的设计中允许只回收新生代minor gc如果能满足对象分配的要求就不需要对整个堆进行回收(full gc),STW时间就会减少。垃圾回收器是垃圾回收算法的具体实现。由于垃圾回收器分为年轻代和老年代除了G1之外其他垃圾回收器必须成对组合进行使用。2.3.1 Serial-SerialOld垃圾回收器Serial是是一种单线程串行回收年轻代的垃圾回收器。回收年代和算法年轻代复制算法优点单CPU处理器下吞吐量非常出色缺点多CPU下吞吐量不如其他垃圾回收器堆如果偏大会让用户线程处于长时间的等待SerialOld是Serial垃圾回收器的老年代版本采用单线程串行回收-XX:UseSerialGC 新生代、老年代都使用串行回收器。回收年代和算法老年代标记-整理算法优点单CPU处理器下吞吐量非常出色缺点多CPU下吞吐量不如其他垃圾回收器堆如果偏大会让用户线程处于长时间的等待2.3.2 ParNew-CMS垃圾回收器ParNew垃圾回收器本质上是对Serial在多CPU下的优化使用多线程进行垃圾回收-XX:UseParNewGC 新生代使用ParNew回收器 老年代使用串行回收器回收年代和算法年轻代复制算法优点多CPU处理器下停顿时间较短缺点吞吐量和停顿时间不如G1所以在JDK9之后不建议使用CMS垃圾回收器关注的是系统的暂停时间允许用户线程和垃圾回收线程在某些步骤中同时执行减少了用户线程的等待时间。参数XX:UseConcMarkSweepGC回收年代和算法老年代标记清除算法优点系统由于垃圾回收出现的停顿时间较短用户体验好缺点内存碎片问题退化问题浮动垃圾问题2.3.3 Parallel Scavenge-Parallel Old垃圾回收器Parallel Scavenge是JDK8默认的年轻代垃圾回收器多线程并行回收关注的是系统的吞吐量。具备自动调整堆内存大小的特点。回收年代和算法年轻代复制算法优点吞吐量高而且手动可控。为了提高吞吐量虚拟机会动态调整堆的参数缺点不能保证单次的停顿时间Parallel Scavenge允许手动设置最大暂停时间和吞吐量。Oracle官方建议在使用这个组合时不要设置堆内存的最大值垃圾回收器会根据最大暂停时间和吞吐量自动调整内存大小。最大暂停时间-XX:MaxGCPauseMillisn 设置每次垃圾回收时的最大停顿毫秒数吞吐量-XX:GCTimeRation 设置吞吐量为n用户线程执行时间 n/n 1自动调整内存大小, -XX:UseAdaptiveSizePolicy设置可以让垃圾回收器根据吞吐量和最大停顿的毫秒数自动调整内存大小Parallel Old是为Parallel Scavenge收集器设计的老年代版本利用多线程并发收集。参数 -XX:UseParallelGC 或-XX:UseParallelOldGC可以使用Parallel Scavenge Parallel Old这种组合。回收年代和算法老年代标记-整理算法优点并发收集在多核CPU下效率较高缺点暂停时间会比较长2.3.4 G1垃圾回收器G1的整个堆会被划分成多个大小相等的区域称之为区Region区域不要求是连续的。分为Eden、Survivor、Old区。Region的大小通过堆空间大小/2048计算得到也可以通过参数-XX:G1HeapRegionSize32m指定(其中32m指定region大小为32M)Region size必须是2的指数幂取值范围从1M到32M。年轻代回收Young GC回收Eden区和Survivor区中不用的对象。会导致STWG1中可以通过参数-XX:MaxGCPauseMillisn默认200 设置每次垃圾回收时的最大暂停时间毫秒数G1垃圾回收器会尽可能地保证暂停时间。1、新创建的对象会存放在Eden区。当G1判断年轻代区不足max默认60%无法分配对象时需要回收时会执行Young GC。2、标记出Eden和Survivor区域中的存活对象3、根据配置的最大暂停时间选择某些区域将存活对象复制到一个新的Survivor区中年龄1清空这些区域。G1在进行Young GC的过程中会去记录每次垃圾回收时每个Eden区和Survivor区的平均耗时以作为下次回收时的参考依据。这样就可以根据配置的最大暂停时间计算出本次回收时最多能回收多少个Region区域了。比如 -XX:MaxGCPauseMillisn默认200每个Region回收耗时40ms那么这次回收最多只能回收4个Region。4、后续Young GC时与之前相同只不过Survivor区中存活对象会被搬运到另一个Survivor区。5、当某个存活对象的年龄到达阈值默认15将被放入老年代。6、部分对象如果大小超过Region的一半会直接放入老年代这类老年代被称为Humongous区。比如堆内存是4G每个Region是2M只要一个大对象超过了1M就被放入Humongous区如果对象过大会横跨多个Region。7、多次回收之后会出现很多Old老年代区此时总堆占有率达到阈值时-XX:InitiatingHeapOccupancyPercent默认45%会触发混合回收MixedGC。回收所有年轻代和部分老年代的对象以及大对象区。采用复制算法来完成。混合回收分为初始标记initial mark、并发标记concurrent mark、最终标记remark或者Finalize Marking、并发清理cleanupG1对老年代的清理会选择存活度最低的区域来进行回收这样可以保证回收效率最高这也是G1Garbage first名称的由来。G1对老年代的清理会选择存活度最低的区域来进行回收这样可以保证回收效率最高这也是G1Garbage first名称的由来。最后清理阶段使用复制算法不会产生内存碎片。2.3.5 Shenandoah GC垃圾回收器Shenandoah 是由Red Hat开发的一款低延迟的垃圾收集器Shenandoah 并发执行大部分 GC 工作包括并发的整理堆大小对STW的时间基本没有影响。2.3.6 ZGC垃圾回收器ZGC 是一种可扩展的低延迟垃圾回收器。ZGC 在垃圾回收过程中STW的时间不会超过一毫秒适合需要低延迟的应用。支持几百兆到16TB 的堆大小堆大小对STW的时间基本没有影响。ZGC降低了停顿时间能降低接口的最大耗时提升用户体验。但是吞吐量不佳所以如果Java服务比较关注QPS每秒的查询次数那么G1是比较不错的选择。