参考: https://www.jianshu.com/p/8f5fa8288d9b
java深入理解虚拟机
极客时间:深入拆解java虚拟机
https://github.com/Snailclimb/JavaGuide
前言
java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人想出来
java内存运行时区域中的程序计数器、虚拟机栈、本地方法栈 这3个区域它们随着线程而生 随线程而亡 方法介绍或线程结束他们的内存就会被回收,其中栈中每一个栈帧中分配多少内存基本在类结构确定下来时就已知。因此这几个区域中的内存分配和垃圾回收都具有确定性。
然而 堆和 方法区的内存分配都是动态的。所以下面的讨论都是基于堆和方法区进行(主要是堆),垃圾收集器所关注的也是这部分内存。
很多人认为方法区是没有垃圾收集 其实是有的但是性价比和效率很低 通常用来收集一些废弃常量和废弃类下文会讲到怎么判断废弃常量和废弃类
既然说起垃圾回收我们首先所需要考虑的就是这3个问题:
- 那些垃圾需要回收?
- 什么时候回收?
- 如何回收?
1.所谓垃圾就是死亡的对象。JVM如何判断对象是否死亡呢?
- 引用计数法
这个算法实现简单 效率也很高: 给对象添加一个引用计数器,每当有一个地方引用它,计数器值加1;当引用失效,计数器值减一;任何时刻计数器为0的对象就是不可能再被使用的
但是! 主流的JVM并没有选用引用计数算法进行内存管理,因为它很难解决对象之间循环引用的问题 比如以下代码
public class ReferenceCountingGc {
Object instance = null;
public static void main(String[] args) {
ReferenceCountingGc objA = new ReferenceCountingGc();
ReferenceCountingGc objB = new ReferenceCountingGc();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
}
}
虽然对象objA 和 objB 都赋值为NULL但是他们因为互相引用对方,导致它们的引用计数器都不为0,于是引用计数算法无法通知 GC 回收器回收他们。
- 可达性分析算法
这个算法的思路是通过一系列称为GC Roots的对象作为起始点,从这些起始点向节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
比如下图:Object5域Object6虽然相连但是到GC Roots是不可达的所以这对象是不可用的
在java语言中,可作为GC Roots的对象包括下面几种- 1.Java 方法栈桢中的局部变量;
- 2.已加载类的静态变量;
- 3.本地方法栈中JNI (Native方法)引用的对象;
- 4. 方法区中常量引用的对象
虽然可达性分析的算法本身很简明,但是在实践中还是有不少其他问题需要解决的。比如 说,在多线程环境下,其他线程可能会更新已经访问过的对象中的引用,从而造成误报(将引用设置为 null)或者漏报(将引用设置为未被访问过的对象)。
误报并没有什么伤害,Java 虚拟机至多损失了部分垃圾回收的机会。漏报则比较麻烦,因为垃圾回 收器可能回收事实上仍被引用的对象内存。一旦从原引用访问已经被回收了的对象,则很有可能会 直接导致 Java 虚拟机崩溃。
事实上为了解决这个问题早前的垃圾收集器采用了Stop-the-world来解决 这个后面介绍垃圾收集器时会介绍到
2. 再谈引用
虽无论是引用计数还可达性分析算法 判断对象是否存活都与引用有关JDK1.2之后,java对引用的概念进行了扩充将引用分为以下4种 (从上到下,依次减弱)
-
强引用(Strong Reference)
强引用指在代码种普遍存在的比如Object obj = new Object()
这类引用 只要强引用还在 垃圾收集器永远不会回收这些被引用对象 -
软引用(Soft Reference)
软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference
类来表示。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。import java.lang.ref.SoftReference; public class Main { public static void main(String[] args) { SoftReference<String> sr = new SoftReference<String>(new String("hello")); System.out.println(sr.get()); } }
-
弱引用(Weak Reference)
弱引用也是来描述非必需对象的,但是它的强度比软引用更弱一些。它只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时 不管此时内存够不够都会回收掉被弱引用关联的对象。在java中,用java.lang.ref.WeakReference
类来表示。import java.lang.ref.WeakReference; public class Main { public static void main(String[] args) { WeakReference<String> sr = new WeakReference<String>(new String("hello")); System.out.println(sr.get()); System.gc(); //通知JVM的gc进行垃圾回收 System.out.println(sr.get()); } }
在使用软引用和弱引用的时候,我们可以显示地通过System.gc()来通知JVM进行垃圾回收,但是要注意的是,虽然发出了通知,JVM不一定会立刻执行,也就是说这句是无法确保此时JVM一定会进行垃圾回收的。 -
虚引用(Phantom Reference)
虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。虚引用的目的就是能在这个对象被收集器回收时收到一个系统通知 在java中用java.lang.ref.PhantomReference
类表示
要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。import java.lang.ref.PhantomReference; import java.lang.ref.ReferenceQueue; public class Main { public static void main(String[] args) { ReferenceQueue<String> queue = new ReferenceQueue<String>(); PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue); System.out.println(pr.get()); } }
虚引用中有一个构造函数,可以看出,其必须和一个引用队列一起存在。get()方法永远返回null,因为虚引用永远不可达。
3.生存还是死亡
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。
标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。
- 第一次标记并进行一次筛选。
-
筛选的条件是此对象是否有必要执行finalize()方法。
当对象没有覆盖finalize方法,或者finzlize方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,对象被回收。2.第二次标记
-
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为:F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象finalize()方法中执行缓慢,或者发生死循环(更极端的情况),将很可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。
Finalize()方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己----只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。记住 任何一个对象的finalize方法都只会被执行一次
其实这个方法的用途就是一种自我安慰 finalize所做的工作使用try-finally或者其他方法完全可以做的更好 所以大家可以忘掉这个方法
4. 如何判断一个常量是废弃常量
运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?
假如在常量池中存在字符串 “abc”,如果当前没有任何String对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,“abc” 就会被系统清理出常量池。
注意:JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
5. 如何判断一个类是无用的类
方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是 “无用的类” :
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。