jvm高级特性(2)(判断存活对象算法,finaliza(),方法区回收)
JVM高级特性与实践(二):对象存活判定算法(引用) 与 回收
垃圾回收器GC(Garbage Collection)
于1960年诞生在MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。
GC需要完成的3件事情:
哪些内存需要回收?
什么时候回收?
如何回收?
内存的动态分配与内存回收机制似乎“自动化”起来。
了解GC和内存分配等底层知识目的:
因为在排查各种内存溢出、内存泄漏问题、垃圾收集成为系统达到更高并发量的瓶颈时,需要对这些“自动化”技术实施必要的监控和调节。
一,Java内存运行时区域的各个部分:
程序计数器、虚拟机栈、本地方法栈 3个区域随着线程而生,也随线程而灭;
栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。
每一个栈帧中分配的内存基本上是在类结构确定下来时就已知的,因此这几个区域的内存分配和回收都具备确定性,
在这几个区域内不需要过多考虑回收问题,因为方法结束或线程结束时,内存自然跟随着回收了。
而Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,
多个分支需要的内存也可能不一样,我们只有在程序处于运行期间才能知道会创建哪些对象,
这部分内存的分配和回收都是动态的,垃圾回收器关注的是这部分内存
二,堆存活对象的判断
1. 引用计数算法(Reference Counting)
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1;任何时刻计数器为0的对象就是不可能再被使用的。
效率分析
例如微软公司的COM(Component Object Model)技术、使用ActionScript3的FlashPlayer、Python语言都是用了该算法进行内存管理。
但是Java虚拟机中并没有引用该算法来管理内存,最主要的原因是它很难解决对象之间互相循环引用的问题。
举例证明
- 对象objA 和对象objB都有字段instance,赋值令
objA.instance = objB;
、objB.instance = objA;
- 互相引用着对方,导致它们引用计数不为0,所以引用计数器无法通知GC收集器回收它们。
/** * testGC()方法执行后,objA和objB会不会被GC呢? */ 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(); } }
[Full GC (System) [Tenured: 0k->210K(10240K), 0.0149142 secs] 4603K->210K(19456K),
[Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
GC日志中包含“4603K->210K”,意味着虚拟机并没有因为这两个对象互相引用就不回收它们,从侧面说明虚拟机并不是通过引用计数算法来判断对象是否存活的。
2 . 可达性分析算法(Reachability Analysis)
(Java、C#、甚至是最古老的Lisp)的实现中,都是通过可达性分析来判定对象是否存活的。
此算法的基本思路是:
通过一系列的称为“GC Roots”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),
当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是GC Roots 到这个对象不可达)时,则证明此对象时不可用的。
对象object5、object6、object7虽然互相有关联,但是它们到GCRoots是不可达的,所以它们将会被判定为可回收对象。
在Java中,可作为GCRoots的对象包括以下几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(即一般说的Native方法)引用的对象
三,finaliza()
使在可达性分析算法中不可达的对象,也并非是要宣判“死亡”的,它们暂时都处于“缓刑”阶段,要真正宣告一个对象“死亡”,首先要经历两次标记过程:
第一次标记:对象在进行可达性分析后发现没有与 GC Roots相连接的引用链,是否要执行对象的finaliza() 方法。
第二次标记:当对象没有覆盖finaliza() 方法,或者finaliza() 方法已经被虚拟机调用过,视为“没有必要执行”。
对象复活:
如果这个对象被判定为有必要执行finaliza() 方法,那么此对象将会放置在一个叫做 F-Queue 的队列中,
并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。
这里所谓的“执行”是指虚拟机会触发此方法,但并不承诺会等待它运行结束,
原因是:
如果一个对象在finaliza() 方法中执行缓慢,或者发生了死循环(更极端的情况),
将很可能导致F-Queue 队列中的其它对象永久处于等待,甚至导致整个内存回收系统崩溃。
finaliza()
方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue 队列中的对象进行第二次小规模的标记。
- 如果对象想在
finaliza()
方法中成功拯救自己,只要重新与引用链上的任何一个对象建立关联即可, - 例如把自己(this关键字)赋值给某个类变量或者对象的成员变量,这样在第二次标记时它将被移出“即将回收”的集合;
如果对象这时候还没有逃脱,基本上它就被回收了。
通过以下代码展示一个对象的finaliza()
被执行,但是它仍然可以存活的例子:
【一次对象自我拯救的演示】 /** * 此代码演示了两点: * 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 method executed!
yes, i am still alive :)
no, i am dead :(
由以上结果可知,SAVE_HOOK 对象的finalize()
方法确实被GC收集器触发过,并且在收集前成功逃脱了。
另一个值得注意的地方,代码中有两段一模一样的代码段,执行结果却是一次逃脱成功,一次失败。
这是因为任何一个对象的finalize()
方法都只会被系统调用一次,如果对象面临下一次回收,它的finalize()
方法不会再被执行,因此第二次逃脱行动失败。
有关finaliza()方法的建议:
- 需要特别说的是,
finalize()
方法,不建议开发人员使用这种方法拯救对象。 - 应当尽量避免使用它,因为它不是C/C++中的析构函数,而是Java刚诞生时为了使C/C++程序员更容易接受它所做的一个妥协。
- 它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序。
- 有些教材中描述它适合做“关闭外部资源”之类的工作,这完全是对此方法用途的一种自我安慰。
finalize()
能做的工作,使用try-finally 或者其它方法都更适合、及时,所以作者建议大家可以忘掉此方法存在。
四 . 回收方法区
在方法区中进行垃圾收集的“性价比”较低。
在堆中,尤其是新生代,常规应用进行一次垃圾收集可以回收70%~95%的空间,而方法区的效率远低于此。
方法区的垃圾收集主要回收两部分:
废弃常量和无用类。
“废弃常量”的回收
回收废弃常量与回收Java堆中的对象非常类似。
以常量池中字面量的回收为例,
例如一个字符串“abc”已经进入常量池,但是无任何String对象引用常量池的此常量,也无其它引用此字面量,“abc”常量会被系统清理出常量池。
常量池中的其他类(接口)、方法、字段的符号引用也是如此。
“无用类”回收的条件
判定一个类是否是“无用类”的3个条件:
- 该类的所有实例已被回收,也就是Java堆中不存在该类的任何实例。
- 加载该类的ClassLoader已被回收。
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法通过反射访问该类的方法。
这里仅说“可以”,并非如同“对象”不使用了就必然回收。
方法区主要应用于:
在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载功能,以保证方法区不会溢出。