Java垃圾回收机制(Gabage Collection)是Java与C/C++之间的巨大差距所在,但垃圾回收不是Java的专利,任何编程语言都可以考虑和设计垃圾回收。
垃圾回收考虑三件事情: 1,哪些内存需要回收? 2,什么时候回收? 3,怎么回收?**
Java垃圾回收自动化地帮我们清理不再使用的内存,既然是“自动化”,为什么还要了解? 答:当需要排查内存泄漏和内存溢出问题时,当垃圾回收机制成为更高并发性的瓶颈时,需要人工对这些环节进行监控和调节。
Java内存分成若干区块,分别是:程序计数器(PRogram Counter Register),虚拟机栈(VM Stack),本地方法栈(Native Method Stack),Java堆(Java Heap)和方法区(Method Area)。
其中,程序计数器,虚拟机栈和本地方法栈是与线程同生死共命运的,因此它们的管理逻辑是比较清晰的。而堆和方法区则不一样,一个接口的不同实现类所需的内存不一样,一个方法的不同分支所需的内存也不一样,这些内存的分配需要在程序运行过程中动态管理。分配与回收都是动态的,所谓Java垃圾回收,管理的就是这部分呢。
回到之前我们说过垃圾回收需要思考三个问题:1,哪些内存需要回收? 2,什么时候回收? 3,怎么回收? 这些问题需要一一解答。
或者换个说法——如何判断一个资源是否不再被使用(“死了”)。
对于Java Heap中的对象回收有两种方案:
1,简单而清晰的第一种方案——引用计数算法
给对象添加一个引用计数器,当有一个地方引用了这个对象,计数器就加一,当这个引用失效,计数器减一,当计数器值减为0,说明对象“已死”,可以回收。
引用计数法简单明了,易于实现,但存在问题,比如,无法解决循环引用情况:
如果对象A和对象B相互引用,即A中有一个属性引用B,B中有一个属性引用A。当A和B都不在被外部所使用时,他们之间的引用仍然存在,计数器值不为0,所以不会被回收。
因此,引用计数器算法有其局限性。
2,根搜索法
通过一系列被称为“GC Roots”的对象作为初始点,逐步向下游进行搜索,搜索做走过的路径称为“引用链”,当一个对象不在任何一条引用链上的时间后,说明它已经“死亡”。
在Java语言中,可作为“GC Roots”的对象如下:
1,虚拟机中的引用对象2,方法区中的类静态属性引用的对象3,方法区中常量引用的对象4,本地方法栈JNI引用的对象可以发现无论是通过引用计数器还是根搜索法,垃圾标定都与引用相关。
因此,为了赋予此更大的弹性空间,引用也有了分类之说。
引用的分类根本上是按照:当被引用的对象和GC遭遇,引用的“强硬”程度区分的。“强硬”程度由高到低,分为“强引用”,“软引用”,“弱引用”,“弱引用”。
强引用:只要强引用还在,GC永远不会回收。
软引用:还有用的引用,内存空间充裕不回收,如果回收发现仍然会超出内存,再把软引用部分回收。
弱引用:没用的引用,GC二话不说,回收!
虚引用(幽灵引用):一个对象是否存在虚引用,对其生存时间不构成影响,无法通过虚引用获得对象实例,只是在垃圾回收时,收到一个系统通知。换句话说,虚引用是辅助垃圾回收机制的设计。
这有点像商贩和城管的关系。
强引用就是证照齐全,固定门点,城管是不会管的。
软引用是也有证照,但是流动摊贩。城管可以不管你,但如果上头来检查,群众有举报,那不好意思,城管要遣走你。
弱引用就是不法小贩,遇到就绝不姑息。
虚引用就是那小贩已经跑了,剩点家什,城管收走,在账上记上一笔,收缴XXX一件,就完了。
实际实现细节不在这里深入,不保证比喻的准确,在Oracle官方博客有文章专门讨论。
回收逃逸
在一个对象被标记为垃圾等待回收过程中,有一次逃脱的机会,就是finalize()方法,当GC要标记一个对象需要回收时,首先进行一次筛选,筛选对象是否覆盖了finalize()方法或者JVM已经执行过了finalize()方法。finalize()方法是Object类下定义的方法,可以被覆盖。如果finalize()被重写并且没有被执行,就有可能执行。如果在重写的finalize()方法中给对象了一个新的引用,对象可以逃过一劫。但是如果第二次遇到GC,即finalize()方法已经执行过了,就不再会执行,直接回收。下面例子说明了这个问题(摘自《深入理解Java虚拟机》):
/** * 演示对象因为被GC而调用finalize自我拯救 * 自我拯救只能拯救一次,系统对一个对象的finalize()方法最多只会调用一次 * */public class FinalizeEscapeGC { public static FinalizeEscapeGC SAVE_HOOK=null; public void isAlive(){ System.out.println("I am still alive"); } @Override public void finalize() throws Throwable{ super.finalize(); System.out.println("Finalize method executes"); FinalizeEscapeGC.SAVE_HOOK=this;//重新建立引用,逃过一劫 } public static void main(String[] args) throws InterruptedException { SAVE_HOOK=new FinalizeEscapeGC(); //第一次垃圾回收,可以逃脱 SAVE_HOOK=null; System.gc(); //Finalizer优先级较低,暂停等待它 Thread.sleep(500); if(SAVE_HOOK!=null){ SAVE_HOOK.isAlive(); }else{ System.out.println("No, I am dead :("); } //第二次垃圾回收,逃脱失败 SAVE_HOOK=null; System.gc(); //Finalizer优先级较低,暂停等待它 Thread.sleep(500); if(SAVE_HOOK!=null){ SAVE_HOOK.isAlive(); }else{ System.out.println("No, I am dead :("); } }}输出
Finalize method executesI am still aliveNo, I am dead :(但,书上说,finalize()是一个很不推荐的方法了。
以上都是关于Java Heap中对象的,而对于方法区,略有区别:
方法区中需要回收的东西较比堆里少的多,回收的效率是比较低的,即每次回收仍然有大比率的对象存活。方法区主要回收两部分对象:废弃常量和无用类。回收废弃常量与回收堆中对象类似,根据引用来确定。无用类的判定需要满足以下条件:
1,该类的所有实例都已经被回收2,加载该类的ClassLoader已经被回收3,该类对应的java.util.Class对象没有地方在引用,无法在任何地方通过反射访问此类。上面回答了“哪些内存需要回收?”的问题,接下来要解释“怎么回收的问题?”的理论。至于具体“怎么回收”以及“什么时候回收”是由具体虚拟机实现,运行参数设置和程序员编写程序决定的。
垃圾收集算法?
我发现在我们日常整理物品,清理自己算磁盘的时候,就在实践着一些垃圾回收的理论。只不过我们没有将其理论化为具体的概念。了解了垃圾收集的具体算法理论,会发现它们就是我们整理东西时可能潜移默化用到的思路
垃圾收集算法有三个:
1,标记-清除算法 2,标记-复制算法 3,标记-整理算法
1,标记-清除算法
根据上面讲的垃圾识别理论,标定完垃圾后,将它们删除掉,就是最基础的标记-清除算法。
优点:简洁,高效 缺点:1,清除之后的内存区间可能变成了“千疮百孔”的内存碎片,如果要存储大的对象,可能找不到大的连续内存存储它。2,标记和清除的效率低。
2,标记-复制算法
根据垃圾标识方法,找到了还“生存”的对象,将它们复制到一块完整的新内存连续空间中。
优点:1,效率高,复制之后,旧的整块内存可以整体执行删除操作。2,回收后的内存空间连续 缺点:1,需要将内存分成两块进行相互复制切换,损失有效内存。2,当对象“生还率”高,复制操作开销大。
实际上,新生代的对象往往“朝生夕死”,因此不需要1:1分割内存,而是分割成一大块Eden区,和两小块Survivor区。每次程序运行时使用Eden区和一个Survivor区。垃圾回收时,将这两块的生还对象复制到另一块Survivor区上。比如Eden区和两个Survivor区的大小比例是8:1:1,那么程序运行的内存可使用率是90%。但是,存在可能,生还对象超过了Survivor区大小,因此需要另一块较大区域做担保,当超出Survivor区域容积,将对象拷贝至担保区域。所以实际内存区是有新生代和老生代之分,新生代快速进行“短命”对象的垃圾回收,老生代区给新生代区做担保,将比较“长寿”的对象放入老生代。老生代区做老生代的垃圾回收。
3,标记-整理法
标记-复制法适合于新生代对象,而对于老生代对象,标记整理法更好,标记整理法是将“生还”对象移动到内存一端的连续空间。
优点:空间连续,不降低可用内存率。适合老生代。
从上面的讨论可以发现,根据内存对象的生命周期特点,应该采取不同的收集方法,因此在实际虚拟机中,都是将内存区分成若干个区域,采用不同的收集方法,已达到整体的最优方案。
垃圾收集器
根据上面的不同收集算法,以及不同性能面向和多线程支持程度,有不同的垃圾收集器实现。垃圾收集器也在向越来越好越来越复杂发展。总体的目标是:更高吞吐量(运行用户程序时间/总时间),更高速度,更短独占线程时间发展。
垃圾回收器图:
内存分配机制 与垃圾回收器相配合的是内存分配机制:一些内存分配的规则例如:
1,优先在Eden区分配 2,大对象直接放老生代区 3,“长寿”对象放老生代等等
相关资料: http://www.importnew.com/16533.html http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html http://blog.csdn.net/qiutongyeluo/article/details/52901325 《深入理解Java虚拟机》
新闻热点
疑难解答