1. 如何判断对象已死?

JVM 中判断对象是否已经死亡的算法主要有 2 种:引用计数法、可达性分析法

1.1 引用计数法

  • 如果一个对象被其他变量所引用,则让该对象的引用计数+1,如果该对象被引用 2 次则其引用计数为 2,依次类推。
  • 某个变量不再引用该对象,则让该对象的引用计数-1,当该对象的引用计数变为 0 时,则表示该对象没用被其他变量所引用,这时候该对象就可以被作为垃圾进行回收。

引用计数法弊端:循环引用时,两个对象的引用计数都为 1,导致两个对象都无法被释放回收。最终就会造成内存泄漏!

image.png

1.2 可达性分析算法

可达性分析算法就是 JVM 中判断对象是否是垃圾的算法:该算法首先要确定 GC Root(根对象,就是肯定不会被当成垃圾回收的对象)。

在垃圾回收之前,JVM 会先对堆中的所有对象进行扫描,判断每一个对象是否能被 GC Root 直接或者间接的引用,如果能被根对象直接或间接引用则表示该对象不能被垃圾回收,反之则表示该对象可以被回收:

image.png

  • JVM 中的垃圾回收器通过可达性分析来探索所有存活的对象。
  • 扫描堆中的对象,看能否沿着 GC Root 为起点的引用链找到该对象,如果找不到,则表示可以回收,否则就可以回收。
  • 在 Java 技术体系里面,固定可作为 GC Roots 的对象包括以下几种
    • 虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
    • 在方法区中类静态属性引用的对象,譬如 Java 类的引用类型静态变量。
    • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
    • 在本地方法栈中 JNI(即通常所说的 Native 方法)引用的对象。
    • 所有被同步锁(synchronized 关键字)持有的对象。
    • Java 虚拟机内部的引用,如基本数据类型对应的 Class 对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。

1.3 Java 中的五种引用

image.png

强引用

上图实心线表示强引用:比如,new 一个对象 M,将对象 M 通过=(赋值运算符),赋值给某个变量 m,则变量 m 就强引用了对象 M。

强引用的特点:只要沿着 GC Root 的引用链能够找到该对象,就不会被垃圾回收;只有当 GC Root都不引用该对象时,才会回收强引用对象。

  • 如上图 B、C 对象都不引用 A1 对象时,A1 对象才会被回收

软引用

image.png

上图中宽虚线所表示的就是软引用:

软引用的特点:当 GC Root 指向软引用对象时,若内存不足,则会回收软引用所引用的对象

  • 如上图如果 B 对象不再引用 A2 对象且内存不足时,软引用所引用的 A2 对象就会被回收。

软引用的使用

  1. public class Demo1 {
  2. public static void main(String[] args) {
  3. final int _4M = 4*1024*1024;
  4. SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);
  5. List <SoftReference<byte[]>> list = new ArrayList<>();
  6. }
  7. }

如果在垃圾回收时发现内存不足,在回收软引用所指向的对象时,软引用本身不会被清理

如果想要清理软引用,需要使用引用队列

image.png

  1. public class Demo04 {
  2. final static int _4M = 4 * 1024 * 1024;
  3. public static void main(String[] args) {
  4. List<SoftReference<byte[]>> list = new ArrayList<>();
  5. ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
  6. for (int i = 0; i < 5; i++) {
  7. SoftReference<byte[]> ref = new SoftReference<>(new byte[_4M], queue);
  8. System.out.println(ref.get());
  9. list.add(ref);
  10. System.out.println(list.size());
  11. }
  12. Reference<? extends byte[]> poll = queue.poll();
  13. while (poll != null) {
  14. list.remove(poll);
  15. poll = queue.poll();
  16. }
  17. System.out.println("==========================");
  18. for (SoftReference<byte[]> reference : list) {
  19. System.out.println(reference.get());
  20. }
  21. }
  22. }

大概思路为:查看引用队列中有无软引用,如果有,则将该软引用从存放它的集合中移除(这里为一个 list 集合)。

弱引用

image.png

只有当弱引用引用该对象,在垃圾回收时,无论内存是否充足,都会回收弱引用所引用的对象。

  • 如上图如果 B 对象不再引用 A3 对象,则 A3 对象会被回收。

弱引用的使用和软引用类似,只是将 SoftReference 换为了 WeakReference。

虚引用

image.png

当引用的对象 ByteBuffer 被垃圾回收以后,虚引用对象 Cleaner 就会被放入引用队列中:

image.png

然后调用 Cleaner 的clean方法(Unsafe.freeMemory())来释放直接内存:

image.png

  • 虚引用的一个体现是释放直接内存所分配的内存,当引用的对象 ByteBuffer 被垃圾回收以后,虚引用对象 Cleaner 就会被放入引用队列中,然后调用 Cleaner 的clean方法来释放直接内存。
  • 如上图,B 对象不再引用 ByteBuffer 对象,ByteBuffer 就会被回收。但是直接内存中的内存还未被回收。这时需要将虚引用对象 Cleaner 放入引用队列中,然后调用它的clean方法来释放直接内存。

终结器引用

image.png

所有的类都继承自 Object 类,Object 类有一个finalize()方法。当某个对象不再被其他的对象所引用时,会先将终结器引用对象放入引用队列中,然后根据终结器引用对象找到它所引用的对象,然后调用该对象的finalize()方法。调用以后,该对象就可以被垃圾回收了。

image.png

  • 如上图,B 对象不再引用 A4 对象。这是终结器对象就会被放入引用队列中,引用队列会根据它,找到它所引用的对象。然后调用被引用对象的finalize()方法。调用以后,该对象就可以被垃圾回收了。

引用队列

  • 软引用和弱引用可以配合引用队列 (也可以不配合):
    • 弱引用虚引用所引用的对象被回收以后,会将这些引用放入引用队列中,方便一起回收这些软 / 弱引用对象。
  • 虚引用和终结器引用必须配合引用队列:
    • 虚引用和终结器引用在使用时会关联一个引用队列。

1.4 回收方法区

方法区的垃圾收集主要回收两部分内容:废弃的常量不再使用的类型。举个常量池中字面量回收的例子,假如一个字符串 “java” 曾经进入常量池中,但是当前系统又没有任何一个字符串对象的值是 “java”,换句话说,已经没有任何字符串对象引用常量池中的“java” 常量,且虚拟机中也没有其他地方引用这个字面量。如果在这时发生内存回收,而且垃圾收集器判断确有必要的话,这个 “java” 常量就将会被系统清理出常量池。常量池中其他类(接口)、方法、字段的符号引用也与此类似。

判定一个常量是否 “废弃” 需要同时满足下面三个条件:

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类及其任何派生子类的实例。
  • 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP 的重加载等,否则通常是很难达成的。
  • 该类对应的 **java.lang.Class** 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

2. 垃圾回收算法

在 Java 堆划分出不同的区域之后,垃圾收集器才可以每次只回收其中某一个或者某些部分的区域——因而才有了 “Minor GC”“Major GC”“Full GC” 这样的回收类型的划分;也才能够针对不同的区域安排与里面存储对象存亡特征相匹配的垃圾收集算法——因而发展出了 “标记 - 复制算法”“标记 - 清除算法”“标记 - 整理算法” 等针对性的垃圾收集算法。

四种 GC 概念的介绍:

■ 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。

■ 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有 CMS 收集器会有单独收集老年代的行为。

■ 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有 G1 收集器会有这种行为。

■ 整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾收集。

下面逐个介绍下 4 种回收算法:

2.1 标记 - 清除

image.png

定义:标记清除算法顾名思义,是指在虚拟机执行垃圾回收的过程中,先采用标记算法确定可回收对象,然后垃圾收集器根据标识,清除相应的内容,给堆内存腾出相应的空间。

  • 这里的腾出内存空间并不是将内存空间的字节清 0,而是记录下这段内存的起始结束地址,下次分配内存的时候,会直接覆盖这段内存。

缺点:(容易产生大量的内存碎片,可能无法满足大对象的内存分配,一旦导致无法分配对象,那就会导致 jvm 启动 gc)。

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

标记 - 清除算法的执行过程(书中配图):

image.png

2.2 标记 - 整理

image.png

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

标记 - 整理:会将不被 GC Root 引用的对象回收,清除其占用的内存空间。然后整理剩余的对象,可以有效避免因内存碎片而导致的问题,但是牵扯到对象的整理移动,需要消耗一定的时间,所以效率较低。

标记 - 整理算法的执行过程(书中配图):

image.png

标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动的

2.3 标记 - 复制

image.png

当需要回收对象时,先将 GC Root 直接引用的的对象 (不需要回收) 放入TO中:

image.png

image.png

然后清除FROM中的需要回收的对象:
image.png

最后 交换 FROMTO 的位置:(FROM 换成 TO,TO 换成 FROM)

image.png

复制算法:将内存分为等大小的两个区域,FROMTO(TO 中为空)。先将被 GC Root 引用的对象从 FROM 放入 TO 中,再回收不被 GC Root 引用的对象。然后交换 FROM 和 TO。这样也可以避免内存碎片的问题,但是会占用双倍的内存空间

标记 - 复制算法的执行过程(书中配图):

image.png

2.4 分代回收

把分代收集理论具体放到现在的商用 Java 虚拟机里,设计者一般至少会把 Java 堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域,顾名思义,在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。

长时间使用的对象放在老年代中(长时间回收一次,回收花费时间久),用完即可丢弃的对象放在新生代中(频繁需要回收,回收速度相对较快),如下图所示:

image.png

回收流程

新创建的对象都被放在了新生代的伊甸园中:

3 垃圾回收算法 - 图23

3 垃圾回收算法 - 图24

当伊甸园中的内存不足时,就会进行一次垃圾回收,这时的回收叫做 Minor GC

Minor GC 会将伊甸园和幸存区 FROM仍需要存活的对象复制到 幸存区 TO中, 并让其寿命加 1,再交换 FROM 和 TO

3 垃圾回收算法 - 图25

伊甸园中不需要存活的对象清除:

3 垃圾回收算法 - 图26

交换 FROM 和 TO

3 垃圾回收算法 - 图27

同理,继续向伊甸园新增对象,如果满了,则进行第二次 Minor GC:

流程相同,仍需要存活的对象寿命+1:(下图中 FROM 中寿命为 1 的对象是新从伊甸园复制过来的,而不是原来幸存区 FROM 中的寿命为 1 的对象,这里只是静态图片不好展示,只能用文字描述了)

3 垃圾回收算法 - 图28

再次创建对象,若新生代的伊甸园又满了,则会再次触发 Minor GC(会触发 stop the world, 暂停其他用户线程,只让垃圾回收线程工作),这时不仅会回收伊甸园中的垃圾,还会回收幸存区中的垃圾,再将活跃对象复制到幸存区 TO 中。回收以后会交换两个幸存区,并让幸存区中的对象寿命加 1

如果幸存区中的对象的寿命超过某个阈值(最大为 15,4bit),就会被放入老年代中:

3 垃圾回收算法 - 图29

如果新生代老年代中的内存都满了,就会先触发 Minor Gc,再触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收:

3 垃圾回收算法 - 图30

分代回收小结:

  • 新创建的对象首先会被分配在伊甸园区域。
  • 新生代空间不足时,触发 Minor GC,伊甸园和 FROM 幸存区需要存活的对象会被 COPY 到 TO 幸存区中,存活的对象寿命+1,并且交换 FROM 和 TO。
  • Minor GC 会引发 Stop The World:暂停其他用户的线程,等待垃圾回收结束后,用户线程才可以恢复执行。
  • 当对象寿命超过阈值15时,会晋升至老年代。
  • 如果新生代、老年代中的内存都满了,就会先触发 Minor GC,再触发 Full GC,扫描新生代和老年代中所有不再使用的对象并回收。