概述
垃圾收集器需要完成的三件事
那些内存需要回收?
什么时候回收?
如何回收?
那些需要进行GC
程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程灭而灭,栈中的栈帧随着方法的进入和退出有条不紊的进行出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知(尽管在运行期间会由即时编译器进行一些优化,但在基于概念模型的讨论里,大体上可以认为是已知的),因此这几个区域内存的分配和回收都具备确定性,在这几个区域内就不需要过多考虑回收的问题,当方法结束或者线程结束时,内存自然就随着回收了
Java堆和方法区这两个区域则有着很显著的不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间,我们才能知道这个程序究竟会创建那些对象。
如何判断对象是否该被回收?
引用计数法
在对象里面添加一个引用计数器,每当有一个对象引用它时,计数器值就加一;每当引用失效,计数器的值就减一;任何时刻计数器为零的对象就是不可能再被使用的
/** * @className: JVM_1 * @description: 测试JVM是否使用引用计数法 * @author: 热爱生活の李 * @since: 2022/5/20 14:52 */ public class JVM_1 { public Object instance = null; private static final int _1MB = 1024 * 1024; public static void main(String[] args) { JVM_1 objectA = new JVM_1(); JVM_1 onjectB = new JVM_1(); objectA.instance = onjectB; onjectB.instance = objectA; objectA = null; onjectB = null; System.gc(); } }
添加GC日志打印
运行配置里加VM option
参数为 -XX:+PrintGCDetails
缺点
可能会造成循环引用,导致无法回收,例如:上面对象objectA和objectB都有字段instance,objectA.instance = objectB,objectB.instance = objectA,除此之外,这两个对象再无其他任何引用,实际上这两个对象不可能被访问,但是他们互相引用对方,导致引用计数器不为0,就无法回收它们。
可达性分析算法
通过一系列称为“ GC Roots ” 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“ 引用链 ” ,如果某个对象到GC Roots 间没有任何链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明对象不可能再被使用
那些可以作为GC Roots对象
固定作为GC Roots
1、在虚拟机栈中引用的对象(各个线程被调用的方法栈中的参数、局部变量、临时变量)
2、在方法区中类静态属性引用的对象(Java类的引用类型静态变量)
3、在方法区常量引用的对象(字符串常量池String Table的引用)
4、在本地方法栈中引用的对象(Native方法)
5、Java虚拟机内部的引用(基本数据类型对应的Class对象,一些常驻的异常对象比如NullPointException,还有系统类加载器)
6、所有被同步锁(synchronized关键字)持有的对象
7、反映Java虚拟机内部情况的JMXBean、JVMTL注册的回调、本地代码缓存等
根据用户所选择的垃圾收集器以及当前回收的内存区域不同,还可以用其他对象"临时性"地加入
引用
Java对引用的概念进行了扩充,将引用分为强引用(Strongly Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)
强引用: 指在程序代码之间存在的引用赋值,即类型Object obj = new Object() 这种引用关系。无论任何关系下,只要强引用关系还存在,垃圾收集器就不会回收掉被引用的对象
软引用: 用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如过这次回收还是没有足够的内存,才会抛出内存溢出异常。
弱引用: 用来描述那些非必要对象,但是它的强度比软引用更弱一些,关联的对象只能生存到下一次垃圾收集为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
虚引用: 最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实列。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收的时候收到一个系统通知。
生存还是死亡
即使是在可达性分析算法中判断为不可达的对象也不是非死不可,要真正宣告一个对象死亡,至少要经历两次标记过程。
package JVM; /** * @className: JVM_2 * @description: 测试finalize()方法 * @author: 热爱生活の李 * @since: 2022/5/20 15:08 */ public class JVM_2 { public static JVM_2 instance = null; @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize 方法执行了 !!!"); JVM_2.instance = this; } public static void main(String[] args) throws InterruptedException { instance = new JVM_2(); instance = null; System.gc(); // finalizer方法优先级很低 Thread.sleep(5000); if(instance != null){ System.out.println("我还活着 !!!"); }else { System.out.println("我死了 !!!"); } instance = null; System.gc(); Thread.sleep(5000); if(instance != null){ System.out.println("我还活着 !!!"); }else { System.out.println("我死了 !!!"); } } }
finalize()方法只会被调用一次
回收方法区
方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型
回收废弃的常量
假如一个字符串“Java” 曾经进入常量池,但是当前系统又没有任何一个字符串对象的值是“Java”,换句话说已经没有任何字符串对象引用常量池中的”Java“常量,且虚拟机中也没有其他地方引用这个字面量。如果这时发生内存回收,而且垃圾收集器判断有必要的话,这个”Java“常量就会被清理出常量池。常量池中其他类(接口)、方法、字段的符号引用也与此类似
回收类型的判断条件
1、该类的所有实例化都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例
2、加载该类的类加载器已经被回收
3、该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是被允许,而并不是和对象一样,没有引用就被必然被回收。
关于是否要对类型进行回收,HotSpot虚拟机提供了 -Xnoclassgc 参数进行控制,还可以使用 -verbose:class 以及 -XX:+TraceClassLoading、-XX:TraceClassUnLoading 查看类加载和卸载信息