(三)GC 垃圾回收

GC(Gabage Collection)垃圾回收

1.概述

程序计数器、虚拟机栈、本地方法栈 3 个区域随线程生灭(因为是线程私有),栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。而 Java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期才知道那些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收期所关注的就是这部分内存。

在C++中,我们知道创建出的对象是需要手动去delete掉的。我们Java程序运行在JVM中,JVM可以帮我们“自动”回收不需要的对象,对我们来说是十分方便的。

首先,JVM回收的是垃圾,垃圾就是我们程序中已经是不需要的了(已经创建了的对象不会再被其他对象引用)。垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”。如何判断呢?

2.判定对象是否存活的算法

2.1 引用计数法

给对象添加一个引用计数器。但是难以解决循环引用问题。

  • 通过判断对象的引用数量来决定对象是否可以被回收
  • 每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1(局部变量的生命周期结束)
  • 任何引用计数为0的对象实例都可以被当作垃圾收集

优点:执行效率高,程序执行受影响较小
缺点:无法检测出循环引用的情况,导致无法回收垃圾,从而引发内存泄漏
在这里插入图片描述
从图中可以看出,如果不下小心直接把 Obj1-reference 和 Obj2-reference 置 null。则在 Java 堆当中的两块内存依然保持着互相引用无法回收。

2.2 可达性分析法

通过一系列的 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用。

在这里插入图片描述
可作为GC Root的对象:

  • 虚拟机栈中引用的对象(栈帧中的本地变量表)
  • 方法区中的常量引用的对象
  • 方法区中的类静态属性引用的对象
  • 本地方法栈中JNI(Native方法)的引用对象
  • 活跃线程的引用对象
  • 如果你细心就会发现,上面的这些对象都是一些“老不死”的对象,都活的很久。
3.判断一个对象生存还是死亡

要真正宣告一个对象死亡,至少需要经历两次标记过程:
3.1 第一次标记

  • 在可达性分析后发现到GC Roots没有任何引用链相连时,被第一次标记,进行一次筛选,此对象是否有必要执行finalize()方法
  • 没有必要执行情况:对象没有覆盖finalize()方法;finalize()方法已被JVM调用过一次;这两种情况可认为对象已死,可以回收;
  • 有必要执行:对有必要执行finalize()方法的对象被放入F-Queue队列中,稍后JVM自动建立、低优先级的Finalizer线程(可能多个线程)中触发此方法;

3.2 第二次标记

  • GC将F-Queue队列中的对象进行第二次小规模标记,finalize()方法是对象逃脱死亡的最后一次机会

    A)若对象在finalize()方法中重新与引用链上任何一个对象建立关联,第二次标记时会将其移出“即将回收”的集合;

    B)若对象没有,也可认为对象已死,可以回收了。

  • finalize()方法执行时间不确定,甚至是否被执行也不确定(Java程序不正常退出),且运行代价高昂,无法保证各对象调用顺序。

4.堆的划分

可以把Java堆分为新生代(Young Generation)和老年代(Old
Generation),又将新生代分为Eden(伊甸)区,From Survivor区,To
Survivor区,它们默认的内存分配比例为8:1:1

在这里插入图片描述
4.1 年轻代 与 老年代

  • Eden(伊甸园)区:

    1.正常情况对象刚被创建出来的时候在eden区,当eden区内存不足时会放到Survivor区,对象很大时会放入老年代。

    2.发生Minor GC后有用对象年龄+1并被放入到Suvivor区

    3.Eden默认占年轻代8的权重,Survivor区两空间分别占1

  • 两个Survivor区(from、to):发生Minor
    GC后有用对象年龄+1,并将存活对象从From使用复制算法到To中,清空From空间,当对象年龄达到一定值后进入老年区(默认15)

  • 老年代(Old Generation):存放多次gc后存活的和一些比较大的对象

4.2 对象如何晋升到老年代

  • 经历一定Minor GC次数后依然存活的对象
  • Survivor区中存放不下的对象
  • 新生成的大对象(-XX:+PretenuerSizeThreshold)

4.2 GC分为3种:Minor GC, Major GC,Full GC

  • Minor GC, Major GC,Full GC的区别
    • Minor GC发生在新生代中,采用复制算法;
    • Major GC发生在老年代中,采用标记—清除算法或标记—整理算法;
    • Full GC包括一次Minor GC和一次Major GC。

Java对象具有“朝生夕灭”的特点(即生命周期很短),所以Minor GC十分频繁,速度快;而Major GC比Minor GC速度一般慢十倍以上。

4.3 GC触发条件 与 回收操作

  • MinorGC回收操作:
    YoungGen区空间不足时,会触发MinorGC,这会把存活的对象转移进入Survivor区。采用复制整理算法进行回收,先扫描出存活的对象,并复制到一块新的完全未使用的空间中,对于新生代,就是在Eden和From Space或To Space之间的复制。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从Eden到Survivor,最后到老年代。

  • MajorGC对象的回收操作:
    由于老年代对象存活时间较长、较稳定,因此它采用标记(Mark)算法来进行回收,先扫描出存活的对象,再进行回收未标记的对象,回收后对空出的空间要么进行合并、要么标记出来便于下次进行分配,总之目的就是要减少内存碎片带来的效率损耗。

  • 触发Full GC的条件:
    1)老年代空间不足
    2)CMS GC时出现promotion failed,concurrent mode failure
    3)Minor GC晋升到老年代的平均大小大于老年代的剩余空间
    4)调用System.gc()(仅是通知,不保证何时执行)
    5)使用RMI来进行PRC或管理的JDK应用,每小时执行1此Full GC

4.4 对象的分配
新建的对象一般被分配到Eden和From区(需要较大连续空间的对象直接被移入老年代),经过一次Minor GC后,Eden和From中存活的对象被移入To区,然后这个To区成为下一次Minor GC的From区,继续扫描(由此可见,From区和To区时不断交替互相成为的,且这两区在任一时刻必有一区为空,所以每次新生代中可用的内存空间为整个新生代空间的90%(80%+10%))。以后对象每熬过一次Minor GC,对象的年龄 +1 岁,当年龄到某个值时(默认15岁),对象晋升到老年代。

注:当移入To区时如果To区空间不够用,就会将To区无法容纳的对象放入老年代,这一机制成为“分配担保”。

5.垃圾回收算法

5.1 标记-清除算法(Mark and Sweep)

标记出需要回收的对象,然后回收所有被标记的对象。这种方法会产生大量不连续的内存碎片。

在这里插入图片描述

  • 标记:从根集合进行扫描,对需要回收的对象进行标记(可达性分析算法)
  • 清除:对堆内存从头到尾进行线性遍历,回收不可达的对象内存(至少标记两次)
  • 缺点:产生大量的碎片,使得无法给较大的对象分配内存。(下图中BE之间的产生的占用两个单位的不连续碎片)

5.2 复制算法(Copying)

把空间分成两块,每次只对其中一块进行 GC。当这块内存使用完时,就将还存活的对象复制到另一块上面。

在这里插入图片描述

  • 优点:
    解决了碎片化问题
    顺序分配内存,简单高效

  • 不足:
    复制耗费时间,且要浪费一半的内存用作空闲面。

  • 适用场景:适用于对象存活率低的场景(年轻代)

因为大多数新生代对象都不会熬过第一次 GC。所以没必要 1 : 1 划分空间。可以分一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另一块 Survivor 上,最后清理 Eden 和 Survivor 空间。大小比例一般是 8 : 1 : 1,每次浪费 10% 的 Survivor 空间。但是这里有一个问题就是如果存活的大于 10% 怎么办?这里采用一种分配担保策略:多出来的对象直接进入老年代。

5.3 标记-整理算法(Compacting)

升级版的标记-清除算法(Mark and Sweep),不同于针对新生代的复制算法,针对老年代的特点,创建该算法。主要是把存活对象移到内存的一端。

在这里插入图片描述

  • 标记:从根集合进行扫描,对存活的对象进行标记

  • 清除:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。

    优点:避免了内存的不连续行,防止出现大量内存碎片,不用浪费一半的内存(对比于复制算法)。

    适用场景:适用于存活率高的场景(老年代)

5.4 标记分代收集算法(Generational Collector)

根据存活对象划分几块内存区,一般是分为新生代和老年代。然后根据各个年代的特点制定相应的回收算法。

6.垃圾收集器

不同虚拟机所提供的垃圾收集器可能会有很大差别,这里讨论的是HotSpot,HotSpot虚拟机垃圾收集器共分为7种:Serial收集器,ParNew收集器,Parallel Scavenge收集器,CMS收集器,Serial Old收集器,Parallel Old收集器,G1收集器。

下图是它们搭配使用关系,有连线说明它们可以相互搭配使用。

在这里插入图片描述
6.1 Serial 收集器

这是一个单线程收集器。意味着它只会使用一个 CPU
或一条收集线程去完成收集工作,并且在进行垃圾回收时必须暂停其它所有的工作线程直到收集结束。

在这里插入图片描述特点:

  • 针对新生代;
  • 采用复制算法;
  • 单线程。 它的“单线程”意义:
    a. 它只会使用一个CPU或一条线程去完成垃圾收集工作
    b. 它进行垃圾收集时必须暂停其他线程的所有工作,直到它收集结束(STW,Stop The World)。

优点:

  • 简单高效。对于单个CPU环境而言,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

应用场景:

  • Serial收集器是虚拟机运行在Client模式下的默认新生代收集器

6.2 ParNew 收集器

可以认为是 Serial 收集器的多线程版本。

在这里插入图片描述特点:

  • (1). 与Serial收集器相同的特点:
    a. 针对新生代;
    b. 采用复制算法;
    c. STW
  • (2). 主要特点:
    多线程。使用多线程进行垃圾收集。

应用场景:

  • 它是Server模式下的虚拟机首选的新生代收集器。但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互的开销。

6.3 Parallel Scavenge 收集器

与ParNew类似,不过 Parallel Scavenge 收集器关注的是CPU的吞吐量,而其他收集器关注的是如何缩短垃圾回收时用户线程的停顿时间(也就是缩短Stop The World的时间)。吞吐量 = 运行代码时间 / (运行代码时间 + 垃圾回收时间)。

在这里插入图片描述特点:

  • (1). 与ParNew收集器相同的特点:
    a. 新生代收集器;
    b. 采用复制算法;
    c. 多线程收集;
  • (2). 主要特点:
    Parallel Scavenge收集器的目标是达一个可控制的吞吐量,而CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间。

应用场景:

  • 1.高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间。
  • 2.当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序

6.4 CMS收集器(Concurrent Mark Sweep)

CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。基于 标记 —— 清除 算法实现。

运作步骤:

  • 初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象
  • 并发标记(CMS concurrent mark):进行 GC Roots Tracing
  • 重新标记(CMS remark):修正并发标记期间的变动部分
  • 并发清除(CMS concurrent sweep)

有3个缺点:

  • 1)对CPU资源非常敏感;
  • 2)无法处理“浮动垃圾”,可能会出现Concurrent Mode Failure(在并发清除时用户线程也在运行,自然就会同时产生新的垃圾,这些垃圾只能留在下次清理,称为“浮动垃圾”),所以并发清楚时需要预留一定内存空间(给“浮动垃圾”),若预留的空间无法满足程序需要,则发生Concurrent Mode Failure,启动后备预案,临时启用Serial Old收集器,导致有一次Major GC);
  • 3)产生大量的内存碎片,这是标记—清除算法导致的。

6.5 Serial Old 收集器

收集器的老年代版本,单线程,使用 标记 —— 整理。

6.6 Parallel Old 收集器

Parallel Old 是 Parallel Scavenge 收集器的老年代版本。多线程,使用 标记 —— 整理

6.7 G1收集器(Garbage-First)

整个过程分为4步:初始标记,并发标记,最终标记,筛选回收。

有以下4个特点:

  • 并行与并发:可以并行执行以减少Stop The World的停顿时间,也可以并发的执行垃圾回收线程与用户线程。
  • 分代收集:虽然保留了分代(新生代与老年代)概念,但G1将堆分为多个大小相等的独立区域(Region)。
  • 空间整合:从整体上看是使用的标记—整理算法,实际上是两个Region之间的“复制”算法。
  • 可预测的停顿:G1可以建立可预测的停顿时间模型是因为G1跟踪各个Region里面的垃圾堆积的价值大小,在后台维护了一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,这也是G1名字中First(价值优先)的含义。
7.回收方法区

在堆中,尤其是在新生代中,一次垃圾回收一般可以回收 70% ~ 95% 的空间,而永久代的垃圾收集效率远低于此。

永久代垃圾回收主要两部分内容:废弃的常量和无用的类。

判断废弃常量:一般是判断没有该常量的引用。

判断无用的类:要以下三个条件都满足

  • 该类所有的实例都已经回收,也就是 Java 堆中不存在该类的任何实例
  • 加载该类的 ClassLoader 已经被回收
  • 该类对应的 java.lang.Class 对象没有任何地方呗引用,无法在任何地方通过反射访问该类的方法
8.Java中的强引用,软引用,弱引用,虚引用的作用

强引用(Strong Reference)

  • 最普遍的引用:Object ob = new Object()
  • 抛出OutOfMemoryError终止程序也不会回收具有强引用的对象
  • 通过将对象设置为null来弱化引用,使其被回收
String str = new String("abc"); //强引用

软引用(Soft Reference)

  • 对象处在有用当非必须的状态
  • 只有当内存空间不足时,GC才会回收该引用的对象的内存
  • 可以用来实现高速缓存
SoftReference softRef = new SoftReference(str); //软引用

弱引用(Weak Reference)

  • 非必须的对象,比软引用更弱一些,也可以用作缓存

  • 只要GC就会被回收

  • 被回收的概率也不大,因为GC线程优先级比较低

  • 适用于引用偶尔被使用且不影响垃圾收集的对象

String str = new String("abc");
WeakReference weakRef = new WeakReference(str);

虚引用(PhantomReference)

  • 不会决定对象的生命周期

  • 任何时候都可能被GC

  • 跟踪对象被GC的活动,起哨兵作用

  • 必须和引用队列ReferenceQueue联合使用

String str = new String("abc");
ReferenceQueue queue = new ReferenceQueue();
PhantomReference ref = new PhantomReference(str, queue);

总结:
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yelvens

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值