垃圾回收 (Garbage Collection)

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

① 如何判断对象可以回收

引用计数法

image-20240229005147197

可达性分析算法

  • Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象。
  • 扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收。
  • 哪些对象可以作为GC Root?

四种引用

  1. 强引用
  • 只有所有GC Roots对象都不通过“强引用”引用该对象,该对象才能被垃圾回收。
  1. 软引用 (SoftReference)
  • 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时回再次触发垃圾回收,回收软引用对象。
  • 可以配合引用队列来释放软引用自身。
  1. 弱引用 (WeakReference)
  • 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象。
  • 可以配合引用队列来释放弱引用自身。
  1. 虚引用 (PhantomReference)
  • 必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放直接内存。
  1. 终结器引用 (FinalReference)
  • 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时才能回收被引用对象。

② 垃圾回收算法

标记清除

定义:Mark Sweep

  • 速度较快
  • 会造成内存碎片

image-20240229022901577

标记整理

定义:Mark Compact

  • 速度慢
  • 没有内存碎片

image-20240229191357704

复制

定义:Copy

  • 不会有内存碎片
  • 需要占用双倍内存空间

image-20240229191451477

③ 分代垃圾回收

image-20240229191720661

  • 对象首先分配在伊甸园区域
  • 新生代空间不足时,触发minor gc,伊甸园和from存活的对象使用copy复制到to中,存活的对象年龄加1并且交换from to
  • minor gc会引发stop the world,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15 (4 bit)
  • 当老年代空间不足,会先尝试触发minor gc,如果之后空间仍不足,那么触发full gc,STW的时间更长

相关JVM参数

  • 堆初始大小 -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:+PrintGCDetails -verbose:gc
  • FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC

④ 垃圾回收器

  1. 串行
  • 单线程
  • 堆内存较小,适合个人电脑
  1. 吞吐量优先
  • 多线程
  • 堆内存较大,多核CPU
  • 让单位时间内,STW的时间最短 0.2+0.2=0.4,垃圾回收时间占比最低,这样就称吞吐量高
  1. 响应时间优先
  • 多线程
  • 堆内存较大,多核CPU
  • 尽可能让单次STW的时间最短 0.1+0.1+0.1+0.1+0.1=0.5

串行

-XX:+UseSerialGC = Serial + SerialOld

image-20240301020636586

吞吐量优先

-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
-XX:+UseAdaptiveSizePolicy
-XX:GCTimeRatio=ratio
-XX:MaxGCPauseMillis=ms
-XX:ParallelGCThreads=n

image-20240301020712395

响应时间优先

-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark

image-20240301020741849

G1

定义:Garbage First

  • 2004 论文发布
  • 2009 JDK 6u14 体验
  • 2012 JDK 7u4 官方支持
  • 2017 JDK 9 默认

使用场景:

  • 同时注重吞吐量(Throughout)和低延迟(Low lantency),默认的暂停时间是200ms
  • 超大堆内存,会将堆划分为多个大小相等的Region
  • 整体上是标记+整理算法,两个区域之间是复制算法

相关JVM参数:

  • -XX:+UseG1GC
  • -XX:G1HeapRegionSize=size
  • -XX:MaxGCPauseMillis=time

(1) G1 垃圾回收阶段

image-20240301214954032

(2) Young Collection

  • 会STW

image-20240301215542158

image-20240301215631972

image-20240301215658003

(3) Young Collection + CM

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

image-20240301220114883

(4) Mixed Collection

会对E、S、O进行全面垃圾回收

  • 最终标记(Remark)会STW
  • 拷贝存活(Evacuation)会STW

-XX:MaxGCPauseMillis=ms

image-20240301235018545

(5) Full GC

  • SerialGC
    • 新生代内存不足发生的垃圾收集 -minor gc
    • 老年代内存不足发生的垃圾收集 -full gc
  • ParallelGC
    • 新生代内存不足发生的垃圾收集 -minor gc
    • 老年代内存不足发生的垃圾收集 -full gc
  • CMS
    • 新生代内存不足发生的垃圾收集 -minor gc
    • 老年代内存不足
  • G1
    • 新生代内存不足发生的垃圾收集 -minor gc
    • 老年代内存不足

(6) Young Collection 跨代引用

  • 新生代回收的跨代引用(老年代引用新生代)问题

image-20240302000558261

  • 卡表与 Remembered Set
  • 在引用变更时通过 post-write barrier + dirty card queue
  • concurrent refinement threads 更新 Remembered Set

image-20240302000636294

(7) Remark

  • pre-write barrier + satb_mark_queue

image-20240302000706877

(8) JDK 8u20 字符串去重

  • 优点:节省大量内存
  • 缺点:略微多占用了CPU时间,新生代回收时间略微增加

-XX:+UseStringDeduplication

1
2
String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}