一、简介
思考一个问题,在 java 里面我们 new 一个对象,等到程序结束后,这个对象就被自动回收了,完成这项工作只需要确定:哪些内存需要回收?什么时候回收?如何回收?接下来我们详细的解释下这三个问题。
二、哪些内存需要回收
由于程序计数器,虚拟机栈,本地方法栈随线程而生,随线程而死,故这几个区不需要过多考虑回收的问题。而 Java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,所以我们考虑回收的是这部分内存。
2.1 堆内存回收
如果想要回收 Java 堆内存,首先需要判断对象是否存活,可以采用引用计数算法(无法解决循环引用的问题)和正向可达算法;其次还要判断对象的引用情况(强软弱虚);即使是正向可达算法不可达的对象,也不是非死不可,宣告一个对象死亡,还要经历两次标记过程,在这个过程中对象还可能发生自救,真正死亡的对象才会被回收。
2.2 方法区内存回收
方法区主要回收两部分内存:废弃的常量和无用的类,假如一个字符串 abc 进入了常量池,但是没有任何 String 对象引用常量池的 abc 常量,也没有其他地方引用这个字面量,若此时发生内存回收,而且必要的话,abc 常量就会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
判定一个类是否是 ”无用的类“ 需要满足 3 个条件:
第一个条件:该类所有的实例都被回收,也就是 Java 堆中不存在该类的任何实例;
第二个条件:加载该类的 ClassLoader 已经被回收;
第三个条件:该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
三、什么时候进行回收
3.1 GC 分类
在堆内存中,内存分为新生代和老年代,所以 GC 分为新生代 GC 和老年代 GC 。新生代 GC 又名 Minor GC,是指发生在新生代动的垃圾收集动作,因为 java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也很快。
老年代 GC 又名 Major GC / Full GC,是指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC,Major GC 的速度一般比 Minor GC 慢 10 倍以上。
当新生代或老年代的内存满了的时候就会发生 GC 的操作,那么内存什么时候会满呢?参照下面所说的对象分配内存问题。
3.2 对象内存分配问题
1、对象优先在 Eden 分配,当 Eden 区没有足够的空间进行分配时,虚拟机将进行一次 Minor GC。
2、大对象直接进入老年代,大对象是指需要大量连续内存空间的 Java 对象,最典型的大对象就是那种很长的字符串以及数组。
3、长期存活的对象将进入老年代,虚拟机给每个对象定义了一个对象年龄 (Age) 计数器,如果对象在 Eden 出生且经过一次 Minor GC 存活且被 Survivor 容纳,此时对象年龄设为 1,对象熬过一次 Minor GC,年龄就增加 1 岁,当年龄到达一定的程度(默认15,可调),就会被晋升到老年代。
4、动态对象年龄判断,如果 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到 15 岁。
四、如何进行回收
JVM 采用的是分代的垃圾收集算法,即新生代采用复制的垃圾收集算法,老年代采用标记清除或标记压缩的垃圾收集算法,不同的垃圾收集器采用不同的垃圾收集算法。
4.1 复制算法 Copying
以前(现在不这么干了):将内存按容量划分为大小相等的两块,每次只使用其中的一块,当一块用完,就把还存活的对象复制到另外一块上,再把上一块内存清理干净,大多数的 JVM 都采用这种算法回收新生代。
现状(现在这么干):将内存分为一块较大的 Eden 区和两块较小的 Survivor 区,每次使用 Eden 和其中一块Survivor,HotSpot 默认的 Eden 和 Survivor 大小比例是 8:1。即每次只浪费 10%,当 Survivor 空间不够用时,需要依赖其他内存(老年代)进行分配担保。
空间分配担保:把 Survivor 无法容纳的对象直接进入老年代
4.2 标记清除算法 Mark-Sweep
首先标记处所有需要回收的对象,然后统一回收所有标记的对象,缺点是效率低,并且标记清除后会产生大量不连续的内存碎片,碎片太多会导致为大对象分配内存时,因无法找到足够的连续内存而不得不提前触发一次垃圾收动作。
4.3 标记压缩算法 Mark-compact
把存活的对象往一端压缩(替换位置),然后清理掉可回收的对象。
五、判断对象是否存活
判断一个对象是否存活有两种基本的算法,一种是引用计数算法,一种是正向可达算法,下面分别来介绍下。
5.1 引用计数算法
给对象添加一个引用计数器,每当有一个地方引用它时,计数器加一,当引用失效时,计数器减一,当该对象的引用计数器为 0 时,我们认为该对象就不能被使用了。
特点:效率高,但是无法解决循环引用的问题(即 A 引用 B,B 引用 A)。
5.2 正向可达算法
以一个 GC Roots 为根节点,从这个节点往下搜索,搜索走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链时,就证明此对象是不可用的。可作为 GC Roots 的对象有:
1、虚拟机栈中引用的对象
2、本地方法栈中引用的对象
3、方法区中类的静态属性引用的对象
4、方法区中常量引用的对象
六、垃圾收集器
6.1 分代模型
把内存分为新生代和老年代,新生代采取复制算法,老年代采用标记压缩或标记算法
6.2 分区模型
把内存里面分成一个一个的小格
G1
ZGC(jdk11 以上)
6.1 新生代垃圾收集器
Serial 收集器:单线程收集器,即只会使用一个 CPU 或一条收集线程去完成垃圾收集,且它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束;但他仍然是 jvm 运行在 Client 模式下默认的新生代的收集器。
Parallel Scavenge 收集器:并行的多线程收集器,它关注的点是达到一个可控制的吞吐量;而像 CMS 收集器关注的是尽可能缩短垃圾收集时用户线程的停顿时间。(jdk1.8 默认的新生代)
ParNew 收集器:其实就是 Serial 收集器的多线程版本,即使用多条线程进行垃圾收集,它是在 Server 模式下首选的新生代垃圾收集器。
6.2 老年代垃圾收集器
Serial Old 收集器:是 Serial 收集器的老年代版本,也是一个单线程的收集器,使用“标记-整理”算法;也是给 Client 模式下的虚拟机使用的
Parallel Old 收集器:是 Parallel Scavenge 收集器的老年代版本,在注重吞吐量和 CPU 资源敏感的场合可以优先考虑。(jdk1.8 默认的老年代)
CMS 收集器:一种获取最短回收停顿时间为目标的收集器(停顿时间最短),响应速度快,基于“标记-清除”算法。