Github:https://github.com/yihonglei/jdk-source-code-reading(java-jvm)
JVM内存结构
JVM类加载机制
JVM内存溢出分析
HotSpot对象创建、内存、访问
JVM垃圾回收机制(1)--如何判定对象可以回收
JVM垃圾回收机制(2)--垃圾收集算法
JVM垃圾回收机制(3)--垃圾收集器
JVM垃圾回收机制(4)--内存分配和回收策略
垃圾收集器是垃圾收集算法(标记-清除算法、复制算法、标记-整理算法)的具体实现。Java虚拟机规范
中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器
都可能会有很大的差别,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用
的收集器。
这里主要讨论HotSpot虚拟机中的垃圾收集器。
JDK7/8后HotSpot虚拟机中的垃圾收集器和组合搭配示意图:
图中表明总的有七种收集器,Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1;
新生代收集器:Serial、ParNew、Parallel Scavenge。
老年代收集器:Serial Old、Parallel Old、CMS。
整理收集器:G1。
这些收集器很多是可以组合使用的,图中连线的表示可以搭配使用。
并行(Parallel)
指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;
如ParNew、Parallel Scavenge、Parallel Old;
并发(Concurrent)
指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行);
用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;
如CMS、G1(也有并行);
Minor GC又称为新生代GC,是指发生在新生代的垃圾收集动作。
因为大多数Java对象都是"朝生夕灭"的,所以Minor GC回收非常的频繁,一般回收速度也非常快。
Full GC又称为Major GC或老年代GC,是指发生在老年代的垃圾收集动作。
出现Full GC经常会伴随至少一次的Minor GC(不是绝对,Parallel Sacvenge收集器就可以选择设置Major GC策略);
Major GC速度一般比Minor GC慢10倍以上;
接下来主要分析各个收集器的特性、基本原理和使用场景。
Serial(串行)收集器是最基本、发展历史最悠久的收集器,在JDK1.3.1之前是虚拟机新生代收集的唯一选择。
Serial收集器主要有以下特性:
1)针对新生代进行回收。
2)采用复制算法。
3)单线程收集。
4)回收时必须停止其他所有工作线程,直到收集结束。即"Stop The Word",给用户的体验不好。
Serial/Serial Old收集器的运行过程示意图:
采用单线程进行收集,收集时停止所有用户工作线程,直到收集完成,收集完成才又启动用户线程。
Serial收集器依然是HotSpot在Client模式下默认的新生代收集器;
也有优于其他收集器的地方:简单高效(与其他收集器的单线程相比);对于限定单个CPU的环境来说,
Serial收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率;在用户的桌面应用场景中,
可用内存一般不大(几十M至一两百M),可以在较短时间内完成垃圾收集(几十MS至一百多MS),
只要不频繁发生,这是可以接受的。
"-XX:+UseSerialGC":添加该参数来显式的使用串行垃圾收集器;
ParNew收集器是Serial收集器的多线程版本。
1)多线程收集;
2)除多线程外,其余特性与Serial(串行)收集器一样,比如控制参数、收集算法(都用复制算法)、
Stop The World(收集时停止所有用户线程)、对象分配规则、回收策略等都与Serial收集器完全一样。
ParNew和Serial实现代码很多都是共用的,可以看成是对Serial收集效率提升的优化版本。
ParNew收集器工作过程示意图:
采用多线程收集,收集时也没逃过停止所有用户线程的宿命,只是用多线程快些,用户感知的GC停顿小些,
用户体验好些。
在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;
CMS下面会介绍,它是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器,它第一次实现了让垃圾
线程和用户线程同时工作。CMS作为老年代收集器,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge
配合工作;因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;
而其余几种收集器则共用了部分的框架代码;
同时,在单个CPU环境中,ParNew收集器不会比Serail收集器有更好的效果,因为存在线程交互开销,
这是多线程避免不了的线程上下文切换开销,但是在多个CPU下,Serial跟ParNew没法相比,现在是多核时代,
"榨干"CPU来提高运行效率是程序追求的,所以ParNew配合CMS成为不二选择。
"-XX:+UseConcMarkSweepGC":指定使用CMS后,会默认使用ParNew作为新生代收集器;
"-XX:+UseParNewGC":强制指定使用ParNew;
"-XX:ParallelGCThreads":指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;
Parallel Scavenge收集器是一个新生代收集器,它也是用复制算法、同时也是多线程收集器,
看上去和ParNew一样,但是它关注的点与其他收集器不同,别的收集器主要关心如何缩短GC停顿时间,
而Parallel Scavenge关心的是吞吐量,所以Parallel Scavenge也称为"吞吐量优先"收集器。
1)有些特点与ParNew相似,比如针对新生代收集,采用复制算法,多线程收集。
2)别的收集器主要关注GC停顿时间,而Parallel Scavenge主要目标是达到一个可控制的吞吐量。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量
则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
例如:长时间处理大数据,科学计算等等。
Parallel Scavenge/Parallel Old收集器工作过程示意图:
Parallel Scavenge收集器提供两个参数用于精确控制吞吐量,分别是控制最大垃圾收集
停顿时间的"-XX:MaxGCPauseMillis"参数以及直接设置吞吐量大小的"-XX:GCTimeRatio"参数。
"-XX:MaxGCPauseMillis"
控制最大垃圾收集停顿时间参数允许的值是一个大于0的毫秒数,收集器将尽可能地保证内存回收花费的
时间不超过设定值;
MaxGCPauseMillis设置得稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降,
因为可能导致垃圾收集发生得更频繁;
"-XX:GCTimeRatio"
设置垃圾收集时间占总时间的比率,该值范围为0 GCTimeRatio相当于设置吞吐量大小; 垃圾收集执行时间占应用程序执行时间的比例的计算方法是:1 / (1 + n) 例如,选项-XX:GCTimeRatio=19,设置了垃圾收集时间占总时间的5%--1/(1+19); 默认值是1%--1/(1+99),即n=99; 垃圾收集所花费的时间是年轻一代和老年代收集的总时间; 如果没有满足吞吐量目标,则增加代的内存大小以尽量增加用户程序运行的时间; 还有一个参数,"-XX:+UseAdptiveSizePolicy" 开启这个参数后,就不用手工指定一些细节参数,如: 新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、 晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等; JVM会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量, 这种调节方式称为GC自适应的调节策略(GC Ergonomiscs); 当你不知道怎么优化的时候,自适应调节策略是一种值得推荐的方式: 只需设置好内存数据大小(如"-Xmx"设置最大堆); 然后使用"-XX:MaxGCPauseMillis"或"-XX:GCTimeRatio"给JVM设置一个优化目标; 那些具体细节参数的调节就由JVM自适应完成; 这也是Parallel Scavenge收集器与ParNew收集器一个重要区别; Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用"标记-整理"算法。 1)针对老年代进行收集; 2)采用"标记-整理"算法; 3)是一个单线程收集器; Serial/Serial Old收集器运行示意图: 主要用于Client模式; 而在Server模式有两大用途: 1)在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配); 2)作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用; Parallel Old是Parallel Scagenge收集器的老年代版本,使用多线程和"标记-整理"算法。 1)针对老年代收集; 2)"标记-整理"算法; 3)多线程收集; Parallel Scavenge/Parallel Old收集器工作过程示意图: Parallel Old收集器是在JDK1.6中才开始提供的,用于替代老年代Serial Old收集器。 因为Serial Old收集器在服务端应用性能上是一个"拖累"。 特别是在Server模式,多CPU的情况下;这样在注重吞吐量以及CPU资源敏感的场景, 就有了Parallel Scavenge加Parallel Old收集器的"给力"应用组合; "-XX:+UseParallelOldGC":指定使用Parallel Old收集器; CMS(Concurrent Mark Sweep)收集器是一种以 获取最短回收停顿时间为目的的收集器, 也称为并发低停顿收集器(Concurrent Low Pause Collector)或低延迟(low-latency)垃圾收集器; 1)正对老年代收集; 2)采用"标记-清除"算法; 3)以获取最短回收停顿时间为目标; 4)并发收集、低停顿; 5)需要更多的内存; CMS收集器收集过程分为四个步骤: 1)初始化标记 仅标记一下GC Roots能直接关联到的对象; 速度很快; 但需要"Stop The World"; 2)并发标记 进行GC Roots Tracing的过程; 刚才产生的集合中标记出存活对象; 应用程序也在运行; 并不能保证可以标记出所有的存活对象; 3)重新标记 为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录; 需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短; 采用多线程并行执行来提升效率; 4)并发清除标记 回收所有的垃圾对象; 其中初始标记和重新标记这两个步骤仍然需要"Stop The Word"。整个过程中耗时最长的并发标记和并发清除都 可以与用户线程一起工作;所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行; CMS收集器运行示意图: 1)CMS收集器对CPU资源非常敏感。 并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。 CMS的默认收集线程数量是=(CPU数量+3)/4;当CPU数量多于4个,收集线程占用的CPU资源多于25%, 对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。 2)CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现"Concurrent Mode Failure"失败 而导致一次Full GC的产生。 浮动垃圾(Floating Garbage) 在并发清除时,用户线程新产生的垃圾,称为浮动垃圾;这使得并发清除时需要预留一定的内存空间, 不能像其他收集器在老年代几乎填满再进行收集;也要可以认为CMS所需要的空间比其他垃圾收集器大; "-XX:CMSInitiatingOccupancyFraction":设置CMS预留内存空间; JDK1.5默认值为68%;JDK1.6变为大约92%; "Concurrent Mode Failure"失败 如果CMS预留内存空间无法满足程序需要,就会出现一次"Concurrent Mode Failure"失败; 这时JVM启用后备预案:临时启用Serail Old收集器,而导致另一次Full GC的产生; 这样的代价是很大的,所以CMSInitiatingOccupancyFraction不能设置得太大。 3)产生大量的内存碎片 由于CMS基于"标记-清除"算法,清除后不进行压缩操作; 产生大量不连续的内存碎片会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作。 内存碎片解决方法: "-XX:+UseCMSCompactAtFullCollection" 使得CMS出现上面这种情况时不进行Full GC,而开启内存碎片的合并整理过程; 但合并整理过程无法并发,停顿时间会变长; 默认开启(但不会进行,结合下面的CMSFullGCsBeforeCompaction); "-XX:+CMSFullGCsBeforeCompaction" 设置执行多少次不压缩的Full GC后,来一次压缩整理; 为减少合并整理过程的停顿时间; 默认为0,也就是说每次都执行Full GC,不会进行压缩整理; 由于空间不再连续,CMS需要使用可用"空闲列表"内存分配方式,这比简单实用"碰撞指针"分配内存消耗大; G1(Garbage-First)是面向服务端应用,JDK7-u4才推出商用的收集器,是当今收集器技术发展的最前沿成果之一; 1)并行与并发 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或CPU核心)来缩短"Stop The World"停顿时间; 也可以并发让垃圾收集与用户程序同时进行; 2)分代收集,收集范围包括新生代和老年代 能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配; 能够采用不同方式处理不同时期的对象以获得更好的收集效果; 虽然保留分代概念,但Java堆的内存布局有很大差别;将整个堆划分为多个大小相等的独立区域(Region); 新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合; 3)空间整合 从整体看,是基于标记-整理算法实现的收集器,从局部(两个Region间)看,是基于"复制"算法实现的; 运作期间都不会产生内存碎片,收集后能提供规整的可用内存,有利于长时间运行,分配大对象是不会因为无法 找到连续内存空间而提前触发一下GC; 4)可预测的停顿:低停顿的同时实现高吞吐量 G1除了追求低停顿处,还能建立可预测的停顿时间模型,可以明确指定M毫秒时间片内, 垃圾收集消耗的时间不超过N毫秒; 不计算维护Remembered Set的操作,可以分为4个步骤(与CMS较为相似)。 1)初始标记(Initial Marking) 仅标记一下GC Roots能直接关联到的对象; 且修改TAMS(Next Top at Mark Start),让下一阶段并发运行时,用户程序能在正确可用的Region中创建新对象; 需要"Stop The World",但速度很快; 2)并发标记(Concurrent Marking) 进行GC Roots Tracing的过程; 刚才产生的集合中标记出存活对象; 耗时较长,但应用程序也在运行; 并不能保证可以标记出所有的存活对象; 3)最终标记(Final Marking) 为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录; 上一阶段对象的变化记录在线程的Remembered Set Log; 这里把Remembered Set Log合并到Remembered Set中; 需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短; 采用多线程并行执行来提升效率; 4)筛选回收(Live Data Counting and Evacuation) 首先排序各个Region的回收价值和成本; 然后根据用户期望的GC停顿时间来制定回收计划; 最后按计划回收一些价值高的Region中垃圾对象; 回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存; 可以并发进行,降低停顿时间,并增加吞吐量; G1收集器运行示意图: 全堆收集,多种算法结合,与用户线程并行进行。 面向服务端应用,针对具有大内存、多处理器的机器; 最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案; 如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒; 用来替换掉JDK1.5中的CMS收集器; 在下面的情况时,使用G1可能比CMS好: 1)超过50%的Java堆被活动数据占用; 2)对象分配频率或年代提升频率变化很大; 3)GC停顿时间过长(长于0.5至1秒)。 是否一定采用G1呢?也未必: 如果现在采用的收集器没有出现问题,不用急着去选择G1; 如果应用程序追求低停顿,可以尝试选择G1; 是否代替CMS需要实际场景测试才知道。 "-XX:+UseG1GC":指定使用G1收集器; "-XX:InitiatingHeapOccupancyPercent":当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45; "-XX:MaxGCPauseMillis":为G1设置暂停时间目标,默认值为200毫秒; "-XX:G1HeapRegionSize":设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region; 到这里,HotSpot虚拟机中收集器大概都了解了一遍。 《深入理解Java虚拟机》 (第二版) 周志明 著;五 Serial Old收集器
1、特性
2、基本原理
3、应用场景
六 Parallel Old收集器
1、特性
2、基本原理
3、应用场景
4、参数设置
七 CMS收集器
1、特性
2、基本原理
3、CMS三个缺点
八 G1收集器
1、特性
2、基本原理
3、应用场景
4、参数设置
参考文献