JVM笔记(二):GC与线上调优
目录
- 思维导图
- 主要内容
- GC
- GC对象的判定方法
- 垃圾收集算法
- Minor GC/Major GC/Full GC
- 收集器
- Stop The World / OopMap / SafePoint
- 线上排查调优
- 生产环境JVM参数
- 排查工具
- CPU占用过高排查
- 内存泄露问题排查
- 相关面试题
1. 思维导图

2. 主要内容
2.1 GC
2.1.1 GC对象的判定方法
2.1.1.1 引用计数法
- 对象添加一个计数器,每次对象被引用则计数+1,引用失效则计数-1,计数=0即为可回收的。
- 引用计数的问题是很难解决循环引用的问题,例如A.instance = B以及B.instance = A,即使实际上A和B对象都已经可回收了,但是计数仍不为0。
2.1.1.2 可达性分析
- 目前主流的垃圾回收器都用了可达性分析算法,即从可以作为根节点的GC Roots对象开始根据引用关系搜索,标记完后在引用的链上的对象保留,没有形成引用链的对象回收。
- 可以作为 GC Roots 的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈中 JNI 引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 三色标记思想
- 三色标记法是一种垃圾回收法,它可以让 JVM 不发生或仅短时间发生 STW(Stop The World),从而达到清除 JVM 内存垃圾的目的。 三色标记法将对象的颜色分为了黑、灰、白,三种颜色:
- 黑色:该对象已经被标记过了,且该对象下的引用也全部都被标记过了。(程序所需要的对象);
- 灰色:对象已经被垃圾收集器扫描过了,但是对象中还存在没有扫描的引用(GC 需要从此对象中去寻找垃圾);
- 白色:标记初始阶段表示对象没有被垃圾收集器访问过,标记结束阶段表示对象不可达需要回收
- 标记过程:
- 开始阶段,所有对象均为白色
- GC Roots直接引用的对象为灰色
- 当前灰色对象如没有子引用,则标黑;否则将所有子引用标灰,当前对象标黑
- 重复3,直到灰色对象全部标黑后,本轮标记完成,白色对象为不可达对象
- 三色标记的问题:
- 漏标: 本来应该存活的对象标记成了可回收,影响程序正确性
- D(黑) -> E(灰) -> F(白),同时满足下面2个条件可能导致漏标
- 灰色->白色的引用删除(E->F的引用删除)
- 新增黑色->白色的引用(新增一条D->F的引用,因黑色的标记过后不会重新标记)
- 解决漏标的思路就是破坏2个条件达成的其中一个,实现是写屏障,类似AOP在赋值前后进行处理
- G1的策略是原始快照(SATB): 当出现1的情况的时候,将这个删除的引用记录下来,在并发结束之后,再将这些记录过的关系中以灰色对象为根,重新扫描一次
- CMS的策略是增量更新: 当出现2到情况的时候,将新增的引用记录下来,等并发标记扫描结束后,再将这些记录过的引用关系中的黑色对象为根,再进行一次扫描
- 为什么G1用原始快照?CMS用增量更新?
- 成本问题: 增量更新是以黑色对象为根进行扫描,原始快照是以灰色对象为根进行扫描。增量更新从黑色对象开始,结果更准确,搜索的深度更深时间更久。反之原始快照从灰色对象开始,更快但是可能有更多的浮动垃圾。
- 多标: 本来可回收的对象标记成了存活(浮动垃圾)
- D(黑) -> E(灰) -> F(白),D->E的引用在标记后删除,因为E已经是灰色了,所以F存活
- 标记过程中新new的对象默认为黑,标记完后对象不再被引用,但是实际扔存活
- 浮动垃圾不影响程序正确性,下次GC会回收
2.1.2 垃圾收集算法
2.1.2.3 标记-清除算法
- 标记: 标记出所有需要回收的对象
- 清除: 回收所有被标记的对象
- 缺点:
- 执行效率不稳定,如果 Java 堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低。
- 内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可会导致需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2.1.2.4 标记-复制算法
- 解决了标记-清除算法面对大量可回收对象时执行效率低的问题。
- 过程也比较简单:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
- 缺点: 一部分空间没有使用,存在空间的浪费。
- 新生代垃圾收集主要采用这种算法,因为新生代的存活对象比较少,每次复制的只是少量的存活对象。
2.1.2.5 标记-整理算法
- 标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
- 主要用于老年代,移动存活对象是个极为负重的操作,而且这种操作需要 Stop The World 才能进行,只是从整体的吞吐量来考量,老年代使用标记-整理算法更加合适。
2.1.3 各种GC
2.1.3.1 部分收集(Partial GC)
- 目标不是完整收集整个 Java 堆的垃圾收集
- 新生代收集(Minor GC/Young GC): 目标只是新生代的垃圾收集。
- 触发条件: 新创建的对象优先在新生代 Eden 区进行分配,如果 Eden 区没有足够的空间时,就会触发 Young GC。
- Minor GC 频繁: 通常情况下,由于新生代空间较小,Eden 区很快被填满,就会导致频繁
Minor GC,因此可以通过增大新生代空间 -Xmn 来降低 Minor GC 的频率。
- 老年代收集(Major GC/Old GC): 目标只是老年代的垃圾收集,目前只有CMS会有。
- 混合收集(Mixed GC): 目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1会有。
2.1.3.2 整堆收集(Full GC)
- 收集整个 Java 堆和方法区的垃圾收集。
- 触发条件,主要是大对象/长生命周期对象/内存泄露/JVM参数不合理:
- Young GC 之前检查老年代: 在要进行 Young GC 的时候,发现老年代可用的连续内存空间 < 新生代历次Young GC后升入老年代的对象总和的平均大小 ,说明本次 Young GC 后可能升入老年代的对象大小,可能超过了老年代当前可用内存空间,那就会触发 Full GC。
- Young GC 之后老年代空间不足: 执行 Young GC 之后有一批对象需要放入老年代,此时老年代就是没有足够的内存空间存放这些对象了,此时必须立即触发一次 Full GC。
- 老年代空间不足,老年代内存使用率过高,达到一定比例,也会触发 Full GC。
- 空间分配担保失败( Promotion Failure),新生代的 To 区放不下从 Eden 和 From 拷贝过来对 象,或者新生代对象 GC 年龄到达阈值需要晋升这两种情况,老年代如果放不下的话都会触发 Full GC。
- 方法区内存空间不足: 如果方法区由永久代实现,永久代空间不足 Full GC。
- System.gc()等命令触发: System.gc()、jmap -dump 等命令会触发 full gc。
2.1.3 收集器
2.1.3.1 CMS
- 初始标记(CMS initial mark):单线程运行,需要 Stop The World,标记 GC Roots 能直达的对象。
- 并发标记((CMS concurrent mark):无停顿,和用户线程同时运行,从 GC Roots 直达对象开始遍历整个对象图。
- 重新标记(CMS remark):多线程运行,需要 Stop The World,标记并发标记阶段产生对象。
- 并发清除(CMS concurrent sweep):无停顿,和用户线程同时运行,清理掉标记阶段标记的死亡的对象
- 标记-清除算法内存碎片问题
- 开启压缩算法:
-XX:+UseCMSCompactAtFullCollection 开启 CMS 的压缩
-XX:CMSFullGCsBeforeCompaction 默认为 0,指经过多少次 CMS Full GC 才进行压缩
- 提早开始老年代GC, 降低触发 CMS GC 的阈值,让浮动垃圾不那么容易占满老年代
-XX:CMSInitiatingOccupancyFraction 92% 可以降低这个值,让老年代占用率达到该值就进行 CMS GC
2.1.3.2 G1
- G1 把连续的 Java 堆划分为多个大小相等的独立区域(Region),每一个 Region 都可以根据需要, 扮演新生代的 Eden 空间、Survivor 空间,或者老年代空间。收集器能够对扮演不同角色的 Region 采用不同的策略去处理。这样就避免了收集整个堆,而是按照若干个 Region 集进行收集,同时维护一个优先级列表,跟踪各个Region 回收的“价值,优先收集价值高的 Region。
- 垃圾收集过程
- 初始标记(initial mark),标记了从 GC Root 开始直接关联可达的对象。需要STW(Stop the
World)执行。
- 并发标记(concurrent marking),和用户线程并发执行,从 GC Root 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象。
- 最终标记(Remark),STW,标记再并发标记过程中产生的垃圾。
- 筛选回收(Live Data Counting And Evacuation),制定回收计划,选择多个 Region 构成回收集,把回收集中 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。需要 STW。
2.1.3.3 G1对比CMS的优势
- G1可以分别制定策略去收集新生代和老年代空间的待回收对象,CMS针对老年代,功能更完善
- CMS的内存碎片问题G1从原理上进行了优化,提升了堆内存利用率
- 因为原始快照机制比增量更新更快,G1的Stop The World的时间更短
- G1可以配置目标暂停时间,更有利于期望低延时的应用
2.1.4 Stop The World / OopMap / SafePoint
- 进行垃圾回收的过程中,会涉及对象的移动。为了保证对象引用更新的正确性,必须暂停所有的用户线程,像这样的停顿,虚拟机设计者形象描述为 Stop The World 。
- 在 HotSpot 中,有个数据结构(映射表)称为 OopMap 。一旦类加载动作完成的时候,HotSpot 就 会把对象内什么偏移量上是什么类型的数据计算出来,记录到 OopMap。在即时编译过程中,也会在特定的位置 生成 OopMap,记录下栈上和寄存器里哪些位置是引用。
- 用户程序执行时并非在代码指令流的任意位置都能够在停顿下来 开始垃圾收集,而是必须是执行到安全点才能够暂停
- 循环的末尾(非 counted 循环)
- 方法临返回前 / 调用方法的 call 指令后
- 可能抛异常的位置
2.2 线上排查调优
2.2.1 生产环境JVM参数
2.2.1.1 通用部分
- Xms2687m
- Xmx2687m 机器内存70% 预分配避免后续再次分配导致用户线程停顿
- Xss 255k 栈内存的大小
- XX: NewSize=2048m
- XX: MaxNewSize=2048m
- XX: MetaspaceSize=512m
- XX: MaxMetaspaceSize=512m
- XX:ParrelGCThreads=2
- XX:+PrintGCDetails
2.2.1.2 CMS相关
- 目标是压缩碎片,开启老年代空间达到一定比例即触发cms
- XX: UseParNewGC
- XX: UseConcMarkSweepGC
- XX:+UseCMSCompactAtFullCollection ## 在FULL GC的时候对年老代的压缩,Full GC后会进行内存碎片整理,过程无法并发,空间碎片问题没有了,但提顿时间不得不变长了
- XX:CMSFullGCsBeforeCompaction=3 ## 多少次Full GC 后压缩old generation一次
- XX:+UseCMSInitiatingOccupancyOnly ##配置则指定在老年代达到下面的值才回收,不配置则第一次达到下面的值即回收,后面jvm自己决策
- XX: CMSInitiatingOccupancyFraction=75 ##老年代空间使用达到75%进行cms回收
- XX:+CMSParallelRemarkEnabled ## 重新标记(CMS remark)阶段启用并行标记,降低标记停顿
G1相关参数
- 目标更频繁的gc,更低的延时
- XX:G1HeapRegionSize=16M C端业务更多的是小对象,region的占用不会很大
- XX:MaxGCPauseMillis=50 更低的目标延时
- XX:G1MixedGCLiveThresholdPercent=85 实验性参数,需要配合 -XX:+UnlockExperimentalVMOptions使用
2.2.2 排查工具
- jstat -gcutil -h20 pid 1000 #查看堆内存各区域的使用率以及GC情况
- jmap -histo pid | head -n20 #查看堆内存中的存活对象,并按空间排序
- jmap -dump:format=b,file=heap pid #dump堆内存文件
2.2.3 CPU占用过高排查
- top 命令找到Java的pid
- ps -mp pid -o %CPU, THREAD, TID, TIME 找到线程id
- printf "%x\n" tid 线程id转16进制java 线程id
- jstack pid | grep tid >> err.txt
2.2.4 内存泄露问题排查
- 分析: 内存飚高如果是发生在 java 进程上,一般是因为创建了大量对象所导致,持续飚高说明垃圾 回收跟不上对象创建的速度,或者内存泄露导致对象无法回收。
- 观察垃圾回收的情况
- jstat -gc PID 1000 查看 GC 次数,时间等信息,每隔一秒打印一次。
- jmap -histo PID | head -20 查看堆内存占用空间最大的前 20 个对象类型,可初步查看是哪个对象占用了内存。
- 如果每次 GC 次数频繁,而且每次回收的内存空间也正常,那说明是因为对象创建速度快导致内存一 直占用很高;如果每次回收的内存非常少,那么很可能是因为内存泄露导致内存一直无法被回收。
- 导出堆内存文件快照
- jmap -dump:live,format=b,file=/home/myheapdump.hprof PID dump 堆内存信息到文件。
- 使用 Eclipse MAT 对 dump 文件进行离线分析,找到占用内存高的对象,再找到创建该对象的业务 代码位置,从代码和业务场景中定位具体问题。
3. 相关面试题
- GC 判定方法
- 可以做gc roots的对象有几种
- SafePoint 是什么
- GC的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用 在什么地方,如果让你优化收集方法,有什么思路?
- GC 收集器有哪些?CMS 收集器与 G1 收集器的特点?G1相对于CMS优化了哪些
- Minor GC/Young GC、Major GC/Old GC、Mixed GC、Full GC什么意思
- Minor GC/Young GC full gc 分别在什么时候发生 频繁 minor gc 怎么办 频繁 Full GC 怎么办
- CMS 的工作过程 三色标记
- G1的工作过程
- 线上JVM配了哪些参数
- 线上服务 CPU 占用过高怎么排查 实战过吗
- 有没有处理过内存泄漏问题?是如何定位的
- 几种常用的内存调试工具:jmap、jstack、jconsole、jhat