脑洞世界

Java垃圾回收机制

垃圾回收的意义

Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再需要考虑内存管理。

由于有了垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。

垃圾回收算法

垃圾回收算法需要做的基本事情:

  • 发现无用对象(标记算法
  • 回收被无用对象占用的内存空间,使该空间可被程序再次使用(回收算法
在这里插入图片描述

1.标记算法

1.1 引用计数法(Reference Counting Collector

引用计数是垃圾收集器中的早期策略。
此方法中,堆中的每个对象都会添加一个引用计数器。每当一个地方引用这个对象时,计数器值 +1;当引用失效时,计数器值 -1。任何时刻计数值为 0 的对象就是不可能再被使用的。

优点: 实现简单,判定效率高。
缺点: 很难解决对象循环引用问题。

public class Main {
    public static void main(String[] args) {
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();

        object1.object = object2;
        object2.object = object1;

        object1 = null;
        object2 = null;
    }
}

最后面两句将 object1object2 赋值为 null,也就是说 object1object2 指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数器都不为 0,那么垃圾收集器就永远不会回收它们。

1.2 可达性分析算法(根搜索算法)

该算法是从离散数学中的图论引入的,程序将所有引用关系看成是一张图,通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来讲,就是从GC Roots到这个对象不可达)时,则说明该对象是不可用的。

在这里插入图片描述

Java中可以作为GC Roots的对象

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(Native方法)引用的对象。

可参考:《JVM 内存模型概述》

2.回收算法

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

标记-清除算法分为两个阶段:

  • 标记阶段:标记出需要被回收的对象。
  • 清除阶段:回收被标记的可回收对象的内部空间。
http://123.57.57.233/wp-content/uploads/2021/02/image.png

标记-清除算法实现较容易,不需要移动对象,但是存在较严重的问题:

  • 算法过程需要暂停整个应用,效率不高。
  • 标记清除后会产生大量不连续的内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

2.2 复制(Copying)算法

为了解决标记-清除算法的缺陷,由此有了复制算法。
复制算法将可用内存分为两块,每次只用其中一块,当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存空间一次性清理掉。

http://123.57.57.233/wp-content/uploads/2021/02/image-1.png

小结:

  • 优点:实现简单,不易产生内存碎片,每次只需要对半个区进行内存回收。
  • 缺点:内存空间缩减为原来的一半;算法的效率和存活对象的数目有关,存活对象越多,效率越低。

2.3 标记-整理(Mark-Compact)算法

为了更充分利用内存空间,提出了标记-整理算法。
此算法结合了“标记-清除”和“复制”两个算法的优点。
该算法标记阶段和“标记-清除”算法一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉边界以外的内存。

http://123.57.57.233/wp-content/uploads/2021/02/image-2.png

2.4 分代收集(Generational Collection)算法

前面已经有三种方法来回收内存,那么 Java 里是如何选择利用这三种回收算法呢?是只用一种还是三种都用呢?

Java 的堆结构

在选择回收算法前,我们先来看一下 Java 堆的结构。

一块 Java 堆空间一般分成三部分,这三部分用来存储三类数据:

  • 刚刚创建的对象。在代码运行时会持续不断地创造新的对象,这些新创建的对象会被统一放在一起。因为有很多局部变量等在新创建后很快会变成不可达的对象,快速死去,因此这块区域的特点是存活对象少,垃圾多。形象点描述这块区域为:新生代
  • 存活了一段时间的对象。这些对象早早就被创建了,而且一直活了下来。我们把这些存活时间较长的对象放在一起,它们的特点是存活对象多,垃圾少。形象点描述这块区域为:老年代
  • 永久存在的对象。比如一些静态文件,这些对象的特点是不需要垃圾回收,永远存活。形象点描述这块区域为:持久代。(不过在 Java 8 里已经把持久代删除了,把这块内存空间给了元空间,后续文章再讲解。)

也就是说,常规的 Java 堆至少包括了 新生代 和 老年代 两块内存区域,而且这两块区域有很明显的特征:

  • 新生代:存活对象少、垃圾多
  • 老年代:存活对象多、垃圾少

结合新生代/老年代的存活对象特点和之前提过的几种垃圾回收算法,可以得到如下的回收方案:

新生代-复制回收机制

对于新生代区域,由于每次 GC 都会有大量新对象死去,只有少量存活。因此采用复制回收算法,GC 时把少量的存活对象复制过去即可。

老年代-标记整理回收机制

根据上面我们知道,老年代一般存放的是存活时间较久的对象,所以每一次 GC 时,存活对象比较大,也就是说每次只有少部分对象被回收。因此,这里选择标记整理回收机制,仅仅通过少量地移动对象就能清理垃圾,而且不存在内存碎片化。

工作原理如下:

http://123.57.57.233/wp-content/uploads/2021/02/image-3.png

年轻代(Young Generation

  1. 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
  2. 新生代内存按照8:1:1的比例分为一个 eden 区和两个 survivor(survivor0,survivor1) 区。一个 Eden 区,两个 Survivor 区(一般而言)。大部分对象在 Eden 区中生成。回收时先将 eden 区存活对象复制到一个 survivor0 区,然后清空 eden 区,当这个 survivor0 区也存放满了时,则将 eden 区和 survivor0 区存活对象复制到另一个 survivor1 区,然后清空 eden 和这个 survivor0 区,此时 survivor0 区是空的,然后将 survivor0 区和 survivor1 区交换,即保持 survivor1 区为空, 如此往复。
  3. survivor1区不足以存放 edensurvivor0 的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次 Full GC ,也就是新生代、老年代都进行回收。
    4.新生代发生的 GC 也叫做 Minor GCMinor GC 发生频率比较高(不一定等 Eden 区满了才触发)。

年老代(Old Generation

  1. 在年轻代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
  2. 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发 Major GCFull GCFull GC 发生频率比较低,老年代对象存活时间比较长,存活率标记高。

持久代(Permanent Generation

用于存放静态文件,如 Java 类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些 class ,例如 Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。

GC 类型:

Minor GC(新生代 GC):
新生代 GC,指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生熄灭的特点,所以 Minor GC 十分频繁,回收速度也较快。

Major GC(老年代 GC):
老年代 GC,指发生在老年代的垃圾收集动作,当出现 Major GC 时,一般也会伴有至少一次的 Minor GC(并非绝对,例如 Parallel Scavenge 收集器会单独直接触发 Major GC 的机制)。 Major GC 的速度一般会比 Minor GC 慢十倍以上。

Full GC:
清理整个堆空间—包括年轻代和老年代。Major GC == Full GC

参考:聊聊JVM(四)深入理解Major GC, Full GC, CMS

产生 Full GC 可能的原因:

  • 年老代被写满。
  • 持久代被写满。
  • System.gc() 被显示调用。
  • 上一次 GC 之后 Heap 的各域分配策略动态变化。

垃圾收集器(GC

不同虚拟机所提供的垃圾收集器可能会有很大差别,下面的例子是 HotSpot

新生代收集器使用的收集器:SerialPraNewParallel Scavenge
老年代收集器使用的收集器:Serial OldParallel OldCMS

https://img-blog.csdnimg.cn/20190728160845449.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3R6dzE5OTI=,size_16,color_FFFFFF,t_70
  1. Serial 收集器(复制算法)
    新生代单线程收集器,标记和清理都是单线程,优点是简单高效。
  2. Serial Old收集器(标记-整理算法)
    老年代单线程收集器,Serial 收集器的老年代版本。
  3. ParNew 收集器(停止-复制算法)  
    新生代收集器,可以认为是 Serial 收集器的多线程版本,在多核 CPU 环境下有着比 Serial 更好的表现。
  4. Parallel Scavenge 收集器(停止-复制算法)
    并行收集器,追求高吞吐量,高效利用 CPU。吞吐量一般为 99%, 吞吐量 = 用户线程时间 / (用户线程时间 + GC线程时间)。适合后台应用等对交互相应要求不高的场景。
  5. Parallel Old 收集器(停止-复制算法)
    Parallel Scavenge 收集器的老年代版本,并行收集器,吞吐量优先。
  6. CMS(Concurrent Mark Sweep) 收集器(标记-清理算法)
    高并发、低停顿,追求最短 GC 回收停顿时间,cpu 占用比较高,响应时间快,停顿时间短,多核 cpu 追求高响应时间的选择。

根据对象的生命周期的不同将内存划分为几块,然后根据各块的特点采用最适当的收集算法。大批对象死去、少量对象存活的(新生代),使用复制算法,复制成本低;对象存活率高、没有额外空间进行分配担保的(老年代),采用标记-清理算法或者标记-整理算法。

四种引用状态

在实际开发中,我们对 new 出来的对象也会根据重要程度,有个等级划分。有些必须用到的对象,我们希望它在其被引用的周期内能一直存在;有些对象可能没那么重要,当内存空间还足够时,可以保留在内存中,如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。
由此,Java 对引用划分为四种:强引用、软引用、弱引用、虚引用,四种引用强度依次减弱。

  1. 强引用
    代码中普遍存在的类似”Object obj = new Object()“这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
  2. 软引用
    描述有些还有用但并非必需的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。Java 中的类 SoftReference 表示软引用。
  3. 弱引用
    描述非必需对象。被弱引用关联的对象只能生存到下一次垃圾回收之前,垃圾收集器工作之后,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。Java 中的类 WeakReference 表示弱引用。
  4. 虚引用
    这个引用存在的唯一目的就是在这个对象被收集器回收时收到一个系统通知,被虚引用关联的对象,和其生存时间完全没关系。Java 中的类 PhantomReference 表示虚引用。

本文是在学习了上面资料中之后整理出来的,并根据自己的理解做了一些改动,如有错误欢迎指正!

脑洞世界

Java垃圾回收机制
垃圾回收的意义 Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再需要考虑内存管理。 由于有…
扫描二维码继续阅读
2021-01-01