内存运行时区域:
- 各线程私有:程序计数器、虚拟机栈、本地方法栈
- 各线程共享:Java堆、方法区
对于程序计数器、虚拟机栈和本地方法栈,它们随线程而生,随线程而灭。栈中的栈帧(每个栈帧分配的内存大小在类结构确定时基本就已知了)随方法的执行而入栈,随方法的结束而出栈。这几个区域的内存分配回收具有确定性,当方法结束或线程结束时内存也就被回收了。
对于Java堆和方法区,内存的分配回收是不确定的,因为只有在程序运行期间才知道创建哪些对象,各对象的内存大小各不相同,因此垃圾回收主要关注的是这两个区域。
1. 判断对象是否需要被回收
引用计数法
给对象添加一个引用计数器,每当有一个地方引用了该对象,计数器+1,当引用失效,计数器-1,当计数器为0时表示对象不再被使用。
引用计数器法很难解决对象间相互循环引用的问题,所以主流的JVM都没有选用该方法来判断。可达性分析算法
基本思路:
(1)以对象“GC Roots”为起点,开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,则认为该对象不可达,此时系统对该对象第一次标记回收,该对象进入“即将回收”集合;
(2)对于不可达的对象,系统进一步筛选。判断是否有必要执行finalize()方法:如果对象没有覆盖finalize()方法或finalize()方法已经被JVM调用过(finalize()只会被系统自动调用一次),则认为该对象没必要执行finalize()方法。
(a) 如果有必要执行finalize()方法,会将该对象置入F-Queue队列中,由Finalizer线程去执行finalize(),如果finalize()方法中该对象与引用链上的任意对象关联上,则第二次标记时将它移除“即将回收”集合。
(b) 如果没必要执行finalize()方法,此时系统对该对象会被第二次标记回收;
(3)如果对象被标记两次回收,则该对象真正死亡,会被GC回收掉。
建议:尽量避免使用finalize()方法
可作为GC Roots的对象有:
(1)虚拟机栈(栈帧中的本地变量表)中引用的对象
(2)本地方法栈中JNI(即Native方法)引用的对象
(3)方法区中类静态属性引用的对象
(4)方法区中常量引用的对象
引用
引用可分为:
1)强引用(Strong Reference)。类似Object obj = new Object()
这样的引用,只要强引用存在,被引用的对象永远不会被GC回收掉。
2)软引用(Soft Reference)。描述一些还有用但并非必须的对象,在系统将要发生内存溢出异常前,会把软引用关联的对象列入回收范围内进行回收,如果此次回收还没有足够内存,则抛出OOM。
3)弱引用(Weak Reference)。描述一些非必须的对象,但强度比软引用更弱,当GC工作时,无论当前内存是否足够,都会回收弱引用关联的对象。
4)虚引用(Phantom Reference)。虚引用的唯一作用就是当虚引用关联的对象被GC回收时收到一个系统通知。
2. 判断常量、类是否需要被回收
方法区(HotSpot虚拟机中的永久代)垃圾回收的效率很低,主要回收:废弃常量和无用的类。
废弃常量:判定方法与Java堆中对象是否需要被回收的方法类似。
无用的类:
(1)该类所有的实例都已经被回收,即Java堆中不存在该类的对象;
(2)加载该类的ClassLoader已经被回收;
(3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
同时满足这3个条件的类,虚拟机才可以对齐进行回收,但是否真正回收由-Xnoclassgc
参数控制。
3. 垃圾收集算法
标记-清除算法
(1)可达性分析
(2)遍历Java堆,标记出所有对象回收与否
(3)回收所有被标记为回收的对象
优点:算法简单,便于实现。
缺点:标记和清除(需搜索整个堆)两个过程的效率不高;标记清除后会产生大量不连续的内存碎片。内存碎片会导致找不到足够的连续空间来分配大对象(例如长字符串、长数组)。复制算法
将内存按容量划分为大小相等的两块,每次使用其中一块,当这一块用完了,将还存活的对象复制到另一块上,将当前使用的这一块内存空间清理掉。
(1)可达性分析
(2)找到存活对象并复制到另一块内存(不用遍历Java堆去标记出所有对象回收与否)
(3)回收之前整个内存空间
优点:吞吐量高,复制算法搜索到存活对象后只需要复制这些对象,不用遍历整个堆内存,因此GC时间短。无碎片化。
缺点:堆的使用率低,可用空间损失了一半。标记-整理算法
(1)可达性分析
(2)遍历Java堆,标记出所有对象回收与否
(3)将所有存活的对象都向一端移动
(4)清除掉端边界外的内存
优点:堆使用率高,可使用整个内存空间。无碎片化。
缺点:效率不高,标记整理过程需要遍历整个堆,同时需要更新存活对象的指针。
-
分代收集算法
当前商业虚拟机都使用分代收集垃圾回收算法。根据对象存活周期将内存划分为几块,一般将Java堆内存分为新生代和老年代。
新生代:使用复制算法。新生代中对象生命周期很短,每次GC只有少量对象存活,因此使用复制算法每次只需要复制少量存活对象。
默认情况下,将1/3的Java堆内存划分为新生代,又将新生代内存分为Eden(8/10)、From Survivor(1/10)、To Survivor(1/10)三块空间,当回收时将Eden和From Survivor中存活的对象一次性复制到To Survivor中,然后清除Eden和From Survivor中内容,将From Survivor和To Survivor角色互换,此时From Survivor变为To Survivor。
新生代中可用内存空间为整个新生代容量的90%,会浪费掉10%。但是每次GC时无法保证那10%的空间够用,因此需要有其他内存分配担保(老年代),如果To Survivor没有足够内存存放上一次新生代中存活的对象,则将这些对象直接通过分配担保机制进入老年代。
老年代:使用标记-整理算法。默认情况下,将2/3的Java堆内存划分为老年代。
新生代GC,又被称为Minor GC,触发频繁,回收速度快。
老年代GC,又被称为Major GC,回收速度慢,一般会伴随至少一次的Minor GC。
整个Java堆GC,又被称为Full GC。
为什么老年代不使用复制算法?
(1)老年代中对象存活率较高,复制算法效率降低
(2)老年代中没有额外的空间进行分配担保
(3)如果不用分配担保,那么至少需要50%的空间用以复制,空间利用率太低
为什么新生代不使用标记-整理算法?
(1)新生代中GC频繁,对GC耗时要求高,但标记-整理算法的效率不高,GC耗时较长
(2)新生代中对象存活率低,可以只使用10%的空间用以复制,同时还有老年代作为分配担保,复制算法的空间使用率问题得到很好的缓解
为什么要有两个Survivor区?
如果只有一个Survivor区,那么整个新生代被分为Eden和Survivor两个部分,设置两个区域大小比例时会出现问题:如果为1:1,那么复制算法会浪费一半的空间;如果为9:1,那么复制算法会在可用空间为1时变得太小。
4. HotSpot中垃圾回收具体实现
-
枚举根节点
可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)和执行上下文(例如栈帧中的本地变量表)中。
当GC进行时,可达性分析工作必须在一个确保一致性的快照中进行(不能出现对象引用关系不断变化的情况),因此必须停顿所有Java线程(Stop The World)。
当系统停顿下来后,并不需要一个不漏的检查所有执行上下文和全局的引用位置,虚拟机知道哪些地方存放对象引用。在HotSpot中,利用OopMap数据结构存储对象引用的位置信息,这样在GC时可以直接从OopMap中获取信息,快速准确的完成根节点枚举。
同时,虚拟机使用Remembered Set来记录新生代和老年代之间的对象引用,当虚拟机发现程序对Reference类型的数据进行写操作时,会去检查老年代中对象是否引用了新生代对象,如果是,则把相关引用信息记录到Remembered Set中。
这样在GC根节点枚举时加入Remembered Set可以不用扫描整个Java堆。
-
Stop The World
由于引用关系不断变化,所以OopMap中的内容是在不断变化的。
在可达性分析的时候需要暂停所有线程来确保引用关系的稳定。那么在什么地方停下来执行GC呢?
暂停所有线程的“特定位置”称为安全点(Safepoint)。安全点不能太多,否则过于频繁会增加运行负荷;安全点不能太少,否则GC等待时间过长。
一个线程中有一个虚拟机栈,一个虚拟机栈中可以压入多个栈帧,每个栈帧对应一个方法,一个方法由多条指令组成,在可能导致程序长时间运行的指令(明显特征是指令序列复用,例如方法调用、循环跳转、异常跳转等)处会产生一个安全点。
那么如何知道该在哪个安全点停下来呢?
采用主动式中断。当GC触发需要中断线程时,设置一个中断标志,所有线程执行时会主动轮询这个标志,当发现这个标志为true时,就会在安全点自动挂起,同时更新OopMap中内容。
对于那些处于Sleep或Blocked状态的线程而言,它们无法响应JVM的中断请求,到安全点出中断挂起,此时利用安全区域(Safe Region)。安全区域是指在一段代码片段中,引用关系不会发生变化,在这个区域中的任意位置GC都是安全的。当线程执行到安全区域中的代码时,表示自己进入安全区域,此时如果发生GC,该线程不用管,当该线程要离开安全区域时,检查系统是否已经完成了可达性分析,如果没有完成则需等待直到收到可以离开安全区域的信号。
简单总结一下:
(1)GC触发时,设置中断标志为true
(2)各线程(进入安全区域的线程不用管)轮询中断标志发现为true,到安全点自动挂起,更新OopMap内容
(3)根据OopMap,枚举根节点,可达性分析
(4)GC
5. 垃圾收集器
如果两个收集器之间存在连线,表示它们可以搭配使用。
-
Serial收集器(新生代)
Serial收集器是一种新生代收集器,采取复制算法。
使用一条GC线程垃圾回收,同时在GC时必须暂停其他所有工作线程。
优点:简单,单线程回收效率高。
运行在Client模式下的虚拟机选用Serial收集器较好。
-
ParNew收集器(新生代)
ParNew收集器是Serial收集器的多线程版本,属于并行收集器,采用复制算法。
使用多个GC线程并行回收垃圾,在GC时必须暂停其他所有工作线程。
许多运行在Server模式下的虚拟机首选ParNew作为新生代收集器,主要原因是除Serial外,目前只有ParNew能与CMS配合工作。
-
Parallel Scavenge收集器(新生代)
Parallel Scavenge收集器是一个并行的多线程收集器,采用复制算法。
与ParNew收集器类似,但区别在于:
(1)Parallel Scavenge收集器利用-XX:MaxGCPauseMillis参数可以设置最大停顿时间;使用-XX:GCTimeRatio参数可以控制吞吐量(CPU运行用户代码时间 / (CPU运行用户代码时间+垃圾收集时间)
)。
(2)使用-XX:+UseAdaptiveSizePolicy参数,虚拟机会根据当前系统运行状况,动态调整新生代大小、Eden/Survivor比例等参数以提供最合适的停顿时间或最大的吞吐量。
停顿时间短适合需要与用户交互的程序;高吞吐量可以高效利用CPU时间,适合后台运算任务,Parallel Scavenge收集器适合做后台运算任务的垃圾回收器。
-
Serial Old收集器(老年代)
Serial Old收集器是Serial收集器的老年代版本,使用标记-整理算法。
它是一个单线程收集器,适用于运行在Client模式下的虚拟机。
-
Parallel Old收集器(老年代)
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,是并行的多线程收集器,使用标记-整理算法。
在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old收集器。
-
CMS收集器(老年代)
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。
CMS收集器采用标记-清除算法。整个过程分为:
(1)初始标记。需要Stop The World,标记GC Roots能直接关联到的对象,速度很快。
(2)并发标记。无需Stop The World,进行GC Roots Tracing过程,耗时长。
(3)重新标记。需要Stop The World,修正并发标记期间因用户线程继续运作而导致标记变化的那一部分对象的标记,停顿比初始标记稍长。
(4)并发清除。无需Stop The World,并发清除被标记为要回收的对象,耗时长。
优点:耗时最长的并发标记和并发清除过程中,收集器线程与用户线程一起工作,需要Stop The World的时间较短。
缺点:
(1)CMS收集器对CPU资源敏感。在并发阶段,用户线程虽然不会停顿,但是由于部分CPU资源被GC占用,导致应用程序变慢。
(2)CMS收集器无法处理浮动垃圾,可能出现”Concurrent Mode Failure“导致另一次Full GC产生。在并发清理阶段,用户线程还在工作,会继续产生垃圾,这部分垃圾在本次GC中无法被回收,只能等到下次GC时回收。也正是由于并发清理阶段用户线程工作,所以不能等到老年代空间快满的时候再收集,需要预留出一部分空间,如果并发清理阶段发现预留的空间仍然不够,则出现”Concurrent Mode Failure“失败,临时启动Serial Old收集器进行老年代回收,此时停顿时间会很长。
(3)由于使用标记-清除算法,会产生碎片化。使用-XX:+UseCMSCompactAtFullCollection
参数可以让CMS收集器在要进行Full GC时进行内存碎片合并整理,内存整理是无法并发的。使用-XX:CMSFullGCsBeforeCompaction
参数设置进行多少次不压缩的Full GC后进行一次带压缩的Full GC。
-
G1收集器
G1收集器是一款面向服务端应用的垃圾收集器。
G1收集器的特点:
(1)可以和用户线程并发执行。
(2)G1可以独立管理整个GC堆,不需要其他收集器配合,尽管如此,G1仍然使用分代收集算法,采用不同的方式去处理新生代和老年代的对象以获取更好的收集效果。
(3)G1收集器从整体上看是采用标记-整理算法实现的,不会产生空间碎片。
(4)可以由使用者指定在长度为M毫秒的时间片段内,垃圾收集的时间不能超过N毫秒。
在G1收集器之前的所有收集器收集的范围都是整个新生代或老年代,G1不再是这样。G1收集器将整个Java堆划分为多个大小相等的独立区域(Region),新生代和老年代的概念仍然保留,但是不再是物理隔离的了,它们各自由若干个Region(不要求连续)组成。
G1收集器跟踪各Region的垃圾回收的价值大小,并在后台维护一个优先列表,每次GC根据允许的收集时间,优先回收价值大的Region。
Region之间的对象引用也是由Remembered Set来维护的,各Region维护一个Remembered Set。根节点枚举时将Remembered Set加入进去可以不用扫描整个Java堆。
G1收集器运作过程与CMS类似:
(1)初始标记。标记GC Roots能直接关联到的对象,耗时短。
(2)并发标记。可达性分析,与用户线程并发,耗时较长。
(3)最终标记。并发标记阶段用户线程产生的对象变化记录在Remembered Set Logs里面,最终标记将Remembered Set Logs的数据合并到Remembered Set中。
(4)筛选回收。根据用户期望的GC停顿时间以及各Region的回收价值,进行GC。
6. GC日志
- 9.464和9.496:GC发生时间
- GC (Allocation Failure)和Full GC (Ergonomics):GC类型
- [PSYoungGen: 8690K->945K(57856K)]:垃圾回收前新生代中已使用内存 -> 回收后新生代中已使用内存(新生代总内存大小)
- [ParOldGen: 18017K->20315K(34304K)]:垃圾回收前老年代中已使用内存 -> 回收后老年代中已使用内存(老年代总内存大小)
- 26707K->21260K(92160K):垃圾回收前Java堆已使用内存 -> 回收后Java堆已使用内存(Java堆总内存大小)
- [Metaspace: 19581K->19581K(1067008K)]:垃圾回收前元数据空间已使用内存 -> 回收后元数据空间已使用内存(元数据空间总内存大小)
- 0.1259466 secs:GC花费的时间
注:
- GC日志中用
[ ]
来划分 - 各区域名称根据收集器的不同而有所区别。例如:Serial收集器中,DefNew表示新生代;ParNew收集器中,ParNew表示新生代;Parallel Scavenge收集器中,PSYoungGen表示新生代。
7. 内存分配与回收策略
对象优先在新生代分配
大多数情况下,新产生的对象在新生代(Eden)中分配,当没有足够空间进行分配时将触发一次Minor GC。
虚拟机提供-XX:+PrintGCDetails
参数,在发生GC时打印内存回收日志,并在进程退出时输出当前内存各区域分配情况。大对象直接在老年代分配
大多数情况下,新产生的对象在新生代中分配,但是也有例外,那就是大对象,所谓的大对象是指需要大量连续内存空间的Java对象,例如很长的字符串以及数组。
虚拟机提供-XX:PretenureSizeThreshold
参数用于设置直接在老年代中分配的对象大小阈值,当对象大于该阈值时直接分配在老年代中。
这样做可以避免Eden和Survivor之间大量的内存复制。
注:PretenureSizeThreshold只对Serial和ParNew两种收集器有效。长期存活的对象进入老年代
虚拟机给每个对象都定义了一个对象年龄计数器。
如果对象在新生代出生,在经过一次Minor GC后仍然存活并被复制到To Survivor中,则将该对象的年龄置为1,此后该对象在Survivor中每熬过一次Minor GC,则年龄+1,当年龄超过-XX:MaxTenuringThreshold
设置的阈值后,该对象将进入老年代。动态对象年龄判定
如果在To Survivor中相同年龄的所有对象大小之和大于To Survivor空间的一半,则年龄大于等于此年龄的对象进入老年代,即使没有达到MaxTenuringThreshold设置的阈值也无所谓。空间分配担保
在Minor GC时,需要老年代作分配担保,以确保To Survivor空间不够时有地方可以保存新生代中存活的对象。
(1)在Minor GC之前,先检查老年代最大可用的连续空间是否大于新生代所有对象总空间大小,如果大于则进行Minor GC;如果小于则说明可能会出现担保失败的情况,此时执行(2)。
(2)检查参数HandlePromotionFailure
是否允许担保失败,如果不允许,则改为进行一次Full GC;如果允许则执行(3)
(3)检查老年代最大可用的连续空间是否大于历次晋升到老年代的对象平均大小,如果小于则改为执行一次Full GC;如果大于则执行(4)
(4)尝试执行Minor GC,Minor GC时仍然有可能出现担保失败的情况(例如Minor GC后存活的对象很多,总大小超过了老年代最大可用的连续空间,那么老年代担保会失败),如果担保失败了则重新发起一次Full GC;如果没有担保失败则Minor GC成功。
注:步骤(2)在JDK 6 Update 24后,参数HandlePromotionFailure
已经没有意义,无论是否允许担保失败,都会执行(3)。
总结:
(1)当产生新的对象时,先检查该对象是否超过-XX:PretenureSizeThreshold
参数设置的阈值,如果超过则将该对象分配在老年中,否则将该对象分配在新生代(Eden)中
(2)当新生代快要被填满的时候,虚拟机准备触发Minor GC,但是此前会先检查老年代中最大可用的连续空间是否大于新生代所有对象总空间大小,如果大于则触发Minor GC,否则(3)
(3)检查老年代最大可用的连续空间是否大于历次晋升到老年代的对象平均大小,如果小于则改为执行一次Full GC,如果大于则(4)
(4)尝试执行Minor GC,Minor GC时如果To Survivor空间不够保存新生代的存活对象,那么需要老年代分配担保,如果老年代剩余的可用连续空间不够存放新生代中存活的对象,则担保失败,会重新发起一次Full GC;如果To Survivor空间足够或老年代担保成功则此次Minor GC成功
(5)Minor GC成功后,如果对象被复制到To Survivor,则该对象年龄+1。
(6)如果To Survivor中相同年龄的所有对象大小之和大于To Survivor空间的一半,则年龄大于等于此年龄的对象进入老年代,否则(7)
(7)如果对象年龄超过-XX:MaxTenuringThreshold设置的阈值后,该对象也将进入老年代