Java垃圾回收略略观
本文主要介绍Java垃圾回收(Garbage Collection),90%干货,文字颇多,需要耐心一点看。
【对象判断状态算法】
------引用计数法
在创建对象时,为对象创建一个伴生的引用计数器,当有其他对象引用该对象时,将引用计数器的值加1,如果其他对象不再引用该对象就将引用计数器的值减1,所以当引用计数器的值为0时,就代表不再有任何对象引用该对象,就说明该对象已经"死亡",就可以被判定为待回收。
但是这样做是有问题的,在某些情况下,比如:两个对象相互引用时,这两个对象就永远不会被回收。
python采用的是引用计数为主,分代回收和 标记清除 两种机制为辅的策略。
------可达性分析算法
通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
在java语言中,可作为GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中JNI(即一般所说的Native方法)引用的对象。
【垃圾回收算法】
------标记-清除算法
最早的垃圾回收算法,它是由标记阶段和清除阶段构成的。
标记阶段会给所有的存活对象做上标记,而清除阶段会把没有被标记的死亡对象进行回收。
标记-清除算法执行完成之后会产生大量的不连续内存,这样当程序需要分配一个大对象时,因为没有足够的连续内存而导致需要提前触发一次垃圾回收动作。
------标记-复制算法
将内存分为大小相同的两块区域,每次只使用其中的一块区域,这样在进行垃圾回收时就可以直接将存活的东西复制到新的内存上,然后再把另一块内存全部清理掉。
可以解决内存碎片的问题,内存的可用率大幅降低。
------标记-整理算法
标记-整理算法的后一个阶段不是直接对内存进行清除,而是把所有存活的对象移动到内存的一端,然后把另一端的所有死亡对象全部清除。
【jvm堆的三个区域】
GC(Garbage Collection):即垃圾回收器。
为了高效的回收,jvm将堆分为三个区域
1.新生代(Young Generation)NewSize和MaxNewSize分别可以控制年轻代的初始大小和最大的大小
2.老年代(Old Generation)
3.永久代(Permanent Generation)(保存方法区,已经移除)
在Java8中,元空间(Metaspace)登上舞台,方法区存在于元空间(Metaspace)。
同时,元空间不再与堆连续,而且是存在于本地内存(Native memory)
堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:
新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小
------新生代
新生代内存按照8:1:1的比例分为一个Eden区和S0,S1区。(Survivor)
正常情况下,大部分对象是在Eden区中生成的,回收的时候,将Eden区内所有的存活对象保存到S0区内,然后清空Eden区。
当S0区也存满的时候,则将Eden区和S0区的存活对象保存到S1区,然后清空Eden区和S0区;然后将S0区和S1区互换。(核心思想就是一直保持S1区域为空)
新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。
新生代垃圾回收的主要目的,就是尽快回收掉那些生命周期比较短的对象。
------老年代
当S1区的空间不足以放下Eden区和S0区的存活对象的时候,就会把对象存放到老年代。
所以,我们可以认为老年代存放的是一些生命周期很长的对象。
如果老年代也满了,就会触发一次Full GC(Major GC + Minor GC),也就是新生代、老年代都进行回收。
Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
出现Full GC经常会伴随至少一次的Minor GC(不是绝对,Parallel Sacvenge收集器就可以选择设置Major GC策略);
Major GC速度一般比Minor GC慢10倍以上。
------Metaspace
用于存放静态文件,如Java类、方法等。
Metaspace包含JVM用于描述应用程序中类和方法的元数据,是由JVM在运行时根据应用程序使用的类来填充的。
【并发垃圾收集/并行垃圾收集】
------并行(Parallel)
指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;
如ParNew、Parallel Scavenge、Parallel Old;
------并发(Concurrent)
指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行);
用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;
如CMS、G1(也有并行);
【垃圾收集器】
图中,基本上涵盖了主流的垃圾收集器,图中紫色的线,代表他们可以配合使用。
【Serial收集器】
Serial(串行)垃圾收集器,JDK1.3.1前是HotSpot新生代收集的唯一选择。
------特点
针对新生代;
采用复制算法;
单线程收集。(会"Stop The World")
------应用场景
HotSpot在Client模式下默认的新生代收集器。
------优势
简单高效(与其他收集器的单线程相比);
对于限定单个CPU的环境来说,Serial收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率;
在用户的桌面应用场景中,可用内存一般不大(几十M至一两百M),可以在较短时间内完成垃圾收集(几十MS至一百多MS),只要不频繁发生,这是可以接受的
------设置参数
-XX:+UseSerialGC
------Stop TheWorld说明
JVM在后台自动发起和自动完成的,在用户不可见的情况下,把用户正常的工作线程全部停掉,即GC停顿;
【ParNew收集器】
ParNew垃圾收集器是Serial收集器的多线程版本。
------特点
除了多线程外,其余的行为、特点和Serial收集器一样
------应用场景
在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;
但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。
------设置参数
-XX:+UseConcMarkSweepGC |
指定使用CMS后,会默认使用ParNew作为新生代收集器; |
-XX:+UseParNewGC |
强制指定使用ParNew; |
-XX:ParallelGCThreads |
指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同; |
【Parallel Scavenge收集器】
Parallel Scavenge垃圾收集器因为与吞吐量关系密切,也称为吞吐量收集器(Throughput Collector)。
------特点
新生代收集器;
采用复制算法;
多线程收集;
CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间;而Parallel Scavenge收集器的目标则是达一个可控制的吞吐量(Throughput)。
------应用场景
高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间;
应用程序运行在具有多个CPU,对暂停时间没有特别高的要求的机器上,即程序主要在后台进行计算,而不需要与用户进行太多交互;
例如:批量处理、订单处理、工资支付、科学计算。
------设置参数
-XX:MaxGCPauseMillis |
控制最大垃圾收集停顿时间,大于0的毫秒数; MaxGCPauseMillis设置得稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降; 因为可能导致垃圾收集发生得更频繁; |
-XX:GCTimeRatio |
设置垃圾收集时间占总时间的比率,0<n<100的整数; GCTimeRatio相当于设置吞吐量大小; |
-XX:+UseAdptiveSizePolicy |
JVM会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略 |
------吞吐量(Throughput)
CPU用于运行用户代码的时间与CPU总消耗时间的比值;
即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间);
高吞吐量即减少垃圾收集时间,让用户代码获得更长的运行时间;
------垃圾收集器期望的目标(关注点)
停顿时间 |
停顿时间越短就适合需要与用户交互的程序; 良好的响应速度能提升用户体验; |
吞吐量 |
高吞吐量则可以高效率地利用CPU时间,尽快完成运算的任务; 主要适合在后台计算而不需要太多交互的任务; |
覆盖区(Footprint) |
在达到前面两个目标的情况下,尽量减少堆的内存空间; 可以获得更好的空间局部性; |
【Serial Old收集器】
Serial Old是 Serial收集器的老年代版本
------特点
针对老年代;
采用"标记-整理"算法(还有压缩,Mark-Sweep-Compact);
单线程收集;
------应用场景
主要用于Client模式;
而在Server模式有两大用途:
在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配);
作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用
【Parallel Old收集器】
Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本
------特点
针对老年代;
采用"标记-整理"算法;
多线程收集
------应用场景
JDK1.6及之后用来代替老年代的Serial Old收集器;
特别是在Server模式,多CPU的情况下;
这样在注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge加Parallel Old收集器的"给力"应用组合
------设置参数
-XX:+UseParallelOldGC
【CMS收集器】
并发标记清理(Concurrent Mark Sweep,CMS)收集器也称为并发低停顿收集器(Concurrent Low Pause Collector)或低延迟(low-latency)垃圾收集器
------特点
针对老年代;
基于"标记-清除"算法(不进行压缩操作,产生内存碎片);
以获取最短回收停顿时间为目标;
并发收集、低停顿;
需要更多的内存
是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器;第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
------应用场景
与用户交互较多的场景;
希望系统停顿时间最短,注重服务的响应速度;
以给用户带来较好的体验;
如常见WEB、B/S系统的服务器上的应用
------设置参数
-XX:+UseConcMarkSweepGC
------运作过程
初始标记(CMS initial mark)
仅标记一下GC Roots能直接关联到的对象;
速度很快;
但需要"Stop The World";
并发标记(CMS concurrent mark)
进行GC Roots Tracing的过程;
刚才产生的集合中标记出存活对象;
应用程序也在运行;
并不能保证可以标记出所有的存活对象;
重新标记(CMS remark)
为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;
需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;
采用多线程并行执行来提升效率;
并发清除(CMS concurrent sweep)
回收所有的垃圾对象;
整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作,所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行。
------缺点
对CPU资源非常敏感 |
并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。 |
无法处理浮动垃圾 |
在并发清除时,用户线程新产生的垃圾,称为浮动垃圾; 这使得并发清除时需要预留一定的内存空间,不能像其他收集器在老年代几乎填满再进行收集; 也要可以认为CMS所需要的空间比其他垃圾收集器大; "-XX:CMSInitiatingOccupancyFraction":设置CMS预留内存空间; |
可能出现"Concurrent Mode Failure"失败 |
如果CMS预留内存空间无法满足程序需要,就会出现一次"Concurrent Mode Failure"失败; 这时JVM启用后备预案:临时启用Serail Old收集器,而导致另一次Full GC的产生; 这样的代价是很大的,所以CMSInitiatingOccupancyFraction不能设置得太大。 |
产生大量内存碎片 |
由于CMS基于"标记-清除"算法,清除后不进行压缩操作; |
总体来看,与Parallel Old垃圾收集器相比,CMS减少了执行老年代垃圾收集时应用暂停的时间;
但却增加了新生代垃圾收集时应用暂停的时间、降低了吞吐量而且需要占用更大的堆空间;
【G1】
G1(Garbage-First)是JDK7-u4才推出商用的收集器
------特点
并行与并发 |
能充分利用多CPU、多核环境下的硬件优势; 可以并行来缩短"Stop The World"停顿时间; 也可以并发让垃圾收集与用户程序同时进行; |
分代收集,收集范围包括新生代和老年代 |
能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配; 能够采用不同方式处理不同时期的对象; 虽然保留分代概念,但Java堆的内存布局有很大差别; 将整个堆划分为多个大小相等的独立区域(Region); 新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合; |
结合多种垃圾收集算法,空间整合,不产生碎片 |
从整体看,是基于标记-整理算法; 从局部(两个Region间)看,是基于复制算法; 这是一种类似火车算法的实现; |
可预测的停顿:低停顿的同时实现高吞吐量 |
G1除了追求低停顿处,还能建立可预测的停顿时间模型; 可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒; |
------应用场景
面向服务端应用,针对具有大内存、多处理器的机器;
最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案;
在下面的情况时,使用G1可能比CMS好:
超过50%的Java堆被活动数据占用;
对象分配频率或年代提升频率变化很大;
GC停顿时间过长(长于0.5至1秒)。
------设置参数
-XX:+UseG1GC |
指定使用G1收集器; |
-XX:InitiatingHeapOccupancyPercent |
当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45; |
-XX:MaxGCPauseMillis |
为G1设置暂停时间目标,默认值为200毫秒; |
-XX:G1HeapRegionSize |
设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region; |
------为什么G1收集器可以实现可预测的停顿
可以有计划地避免在Java堆的进行全区域的垃圾收集;
G1跟踪各个Region获得其收集价值大小,在后台维护一个优先列表;
每次根据允许的收集时间,优先回收价值最大的Region(名称Garbage-First的由来);
这就保证了在有限的时间内可以获取尽可能高的收集效率;
------一个对象被不同区域引用
一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确?
无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描:
每个Region都有一个对应的Remembered Set;
每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作;
然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象);
如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;
当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set;
就可以保证不进行全局扫描,也不会有遗漏。
------运作过程
初始标记(Initial Marking)
仅标记一下GC Roots能直接关联到的对象;
且修改TAMS(Next Top at Mark Start),让下一阶段并发运行时,用户程序能在正确可用的Region中创建新对象;
需要"Stop The World",但速度很快;
并发标记(Concurrent Marking)
进行GC Roots Tracing的过程;
刚才产生的集合中标记出存活对象;
耗时较长,但应用程序也在运行;
并不能保证可以标记出所有的存活对象;
最终标记(Final Marking)
为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;
上一阶段对象的变化记录在线程的Remembered Set Log;
这里把Remembered Set Log合并到Remembered Set中;
需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;
采用多线程并行执行来提升效率;
筛选回收(Live Data Counting and Evacuation)
首先排序各个Region的回收价值和成本;
然后根据用户期望的GC停顿时间来制定回收计划;
最后按计划回收一些价值高的Region中垃圾对象;
回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存;
可以并发进行,降低停顿时间,并增加吞吐量;
【ZGC】
在 ZGC 收集器中没有新生代和老生代的概念,它只有一代。
ZGC 收集器采用的着色指针技术,利用指针中多余的信息位来实现着色标记,并且 ZGC 使用了读屏障来解决 GC 线程和应用线程可能存在的并发(修改对象状态的)问题,从而避免了Stop The World(全局停顿),因此使得 GC 的性能大幅提升。
ZGC 的执行流程和 CMS 比较相似,首先是进行 GC Roots 标记,然后再通过指针进行并发着色标记,之后便是对标记为死亡的对象进行回收(被标记为橘色的对象),最后是重定位,将 GC 之后存活的对象进行移动,以解决内存碎片的问题。
【参考】
https://www.cnblogs.com/cxxjohnson/p/8625713.html
《深入理解Java虚拟机:JVM高级特性与最佳实践》
《Thinking in Java》