美文网首页
JVM相关 : 2.垃圾回收

JVM相关 : 2.垃圾回收

作者: lilykeke | 来源:发表于2021-09-13 13:25 被阅读0次

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

1.1 引用计数法

算法:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加一;引用失效时,计数器值减一。计数器为0 的对象时不被使用的。

缺点:无法回收两个相互引用的对象

引用计数.png

1.2 可达性分析算法

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

可作为 GC Root 对象:

  • 在虚拟机栈(栈中的本地变量表)中引用的对象。各个线程被调用的方法栈中用到的参数、局部变量等
  • 在方法区中类静态属性引用的对象
  • 在方法区中常量的引用对象,例如 字符串常量池(StringTable)里的引用
  • 在本地方法栈中JNI引用的对象
  • Java虚拟机内部的引用,例如Class对象,系统类加载器等
  • 所有被同步锁(synchronized 关键字)持有的对象

1.3 四种引用

1.强引用

  • 只有所有的 GC Root 都不引用该对象,该对象才能被回收

2.软引用

  • 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象
  • 可以配合引用队列来释放软引用自身 (如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列 ReferenceQueue 中)
public class SoftReferenceTest {

    public static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) {
        List<SoftReference<byte[]>> list = new ArrayList<>();

        //引用队列
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
        for (int i = 0; i < 5; i++) {
            //关联软引用和引用队列 当软引用关联的byte[] 被回收时,软引用自身被加入到引用队列中
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
            list.add(ref);
        }
    }
}

3.弱引用

  • 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否足够,都会回收弱引用对象
  • 可以配合引用队列 ReferenceQueue 来释放弱引用自身

虚引用 和 终结器引用必须配合引用队列

4.虚引用

  • 必须配合引用队列使用,主要配合 ByteBuffer 使用。被引用对象回收时,会将虚引用入队,由ReferenceHandler 线程调用虚引用相关方法释放直接内存(虚引用对象cleaner 实际上调用Unsafe.freeMemory() , 来释放直接内存)

5.终结器引用

  • 无需手动编码。但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer线程(优先级很低)查看引用队列中的终结器引用,找到被引用对象并调用它的 finalize() 方法。第二次 GC时,才能回收被引用对象。

java 编程时不要复写finalize()方法来释放内存,因为效率很低

2. 垃圾回收算法

2.1 标记-清除 Mark-Sweep

算法:首先标记出所有需要回收的对象,标记完成后,统一回收掉所有被标记的对象。
或者,标记出所有存活的对象。清除掉未标记的对象。

优点:

  • 速度比较高
    • 只需要把需要清除的内存起始地址和结束地址记录到一个表中。

缺点:

  • 执行效率不稳定
    • 如果堆中包含大量对象,大部分都是需要被回收的,这时必须进行大量标记和清除动作,导致标记和清除过程的执行效率都随着对象增长而降低。
  • 会造成内存碎片。可能造成以后需要 分配较大对象时无法找到足够的连续内存而提前触发一次垃圾收集
标记-清除 (1).png

2.2 标记-整理 Mark-Compack

算法:标记需要回收的对象,让所有存活的对象都向内存空间的一端移动,然后直接清理掉边界以外的内存。

缺点:

  • 速度慢
    • 移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作

优点:

  • 没有内存碎片
标记-整理.png

2.3 标记-复制 Mark-Copy

算法:将可用内存按容量划分大小相等的两块,每次只使用其中一块。当一块内存用完了,就将存活的对象复制到另一块上面,然后再把已使用的内存空间一次清理掉。

现在的商用java虚拟机大多都优先采用了这种收集算法去回收新生代。
新生代中的对象有98%熬不过第一轮收集,因此并不需要按照1:1的比例来划分新生代的内存空间。

优点:

  • 不会有内存碎片

缺点:

  • 需要占用双倍内存空间
    • 将可用的空间缩小为原来的一半,空间浪费大


      复制.png

3. 分代垃圾回收

分代垃圾回收.png

特点:

  • 对象优先分配在伊甸区

  • 大对象直接晋升老年代(新生代空间不足,老年代空间足够,直接放到老年代,不会引起Minor GC)

  • 新生代空间不足时,首次触发 Minor GC, 伊甸区和 FROM 存活的对象会复制到 TO 中。存活的对象年龄加1,并且交换 FROM 和 TO.

  • Minor GC会引发 stop the world. 暂停其他用户线程,等待垃圾回收完成,用户线程才恢复运行。

  • 当对象年龄超过阈值时,会晋升至老年代(最大寿命是15)

  • 当老年代空间不足,会先尝试触发 Minor GC, 如果之后空间仍不足,那么触发Full GC, 那么 stop the world 的时间更长。

3.1 相关 VM 参数

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

示例:

//-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose gc
public static void main(String[] args) {
     
}

堆信息

Heap
 def new generation   total 9216K, used 1147K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  14% used [0x00000000fec00000, 0x00000000fed1edf0, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,   0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
 Metaspace       used 2871K, capacity 4480K, committed 4480K, reserved 1056768K
  class space    used 317K, capacity 384K, committed 384K, reserved 1048576K

3.2 小结

堆内存空间分为较大的 Eden 和 两块较小的 Survivor 区。每次只使用 Eden 和 survivor 中的一块。

这种情况下标记-复制 算法 减少了内存空间的浪费。

复制算法-现作为主流的YGC算法进行新生代垃圾回收

4. 垃圾回收器

垃圾回收器分类:

1.串行

  • 单线程
  • 适用堆内存较小,个人电脑

2.吞吐量优先

  • 多线程
  • 堆内存较大,需要多核 CPU 支持
  • 单位时间内,stop the world的时间最短 (垃圾回收时间占总时间占比低)

3.响应时间优先

  • 多线程
  • 堆内存较大,需要多核 CPU 支持
  • 垃圾回收时,(单次的)尽可能让stop the world时间最短

4.1 串行

开启串行垃圾回收器:

-XX:+UseSerialGC=Serial + SerialOld

  • Serial:工作在新生代,采用复制算法
  • SerialOld:工作在老年代,采用标记-整理算法

为什么工作线程要在安全点停下来?

因为垃圾回收过程中对象的地址可能发生改变,为了保证安全的使用这些对象地址

串行.png

4.2 吞吐量优先

开启吞吐量优先开关

  • -XX:+UseParallelGC 工作在新生代 复制算法
  • -XX:+UseParallelOldGC 工作在老年代 标记-整理算法

与之有关的一些参数:

-XX:+UseAdaptiveSizePolicy

  • 采用自适应大小调整 ,动态调整新生代eden 和 survivor 的大小 ,包括晋升年龄等

-XX:GCTimeRatio=ratio

  • 用来调整垃圾回收时间和总时间占比 ratio默认值99 1/(1+ratio)

-XX:MaxGCPauseMillis=ms

  • 最大暂停毫秒数 默认200ms

上面两个设置是冲突的

-XX:ParallelGCTheads=n

  • 设置可以开启的垃圾回收线程

垃圾回收线程跟CPU核数相关 ,也可以人为设置最大线程数-XX:ParallelGCTheads=n

吞吐量优先.png

4.3 响应时间优先

开启响应时间优先的开关:CMS

-XX:+UseConcMarkSweepGC~SerialOld -XX:+UseParNewGC

  • -XX:+UseConcMarkSweepGC 工作在老年代 标记-清除算法 并发:垃圾回收线程工作的同时用户线程也能工作。有可能并发失败(由于内存碎片过多),导致退化到 SerialOld垃圾回收器

  • -XX:+UseParNewGC 工作在新生代 复制算法

与之有关的一些参数:

-XX:ParallelGCTheads=n -XX:ConcGCThread=threads

  • 这两个参数,影响初始标记

-XX:CMSInitiatingOccupancyFraction=percent

  • 控制何时来进行CMS垃圾回收时机(例如老年代内存占用达到80% 触发一次内存回收)

-XX:+CMSScavengeBeforeRemark

  • 重新标记之前对新生代做一次垃圾回收

初始标记时(找到那些跟对象),stop the world

并发标记:从根对象出发,顺着引用链标记其他对象

重新标记时,stop the world

响应时间优先.png

4.4 G1 Garbage First

取代了之前的CMS垃圾回收器

适用场景

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

相关JVM参数

-XX:+UseG1GC

-XX:G1HeapRegionSize=size

-XX:MaxGCPauseMillis=ms

4.4.1 G1 垃圾回收阶段

G1垃圾回收阶段.png

这是一个循环的过程

4.4.2 Young Collection

划分成一个个大小相等的区域,每个区域都可以独立作为 Eden 、幸存区、老年代

  • 会stop the world

经历一次young collection 伊甸中幸存的对象拷贝到survivor区

幸存区内存不足时拷贝到老年代

G1_youngCollection.png
4.4.3 Young Collection + CM
  • 在young GC 时会进行 GC Root 的初始标记

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

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

4.4.4 Mixed Collection

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

  • 最终标记(Remark)(标记并发标记漏掉的对象),会STW
  • 拷贝存活(Evacuation),会STW (对老年代来说,并不会回收所有区域。会回收垃圾最多的老年区)

-XX:MaxGCPauseMillis:ms


G1MixedCollection.png
4.4.5 Full GC
  • Serial GC
    • 新生代内存不足发生的垃圾收集 Minor GC
    • 老年代内存不足发生的垃圾收集 Full GC
  • Parallel GC
    • 新生代内存不足发生的垃圾收集 Minor GC
    • 老年代内存不足发生的垃圾收集 Full GC
  • CMS
    • 新生代内存不足发生的垃圾收集 Minor GC
    • 老年代内存不足----当垃圾回收的速度跟不上垃圾产生的速度会并发失败退化为 SerialGC收集器 Full GC
  • G1
  • 新生代内存不足发生的垃圾收集 Minor GC
  • 老年代内存不足(首先:Mixed Collection)----当垃圾回收的速度跟不上垃圾产生的速度会并发失败退化为 SerialGC收集器 Full GC
4.4.6 Young Collection 跨代引用

减少GC Root 的标记时间

  • 卡表与 Remembered Set
  • 在引用变量时通过post-writer barrier + dirty card queue
  • concurrent refinement threads 更新 Remembered Set
G1跨代引用.png
4.4.7 Remark

重新标记阶段

4.4.8 JDK 8u20 字符串去重
  • 优点:节省大量内存
  • 缺点:略微多占用了 CPU 时间, 新生代回收时间略微增加

-XX:+UseStringDeduplication

String s1 = new String("hello");
String s2 = new String("hello");
  • 将所有新分配的字符串放入一个队列
  • 当新生代回收时,G1并发检查是否有字符串重复
  • 如果它们值一样,让他们引用char[]
  • 注意,与String.intern() 不一样
    • String.intern() 关注的是字符串对象
    • 而字符串去重关注的是char[]
    • 在JVM内部,使用了不同的字符串表
4.4.9 JDK 8u40 并发标记类卸载

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

-XX:+ClassUnloadingWithConcurrentMark 默认开启

4.4.10 JDK 8u60 回收巨型对象
  • 一个对象大于region 的一半时称为巨型对象
  • G1 不会对巨型对象进行拷贝
  • 回收时被优先考虑
  • G1会跟踪老年代所有的incoming 引用,这样老年代 incoming 为 0的巨型对象就可以在新生代回收时处理掉
4.4.11 JDK9 并发标记起始时间调整
  • 并发标记必须在堆空间占满前完成,否则退化为Full GC
  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent=percent

相关文章

网友评论

      本文标题:JVM相关 : 2.垃圾回收

      本文链接:https://www.haomeiwen.com/subject/rvycfktx.html