堆内存逻辑分区 Eden区,survivor区 old区
除Epsilon ZGC Shenandoah之外的GC都是使用逻辑分代模型
G1是逻辑分代,物理不分代
除此之外不仅逻辑分代,而且物理分代
标记清除会产生碎片 下次分配大对象会内存不够然后触发GC
复制收集算法不适用于对象存活较多情况 存活较多 复制来复制去的对象太多比较慢
整理是将存活的向另一端移动 清理掉边界以外的内存
Young区对象是刚new的时候放young区 所以存活的少很多都不被引用了 所以可以用复制收集算法 因为存活少所以复制来复制去的对象比较少
old区对象都是存活时间比较长的了 所以复制来复制去没必要 直接做个标记清理掉就行
吞吐量:
吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)比如虚拟机总共运行了100分钟,垃圾收集时间用了1分钟,吞吐量=(100-1)/100=99%。
若吞吐量越大,意味着垃圾收集的时间越短,则用户代码可以充分利用CPU资源,尽快完成程序的运算任务。
越大说明GC占用系统资源少,用户代码充分利用
serial 单线程收集 暂停其他线程 复制算法 用在young区
serial old 单线程收集 暂停其他线程 标记整理 用在old区
ParNew 多线程收集 暂停其他线程 复制算法 用在young区 单CPU时比Serial效率差。
Parallel Scavenge 多线程收集 暂停其他线程 复制算法 用在young区 关注吞吐量
Parallel Old 多线程收集 暂停其他线程 整理算法 用在old区 关注吞吐量
CMS
初始标记 可达性标记根对象 根对象少 虽有STW但也很快
并发标记 顺着根找 耗时但和其他线程并发 所以不影响系统执行 但是这个阶段垃圾变动的对象放在重新标记处理
重新标记 虽有STW 但是并发标记产生的改动不多 所以也很快执行完STW比较小
并发清除 并发回收 回收过程产生的浮动垃圾会在下一轮回收
1.CMS
CMS从大的方面来分它分为4个阶段
第一个阶段叫initial mark初始标记
第一个阶段他叫initial mark初始标记,意思就是他先通过我们GCroots我们的根找到根对象,这第一个阶段就完了,但是要注意的是他是STW,不过即便他是STW,因为我们找到的根对象特别少,所以它STW的时间会非常的短。搞定第一阶段之后看第二阶段。
第二个阶段叫concurrent mark并发标记
第二个阶段叫concurrent mark并发标记,并发标记会发生很多次,与此同时我们在病发的时候,我们其他的工作线程也在不断改变的这些引用的它的指向,这时候就非常容易出错,他是最耗时间的阶段,并发执行了,这时候它不产生STW,他就对用户的响应就比较及时,他到底怎么做的?找到根上对象之后,这个阶段叫并发标记也是最耗时的一个阶段,它把最耗时的一个阶段并发执行了,和我们工作线程一块儿执行,这时候不产生STW,对于用户的响应就比较及时,怎么做的呢? 他会从根对象继续往下找,但是找的过程之中很有可能会发生一件事,就是原来那个垃圾被我加了一个引用他就不是垃圾了,我就不会把他给回收掉,如果说是在我并发标记过程之中它变成不是垃圾的,这个时候就会进入remark阶段重新标记,标记那些在我上一个阶段改的过程中改的这些对象,因为应用改的不是特别多,remark也是个STW的过程,不过他的时间也不长,所以他就可以有效的控制暂停时间。
第三个阶段叫remark重新标记
第三个阶段叫remark重新标记,从垃圾变成不是垃圾的,把漏标的重新标记,虽然他是STW但时间也不长
第四个阶段叫concurrent sweep并发的回收
第四个阶段叫concurrent sweep并发回收,把不用的垃圾回收,回收的过程中产生的新垃圾也就是浮动垃圾他会在下一轮进行回收
2.G1
▪ G1是一种服务端应用使用的垃圾收集器,目标是用在多核、大内存的机器上,它在大多数情况下可以实现指定的GC暂停时间,同时还能保持较高的吞吐量。
▪ server-style:主要运行在服务器端的一个垃圾回收器,主要用于Hotspot上面。它的目标是通过并行和并发的手段,能够达到暂停时间特别短,维持不错的吞吐量,据研究他的吞吐量比PS降低了10%-15%,他的停顿时间达到200毫秒。就看你的自己的一个预期了,要是想让你的应用程序不管怎么
样200毫秒之内都有响应用G1。这就是它的由来。
▪ 原来在物理上都是俩块连续的内存一快是新生代一个老年代,每次对他们操作都是在这么连续的一块操作,空间太大了,无论怎么样你的效率都已经提升不了太多了。所以,这时候我们就要改变自己的内存布局,在软件架构设计有两大思想极其重要一个是分而治之,一个是分层,现在分而治之用的越来越多,他把内存分为reguion,从G1开始他的内存变成一小块一小块的,从1M2M最大到32M。每一份reguion在逻辑上依然属于某一个分代,这个分代分为四种,第一种Old区都是放老对象的、Survivor区放存活对象的、Eden区放新生对象的、Humongous区大对象区域,对象特别大有可能会跨两个reguion。所以G1的内存模型已经完完全全的和以前的分代模型不一样了。
特点
▪ 并发收集
▪ 压缩空闲空间不会延长GC的暂停时间;
▪ 更易预测的GC暂停时间;
▪ 适用不需要实现很高的吞吐量的场景
CMS他也是并发收集,他们两个算法在本质上没有区别,但是ZGC和Shenandoah的算法就有本质上的区别了,那两个是颜色指针(Color
pointer)。
三色标记:把对象分为三个不同额颜色,每个不同的颜是标志他到底有没有标记过,还是标记一半了,
还是没有标记。
颜色指针:一个指针猜我们内存中没有进行压缩他是64位,他会拿出来3位,来在 其中做一下标记,来
标记变化过得指针。
每个分区都可能是年轻代也可能是老年代,但是在同一时刻只能属于某个代。年轻代、幸存代、老年代这些概念还存在,成为逻辑上的概念,这样方便复用之前分代框架的逻辑。在物理上不需要连续,则带来了额外的好处----有的分区内垃圾对象特别多,有的分区内垃圾对象很少,G1会优先回收垃圾对象特别多的分区,这样可以花费较少的时间来回收这些分区的垃圾,这也就是G1名字的由来,即首先收集垃圾最多的分区。新生代其实并不是使用与这种算法的,依然是在新生代满了的时候,对整个新生代进行回收----整个新生代中的对象,要么被回收、要么晋升,至于新生代也采取分区机制的原因,则是因为这样跟老年代的策略统一,方便调整代的大小。G1还是一种带压缩的收集器,在回收老年代的分区时,是将存活的对象从一个分区拷贝到另外一个可用分区,这个拷贝的过程就实现了局部的压缩。每个分区的大小从1M到32M不等,但是都是2的幂次方。
第一个特点是G1的内存区域不是固定的E或者O,比如说:在某一个时间段这一块区域是E区,但是我进行一次YGC回收之后我把这个E区擦除了,下一次回收时候我可能就把他当做O区来用了,所以说他比较灵活。
cardtable:主要用于分代模型里头帮助我们垃圾回收比较快
比如说在我的年轻代有好多对象,在我的老年代里也有好多对象,那你怎么确定他活着呢?通过根对象找的话他很有可能到了o的区了,然后o区他可能还指向了年轻代里面,这样的话你要去遍历整个老年代,效率得多低啊,所以他这把内部分为一个一个card,所以这些对象存在不同的card里面,如果有一个card里面对象指向到年轻代,他会把这个标记为Dirty,会有一个bitmap来代表
Card Table 由于做YGC时,需要扫描整个OLD区,效率非常低,所以JVM设计了CardTable, 如果一个OLD区CardTable中有对象指向Y区,就将它设为Dirty,下次扫描时,只需要扫描DirtyCard, 在结构上,Card Table用BitMap来实现。
CSet
• CSet = Collection Set
• 一组可被回收的分区的集合。
在CSet中存活的数据会在GC过程中被移动到另一个可用分区,CSet中的分区可以来自Eden空间、survivor空间、或者老年代。
CSet会占用不到整个堆空间的1%大小。
RSet
RSet = RememberedSet
记录了其他Region中的对象到本Region的引用
RSet的价值在于使得垃圾收集器不需要扫描整个堆找到谁引用了当前分区中的对象,只需要扫描RSet即可。
RSet 与 赋值的效率
▪ 由于RSet 的存在,那么每次给对象赋引用的时候,就得做一些额外的操作,
▪ 指的是在RSet中做一些额外的记录(在GC中被称为写屏障)
▪这个写屏障 不等于 内存屏障
▪No Silver Bullet
阿里的多租户JVM
▪ 每租户单空间
▪ session based GC
新老年代比例
▪ 5% - 60%
– 一般不用手工指定
– 也不要手工指定,因为这是G1预测停顿时间的基准
每个Region有多大,可以通过参数来指定,我们到参数总结的时候会把这个总结在一起。G1还有一个特点,就是它新老年代的一个比例,原来默认是1:2在原来分代模型里头。但是这个比例也是可以指定的,不过你一旦指定之后是不能变的。作为G1来说它的新老年代比例是动态的,一般不用手工指定。因为这个G1预测停顿时间的基准。就是G1会跟踪每一次停顿,每次STW,假如你的Y区用时比较长,他就会把Y区调小一点,我就不用手动调,我是动态调的。
GC何时触发
▪ YGC
Eden空间不足
多线程并行执行
▪ FGC
Old空间不足
System.gc()
如果G1产生FGC,你应该做什么?
▪ 扩内存
▪ 提高CPU性能(回收的快,业务逻辑产生对象的速度固定,垃圾回收越快,内存空间越大)
▪ 降低MixedGC触发的阈值,让MixedGC提早发生(默认是45%)
MixedGC就比如YGC不行了,对象产生特别多到45%,我站堆内存的空间超过45%,默认就启动MixedGC,这个值是可以自己定的,MixedGC相当于一套完整的CMS。
MixedGC的过程回想整个过程就和CMS的一样
▪ 初始标记 STW
▪ 并发标记
▪ 最终标记 STW (重新标记)
▪ 筛选回收 STW (并行)
G1也会有FGC,而且JDK10以前都是串行的,之后才是并行。我们说G1和CMS调优目标其中之一叫做FGC尽量别有。
并发标记算法
难点:在标记对象过程中,对象引用关系正在发生改变
三色标记法
CMS和G1用的标记算法
漏标:不是垃圾,但是扫描没有引用,所以就当垃圾回收了。
什么情况下会产生漏标?黑色增加个引用指向白色,灰色指向白色对象其他引用没了,这样就会产生漏标,黑色不会再扫描,灰色指向黑色丢失,你就遍历不到了。必须两个条件同时具备,黑色指向白色,灰色指向白色的没了。我们CMS的和G1的核心就是在于并发标记的线程和我们工作的线程同时进行嘛,只有这个阶段会发生漏标。
黑色指向白色,灰色指向白色的没了,因为是黑色所以不会再标,没有其他指向白色,所以会漏标。发生在并发标记阶段
产生漏标条件:
为什么G1用SATB?
灰色 → 白色 引用消失时,如果没有黑色指向白色,引用会被push到堆栈
下次扫描时拿到这个引用,由于有RSet的存在,不需要扫描整个堆去查找指向白色的引用,效率比较高.SATB 配合 RSet ,浑然天成
执行命令:java -Xms20M -Xmx20M -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC com.mashibing.jvm.gc.T15_FullGC_Problem01
指定堆大小;用cms垃圾回收;来启动类方法
CMS运行在老年代
[GC (Allocation Failure) [ParNew: 6144K->640K(6144K), 0.0265885 secs] 6585K->2770K(19840K), 0.0268035 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
ParNew:年轻代收集器,对象刚开始放在young区,放不下了开始YGC,用parNew。
6144->640:收集前后的对比 收集了6144-640;
(6144):整个年轻代容量
6585 -> 2770:整个堆的情况
(19840):整个堆大小
old空间不足触发CMS,启动老年代垃圾回收
[GC (CMS Initial Mark) [1 CMS-initial-mark: 8511K(13696K)] 9866K(19840K), 0.0040321 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
8511K/13696K 比例达到启动要求则触发CMS,为了稳妥应该调小,能尽早触发CMS回收,否则占用比例大再回收可能会内存太小,这时有新new大对象young区放不下此时来old区,old不足以盛放新new过大的对象而触发FGC。
//8511 (13696) : 老年代使用(最大)
//9866 (19840) : 整个堆使用(最大)
-XX:CMSInitiatingOccupancyFraction 使用多少比例的老年代后开始CMS收集,默认是68%(近似值),如果频繁发生SerialOld卡顿,应该调小,(频繁CMS回收)
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.018/0.018 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
//这里的时间意义不大,因为是并发执行
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//标记Card为Dirty,也称为Card Marking
[GC (CMS Final Remark) [YG occupancy: 1597 K (6144 K)][Rescan (parallel) , 0.0008396 secs][weak refs processing, 0.0000138 secs][class unloading, 0.0005404 secs][scrub symbol table, 0.0006169 secs][scrub string table, 0.0004903 secs][1 CMS-remark: 8511K(13696K)] 10108K(19840K), 0.0039567 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//STW阶段,YG occupancy:年轻代占用及容量
//[Rescan (parallel):STW下的存活对象标记
//weak refs processing: 弱引用处理
//class unloading: 卸载用不到的class
//scrub symbol(string) table:
//cleaning up symbol and string tables which hold class-level metadata and
//internalized string respectively
//CMS-remark: 8511K(13696K): 阶段过后的老年代占用及容量
//10108K(19840K): 阶段过后的堆占用及容量
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.005/0.005 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
//标记已经完成,进行并发清理
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//重置内部结构,为下次GC做准备
JDK1.8+可以用G1
1.6和1.7 cms
分而治之,分成过个region分区,先YGC有STW。
PS+PO 和PN+CMS指定YOung大小
G1不需要指定固定Y大小,原因是G1有暂停时间,会根据设置的暂停时间来动态调整Y大小以便能实现指定的暂停时间。
启动:
java -Xms20M -Xmx20M -XX:+PrintGCDetails -XX:+UseG1GC com.jvm.gc.T15_FullGC_Problem01 。
YGC-MIXEDGC-FGC
G1也有FGC,调整目标就是不发生FGC,设置MIXEDGC:XX:InitiatingHeapOccupacyPercent
[GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0015790 secs]
//pause 肯定是STW
//young -> 年轻代 Evacuation-> 复制存活对象
//initial-mark 混合回收的阶段,这里是YGC混合老年代回收
[Parallel Time: 1.5 ms, GC Workers: 1] //一个GC线程
[GC Worker Start (ms): 92635.7]
[Ext Root Scanning (ms): 1.1]
[Update RS (ms): 0.0]
[Processed Buffers: 1]
[Scan RS (ms): 0.0]
[Code Root Scanning (ms): 0.0]
[Object Copy (ms): 0.1]
[Termination (ms): 0.0]
[Termination Attempts: 1]
[GC Worker Other (ms): 0.0]
[GC Worker Total (ms): 1.2]
[GC Worker End (ms): 92636.9]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms]
[Other: 0.1 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.0 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 0.0B(1024.0K)->0.0B(1024.0K) Survivors: 0.0B->0.0B Heap: 18.8M(20.0M)->18.8M(20.0M)]
[Times: user=0.00 sys=0.00, real=0.00 secs]
//以下是混合回收其他阶段
[GC concurrent-root-region-scan-start]
[GC concurrent-root-region-scan-end, 0.0000078 secs]
[GC concurrent-mark-start]
//无法evacuation,进行FGC
[Full GC (Allocation Failure) 18M->18M(20M), 0.0719656 secs]
[Eden: 0.0B(1024.0K)->0.0B(1024.0K) Survivors: 0.0B->0.0B Heap: 18.8M(20.0M)->18.8M(20.0M)], [Metaspace: 38
76K->3876K(1056768K)] [Times: user=0.07 sys=0.00, real=0.07 secs]
-Xmn -Xms -Xmx -Xss 年轻代 最小堆 最大堆 栈空间
-XX:+UseTLAB 使用TLAB,默认打开
-XX:+PrintTLAB 打印TLAB的使用情况
-XX:TLABSize 设置TLAB大小
-XX:+DisableExplictGC System.gc()不管用 ,System.gc()是FGC
-XX:+PrintGC 打印GC
-XX:+PrintGCDetails 打印GC详细
-XX:+PrintHeapAtGC 打印GC的堆栈情况
-XX:+PrintGCTimeStamps 打印GC发生的时间戳
-XX:+PrintGCApplicationConcurrentTime (低) 打印应用程序时间
-XX:+PrintGCApplicationStoppedTime (低) 打印暂停时长
-XX:+PrintReferenceGC (重要性低) 记录回收了多少种不同引用类型的引用
-verbose:class 类加载详细过程
-XX:+PrintVMOptions 打印JVM运行的参数
-XX:+PrintFlagsFinal -XX:+PrintFlagsInitial 必须会用 查找参数如何设置
java -XX:+PrintFlagsFinal -version | grep G1 打印出G1特定相关的设置参数
-Xloggc:opt/log/gc.log
-XX:MaxTenuringThreshold GC升代年龄,最大值15
锁自旋次数 -XX:PreBlockSpin
热点代码检测参数-XX:CompileThreshold 逃逸分析 标量替换 … 这些不建议设置
-XX:SurvivorRatio
-XX:PreTenureSizeThreshold 大对象到底多大
-XX:MaxTenuringThreshold GC升代年龄,最大值15
-XX:+ParallelGCThreads 并行收集器的线程数,同样适用于CMS,一般设为和CPU核数相同
-XX:+UseAdaptiveSizePolicy 自动选择各区大小比例
–XX:+UseConcMarkSweepGC
-XX:ParallelCMSThreads CMS线程数量 CMS只运行在old区 所以不能把所有cpu线程占了
-XX:CMSInitiatingOccupancyFraction 使用多少比例的老年代后开始CMS收集,默认是68%(近似值),如果频繁发生SerialOld卡顿,应该调小,(频繁CMS回收)
-XX:+UseCMSCompactAtFullCollection 在FGC时进行压缩 解决CMS碎片化
-XX:CMSFullGCsBeforeCompaction 多少次FGC之后进行压缩 解决CMS碎片化
-XX:+CMSClassUnloadingEnabled 回收metspace永久代
-XX:CMSInitiatingPermOccupancyFraction 达到什么比例时进行Perm回收
GCTimeRatio 设置GC时间占用程序运行时间的百分比
-XX:MaxGCPauseMillis 停顿时间,是一个建议时间,GC会尝试用各种手段达到这个时间,比如减小年轻代
-XX:+UseG1GC
-XX:MaxGCPauseMillis 建议值,G1会尝试调整Young区的块数来达到这个值
-XX:GCPauseIntervalMillis ?GC的间隔时间
-XX:+G1HeapRegionSize 分区大小,建议逐渐增大该值,1 2 4 8 16 32。 随着size增加,垃圾的存活时间更长,GC间隔更长,但每次GC的时间也会更长 ZGC做了改进(动态区块大小)
G1NewSizePercent 新生代最小比例,默认为5%
G1MaxNewSizePercent 新生代最大比例,默认为60%
GCTimeRatio GC时间建议比例,G1会根据这个值调整堆空间
ConcGCThreads 线程数量
InitiatingHeapOccupancyPercent 启动G1的堆空间占用比例