目录
CMS垃圾回收器(Concurrent Mark Sweep)
gc - 垃圾回收
什么情况下对象会被视作垃圾
-
引用计数算法
-
对象中添加一个计数器,当有引用时,计数器加一,计数为0的对象就被视作垃圾
-
无法解决循环引用的问题,所以被java启用
-
-
可行性分析算法
-
通过一系列被称为GC Root 的对象作为起点,从节点向下搜索,搜索走过的路径叫做 引用链(reference Chain),当一个对象没有任何引用链,则证明对象不可用
-
当GC Root不可达时,还会判断对象的Finalize方法是否覆盖,未覆盖则直接回收,否则放入F-Queue队列执行Finalize方法,之后再次判断GC Root 是否可达
-
java的各种引用
-
强引用:平时的所有引用都属于强引用,只要沿着gcroot的调用链可以找到,就不会被垃圾回收
-
虚引用:bytebuffer直接内存对象被回收后,java不能回收直接内存,由虚引用管理,虚引用存入引用队列,由固定的线程调度,从而间接的去释放直接内存
-
软引用:只要对象没有被直接的强引用所引用,那么当垃圾回收时,内存不足时就有可能被回收
-
弱引用:当没有强引用时,只要发生了垃圾回收,不管内存够不够,都会回收掉弱引用的对象
-
终结器引用:object父类中有一个finalize(终结)方法,当重写了finalize方法并且没有被强引用时就会被回收,第一次回收时,先将终结器引用对象放入引用队列,再由一个优先级很低的队列定时去查找终结器引用,找到finalize对象调用其finalize方法后,再将其回收
引用队列:各个引用本身也是一个对象,当被引用的对象被回收时,各种引用都会被回收到引用队列,进一步回收资源
JVM分代模型
-
整个JVM堆内存分为 三部分 新生代 - 伊甸园区,幸村区(from,to),老年代区
-
第一次MinorGc回收,将伊甸园和from存活的对象复制到to,清空伊甸园和from,并将其年龄加一
-
第二次MinorGc回收, 伊甸园和to存活的对象复制到from,from和to交换位置,清空伊甸和to,年龄加一
-
当年龄超过一定的阈值,就把对象存放到老年代,老年代不会轻易的gc和清理对象
-
当老年代空间不足时,先尝试触发一次Minor GC,Minor GC后仍不足,则触发FullGc,清理整个新生代与老年代
-
大对象会直接进入老年代(比如很长的字符串,数组),这种情况下不会触发垃圾回收
-
若survivor区中相同年龄的对象总和大于等于survivor空间的一半,则年龄大于等于该年龄的对象直接进入老年代
-
Minor GC前会先判断老年代的最大可用连续空间是否大于Minor GC的空间,如果小于,则查看HandlePromotionFailure的值是否允许失败,如果允许,就检查老年代的最大可用连续空间是否大于历史晋升到老年代的平均大小,大于,则Minor GC,小于,则JVM
-
FullGC和MinorGC
-
JVM的触发条件
-
调用System.gc()
-
老年代空间不足
-
空间分配担保失败
-
jdk1.7之前的永久代空间不足
-
Concurrent Mode Failure 执行CMS GC的过程中同时有对象放入老年代,而此时老年代空间不足(可能是GC过程中垃圾浮动过多造成的暂时性空间不足),便会报Concurrent Mode Failure错误,并触发JVM
-
-
Minor GC触发条件
-
Eden区满
-
-
JVM一定会进行STW,而Minor GC可能会进行STW
-
垃圾回收算法
-
标记清除算法 (mark sweep)
-
标记所有gc root能找到的对象
-
清除释放所有未被标记的对象
-
优点:速度较快,解决了引用计数法中循环引用的问题
-
缺点:
-
产生大量内存碎片,清理出来的内存不连贯。
-
效率差,标记和清除都需要遍历所有对象,并且GC时,需要stw
-
-
-
-
标记压缩算法
-
根据老年代的特点提出的一种标记算法,标记过程与清除算法一致
-
清理阶段不是简单的清理,而是会将存活的对象向一端压缩,然后清理边界外的垃圾,解决内存碎片化的问题
-
优点:解决了内存碎片化的问题
-
缺点:相比标记清除多了一步整理的操作,效率有一定影响
-
-
-
标记-复制算法
-
将内存区化成大小相等的两块区域,每次只用其中的一块,在垃圾回收时,将还存活的对象复制到另一个空间,并清除本来的空间,交换两个内存的角色,完成回收
-
标记复制算法在GC Root过程中就已经完成了复制操作,在复制完成后会直接清理另一边的空间
-
所以标记复制实际上是没有标记阶段的
[STW] stop the world,暂停其他用户线程,等垃圾回收结束才可以继续运行,STW的作用,如果不暂停其他用户线程,那么GC的时候一定会有用户线程产生垃圾,这样永远都清理不干净,并且GC过程中用户线程还可能改变对象的引用,导致漏标和错标
-
垃圾回收器
[^] 不同的垃圾回收器在垃圾回收时有着不同的策略
串行SerialGC
-
单线程
-
堆内存较小
-
-XX: +UseSerialGc = Serial(新生代) + SerialOld(老年代) 开启串行回收器,复制算法
-
在垃圾回收线程结束前,其他的所有用户线程都在等待垃圾回收线程的结束
吞吐量优先ParallelGC
-
多线程
-
堆内存较大的场景,需要多核cpu来支持
-
让单位时间内,STW的总时间最短
-
-XX: +UseParallelGc(新生代) - XX: +UseParllelOldGc(老年代),默认开启,开启一个时默认连带开启另一个
-
当开始时,让所有用户线程找到一个安全点,然后开启多个垃圾回收线程,工作时的线程数量取决于cpu,当垃圾回收的过程中,cpu的内存占用率会飙到100%
-
-XX:ParallelGCThreads=n 设定线程数
-
-XX:GcTimeRatio=ratio 调整吞吐量(公式 1/(1+ratio)) 也就是在工作时间内,垃圾回收的时间不能超过 1 / (1+ratio) % 的时间 ratio默认99,一般设置的时间为19
-
-XX:MaxGcPauseMillis=ms 与GcTimeRatio相反
CMS垃圾回收器(Concurrent Mark Sweep)
-
CMS回收器是一种以获取最短回收停顿时间为目标的收集器,CMS回收器工作时,用户线程和工作线程可以并发
-
CMS使用三色回收-标记清除算法
-
三色回收算法分为四个步骤
-
初始标记(CMS initial mark) - 标记一下GCRoot直接关联的对象,速度很快 需要STW
-
并发标记(CMS concurrent mark) - GC Root Tracing的过程 向下标记找出所有可达对象,这个过程耗时长但是不需要停顿用户线程,可以与垃圾收集线程并发
-
重新标记(CMS remark) - 修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始阶段稍长一些,但远比并发标记的时间短
-
并发清除(CMS concurrent sweep)
-
cms拆分了收集周期,将需要STW的阶段拆分出来,并且保证STW的阶段耗时减少到最短,实现了类似于并发的效果
-
-
CMS适用于堆内存较大的场景,需要多核cpu来支持
-
CMS适用于老年代收集器
-
-
CMS的优点
-
并发收集
-
低停顿
-
-
CMS的缺点
-
CMS对CPU非常敏感
-
CMS无法处理浮动的垃圾
-
CMS基于标记-清除算法,标记清除算法的所有缺点CMS都有
-
当内存碎片过多时会导致并发失败,导致退回为SerialOld,导致响应时间大幅度加长
-
CMS采用 标记-清除算法对算法进行了细粒度的划分,产生大量内存碎片,这对新生代是难以忍受的
-
-
安全点
-
程序在运行时并非在所有的时候都能停下来GC,而是要先找到一个安全点才可以暂停
-
安全点:长时间执行的程序段,比如循环,方法调用,异常跳转
-
安全区域:指在一段代码片段中,引用关系不会发生变化。这个区域中所有地方都可以发生GC
-
漏标:原本不是垃圾,GC过程中用户将其变为垃圾,这次GC就不会回收,它变成浮动垃圾
-
错标:原本是垃圾,但是GC中用户重新指向了他,但是GC将其回收,会产生程序的错误
-
-XX:+UseConcMarkSweepGc(老年代) -XX: +UseParNewGc(新生代) SerialOld
-
-XX: ParallelGCThreads = n -XX:ConcGCThreads = threads
-
-XX:CMSInitiatingOccupancyFraction = percent 执行cms内存回收的内存占比
-
-XX: +CMSScavengeBeforeRemark 开关,重新标记前,对新生代进行一次垃圾回收
-
G1垃圾回收器(Garbage First)
-
一句话概括G1:G1打破了分代模型,将堆内存划分为一个个区域(Region)。这让G1在收集时不再需要进行在全堆范围内进行,区域划分带来了STW时间可观测的收集模型,让用户可以指定操作在多少时间内完成。
-
G1新生代和老年代的隔离由原来的物理隔离变成了概念上的隔离
并且G1最大化的释放了内存的空间
Region的大小是一致的,数值是在1M到32M字节之间的一个2的幂值数,JVM会尽量划分2048个左右、同等大小的Region,这个数字可以调整
-
G1的特点:
-
并行和并发:G1充分应用多核CPU的硬件优势,用多个CPU缩短STW的时间,G1可以用并发代理部分原本需要停顿的操作
-
分代收集
-
空间整合:与标记清除算法不同,G1整体是基于标记-整理算法的收集器,从局部(region)上看则是基于复制算法实现,这两种算法都意味着G1不会产生内存碎片。这种特性有利于程序长时间运行,分配大对象不会因为内存空间不足而产生FullGC
-
可观测的停顿
-
G1打破了以往收集范围固定在新生代或老年代的模式,避免进行在整个堆中进行的垃圾收集
-
-
G1的运作过程
-
初始标记(Inital Marking):标记一下GC Roots能直接关联到的对象,修改TAMS(Nest Top Mark Start)的值,让下阶段用户并发时能在正确的Region中创建新的对象,这阶段需要STW,但是耗时很短
-
并发标记(Concurrent Marking):从GC Roots开始向下搜索可达对象,这阶段耗时长,但是可以与用户进程并发
-
最终标记(Final Marking):修正在并发标记阶段因用户线程导致的变动的那一部分标记记录,虚拟机将这段时间的对象变化记录在Remembered Set Logs里,最终标记阶段需要把Remembered Set Logs 的内容合并到 Remembered Set 中,这阶段需要停顿线程,但是可以并行执行
-
筛选回收(Live Data Counting and Evacuation):首先对各个Region的回收价值和成本进行排序,根据期望的GC时间制定计划。此阶段可以与用户进程并发执行,但是只回收一部分Region,时间是用户可控的,而且停顿用户线程将大量提高效率
全局变量和栈中引用的对象是可以列入根集合的,这样在寻找垃圾时,就可以从根集合出发扫描堆空间。在G1中,引入了一种新的能加入根集合的类型,就是
记忆集
(Remembered Set)。Remembered Sets(也叫RSets)用来跟踪对象引用。G1的很多开源都是源自Remembered Set,例如,它通常约占Heap大小的20%或更高。并且,我们进行对象复制的时候,因为需要扫描和更改Card Table的信息,这个速度影响了复制的速度,进而影响暂停时间。
-
g1和CMS的对比
-
区别:
-
g1默认在jdk9中替代了cms,可以在jdk8中手动指定g1垃圾回收器
-
stw的时间,g1的stw时间是可预测,可控的,cms的stw时间则是以最小为目标
-
cms是老年代的收集器,可以配合serial和parnew收集器一起使用,g1的收集范围是老年代和新生代
-
垃圾时间:cms使用标记-清除算法进行垃圾回收,容易产生内存碎片,g1使用标记-整理算法,进行了空间整合,降低了内存空间碎片
-
g1把内存拆分成多个域(region),逻辑上存在新生代和老年代概念,但是没有严格区分
-
-
什么情况下考虑使用g1
-
实时数据占用超过一半的堆空间
-
对象分配或者晋升的速度变化大
-
希望消除长时间的GC停顿(超过0.5-1秒)
-
同时注重吞吐量和低延迟,默认的暂停目标是200
-
超大堆内存,会将堆划分为多个大小相等的Region
-
整体上是标记+整理算法,两个区域之间是复制算法
-
-XX:+UseG1GC
-
-XX: G1HeapRegionSize=size
-
-XX: MaxGCPauseMillis = time
-
jdk各版本的一些补丁
JDK 8u40并发标记类卸载
-
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
-
-XX:+ClassUnloadingWithConcurrentMark 默认使用
JDK 8u60回收巨型对象
-
一个对象大于region的一半时,称之为巨型对象,g1会对其进行跨域的存放
-
G1不会对巨型对象进行拷贝
-
回收时被优先考虑
-
G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0时的巨型对象就可以在新生代的垃圾回收时处理掉
JDK9 并发标记起始时间的调整
-
并发标记必须在堆空间占满前完成,反正退化为FullGC
-
JDK9 之前需要使用 -XX: InitiatingHeapOccupancyPercent
-
JDK9 可以动态调整
-
-XX: InitiatingHeapOccupancyPercent用来调整初始值
-
进行数据采样并动态调整
-
总会添加一个安全的空挡空间
-
JVM调优
确定目标
-
【低延迟】还是【高吞吐量】,选择合适的垃圾回收器
-
CMS,G1(集成了高吞吐和低延迟),ZGC(JDK12,延迟极低)
-
ParallelGC(高吞吐),Zing(几乎没有stw时间)
-
调优的目标
-
减少GC的次数
-
减少对象内存的使用
-
最快的Gc是最快的Gc
-
数据是不是太多
-
resultSet = statement.executeQuery("select * from 大表")
-
-
数据是否太臃肿?
-
对象图
-
对象大小 最小的object16字节,包装类型的对象头16字节,内容4-24字节
-
-
是否存在内存泄漏
-
不断向静态map中存放map对象(错误方法)
-
正确方法,软引用,弱引用,或第三方缓存软件使用
-
新生代调优(新生代的内存调优操作空间更大)
-
所有的new操作的内存分配非常廉价
-
TLAB thread-local allocation buffer
-
-
死亡对象的回收代价是零
-
大部分对象用过即死
-
Minor GC 的时间远远低于JVM (1-2个重量级)
-
调优方法
-
加大新生代的内存(如果新生代的内存太大,会导致老年代的内存紧张,导致FullGC频率加大),Oracle建议新生代的大小处于heap的25%小于50%之间
-
新生代能容纳所有【并发量*(请求-响应)】的数据
-
幸存区大到能保留【当前活跃对象+需要晋升对象】
-
晋升阈值配置得当,让长时间存活对象尽快晋升
-
-XX:MaxTenuringThreshold = threshold
-
-XX:+PrintTenuringDistribution
-
-
老年代调优
-
以CMS为例
-
CMS的老年代内存越大越好,预留空间,避免浮动垃圾导致的并发失败(回退SerialGC)
-
先尝试不做调优,如果没有JVM,那么说明老年代没有太大问题,否则先尝试调优新生代
-
观察发生JVM时老年代内存占用,将老年代内存预设调大1/4-1/3
-
-XX:CMSInitiatingOccupancyFraction=percent 占了老年代百分之多少后就进行垃圾回收(比较极端,一段为75%或者80%)
-
-
举例:
-
FullGC和MinorGC频繁
-
思路:内存空间不足,导致FullGC和MinorGC频繁
-
解决方法:先试着增大新生代内存,增大晋升阈值和幸存区的空间,将生命周期短的对象留在新生代
-
-
请求高峰出现FullGC,单次暂停时间特别长(CMS)
-
思路:定位问题发生在CMS的哪个阶段
-
解决方法:
-
查看GC日志,查看是CMS的哪个阶段耗时较长
-
发现时间耗时主要在重新标记之前
-
重新标记阶段需要扫描所有新生代和老年代
-
使用参数-XX:+CMSScavengeBeforeRemark , 在重新标记之前先对新生代进行一次清理,减少重新标记阶段的遍历扫描数量
-
-
-
老年代充裕情况下,发生FullGC(1.7)
-
思路:可能原因:并发失败(Serial),空间碎片过多,1.7的永久代空间不足
-
解决方法:
-
查看日志排除前几点,定位到永久代空间问题
-
增大永久代空间的不足
-
-
相关指令
-
system.gc():显式的垃圾回收,是fullGc 影响老年代新生代,很影响性能,可以通过 -XX +DisableExplicitGc禁用
-
-XX: +PrintFlagsFinal -version | findstr "GC"
-
XMN:新时代大小 (newSize初始大小 MaxNewSize最大大小)
-
XX:SurvivorRator=8: 表示堆内存中新生代、老年代和永久代的比为8:1:1
-
XX:PretenureSizeThreshold = 3145728 表示当创建(new)的对象大于3M的时候直接进入老年代
-
XX:MaxTenuringThreshold = 15 当对象的年龄大于15时进入老年代
-
XX: -DisableExplicirGc:表示是否打开GC日志 (+代表是 ,- 代表否)
-
-XX:+PrintTenuringDistribution 打印晋升详情
-
-XX:+ScavengeBerforFullGc FullGc 前是否先执行一次 Minor GC
-
-XX:PretenureSizeThreshold 大于此值的对象直接在老年代分配,避免在Eden和Survior之间大量复制
其他知识点
-
JVM运行在一个线程中,JVM所在的线程发生内存溢出时,会抛出oom异常,但是java程序此时并不会结束
-
最快的gc是不发生gc
-
三色标记
-
黑色:对象被标记了,field也被标记完了
-
灰色:对象被标记了,但是它的field还没有被标记或标记完
-
白色:对象没有被标记到,标记阶段结束后,会被当做垃圾回收掉
-