第三章 垃圾收集器与内存分配策略 | part 1

要说 Java 和 C++ 最显著的差别之一,应该就是内存动态分配和回收机制了吧。Java 的面试必问 JVM 的垃圾回收,C++ 的面试则必问内存泄漏,我好想逃~~ 却逃不掉~~

对象死亡判断

在垃圾回收之前,一定要判断清楚哪些对象仍然存活,哪些对象已经死去,要是不小心把仍然存活的对象埋葬了,那可就成为 "sha人凶手" 了。

引用计数法

引用计数法是一个简单高效的算法,具体原理是在对象中添加一个计数器 count,每当一个地方引用它,count 值加一;当引用失效时,count 值减一;count0 的对象就认为是死亡对象。

这种方法在很多领域都有应用,如 C++ 中的智能指针或是 Python 中的 GC 设计,它最大的好处就是回收及时 :一个对象的引用计数归零的那一刻即是它成为垃圾的那一刻,同时也是它被回收的那一刻。相较而言, Java 中死亡对象就得等到下一次 GC 时才被清理。但是该方法会遇到很多 "例外情况",其中最重要的就是无法解决循环引用的问题。

在下面的例子中,objA 和 objB 形成了循环引用,但是 Java 并没有放弃回收它们,这也侧面证明了 Java 没有使用引用计数算法。

java 复制代码
// VM 参数:-XX:+PrintGC
public class ReferenceCountingGC {
    public Object instance = null;
    private static final int _1MB = 1024 * 1024;
    // 占内存,方便查看是否发生了 GC
    private byte[] bigSize = new byte[2 * _1MB];

    public static void testGC() {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
        System.gc();
    }

    public static void main(String[] args) {
        testGC();
    }
}

可达性分析法

Java 中真正使用的判断对象是否存活的算法其实是可达性分析法 。这个算法通过一系列 "GC Roots" 作为起始节点集合,并根据引用关系进行 BFS 搜索 ,得到的路径被称为 "引用链"。如果某个对象到任意 GC Roots 间都没有引用链,则认为该对象死亡。如下图所示, object5、6、7 便是死亡对象。

可达性分析本身的思想是很简单的,那到底有哪些对象可以作为 GC Roots 呢?事实上在 Java 技术体系中,能够固定作为 GC Roots 的对象包括以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的局部变量、临时变量等。
  • 方法区中类静态属性引用的对象,譬如 Java 类的引用类型静态变量。
  • 方法区中常量引用的对象,譬如字符串常量池里的引用。
  • 本地方法栈中 JNI(即一般说的Native方法)引用的对象。
  • JVM 内部的引用,如基本数据类型对应的 Class 对象,系统类加载器等等。
  • 所有被同步锁(synchronized 关键字)持有的对象。

再谈引用

无论是哪种算法,都不可避免地涉及到了 "引用" 这个概念。在 JDK 1.2 以前,reference 类型指的就是传统意义上的引用,而为了能让程序自己决定 对象的生命周期,JDK 1.2 引入了强引用、软引用、弱引用、虚引用四种引用类型。

  • 强引用

    强引用就是指最传统的引用,是在程序中普遍存在的引用赋值,即类似 Object obj = new Object() 这类的引用关系。只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

    下面代码中,尽管 o1 已经被回收,但是 o2 的强引用一直存在,所以对象不会被 GC 回收:

    java 复制代码
    public class StrongReferenceDemo {
        public static void main(String[] args) {
            Object o1 = new Object();
            Object o2 = o1;
            o1 = null;
            System.gc();
            System.out.println(o1);
            System.out.println(o2);
        }
    }

    运行结果:

  • 软引用

    软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。

    下面的代码模拟了内存充足和内存不足两种情况下,软引用对象的回收情况:

    java 复制代码
    public class SoftReferenceDemo {
        public static void main(String[] args) {
            System.out.println("------内存足够的情况------");
            softRefMemoryEnough();
            System.out.println("------内存不够用的情况------");
            softRefMemoryNotEnough();
        }
    
        private static void softRefMemoryEnough() {
            Object o1 = new Object();
            SoftReference<Object> s1 = new SoftReference<Object>(o1);
            System.out.println(o1);
            System.out.println(s1.get());
    
            o1 = null;
            System.gc();
    
            System.out.println(o1);
            System.out.println(s1.get());
        }
        private static void softRefMemoryNotEnough() {
            Object o1 = new Object();
            SoftReference<Object> s1 = new SoftReference<Object>(o1);
            System.out.println(o1);
            System.out.println(s1.get());
    
            o1 = null;
    
            byte[] bytes = new byte[10 * 1024 * 1024];
    
            System.out.println(o1);
            System.out.println(s1.get());
        }
    }

    运行结果:

  • 弱引用

    弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。弱引用可以和引用队列结合使用,一旦对象只有弱引用,GC thread 就会把弱引用直接插入到引用队列,与将 finalizable 对象插入 finalization queue 是同一时机。

    下面代码中,由于强引用 o1 不存在了,只剩下弱引用 w1,因此无论内存是否充足,对象都会被回收:

    java 复制代码
    public class WeakReferenceDemo {
        public static void main(String[] args) {
            Object o1 = new Object();
            WeakReference<Object> w1 = new WeakReference<Object>(o1);
    
            System.out.println("强引用存在时,o1 = " + o1);  
            System.out.println("强引用存在时,w1 = " + w1.get());
    
            o1 = null;
            System.gc();
    
            System.out.println("强引用不存在时,o1 = " + o1);  
            System.out.println("强引用不存在时,w1 = " + w1.get());
        }
    }

    运行结果:

  • 虚引用

    虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例(即 get() 方法永远返回 null)。虚引用必须和引用队列结合使用,当某个被引用的对象被回收后,JVM 会将指向它的引用加入到引用队列的队列末尾 ,这相当于是一种通知机制 ,由 ReferenceHandler 守护线程完成。虚引用最常用的地方是配合 Cleaner 完成堆外内存的释放

    下面的代码对虚引用和引用队列进行了简单的使用:

    java 复制代码
    public class PhantomReferenceDemo {
        public static void main(String[] args) throws InterruptedException {
            Object o1 = new Object();
            ReferenceQueue<Object> referenceQueue = new ReferenceQueue<Object>();
            PhantomReference<Object> phantomReference = new PhantomReference<Object>(o1,referenceQueue);
    
            System.out.println("------强引用存在时------");
            System.out.println(o1);
            System.out.println(referenceQueue.poll());
            System.out.println(phantomReference.get());
    
            o1 = null;
            System.gc();
            Thread.sleep(3000);
            
            System.out.println("------强引用消失后------");
            System.out.println(o1);
            System.out.println(referenceQueue.poll()); //引用队列中
            System.out.println(phantomReference.get());
        }
    }

    运行结果:

生存还是死亡?

一个对象在确定回收之前,仍然有机会通过 finalize() 方法 "拯救" 自己,具体过程如下:

  1. 重写了 finalize() 的类实例化时,JVM 会标记该对象为 finalizable;
  2. GC thread 检测到对象不可达时,如果对象是 finalizable,会将对象添加到 finalization queue,对象被 finalizer daemon thread 的 Finalizer class 引用,重新可达,推迟 GC;
  3. finalizer daemon thread 在一段时间之后(某个不确定时间) ,将会从 finalization queue 出队对象,调用对象的 finalize(),随后标记对象为 finalized ,并断开 Finalizer class 的强引用
  4. GC thread 重新检测到对象不可达时,才会回收对象。
  5. 对于虚引用,在对象被销毁了之后会被加入到引用队列(注意弱引用和虚引用加入队列的不同时机)。

注一:任何一个对象的 finalize() 方法都只会被系统自动调用一次。
注二:finalize() 方法运行代价高昂,不确定性大,不建议使用。(可以用 try-finally 代替)

回收方法区

Java 虚拟机规范中说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区中进行垃圾收集的 "性价比" 一般比较低。

方法区的垃圾收集主要回收两部分内容:废弃常量和无用的类:

  • 回收废弃常量与回收 Java 堆中的对象非常类似。以常量池中字符串对象 "java" 为例,如果没有任何地方引用了这个字面量,那么在发生内存回收,且垃圾回收器判断确有必要的话, "java" 常量就会被系统清理出常量池。

  • 无用的类需要同时满足以下三个条件:

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

    在类同时满足这三个条件时,将被允许回收(非必须)。在大量使用反射、动态代理等的场景下,通常会需要进行类的卸载以保证不会对方法区造成过大的内存压力。

相关推荐
禁默41 分钟前
深入浅出:AWT的基本组件及其应用
java·开发语言·界面编程
Cachel wood1 小时前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
Code哈哈笑1 小时前
【Java 学习】深度剖析Java多态:从向上转型到向下转型,解锁动态绑定的奥秘,让代码更优雅灵活
java·开发语言·学习
gb42152871 小时前
springboot中Jackson库和jsonpath库的区别和联系。
java·spring boot·后端
程序猿进阶1 小时前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
zfoo-framework1 小时前
【jenkins插件】
java
风_流沙1 小时前
java 对ElasticSearch数据库操作封装工具类(对你是否适用嘞)
java·数据库·elasticsearch
ProtonBase2 小时前
如何从 0 到 1 ,打造全新一代分布式数据架构
java·网络·数据库·数据仓库·分布式·云原生·架构
乐之者v2 小时前
leetCode43.字符串相乘
java·数据结构·算法