【JVM】(2)垃圾收集

基本问题

1. 哪部分内存需要回收?

  • 对于线程私有内存(程序计数器、虚拟机栈、本地方法栈)来说,这几个区域的内存分配和回收都具备确定性,每一个栈帧分配多少内存在编译期是可知的,当方法或线程结束时,内存自然就跟随者回收了
  • 对于线程共享内存(堆、方法区)来说,只有处于运行期间,我们才知道程序究竟会创建多少个对象,因此这部分内存回收是动态的,需要垃圾收集器的参与。
  • 因此,我们需要关注堆和方法区的内存回收,以及如何实现垃圾收集器。

2. 垃圾回收器应该回收哪些对象?

垃圾回收器需要回收没有被引用的对象,这种对象不会再被使用,因此需要被回收。

// new出来的Object需要被回收
Object o = new Object();
o = null;

3. 垃圾回收器怎样判断对象没有引用?

两种方法

  • 引用计数法
    给对象添加一个引用计数器,有一个引用,计数器加1,失去一个引用,计数器减1,计数器为0时,代表对象需要被回收。
    这种方法不能解决循环引用问题
public class TestCircularRef {
    static class Obj{
        public Obj ref;
    }
    public static void main(String[] args) {
        Obj A = new Obj();
        Obj B = new Obj();
        A.ref = B;
        B.ref = A;
        A = null;
        B = null;
    }
}

在这里插入图片描述
在A和B置为null后
在这里插入图片描述
可以看到,我们从外部已经无法访问这两个对象了,这两个对象是需要被回收的,但还被引用着,即引用计数器不为0
如果更进一步的实验,如在最后调用System.gc(),我们可以看到这两个对象是被回收了的,因此JVM没有使用引用计数法

  • 可达性分析算法
    这个算法的基本思想就是通过一系列的称为GC Roots的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots没有任何引用链相连的话,则证明此对象是不可用的。
    在这里插入图片描述

4. finalize()方法

  • 在对象需要被回收时,系统只会调用1次对象的finalize()方法,如果对象面临下一次回收,它的finalize()方法不会被再次执行。
  • finaliz e()可以用来回收资源,但使用try -finally 或者其他方式都可以做得更好、 更及时,所以不建议使用这个方法

5. 回收方法区

  • 方法区垃圾收集的“性价比”较低:在Java堆中,依次gc可以回收70%至99%空间,方法区回收效果远远低于这个数值。
  • 主要回收两种内容:废弃的常量和不再使用的类型
  • 判断废弃常量:一个字符串“ java”曾经进入常量池 中,但是当前系统又没有任何一个字符串对象的值是“ java”,那么这个常量需要被回收。
  • 判断无用的类:满足3个条件,所有实例被回收、加载该类的ClassLoader被回收、Class对象没有被引用。

四种引用

1. 强引用

  • Object o = new Object()
  • 只要o不为null,对象就不会被回收。

2. 软引用

  • SoftReference<Object> o = new SoftReference<>(new Object());
  • 软引用表示还有用,但非必须的对象。
  • 在系统即将发生OOM时,对象会被回收。

3. 弱引用

  • WeakReference<Object> o = new WeakReference<>(new Object());
  • 弱引用表示非必须对象
  • 在GC时,对象会被回收。

4. 虚引用

  • PhantomReference<Object> o = new PhantomReference<>(new Object(), null);
  • 为一个对象设置虚 引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
  • 无法通过虚引用来取得一个对象实例,即执行o.get()得到null。

垃圾收集算法

分代收集理论

  • JVM一般会将Java堆划分成新生代老年代两个区域。之所以这样划分,取决于2个假设:
  1. 弱分代假说:大部分对象都是朝生夕灭的,即大部分对象存活时间不会太长
  2. 强分代假说:经过越多次GC而没有被回收的对象就越难以消亡。

因此,根据弱分代假说,GC应当更关注存活的对象,而不是去标记大量将要被回收的对象。根据强分代假说,JVM应当将存活久的对象放在一起,然后用较低的频率回收这个区域。

  • 划分区域后,JVM可以指定每次回收的区域,对新生代的回收称为Minor GC,对老年代的回收称为Major GC(目前只有CMS收集器单独收集老年代),整个堆收集称为Full GC
  • 每次在新生代进行Minor GC后就会有大量对象死去,存活的少量对象会逐步进入老年代。

在进行Minor GC时,对象可能被老年代引用,那么需不需要扫描整个老年代?

答案:不需要。基于跨代引用假说:跨代引用相对于同代引用来说仅占极少数,如果某个新生代对象存在跨代引用,由于老年代对象难以 消亡,该引用会使得新生代对象在收集时同样得以存活,进而在年龄增长之后晋升到老年代中,这时 跨代引用也随即被消除了。
因此我们就不应再为了少量的跨代引用去扫描整个老年代。

1. 标记-清除算法

  • 分为标记和清除两个阶段:首先标记出所有需要回 收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回 收所有未被标记的对象。
    在这里插入图片描述
  • 缺点:
  1. 执行效率不稳定:如果Java堆中包含大量对 象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过 程的执行效率都随对象数量增长而降低.
  2. 造成内存空间碎片化:标记、清除之后会产生大 量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找 到足够的连续内存而不得不提前触发另一次垃圾收集动作.

2. 标记-复制算法

  • 将可用 内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着 的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
    在这里插入图片描述
  • 优点:不会产生内存碎片;
  • 缺点:浪费一半的内存;如果存活对象数量较多,需要大量的复制时间开销。
  • 该算法可应用于新生代内存区域的垃圾收集,因为新生代存活对象数量较少。
  • HotSpot虚拟机采用这种算法收集新生代,将新生代按照8:1:1的比例划分为Eden区SurvivorFrom区SurvivorTo区。可用空间为Eden区+1个Survivor区,另一个Survivor区保持空闲。MinorGC时,将存活对象放入空闲的Survivor区,然后一次性清除其他区域。

3.标记-整理算法

  • 将存活的对象向一端移动,然后清理掉边界以外的内存。
    在这里插入图片描述
  • 优点:内存得到充分使用,不会产生内存碎片
  • 缺点:耗时。移动存活对象并更新
    所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用 程序才能进行。

垃圾收集器

1. Serial收集器

在这里插入图片描述

  • 新生代采用复制算法,老年代采用标记-整理算法
  • 单线程收集器,只有1个GC线程
  • 垃圾收集时,必须暂停所有工作线程
  • 额外内存消耗最小,收集几十M至一两百M的新生代,停顿时间在十几、几十毫秒
  • 对于运行在客户端下的虚拟机是一个很好的选择。

2. ParNew收集器

在这里插入图片描述

  • 是Serial收集器的多线程版本
  • 多核环境下,ParNew收集器比Serial收集器效率更高;单核环境下,Serial收集器更好
  • 通常用于服务器端,ParNew和CMS收集器只能互相搭配使用

3. Parallel Scavenge收集器

  • 是新生代收集器,采用标记-复制算法
  • 多线程收集器,与ParNew类似
  • Parallel Scavenge收集器的目标是控制吞吐量
    在这里插入图片描述
    吞吐量越高,用户线程的工作时间越长,垃圾收集的时间越短
  • 用户可以调整两个参数
  1. -XX:MaxGCPauseMillis
    每次GC停顿的最大时间。这个值设置过小,并不会使得垃圾收集得更快,反而可能会导致GC的次数变得频繁。
  2. -XX:GCTimeRatio。垃圾收集时间占总时间的比例

4. Serial Old收集器

  • 是Serial收集器的老年代版本,单线程收集器,使用标记-整理算法。

5. Parallel Old收集器

  • 是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。

6. CMS收集器

  • CMS(Cocurrent Mark Sweep, 并发标记清除),使用标记-清除算法,主要目标是降低停顿时间
    在这里插入图片描述
  • 四个步骤
  1. 初始标记暂停用户线程,标记一下GC Roots能直接关联到的对象,耗时短
  2. 并发标记:从直接关联对象开始遍历整个对 象图,这个过程耗时较长,但可以与用户线程并发执行,耗时长
  3. 重新标记暂停用户线程,修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的 标记记录,耗时短
  4. 并发清除:清除死亡对象,与用户线程并发执行。
  • 优点:并发收集低停顿
  • 缺点:
  1. 对CPU核心数敏感,由于是并发执行,当CPU核心数过少,GC线程也会占用过多CPU时间,用户线程执行速度降低。
  2. 无法处理浮动垃圾 ,在并发标记和并发清理阶段,用户线程还是会产生垃圾对象,这部分垃圾对象称为浮动垃圾,只能在下一次GC时清理。
  3. 产生内存碎片,标记清理算法会产生内存碎片,可能会导致Full GC来进行碎片合并。

7. Garbage First(G1)收集器

  • 面向服务端
  • 整堆收集,即向堆内存的任何部分进行收集,不局限于该部分处于哪个分代。
  • G1把Java堆划分为多个大小相等的独立区域(Region),每个Region都可以扮演新生代的Eden空间、Survivor空间、老年代空间。还有一类特殊的Humongous区域存大对象(大小超过了Region一半的对象),对于超过了整个Region的对象,将用多个连续的Humongous Region储存。Humongous区域可以被作为老年代看待。
    在这里插入图片描述
  • 每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
  • 四个步骤
    在这里插入图片描述
  1. 初始标记: 与CMS类似,暂停用户线程,标记一下GC Roots能直接关联到的对象,耗时短
  2. 并发标记:与CMS类似,从直接关联对象开始遍历整个对 象图,这个过程耗时较长,但可以与用户线程并发执行,耗时长
  3. 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将 这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,暂停用户线程
  4. 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。
  • 优点:
  1. 可预测的停顿:-XX:M axGCPauseM illis指定停顿时间,可以在不超过期望停顿时间的约束下获得最高的收益,即回收更多的内存
  2. 不会产生碎片,基于复制算法。
  • 缺点:需要额外内存(约10~20%堆容量)

内存分配与回收策略

  1. 对象有限在Eden分配。
    大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。
  2. 大对象直接进入老年代。
    大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。 经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
    -XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。
  3. 长期存活的对象进入老年代
    为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁, 增加到一定年龄则移动到老年代中。
    -XX:MaxTenuringThreshold 用来定义年龄的阈值。
  4. 动态对象年龄判定
    虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄 所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
  5. 空间分配担保
    在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代 最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小 于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。

什么时候出现Full GC?

  1. 调用 System.gc()
    用户建议JVM进行FullGC,但JVM不一定执行
  2. 老年代空间不足
    解决方案:应当尽量不要创建过大的对象以及数组;-Xmn调整新生代大小,让对象在新生代就被回收;-XX:MaxTenuringThreshold调大对象进入老年代的年龄。
  3. 空间分配担保失败
    使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC
  4. Java8之前永久代空间不足
    当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满
  5. Concurrent Mode Failure
    执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时 性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。

垃圾回收器实验

1. 垃圾回收器选择

2. 垃圾回收器参数

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值