JVM突击-垃圾收集篇
总结下自己掌握的关于JVM垃圾回收相关的知识点~
1. 什么样的对象为垃圾?
引用计数法
无法解决循环依赖问题
可达性分析法
从一些roots对象出发,沿着roots对象追踪对象上的引用,能追踪到的对象都是存活的对象;没有被roots对象引用的视为垃圾对象。
可以作为GC Roots
的对象有:
- 虚拟机栈中引用的对象
常见的就是:Object obj = new Object(); - 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
2. 垃圾收集算法
标记-清除
标记出所有需要回收的对象,标记完成后回收被标记的对象。
有两个缺点:
- 会产生内存碎片,内存碎片太多可能会导致无法分配较大的对象(需要连续的内存空间)
- 标记和清除效率不高
复制
内存1分为2,每次只使用其中的1半。当其中的一块空间用完了,就将还存活着的对象复制到另外一块内存空间上,然后把已使用的那一块空间一次清理掉
有2个缺点:
- 内存空间浪费,因为每次只使用一半
- 存活对象较多时,进行较多的对象复制,效率下降
标记-整理
标记过程仍与“标记-清除”算法一样,但是后续不是直接对可回收对象进行清理。而是让存活的对象都向一端移动,最后清楚掉端边界以外的内存。
3. JVM堆的分代
根据相关实验(?....?), 很多对象存活时间都是很短的, 少部分的对象存活时间会稍长一些,根据这些特性吧,JVM将堆化分为新生代
和老年代
:
相关参数配置:
堆的大小: -Xms4G -Xmx4G
新生代和老年代比例:默认1:2,
新生代和survior比例默认: 8:1:1
4. 垃圾回收器的分类
4.1 工作在Young区的垃圾收集器
名称 | 介绍 | 采用的算法 | STW |
---|---|---|---|
Serial | 单线程收集年轻代 | 复制算法 | STW |
ParNew | 多线程收集年轻代 | 复制算法 | STW |
Parallel Scavenge | 吞吐量优先的多线程收集年轻代 | 复制算法 | STW |
4.2 工作在Old区的垃圾收集器
名称 | 介绍 | 采用的算法 | STW |
---|---|---|---|
Serial Old | 单线程收集Old区 | 标记-整理 | STW |
Parallel Old | 多线程收集Old区 | 标记-整理 | STW |
CMS | 并发标记清除收集Old区 | 标记-清除 | 和用户线程并行 |
4.3 YoungGC、MajorGC、MixedGC、FullGC
- YoungGC:发生在年轻代的GC
- MajorGC:发生在老年代的GC ,只有CMS
- MixedGC: 发生在年轻代和老年代的GC, 只有G1
- FullGC指的是:年轻代+老年代+方法区(元空间/永久代) 整个堆上进行垃圾收集
产生FullGC的原因:- 当Eden区分配满的时候,发现之前younggc的平均晋升对象的大小超过当前Old区的剩余空间时,则不会触发Younggc了而会触发FullGC(除CMS外[它只收集old区],其他能收集old区的垃圾收集器都会收集整个堆)。所以不需要额外触发一次YoungGC,如果触发了YoungGC,可以理解为了减轻fullgc的压力吧。
- 当方法区无法分配内存的时候,如Perm Generation、Metaspace满了,会触发FullGC
- 调用
System.gc()
会触发FullGC - 执行
jmap -dump:live,format=b,file=heap.bin <pid>
的时候触发FullGC
4.4 CMS-并发标记清除
一款工作在Old区、且可以和用户线程并发执行的基于标记清除算法的老年代收集器,为了缩短用户线程的暂停时间。
1. CMS的工作步骤:
步骤 | 解释 | STW |
---|---|---|
初始标记 | 标记那些roots对象 | STW |
并发标记 | 从roots对象出发标记存活的对象,用户线程不用暂停 | 非STW |
重新标记 | 由于步骤2用户线程也在执行,需要重新标记那些引用发生变化的部分 | STW |
并发收集 | 多线程并发收集,和用户线程并发执行 | 非STW |
2. CMS的优缺点
- 优点
可以让垃圾收集工作和用户线程并发执行,减少系统stw卡顿时间,对响应时间要求高的系统比较何时。 - 缺点
1.系统吞吐量降低,因为垃圾收集线程占用了cpu的时间
2.产生内存碎片由于CMS是基于“标记-清除”算法实现的收集器,所以会产生内存碎片。 CMS收集器额外又提供了一个可选择的参数
-XX:+UseCMSCompactAtFullCollection
开关参数(默认开启),用于CMS收集器在顶不住要进行FullGC的时候开启内存碎片的合并整理过程,内存整理的过程是无法并发执行的,所以会导致停顿时间比长。 虚拟机又提供了一个参数-XX:CMSFullGCsBeforeCompaction
用于设置执行多少次不压缩的FULL GC后,跟着来一次带压缩的。默认值是0,表示每次进入FULL GC都进行碎片整理
- 产生浮动垃圾
因为并行收集阶段,用户线程也在执行,有可能新产生垃圾对象,这些对象在本次回收阶段无法被回收,只能等下次了
所以CMS收集器需要预留足够的空间给用户线程使用,不然会出现Concurrent Mode Failure
失败。JDK1.6中,CMS收集器启动的阈值是92%,也就是当老年代使用了92%的空间后就会开启CMS收集,不过我们可以通过参数-XX:CMSInitiatingOccupancyFraction=<n>
的值来提高触发百分比,可以降低这个值,给用户线程留多一些空间 - FullGC会很慢
当cms清理不及时的时候,应用线程新产生的对象没有足够的内存可分配时,CMS顶不住了会启动Serial Old进行一次FullGC,这个可就慢了啊! (CMS是不进行FullGC的)
3. CMS的参数
3.1 -XX:+UseConcMarkSweepGC:启用cms
3.2 -XX:ConcGCThreads:并发的GC线程数
3.3 -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理,清理碎片
3.4 -XX:CMSFullGCsBeforeCompaction=0:多少次FullGC之后压缩一次,默认是0,代表每次 FullGC后都会压缩一次
3.5 -XX:CMSInitiatingOccupancyFraction=92: 默认值: 92 当老年代使用达到该比例时会触发CMSGC
3.6 -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(- XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定 值,后续则会自动调整
3.7 -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少 老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在 remark阶段
正式因为上面这些问题,所以没有任意一个jdk版本默认采用CMS收集器,但是它产生的意义是很大的,并发收集!
4.5 G1
4.5.1 G1堆的划分
G1的出现就是来替代CMS的,它能提供更低的延迟。与其他垃圾收集器相比,G1在内存上属于逻辑分代,物理不分代。堆内存不再划分为连续的年轻代和老年代,而是划分为一定数量的堆区域 heap region
,这些region可以是Eden,Surviror,Old区的任何一种。所有的Eden区和Surviror区统称为年轻代,所有的Old区称为老年代。 所以是逻辑上分代, 而且某个区域的类型是可变的(当前时刻是属于Eden区,可能等会就是Old区了)。
有些点需要注意:
- 整个堆被划分为很多个(不超过2048)固定大小的Region,大小可以配置从1M(默认)~32M(
-XX:G1HeapRegionSize
但是不建议手动改) - 所有的Eden、Survivor在逻辑上统称为年轻代,且不需要连续,这样可以使得Resize Region变得容易
- 所有的Old Region在逻辑上统称为老年代,且不需要连续
- 还有一种Region类型是:Humongous regions,它用来存放超过Region 50%大小的对象,他们被存储到专门的连续的Region中。而不是把他们存到Old的Region,因为可以节约Old区的空间,降低发生gc的频率。
- 一个Region在当时是属于Eden,可能过会儿就成为了Old,也可能是Survior
4.5.2 G1的YoungGC
注意到上图的Young区是不需要连续的, 存活的对象将被转移(复制或者移动)到Survivor区或者Old区(age达到了阈值)。
总结YoungGC的收集过程:
- YoungGC只收集Young generation里面的regions
- YoungGC是需要STW的,younggc使用多线程完成。
- Eden区和Surviror区的大小会被计算用于下一次YoungGC,用于优化给定的gc暂停时间,这种方式使得resize region变得容易,可以在需要的时候把他们变大,或者变小。
YoungGC并不是说现有的Eden区放满了就会马上触发,而且G1会计算下现在Eden区回收大 概要多久时间,如果回收时间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增加年轻代 的region,继续给新对象存放,不会马上做Young GC,直到下一次Eden区放满,G1计算回收时 间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC
4.5.3 G1的MixedGC
在old区回收会经历下面几个阶段,一些阶段也会在Young区发生。
阶段 | 描述 |
---|---|
(1) Initial Mark (Stop the World Event) |
会STW. 依托于一次普通的YoungGC. 在Old区标记出roots对象. 逻辑上global concurrent marking与evacuation是相对独立的。但是global concurrent marking之中initial marking要做的事情正好跟young GC要做的事情有重叠——遍历根集合,所以在实现上把它们安排在一起做。 一个young GC可以顺带做initial marking,也可以不做; 一个正常的initial marking(除非是System.gc()+ -XX:+ExplicitGCInvokesConcurrent)总是搭在young GC上做。 |
(2) Root Region Scanning | Scan survivor regions for references into the old generation. This happens while the application continues to run. The phase must be completed before a young GC can occur. |
(3) Concurrent Marking | 在整个堆上查找存活的对象,计算存活率较低的regions. 此时用户线程可以运行. 这个阶段可以被年轻代垃圾收集中断. |
(4) Remark (Stop the World Event) |
重新标记存活的对象. 用一种叫做 snapshot-at-the-beginning (SATB) 的算法(比CMS的算法快). |
(5) Cleanup (Stop the World Event and Concurrent) |
1. 对存活的对象和空regions做计算. (Stop the world) 2. Scrubs the Remembered Sets. (Stop the world) 3. 回收空的Regions. (Concurrent) |
(*) Copying (Stop the World Event) |
移动或者复制存活的对象到未使用的regions里面,此阶段是STW的. 可以在年轻代完成(在log中标记为: [GC pause (young)] ). 也可以在年轻代和老年代完成(在log中标记为: [GC Pause (mixed)] ). |
MixedGC: 收集young gen里的所有region,外加若干选定的old gen region和大对象Region。控制mixed GC开销的手段是选多少个、哪几个old gen region, 根据用户设置的期望暂停时间,来选择那些收益较高的old regions。 所以:MixedGC不是FullGC。
4.5.4 G1的FullGC
如果MixedGC的速度跟不上程序程序分配新对象的速度,导致Old区被填满
会切换到G1之外的Serial Old单线程GC来收集整个堆FullGC.
4.5.5 G1最佳实践配置
- 开启G1
java -Xmx4G -Xms4G -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:InitiatingHeapOccupancyPercent=45
- 完整的G1的配置:
Option and Default Value | Description |
---|---|
-XX:+UseG1GC | Use the Garbage First (G1) Collector |
-XX:MaxGCPauseMillis=n | Sets a target for the maximum GC pause time. This is a soft goal, and the JVM will make its best effort to achieve it. |
-XX:InitiatingHeapOccupancyPercent=n | Percentage of the (entire) heap occupancy to start a concurrent GC cycle. It is used by GCs that trigger a concurrent GC cycle based on the occupancy of the entire heap, not just one of the generations (e.g., G1). A value of 0 denotes 'do constant GC cycles'. The default value is 45. |
-XX:NewRatio=n | Ratio of new/old generation sizes. The default value is 2. |
-XX:SurvivorRatio=n | Ratio of eden/survivor space size. The default value is 8. |
-XX:MaxTenuringThreshold=n | Maximum value for tenuring threshold. The default value is 15. |
-XX:ParallelGCThreads=n | Sets the number of threads used during parallel phases of the garbage collectors. The default value varies with the platform on which the JVM is running. |
-XX:ConcGCThreads=n | Number of threads concurrent garbage collectors will use. The default value varies with the platform on which the JVM is running. |
-XX:G1ReservePercent=n | Sets the amount of heap that is reserved as a false ceiling to reduce the possibility of promotion failure. The default value is 10. |
-XX:G1HeapRegionSize=n | With G1 the Java heap is subdivided into uniformly sized regions. This sets the size of the individual sub-divisions. The default value of this parameter is determined ergonomically based upon heap size. The minimum value is 1Mb and the maximum value is 32Mb. |
5. 记录GC的日志
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintGCDateStamps
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=10
-XX:GCLogFileSize=10m
-Xloggc:/xxx/logs/gc.log \
持续更新~~