JVM_3.垃圾回收
侧边栏壁纸
  • 累计撰写 53 篇文章
  • 累计收到 5 条评论

JVM_3.垃圾回收

bbchen
2023-05-18 / 0 评论 / 63 阅读 / 正在检测是否收录...

1.如何判断对象可以回收

1.1 引用计数法

Reference Counting

记录每个对象被引用的次数,当对象被引用时,引用计数器加1,当引用被释放时,引用计数器减1。当引用计数器为0时,说明该对象没有任何引用,可以被回收。

引用计数法的优点是实现简单,回收对象的时间可以很快。但是,引用计数法也存在一些问题,例如无法处理循环引用的情况。如果两个对象相互引用,它们的引用计数器会一直不为0,因此这两个对象将永远不会被回收,从而导致内存泄漏。Java虚拟机(JVM)没有使用引用计数算法来进行垃圾回收。

image-20230513154302452

1.2 可达性分析算法

它的基本思想是通过一系列称为“根集”(Root Set)的对象作为起点,递归地遍历对象图,标记所有可达的对象,然后清除所有不可达的对象。

可以使用Eclipse Memory Analyzer Tool(MAT)来查看分析Java堆转储文件中的内存使用情况和对象引用关系

1.3 五种引用

image-20230513155918338

强引用:强引用是最为常见的引用类型,它是指通过一个普通的Java对象变量引用另一个Java对象。只要强引用存在,垃圾回收器就不会回收被引用的对象。只有所有GC Roots对象都不通过[强引用]引用该对象,该对象才能被垃圾回收

Object obj = new Object();

软引用:软引用是一种比强引用弱一些的引用类型,它可以让对象存活一段时间,直到系统内存不足时才会回收。当JVM需要回收内存时,会先回收所有的弱引用对象,然后再回收所有的软引用对象。软引用可以通过java.lang.ref.SoftReference类来实现。仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象可以配合引用队列来释放软引用自身

Object obj = new Object();
SoftReference<Object> softRef = new SoftReference<>(obj);

弱引用:弱引用是一种比软引用更加弱的引用类型,它可以让对象存活直到没有任何强引用指向它为止,即只有弱引用指向它时垃圾回收器会回收它。弱引用可以通过java.lang.ref.WeakReference类来实现。仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象,可以配合引用队列来释放弱引用自身

Object obj = new Object();
WeakReference<Object> weakRef = new WeakReference<>(obj);

虚引用:虚引用是一种最弱的引用类型,它几乎没有引用价值,主要用于在对象被回收之前,做一些必要的清理工作。当垃圾回收器决定回收一个对象时,如果它存在虚引用,就会在回收对象之前将虚引用加入到一个队列中,程序可以通过这个队列来获取对象被回收的通知。虚引用可以通过java.lang.ref.PhantomReference类来实现。必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放直接内存

Object obj = new Object();
PhantomReference<Object> phantomRef = new PhantomReference<>(obj, referenceQueue);

终结器引用:终结器引用是一种特殊的引用类型,它与对象的终结器相关联。在Java中,对象的终结器是一个方法,当垃圾回收器准备回收对象时,会先调用对象的终结器方法。终结器引用可以通过java.lang.ref.FinalizerReference类来实现,但是由于终结器引用的实现过于复杂,因此在Java 9中已经被弃用。无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器弓|用找到被弓|用对象并调用它的finalize方法,第二次GC时才能回收被引用对象

2.垃圾回收算法

2.1 标记清除算法

清除时,并不需要将垃圾内存置零,而是记录下起始和结束位置,以提供给接下来的程序使用。然而,这样的做法可能会导致内存空间的碎片化,从而影响程序的性能。

image-20230513210034132

2.2 标记整理算法

image-20230513210323497

标记-整理算法则会先标记出所有需要回收的对象,然后将所有存活的对象向一端移动,最后清理掉不需要的对象。这个过程中,整理出来的存活对象会被移动到一端,释放出来的空闲内存空间会被整理成一块连续的内存区域,以便后续程序使用。这样的做法可以减少内存空间的碎片化,提高程序的性能。

2.3 复制算法

复制算法是一种基于空闲列表的垃圾回收算法,它将堆空间分成两块大小相同的区域,每次只使用其中一块,称为“From Space”,另一块则保持空闲,称为“To Space”。当From Space中的对象需要进行垃圾回收时,复制算法会将其中存活的对象复制到To Space中,并将From Space中的所有对象全部清除。这样一来,To Space中就会有一块连续的内存空间,可以供程序使用。

image-20230513210625675

image-20230513210643477

image-20230513210659073

image-20230513210719737

需要注意的是,Java虚拟机的垃圾回收算法是动态选择的,根据垃圾回收器的实现和运行时情况来选择合适的算法。因此,在实际应用中,可能会使用不同的垃圾回收器和不同的垃圾回收算法,以达到最优的垃圾回收效果。

3.分代垃圾回收

image-20230514110324716

JVM中的内存被分为不同的区域,其中包括新生代和老年代。

新生代是JVM内存的一部分,它是用于存放新创建的对象的区域。当创建一个新的对象时,JVM将其分配到新生代中的Eden空间。如果Eden空间没有足够的空间来存放新对象,JVM将会启动垃圾回收机制,将不再被引用的对象清理出内存。如果存活下来的对象足够多,它们将会被移动到新生代中的Survivor空间。经过多次回收后仍然存活下来的对象会被晋升到老年代中。

老年代是JVM内存的另一部分,它是用于存放已经存活了一段时间的对象的区域。当一个对象在新生代经历了多次垃圾回收仍然存活下来,它将被移动到老年代中。老年代是相对稳定的,垃圾回收的频率比新生代要低。

  • 对象首先分配在Eden(伊甸园)区域
  • 新生代空间不足时,触发 minor gc,伊甸园和from存活的对象使用copy复制到to中,存活的对象年龄+1且交换 from to
  • minor gc 会引发 stop the world,即暂停其他用户的线程,待垃圾回收结束,用户线程才恢复运行
  • 当对象寿命超过阈值(最大是15->4bit)时,会晋升到老年代

    • 当需要分配一个大对象时,JVM会先检查老年代的剩余空间是否足够,如果足够,则将该对象直接分配到老年代中。
  • 当老年代空间不足,会先尝试触发minor gc,如果之后空间仍然不足,那么触发 full gc,STW(stop the world)的时间更长

相关VM参数

参数含义
-Xms堆初始大小
-Xmx 或 -XX:MaxHeapSize=size堆最大大小
-Xmn 或 -XX:NewSize=size + -XX:MaxNewSize=size新生代大小
-XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptivePolicy幸存区比例(动态)
-XX:SurvivorRatio=ratio幸存区比例
-XX:MaxTenuringThreshold=threshold晋升阈值
-XX:+PrintTenuringDistribution晋升详细
-XX:+PrintGCDetails -verbose:gcGC详细
-XX:+ScavengeBeforeFullGCFullGC 前 MinorGC

4. 垃圾回收器

  1. 串行
  • 单线程
  • 堆内存较小,适合个人电脑
  1. 吞吐量优先
  • 多线程
  • 堆内存较大,多核CPU
  • 使得单位时间内的STW的时间最短
  1. 响应时间优先
  • 多线程
  • 对此内存较大,多核CPU
  • 尽可能缩短单次STW时间

4.1 串行

-XX:+UseSerialGC=Serial + SerialOld

Serial->复制算法;SerialOld->标记整理算法

image-20230514220126283

4.2 吞吐量优先

-XX:+UseParallelGC ~ -XX:+UseParallerOldGC

-XX:+UseAdapticeSizePolicy(动态调整)

-XX:GCTimeRatio=ratio(1/1+ratio -> 垃圾回收占总时间的比例)

-XX:MaxGCPauseMillis=ms

-XX:ParallelGCThreads=n(线程数)

4.3 响应时间优先

-XX:+UseConCMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld

-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads

-XX:CMSInitiatingOccupancyFraction=percent(老年代堆空间使用率的百分比阈值,默认是68%)

-XX:+CMSScavengeBeforeRemark

image-20230514221206392

4.4 G1

定义: Garbage First

  • 2004论文发布
  • 2009 JDK 6u14体验
  • 2012 JDK 7u4

官方支持

  • 2017JDK9默认

适翻场景

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

相关JVM参数

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

1)G1垃圾回收阶段

image-20230515082258623

3)Young Collection + CM

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

    -XX:InitiatingHeapOccupancyPercent=percent (默认45%)

4)Mixed GC

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

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

-XX:MaxGCPauseMillis=ms

优先收集较大的老年代内存区

image-20230517095518457

5)Full GC

  • SerialGC

    • 新生代内存不足发生的垃圾回收 - minor gc
    • 老生代内存不足发生的垃圾回收 - full gc
  • ParallelGC

    • 新生代内存不足发生的垃圾回收 - minor gc
    • 老生代内存不足发生的垃圾回收 - full gc
  • CMS

    • 新生代内存不足发生的垃圾回收 - minor gc
    • 老生代内存不足发生的垃圾回收

      • 并发收集失败 -> full gc
  • G1

    • 新生代内存不足发生的垃圾回收 - minor gc
    • 老生代内存不足发生的垃圾回收

      • 新生代内存不足 -> Mixed GC
      • 老生代内存不足时 -> full gc

6)Young Collection 跨代引用

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

image-20230517100200699

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

7)Remark

pre-write barrier + satb_mark_queue

8)JDK 8u20 字符串去重

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

-XX: +UseStringDeduplication

String s1 = new String("hello"); // char[]{'h','e','l','1','o'}
String s2 = new String("hello"); // char[]{'h','e','1','l','o'}
  • 将所有新分配的字符串放入一个队列
  • 当新生代回收时,G1并发检查是否有字符串重复
  • 如果它们值-样,让它们引用同一个char[]|
  • 注意,与String. intern()不一样

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

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

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

条件包括:

  1. 该类所有的实例都已被回收。
  2. 该类的 ClassLoader 已经被回收。
  3. 该类没有在其他地方被引用。

-XX:+ClassUnloadingWithConcurrentMark (默认启用)

10)JDK 8u60回收巨型对象

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

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

  • 并发标记必须在堆空间占满前完成,否则退化为FullGC
  • JDK 9之前需要使用-XX: InitiatingHeapOccupancyPercent
  • JDK 9可以动态调整

    • -XX: InitiatingHeap0ccupancyPercent用来设置初始值
    • 进行数据采样并动态调整
    • 总会添加一个安全的空档空间

5.垃圾回收调优

java -XX:+PrintFlagsFinal -version | findstr "GC"

5.1 新生代调优

新生代的特点:

  • 所有new操作的内存分配非常廉价

    • TLAB thread-local allocation buffer
  • 死亡对象的回收代价是0
  • 大部分对象用过即死
  • Minor GC 的时间远远低于Full GC

调优原则:

  • 晋升阈值配置得当,让长时间存活对象尽快晋升

-XX:MaxTenuringThreshold=threshold

-XX:+PrintTenuringDistribution

5.2 老年代调优

以CMS为例

  • CMS的老年代内存越大越好
  • 先尝试不做调优,如果没有Full GC说明老年代OK,否则先尝试调优新生代
  • 观察发生Full GC时老年代的内存占用,将老年代内存预设调大1/4 ~ 1/3

    • -XX:CMSInitiatingOccupancyFraction=percent
0

评论

博主关闭了所有页面的评论