Java虚拟机的内存模型分为五个部分,分别是:程序计数器、Java虚拟机栈、本地方法栈、堆、方法区。程序计数器、Java虚拟机栈、本地方法栈都是线程私有的,也就是每条线程都拥有这三块区域,而且会随着线程的创建而创建,线程的结束而销毁。在这几个区域内就不需要过多的考虑回收的问题。
然而,堆和方法区中的内存清理工作就没那么容易了。
堆和方法区所有线程共享,并且都在JVM启动时创建,一直得运行到JVM停止时。因此它们没办法根据线程的创建而创建、线程的结束而释放。
堆中存放JVM运行期间的所有对象,虽然每个对象的内存大小在加载该对象所属类的时候就确定了,但究竟创建多少个对象只有在程序运行期间才能确定。
方法区中存放类信息、静态成员变量、常量。类的加载是在程序运行过程中,当需要创建这个类的对象时才会加载这个类。因此,JVM究竟要加载多少个类也需要在程序运行期间确定。 因此,堆和方法区的内存回收具有不确定性,因此垃圾收集器在回收堆和方法区内存的时候花了一些心思。
判断对象的死活
1 引用计数法:给对象添加一个引用计数器每当有一个地方引用它时,计数器就加1,当引用失效时 计数器减1,当计数器为0时表示对象不可能再被使用 表示对象已死。
这个方法效率很高,但是它无法解决对象之间的互相依赖的问题。
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; // 假设在这行发生GC,objA和objB是否能被回收? System.gc(); } public static void main(String[] args) { testGC(); } }
但是运行结果显示并未回收它们,间接说明jvm不是通过引用计数法来判断对象的死活。
2 可达性分析
通过一系列 GC Roots对象作为起点,从这些节点向下搜索 搜索的路径称为引用链 当一个对象没有任何一个引用链相连 则说明对象是不可用的。如Object 5,Object 6,Object 7 虽然有互相关联 但是GC Roots 不可达,所以他们都是可以回收的对象。
java中可以作为GC Roots的对象有下面几种
- Java虚拟机栈所引用的对象(栈帧中局部变量表中引用类型的变量所引用的对象)
- 方法区中静态属性引用的对象
- 方法区中常量所引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
java里面的各种引用的分类:这几种引用级别由高到低分别为:强引用、软引用、弱引用和虚引用。
强引用:如:Object object=new Object();
那object就是一个强引用了,垃圾回收器绝不会回收它。当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
软引用:软引用就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存 空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
弱引用:
对象的死亡与拯救
即使在可达性分析算法中不可达的对象,也不是一定会死亡的,它们暂时都处于“缓刑”阶段,要真正宣告一个对象“死亡”,至少要经历两次标记过程:
如果对象在进行可达性分析后发现没有与 GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finaliza()
方法。当对象没有覆盖finaliza()
方法,或者finaliza()
方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
如果这个对象被判定为有必要执行finaliza()
方法,那么此对象将会放置在一个叫做F-Queue
的队列中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer
线程去执行它。这里所谓的“执行”是指虚拟机会触发此方法,但并不承诺会等待它运行结束,原因是:如果一个对象在finaliza()
方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能导致F-Queue
队列中的其它对象永久处于等待,甚至导致整个内存回收系统崩溃。
/** * 此代码演示了两点: * 1.对象可以在被GC时自我拯救。 * 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次 * @author zzm */ public class FinalizeEscapeGC { public static FinalizeEscapeGC SAVE_HOOK = null; public void isAlive() { System.out.println("yes, i am still alive :)"); } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize mehtod executed!"); FinalizeEscapeGC.SAVE_HOOK = this; } public static void main(String[] args) throws Throwable { SAVE_HOOK = new FinalizeEscapeGC(); //对象第一次成功拯救自己 SAVE_HOOK = null; System.gc(); // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("no, i am dead :("); } // 下面这段代码与上面的完全相同,但是这次自救却失败了 SAVE_HOOK = null; System.gc(); // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("no, i am dead :("); } } }
值得注意的是,如果代码中有两段一模一样的代码段,执行结果却是一次逃脱成功,一次失败。这是因为任何一个对象的finalize()
方法都只会被系统调用一次,如果对象面临下一次回收,它的finalize()
方法不会再被执行,因此第二次逃脱行动失败。因此这个方法是非常不确定的,连是否会被执行都不能确定 因此这个方法无意义的。
方法区回收
很多人以为方法区(或者HotSopt VM中的永久代)是没有垃圾收集的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且性价比一般较低,在对的新生代生一般能回收70%~95%的空间,而永久代远低于此。
永久代的垃圾手机主要回收两部分内容:废弃常量和无用的类。 回收废弃常量与回收Java堆中的对象非常相似。以常量池中字面量的回收为例,若字符串“abc”已经进入常量池中,但当前系统没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用该字面量,若发生内存回收,且必要的话,该“abc”就会被系统清理出常量池。常量池中其他的类(接口)、方法、字段的符号引用与此类似。
无用的类需要满足3个条件:
(1)该类所有的实例都已经被回收,即Java堆中不存在该类的任何实例;
(2)加载该类的ClassLoader已经被回收;
(3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
虚拟机可以对满足上述3个条件的无用类进行回收,此处仅仅是“可以”,而并不是和对象一样(不使用了就必然回收)