【JVM笔记】垃圾回收

【JVM笔记】垃圾回收

  • 如何判断对象可以回收
  • 垃圾回收算法
  • 分代垃圾回收
  • 垃圾回收器
  • 垃圾回收调优

1.如何判断垃圾可以回收

1.1 引用计数

简单但速度很慢的垃圾回收技术
常用来说明垃圾收集的工 作方式,似乎从未被应用于任何一种JVM实现

  • 每个对象含有一个引用计数器,当有引用连接至对象时,引用计数加1。
  • 当引用离开作用域或被置为null,引用计数减1。
  • 当某个对象的引用计数为0时,释放其占用的空间

缺陷:循环引用(对象应该被回收,但引用计数不为0)

1.2 可达性分析算法

  • JVM中的垃圾回收器采用可达性分析来探索所有存活的对象
  • 扫描堆中对象,查找是否能够沿着GC Root对象为起点的引用链找到该对象。找不到,表示可以回收
$ jmap -dump:format=b,live,file=文件名 进程ID

# format=b 二进制格式存储
# live 存储快照前进行一次垃圾回收
# file=文件名 存储文件名

利用Eclipse的MAT工具查看GC Roots

1.3 五种引用

  1. 强引用
    • 只有所有GC Roots对象都不通过强引用引用该对象,它才能被垃圾回收
  2. 软引用
    • 仅有软引用引用该对象时,垃圾回收后,内存仍不足时会再次触发垃圾回收,回收该对象
    • 可以配合引用队列释放软引用自身
  3. 弱引用
    • 仅有弱引用引用该对象时,垃圾回收时,无论内存是否充足,都会回收该对象
    • 可以配合引用队列释放弱引用自身
  4. 虚引用
    • 必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由Referce Handler 线程调用虚引用相关方法释放直接内存
  5. 终结器引用
    • 某对象重写了finalize()方法。垃圾回收时,终结器引用入队(被引用对象暂时没有被回收)。由Finalize Handler 线程通过终结器引用找到被引用对象并调用它的finalize()方法。下一次GC时真正回收这个对象。

2.垃圾回收算法

2.1 标记-清除(Mark Sweep)

  • 速度较快
  • 产生内存碎片

2.2 标记-整理(Mark Compact)

  • 没有内存碎片
  • 速度较慢

2.3 复制(Copy)

  • 没有内存碎片
  • 占用双倍的内存空间

3.分代垃圾回收

  • 大多数对象首先分配在Eden区
  • 新生代空间不足时,触发Minor GC,Eden区和From幸存区存活的对象使用Copy算法复制到To幸存区,存活对象年龄+1,并且交换From和To
  • Minor GC会引发stop the world,暂停其他用户线程,等GC结束,用户线程恢复运行
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
  • 当老年代空间不足,会先尝试触发Minor GC。如果空间仍然不足,将触发Full GC,STW时间更长
  • 大对象直接放入老年代

3.1 相关VM参数

含义参数
堆初始大小-Xms
堆最大大小-Xmx 或 -XX:MaxHeapSize=size
新生代大小-Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size)
幸存区比例(动态)-XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例-XX:SurvivorRatio=ratio
晋升阈值-XX:MaxTenuringThreshold=threshold
晋升详情-XX:+PrintTenuringDistribution
GC详情-XX:+PringGCDetails -verbose:gc
FullGC 前 Minor GC-XX:+ScavengeBeforeFullGC

4.垃圾回收器

  1. 串行
    • 单线程
    • 堆内存较小,适合个人PC
  2. 吞吐量优先
    • 多线程
    • 堆内存较大,多核cpu
    • 单位时间STW时间最短 0.2 0.2 = 0.4
  3. 响应时间优先
    • 多线程
    • 堆内存较大,多核cpu
    • 尽可能让单次STW时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5

4.1 串行

-XX:+UserSerialGC = Serial + SerialOld (新生代复制,老年代标记整理

4.2 吞吐量优先

-XX:+UseParallelGC ~ -XX:+UseParallelOldGC (JDK8默认开启)
-XX:ParallelGCThreads=n 控制线程数
-XX:+UseAdaptiveSizePolicy 动态调整新生代大小,Eden与Survivor的比例,晋升老年代对象大小等参数
-XX:GCTimeRatio=ratio 垃圾收集时间占总时间的比率
-XX:MaxGCPauseMillis=ms 最大垃圾收集停顿时间

4.3 响应时间优先

-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads 并行线程数一般是cpu数,并发线程一般设置为并行线程数的1/4
-XX:CMSInitiationOccupancyFraction=percent 执行垃圾回收时的内存占比
-XX:+CMSScavengBeforeRemark 重新标记前对新生代进行GC

4.4 G1

适用场景

  • 同时注重吞吐量和低延迟,默认的暂停目标200ms
  • 超大堆内存,会将堆划分为多个大小相等的Region
  • 整体上是标记整理算法,两个区域间是复制算法
    相关JVM参数
    -XX:+UseG1GC (JDK9默认启用)
    -XX:G1HeapRegionSize=size
    -XX:MaxGCPauseMillis=time

1) G1垃圾回收阶段

2) Young Collection

  • 会STW
  • 每个区域都可独立作为Eden区,Survivor区和老年代

新对象分配到Eden区

当Eden区占满时触发新生代垃圾回收,将幸存的对象以复制算法放到幸存区

幸存区占满,触发新生代垃圾回收,一部分晋升到老年代。不够年龄的对象拷贝到另一个幸存区

3) Young Collection + CM

  • 在Young GC时会进行GC Root的初始标记
  • 老年代占用堆空间比例达到阈值,进行并发标记(不会STW),由-XX:InitiatingHeapOccupancyPercent=percent(默认45%)参数决定

4) Mixed Collection
会对E、S、O进行全面垃圾回收

  • 最终标记(Remark)会STW
  • 拷贝存回(Evacuation)会STW
    -XX:MaxGCPauseMillis=ms

Eden区幸存对象复制到Survivor区。另外的Survivor区不够年龄的对象也会复制到Survivor区,符合晋升条件的晋升到老年代。
G1垃圾回收器为了达到最大暂停时间的目标,有选择地回收垃圾最多的老年代区域。(如果最大暂停时间目标可以达到,会把所有的老年代进行回收。)

5) Full GC

  • SerialGC
    • 新生代内存不足 - minor gc
    • 老年代内存不足 - full gc
  • ParallelGC
    • 新生代内存不足 - minor gc
    • 老年代内存不足 - full gc
  • CMS
    • 新生代内存不足 - minor gc
    • 老年代内存不足 (并发失败转成串行full gc)
  • G1
    • 新生代内存不足 - minor gc
    • 老年代内存不足(垃圾回收的速度小于垃圾产生的速度,退化为串行full gc)

6) Young Collection 跨代引用

7) Remark

8) JDK 8u20 字符串去重

-XX:+UseStringDeduplication

  • 优点:节省大量内存

  • 缺点:略微占用cpu时间,新生代回收时间略微增加

  • 将所有新分配的字符串放入一个队列

  • 当新生代回收时,G1并发检查是否有字符串重复

  • 如果它们值相同,让它们引用同同一个char[]

  • 与String的intern()不同

    • intern()关注的是字符串对象
    • 字符串去重关注的是char[]
    • JVM内部使用了不同的字符串表

9) JDK 8u40 并发标记类卸载

-XX:+ClassUnloadingWithConcurrentMark 默认启用
所有对象经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类

10) JDK 8u60 回收巨型对象

  • 一个对象大于region的一半时,称为巨型对象
  • G1不会对巨型对象进行拷贝
  • 回收时被优先考虑
  • G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉

11) JDK 9并发标记起始时间的调整

  • 并发标记必须在堆空间占满前完成,否则退化为Full GC
  • JDK 9 之前需要使用-XX:InitiatingHeapOccupancyPercent
  • JDK 9 可以动态调整
    • -XX:InitiatingHeapOccupancyPercent用来设置初始值
    • 进行数据采样并动态调整
    • 总会添加一个安全的空档空间

5.垃圾回收调优

5.1 调优领域

  • 内存
  • 锁竞争
  • cpu占用
  • io

5.2 确定目标

  • 【低延迟】还是【高吞吐量】,选择合适回收器
  • CMS,G1,ZGC
  • ParallelGC

5.3 最快的GC是不发生GC

  • 查看FullGC前后的内存占用,考虑下面几个问题
    • 数据是否太多?
    • 数据表示是否太臃肿?
      • 对象图
      • 对象大小
    • 是否内存泄露?
      • 第三方缓存实现
      • 软引用
      • 弱引用

5.4 新生代调优

  • 新生代特点
    • 所有的new操作的内存分配非常廉价
      • TLAB thread-local allocation buffer
    • 死亡对象的回收代价为零
    • 大部分对象用过即死
    • Minor GC的时间远远低于Full GC

越大越好吗?

-Xmn 设置新生代初始和最大大小

如果太小,会触发多次minor gc
如果太大,只触发full gc,暂停时间很长
建议新生代占整个堆大小25%~50%

新生代能容纳所有【并发量 * (请求-响应)】的数据
幸存区达到能保留【当前活跃对象+需要晋升对象】(如果太小,JVM动态调整晋升阈值,可能会把存活时间小的对象晋升到老年代,只有等到full gc时才能将其回收)
晋升阈值配置得当,让长时间存活对象尽快晋升(-XX:MaxTenuringThreshold=threshold-XX:PrintTenuringDistribution

5.5 老年代调优

CMS为例

  • CMS的老年代内存越大越好
  • 先尝试不做调优,如果没有Full GC,说明系统工作比较正常,否则先尝试调优新生代
  • 观察发生Full GC时老年代内存占用,将老年代内存预设调大1/4 ~ 1/3
    • -XX:CMSInitiationOccupancyFraction=precent
# java  jvm  gc 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×