JVM垃圾收集策略

JVM垃圾收集策略

1.垃圾收集

1.1 垃圾回收策略

  • 对内存要求苛刻的场景:想办法提高对象的回收效率,多回收掉一些对象,腾出更多内存
  • 在CPU使用率高的情况下:降低高并发时垃圾回收的频率,让CPU更多地去执行业务而不是垃圾回收

1.2 垃圾回收区域

堆:回收对象

方法区:回收常量和类(回收频率低)

1.3 对象回收的时机

1.3.1 引用计数算法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时候计数器值为零的对象就是不可能再被使用的。

引用计数法

引用计数法无效情况(循环引用):

引用计数法循环引用1 引用计数法循环引用2

1.3.2 可达性分析算法

基本思路:通过一系列称为"GC Roots"的根对象作为初始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链,如果某个对象到GC Roots间没有任何引用链相连,则证明此对象是不可能再被使用的。

可达性分析算法

GC Roots的对象

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中的类静态属性引用的对象
  3. 方法区中的常量引用的对象
  4. 本地方法栈中JNI引用的对象
  5. 虚拟机内部的引用
  6. 所有被同步锁持有的对象

可达性算法注意点

  • 一个对象即使不可达,也不一定会被回收
对象自我拯救
  • finalize()优先级低,何时会被调用无法确定,因为什么时间发生GC不确定

  • 尽量不使用finalize()来拯救对象,使用try-catch-finally来进行替换

1.4 引用

JDK1.2之前:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的引用。

JDK1.2后:将引用分为强引用、软引用、弱引用、虚引用

1.4.1 强引用

强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

  • Object object = new Object()
  • 只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象

1.4.2 软引用

用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列入进回收范围之中进行第二次回收。

  • SoftReference<String> sr = new SoftReference<>("hello");
  • 用来描述一些有用但非必须的对象
  • 只有在内存不足的时候才会回收
  • 可用于实现缓存

1.4.3 弱引用

用来描述那些非必须对象,但是它的强度比软引用还要更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论内存是否充足,都会回收掉只被弱引用关联的对象。

  • WeakReference<String> sr = new WeakReference<>("hello")
  • 弱引用也是用来描述非必须的对象
  • 无论内存是否充足,都会被垃圾收集器回收

1.4.4 虚引用

虚引用也成为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取消一个对象实例。唯一目的只是能在这个对象被收集器回收的时候收到一个通知

  • ReferenceQueue<String> queue = new ReferenceQueue<>();

    PhantomReference<String> pr = new PhantomReference<>('hello', queue);

  • 不影响对象的生命周期,如果一个对象只有虚引用,那么它就和没有任何引用一样,在任何时候都可以被回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动,必须要和引用队列配合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动

1.5 垃圾收集算法

1.5.1 标记-清除算法(Mark-Sweep)

首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收未被标记的对象。

标记清除算法

缺点:

  • 执行效率不稳定 - 如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的操作,导致标记和清除两个过程的执行效率都随着对象数量增长而降低
  • 内存空间碎片化 - 标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发一次垃圾收集动作

1.5.2 标记-复制算法(Mark-Copy)

它将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块内存用完了,就将还存活的对象复制到另外一块上买了,然后再把已使用过的内存空间一次处理掉。

image-20210209210041097

缺点:

  • 将可用内存缩小为了原来的一半
  • 标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接使用这种算法

1.5.3 标记-整理(Mark-Compact)

标记过程仍然和标记-清除算法一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。

标记-整理算法

缺点:

  • 移动存活对象需要开销,尤其是在老年代这种每次回收都有大量对象存活区域,而且移动老年代区域对象需要全程暂停用户应用程序才能进行。
回收算法优点缺点
标记-清除实现简单1.空间碎片化
2.标记和清除时间随对象数量增长而增加
标记-复制性能好、无碎片1.空间利用率低
标记-整理无碎片1.开销大

1.5.4 分代收集算法

基于两个分代假说之上:

  1. 弱分代假说:绝大多数对象都是朝生夕灭的
  2. 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡

收集器将Java堆划分出两个不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾回收过程的次数)分配到不同的区域之中存储。

回收类型

  • 新生代回收(Minor GC | Young GC)
  • 老年代回收(Major GC)
  • 清理整个堆(Full GC)
  • Major GC ≈ Full GC

根据不同的区域安排与里面存储对象存亡特征相匹配的垃圾回收算法

Java堆

新生代一般采用标记-复制算法,Eden:Survivor=8:1,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要其他内存区域(大多为老年代)进行分配担保,即如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便将通过分配担保机制直接进入老年代。

老年代一般采用标记-清除或标记-整理算法。

重点

  1. 新建的对象不一定分配到Eden区
    • 对象大于-XX:PretenureSizeThreshold,就会直接分配到老年代
    • 新生代空间不足,分配担保直接进入老年代
  2. 对象不一定要达到年龄才进入老年代
    • 动态年龄:如果Survivor空间中所有相同年龄对象大小的总和大于Survivor空间的一半,那么年龄大于等于该年龄的对象就可以直接进入老年代

触发垃圾回收的条件

  1. 新生代(Minor GC)

    • Eden空间不足
  2. 老年代(Full GC)

    • 老年代空间不足(空间实际不足或内存碎片过多)
    • 元空间不足
    • 要晋升到老年代的对象说要占用的空间大于老年代的剩余空间
    • 调用System.gc()

分代的好处

  • 更有效的清除不再需要的对象
  • 提升了垃圾回收的效率

分代收集算法调优的原则

  • 合理设置Survivor区的大小,避免内存浪费
  • 让GC尽量发生在新生代,尽量减少Full GC的发生
相关推荐
©️2020 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页