Jusene's Blog

JVM GC and GC tuning(公开课笔记)

字数统计: 4.4k阅读时长: 16 min
2020/07/12 Share

什么是垃圾

  1. 没有任何引用指向的一个对象或多个对象(循环引用)
  2. 申请、释放内存
  • C语言: malloc,free
  • C++: new, delete
  • Java: new, 自动释放
  1. 自动回收与手动回收缺点
  • 自动回收: 编程简单,系统不容易出错
  • 手动回收: 可能出现忘记回收(内存泄漏),多次回收(回收有用的的数据)

    定位垃圾

  1. 引用计数(reference counting):对象上存放它的引用数量,当引用数量为0,认为是垃圾,但这种方法找不到循环引用的垃圾
  2. 根可达(root searching):以静态变量、线程栈中变量、常量池(指向class对象)、jni指针(指向native方法用到的类和对象),作为根引用,顺着根引用能找打的对象不是垃圾,其他都是垃圾, Hotspot就使用这种方式定位垃圾

垃圾回收算法

  1. 标记清除(Mark-Sweep)
  • 两遍扫描:
    • mark:将根对象可以访问到的对象都打上一个标识,表示可达
    • sweep:遍历堆内存,将不可达对象清理
  • 产生内存碎片
  • 存活对象多时,效率高,因为需要清理的少
  1. 拷贝算法(Copying)
  • 将堆内存对半分两个半区,只用其中一个半区来进行对象内存的分配,如果在这个半区不够给新的对象分配了,那么就开始进行垃圾收集,将这个半区中的所有可达对象拷贝到另外一个半区中去
  • 需要移动和复制对象,因此对同一个对象,需修改引用指向
  • 存活对象少,效率高,因此需要复制的对象少
  • 浪费内存空间
  1. 标记压缩(Mark-Compact)
  • 两遍扫描
    • mark
    • compact:移动所有的可达对象到堆内存的同一个区域中,使他们紧凑排列在一起,从而将所有非可达对象释放出来的空闲内存都集中在一起
  • 无碎片,不会产生内存减半
  • 效率低
    • 算法复杂
    • 多线程移动还需要时间进行同步

JVM 分代算法

堆内存逻辑分区(不适用不分代垃圾收集器)

  • new 新生代大量死去,少量存活,采用复制算法
  • old 老年代存活率高,回收较少,采用MC或MS
  • new:old = 1:3

有些垃圾回收器逻辑上并不是如此划分的

  1. 除Epsilon、ZGC、Shenandoah之外的垃圾回收器都是使用逻辑分代模型
  2. G1是逻辑分代,物理不分代

新生代

  1. 新生代 = eden + 2个survivor区 = 8:1:1
  2. YGC(Minor GC): 无法在新生代为对象分配空间时产生,对新生代的内容进行回收,由于新生代内容通常全能被回收掉,因此YGC采用拷贝算法进行回收,而拷贝算法需要额外空间,因此产生了survivor区
  3. 第一次YGC: 大多数的对象会被回收,eden -> s0
  4. 第二次YGC: eden + s0 -> s1
  5. 第三次YGC: eden + s1 -> s0
  6. 年龄足够: 老年代
  • 年龄指对象复制的次数
  • -XX:MaxTenuringThreshold: 设置年龄阈值
  • PS默认15,因为gc的age是4位,最大是15,CMS是6,G1是15
  • 动态年龄判定: Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累计,当累计的某个年龄大小超过了survivor区的一半时,取这个年龄的MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值
  • 内存分配担保: YGC时,JVM会首先检查老年代最大的可用连续空间,是否大于新生代所有对象的总和,如果大于,那么这次YGC是安全的,如果不大于的话,JVM就需要判HandlePromotionFailure是否允许空间分配担保
    • 允许空间分配担保:JVM继续检查老年代最大的可用连续空间是否大于历次晋升到老年代的所有对象总的平均大小,如果大于表示此处YGC相对安全,正常进行一次YGC,如果小于,进行FGC
    • 不允许空间分配担保:FGC

老年代

  1. FGC(Major GC, Full GC): 老年代满了会产生FGC,整个内存都回收,效率低,会暂停所有当前运行的线程,产生stw(stop the world)停顿,进行垃圾清理
  2. FGC本身采用标记压缩算法

GC Tuning(Generation)

尽量减少FGC

  • -Xmn: 指定新生代内存大小
  • -Xms: 指定堆内存大小
  • -Xmx: 指定堆内存的最大值

线上分配、TLAB

  1. 一些小的、无逃逸、线程私有的对象,会使用标量替换,以标量形式将该对象存放于栈内存中,而不是在堆内存中
  • 无逃逸:某个对象的引用不会传递给其他线程或方法拿到
  • 线程私有:不是线程私有,就意味着该对象一定不会被其他线程访问到,也就一定是逃逸对象了
  • 标亮替换:例如user会被替换为一个int和一个string
  1. 栈上分配比堆上分配更快,栈中的内存不需要垃圾回收,因为变量出栈,就没了
  2. TLAB(Thread Local Allocation Buffer)线程本地分配
  • 为防止各线程对堆内存的征用导致效率降低
  • 提前为每个线程分配独有的一块空间,eden区的1%,线程分配空间时,先往这块空间里分配

常见的垃圾回收器

  1. Serial: 串行回收,YGC时,所有线程停止工作STW,启动一个单线程的垃圾回收器开始执行,回收完垃圾后,其他线程重新执行
  • safe point:线程不是马上就能停止,回到一个安全的点,才能停
  • 早期内存小的时候,一个线程做清理很快就清理完,对现在大内存的系统效率太低了,因此用的极少
  1. Parallel Scavenge: 简称PS,并行回收,垃圾清理的线程处于不同cpu中;是并行的,也是所有线程停止,只不过启动多个垃圾回收线程同时进行垃圾回收
  2. ParNew: 与PS功能相同,但可以与CMS配合,而PS不行
  3. Serial Old
  4. Parallel Old
  5. CMS(Concurrent Mark Sweep): 并发回收,所谓并发是指垃圾回收线程可与工作线程同时进行,这些线程可能处于同一个cpu,所以是并发的,降低STW的时间至200ms以内,并发是因为无法忍受stw
  • 诞生了一个里程碑的gc,毛病大,没有一个版本的jdk默认使用cms,jdk1.8 parallel
  • 算法:三色标记 + increment update
  • cms的几个阶段
    • 初始阶段:stw,单线程,找根引用指向的对象,并标记,很快
    • 并发标记:一边标记可达对象,一边产生垃圾,gc的时间的时间都浪费在这,这块与用户线程同时运行
    • 重新标记:stw,多线程,并发标记同时产生新的可达对象,这些新对象需要被重新标记进行标记,也不长
    • 并发清理:不需要移动存活对象,因此可以与工作线程同时进行,清理过程中产生的垃圾,叫浮动垃圾,浮动垃圾在下一个周期进行清理,jdk1.8的回收使用Serial Old
  1. CMS的缺点:
  • 内存碎片化:采用标记清除算法,因此CMS在指定参数次(默认每次)FGC后,下一次FGC前会,会启动单线程Serial Old对内存进行合并整理,浪费时间
  • 浮动垃圾:
    • 由于CMS与工作线程同时执行,因此不能等内存满了才进行cms,老年代使用内存占老年代内存的一定百分比就开始cms,jdk5默认68%,jdk6默认92%
    • 如果预留的这8%无法满足工作线程分配内存的需求,会引发并发失败(垃圾回收与工作线程同时处理失败Concurrent Mode Failure)
    • 此时系统不得不启用后备方案,stw,然后启用Serial Old进行清理
    • 可以降低触发CMS的阈值,-XX:CMSInitiatingOccupancyFraction
  1. G1: 算法:三色标记+SATB
  2. ZGC: 可以与C++回收速度抗衡,算法:coloredpointer+写屏障
  3. Epsilon: 调试jdk用的
  4. 垃圾回收器与内存大小的关系
  • Serial: 几十兆
  • Ps: 上百兆-几个G
  • CMS: 20G
  • G1: 上百G
  • ZGC: 最高4t

GC常见参数

  1. -XX:+UseSerialGC = Serial New(DefNew)+Serial Old
    小型程序,默认情况下不会是这种选项,HotSpot会根据计算及配置和JDK版本自动选择收集器
  2. -XX:+UseParNewGC = ParNew + SerialOld
    这个组合已经很少用(在某个版本中已经废弃)
  3. -XX:+UseConMarkSweepGC = ParNew+CMS+Serial Old
  4. -XX:+UseParallelGC = Parallel Scavenge + Parallel Old(1.8默认)
  5. -XX:+UseParallelOldGC = Parallel Scavenge + Parallel Old
  6. -XX:+UseG1GC = G1
  7. java -XX:+PrintCommandLineFlags -version 通过GC的日志来分辨

GC同用参数

  1. -Xmn -Xms -Xmx -Xss 年轻代 最小堆 最大堆 栈空间
  2. -XX:+UseTLAB 使用TLAB 默认打开
  3. -XX:+PrintTLAB 打印TLAB的使用情况
  4. -XX:TLABSize 设置TLAB大小
  5. -XX:+DisableExplictGC System.gc()不管用(FGC),线上系统都启用
  6. -XX:+PrintGC
  7. -XX:+PrintGCDetails
  8. -XX:+PrintHeapAtGC
  9. -XX:+PrintGCTimeStamps
  10. -XX:+PrintGCApplicationConcurrentTime(低) 打印应用程序时间
  11. -XX:+PrintGCApplicationStoppedTime(低) 打印暂停时长
  12. -XX:+PrintReferenceGC(重要性低) 记录回收了多少种不同引用类型的引用
  13. -verbose:class 类加载详细过程
  14. -XX:+PrintVMOptions 打印JVM启动时参数
  15. -XX:+PrintFlagsFinal -XX:+PrintFlagsInitial 必须会用
  16. -Xloggc:opt/log/gc.log
  17. -XX:MaxTenuringThreshold 升代年龄,最大值15
  18. -XX:PreBlockSpin 锁自旋多久升级为重量级锁(不建议设置)
  19. -XX:CompileThreshold 热点代码检测参数多少次后编译为热点函数,逃逸分析 标量替换(不建议设置)

Parallel常用参数

  1. -XX:SurvivorRatio survivor区比例,默认8,eden占新生代8/10,2个survivor分别占1/10
  2. -XX:PreTenureSizeThreshold 有些大对象会直接分配到old区,大对象到底多大
  3. -XX:MaxTenuringThreshold 设置年龄阈值,默认15
  4. -XX:+ParallelGCThreads 并行收集器的线程数,同样适用于cms,一般设为和cpu核数相同
  5. -XX:+UseAdaptiveSizePolicy 自动选择各区大小比例

CMS 常用参数

  1. -XX:+UseConcMarkSweepGC
  2. -XX:ParallelCMSThreads CMS线程数量,默认cpu核数的一半,因为CMS与应用程序线程并行,要给应用线程留位置
  3. -XX:+UseCMSCompactAtFullCollection 在FGC时进行压缩,因为CMS实际上是不进行压缩的
  4. -XX:CMSInitiatingOccupancyFraction 使用多少比例的老年代开始CMS收集,默认是68%,如果频繁发生SerialOld卡顿应该调小
  5. -XX:+CMSClassUnloadingEnabled 回收永久代/元数据区
  6. -XX:CMSFullGCsBeforeCompaction 多少次FGC之后进行压缩
  7. -XX:CMSInitiatingPermOccupancyFraction 达到什么比例进行Perm回收
  8. GCTimeRatio 设置GC时间占用程序运行时间的百分比
  9. -XX:MaxGCPauseMillis 停顿时间,是一个建议时间,GC会尝试用各种手段达到这个时间,比如减少年轻代

G1常用参数

  1. -XX:+UseG1GC
  2. -XX:MaxGCPauseMillis 建议值,G1会尝试调整young区的块数来达到这个值
  3. -XX:GCPauseIntervalMillis GC的间隔时间
  4. -XX:G1HeapRegionSize 每个分区块大小,建议逐渐增大该值,1 2 4 8 16 32。随着size增加,region越不容易满,垃圾存活的时间更长,GC间隔更长,但每次GC的时间也会更长。ZGC做了改进(动态区块大小,根据之前几次垃圾回收时间,安排region大小)
  5. -XX:G1NewSizePercent 新生代最小比例,默认为5%
  6. -XX:G1MaxNewSizePercent 新生代最大比例,默认为60%
  7. -XX:GCTimeRatio GC时间建议比例,G1会根据这个值调整堆空间
  8. -XX:ConcGCThreads 线程数量
  9. -XX:InitiatingHeapOccupancyPercent 启动G1的堆空间占用比例

G1详解

  1. G1将内存划分为多个(默认2000多个)大小相同的内存块称为Region,每个Region是逻辑连续的一段内存,在被使用时都充当一种角色。
  2. Region可充当old、survivor、eden、humongos,当新建的对象超过Region的50%,直接在新的一个或多个连续Region中分配,并标记为humongos区,某Region的角色不是固定的eden或old,随时会变化
  3. 当垃圾回收时,优先收集存活对象最少的,也就是垃圾最多的Region,所以叫garbage first
  4. 特点:
  • 并发收集:与cms差不多,采用三色标记算法
  • 压缩空闲空间,不会延长GC的暂停时间
  • 每次收集指定的几个Region,因此更容易预测GC暂停时间
  • 适用于不需要实现很高吞吐量的场景,使用硬件弥补吞吐量的问题
  1. 当YGC时,只需要先找到Young区中的所以root对象,在顺着这些root对象继续找,找到的所有对象就是可达对象,剩余对象就可以全部清理,但有一种情况,如果老年代中的根对象A,有成员变量,指向了年轻代中的一个对象B,此时B对象无法被Young区的根对象找到,为了将B也正常标记为可达,就需要遍历所有Old区中的对象,看是否有指向B的引用,为了避免这种情况的产生,引入RSet
  2. RSet: 是一种用于记录从非收集区域收集区域的指针集合的抽象数据结构,Card Table是G1和CMS对Rset的一种实现
  3. Card Table: 将堆空间划分为一系列2的n次幂大小的空间,称为card(卡页),对于HotSpot JVM,卡页大小为512字节,将所有卡页的起始地址左移9位,得到的数值,作为一个byte[]的索引,这个byte[]就是所谓的Card Table(卡表),也就是根据byte[]的索引就能找到指定的卡页,同时该byte[]中的元素表,代表对应的卡页的状态
  4. CSet: Collection Set,表示本次要被收集的Card的集合
  5. CMS处理跨代引用时对Card Table的使用
  • 这个Card Table的索引,包含所有老年代的卡页
  • 老年代中的某个卡页中的对象,指向了年轻代中的对象,就将以该卡页的地址作为索引的byte[]处元素值,设置为0表示dirty
  • YGC时,只需要顺着Young区所有Root对象进行扫描,并加上顺着dirty的card中的引用进行扫描即可找到所有Young区的可达对象
  1. G1处理跨代(Region)引用时对Card Table的使用
  • 在上面介绍的Card Table使用方法的基础上,加入了一个存放于当前Region的Card Table
  • 这个Card Table包含所有指向存放它的Region的其他Region中的卡页
  • 这样当想扫描Regin时,对于跨代的引用,只需要扫描Region中的RSet所代表的卡页中,是否有可达对象指向自身即可
  • RSet会需要堆的10%-20%的内存用来维持垃圾收集器工作
  1. 可达性分析算法:三色标记
  • 难点: 标记对象过程中,对象引用关系正在发生改变
  • 做法:
    • 白色:尚未被标记,最后白色对象被回收
    • 灰色:自身被标记,成员变量指向对象未标记
    • 黑色:自身和成员变量指向的对象均标记完成
  • 漏标
  1. 工作线程将B到D的引用切断,由A中的成员引用D
  2. 此时由于垃圾回收线程已经扫描过A,不会再对其成员引用的D对象进行标记,而扫描B的属性时,又无法通过该属性的引用找到D
  3. 此时D无法正确被标记为可达对象


12. 漏标解决方案

  • 跟踪A指向D的增加(incremental update): CMS引入了另一个数据结构mod union table,这里一个bit对应一个Card,young gc在将Card Table设置为clean的时候会将对应的mod union table置为dirty,重新标记的时候会将Card Table或者mod union table是dirty的Card也作为root去扫描
  • 跟踪B指向D的消失(SATB,snapshot at the beginning):将B到D的引用放入一个特殊的栈中,重新标记时,会继续对栈中的对象进行标记
  1. G1的新生代大小默认占堆的5%-60%,收集过程目标时间默认为200ms,不要手工指定G1的年轻代和老年代的大小,由于G1会造成STW,所以G1会根据收集过程目标时间,自动优化新生代大小,比如本次YGC较慢,下一次就会将年轻代比例调小

  2. G1的GC模式

  • young gc:stw

    • eden region被耗尽无法申请内存时触发
    • 活跃对象会被拷贝到survivor region或者晋升到old region中
  • mixed gc

    • 由-XX:InitiatingHeapOccupancyPercent参数设定,默认45%,即当老年代占整个堆的45%以上时触发mixed gc
    • 过程相当于完整的CMS
      • 初始标记
      • 并发标记
      • 最终标记
      • 筛选回收:与CMS不同
        • 会多了筛选的步骤,优先选择垃圾最多的Region进行回收
        • 会将Region中存活的对象复制到空的Region中,并清理旧的Region空间
        • 由于涉及存货对象的移动,因此无法与用户线程并行执行
  • full gc

    • 对象内存分配速度过快,mixed gc来不及回收,导致老年代被填满时触发
    • G1的full gc,在java10以前为单线程的Serial Old,java10以后才是并行回收,效率非常低
    • 应尽量避免full gc
      • 扩内存
      • 提高cpu性能(加快回收速度)
      • 降低mixed gc的阈值,让mixed gc提早发生
CATALOG
  1. 1. 什么是垃圾
  2. 2. 定位垃圾
  3. 3. 垃圾回收算法
  4. 4. JVM 分代算法
  5. 5. 新生代
  6. 6. 老年代
  7. 7. GC Tuning(Generation)
  8. 8. 线上分配、TLAB
  9. 9. 常见的垃圾回收器
  10. 10. GC常见参数
  11. 11. GC同用参数
  12. 12. Parallel常用参数
  13. 13. CMS 常用参数
  14. 14. G1常用参数
  15. 15. G1详解