Java垃圾回收机制

什么是Java垃圾回收

Java垃圾回收机制(Gabage Collection)是Java与C/C++之间的巨大差距所在,但垃圾回收不是Java的专利,任何编程语言都可以考虑和设计垃圾回收。

垃圾回收考虑三件事情:
1,哪些内存需要回收?
2,什么时候回收?
3,怎么回收?

Java垃圾回收自动化地帮我们清理不再使用的内存,既然是“自动化”,为什么还要了解?
答:当需要排查内存泄漏和内存溢出问题时,当垃圾回收机制成为更高并发性的瓶颈时,需要人工对这些环节进行监控和调节。

Java垃圾回收的管理范围?

Java内存分成若干区块,分别是:程序计数器(Program Counter Register),虚拟机栈(VM Stack),本地方法栈(Native Method Stack),Java堆(Java Heap)和方法区(Method Area)。

这里写图片描述

Java内存分区详细见:http://blog.csdn.net/picway/article/details/53933349

其中,程序计数器,虚拟机栈和本地方法栈是与线程同生死共命运的,因此它们的管理逻辑是比较清晰的。而堆和方法区则不一样,一个接口的不同实现类所需的内存不一样,一个方法的不同分支所需的内存也不一样,这些内存的分配需要在程序运行过程中动态管理。分配与回收都是动态的,所谓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引用的对象

内存泄漏和内存溢出

补充一个知识点,我们经常说内存泄漏和内存溢出。这两个名词有什么区别?
内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出;再比如要存一个超大的对象,虚拟机内存空间不足,也会内存溢出。
内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
所谓GC,是为了在JVM运行过程中尽可能保持内存空间的高可用,即防止内存溢出。但GC对于内存泄漏也是无能为力的。因为内存泄漏的对象不释放内存,存在引用,所以根据上面的垃圾标记方法,GC是识别不了发生内存泄漏的对象的。

以发生的方式来分类,内存泄漏可以分为4类:

  1. 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
  2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
  3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
  4. 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

因此虽然有垃圾回收,但Java的服务的运行维护还是需要谨慎地管理内存。而开发者能做的是尽可能的减少内存泄露的代码,对象作用域最小化原则在这里很有价值。

对象回收过程

在一个对象被标记为垃圾等待回收过程中,有一次逃脱的机会,就是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 executes
I am still alive
No, I am dead :(

但,书上说,finalize()是一个很不推荐的方法了。

方法区回收

以上都是关于Java Heap中对象的,而对于方法区,略有区别:

方法区中需要回收的东西较比堆里少的多,回收的效率是比较低的,即每次回收仍然有大比率的对象存活。方法区主要回收两部分对象:废弃常量无用类。回收废弃常量与回收堆中对象类似,根据引用来确定。无用类的判定需要满足以下条件:

1,该类的所有实例都已经被回收
2,加载该类的ClassLoader已经被回收
3,该类对应的java.util.Class对象没有地方在引用,无法在任何地方通过反射访问此类。

怎么回收

上面回答了**“哪些内存需要回收?”的问题,接下来要解释“怎么回收的问题?”的理论。至于具体“怎么回收”以及“什么时候回收”**是由具体虚拟机实现,运行参数设置和程序员编写程序决定的。

垃圾收集算法

作为垃圾回收的主要目标,Java Heap可分为新生代和老生代。这是按照变量的“寿命”长短,兼顾大小来划分的。对于不同的分类,回收的方法是不一样的。

这里写图片描述

垃圾收集算法有三个:

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,Stop-the-world vs 并发:垃圾回收和程序运行是不可能完全并行的,即当要进行垃圾回收时需要把程序的线程停下来。这种现象称为Stop-the-world。与此相对的是并发,指的是应用程序和垃圾回收可以同时运行。
2,单线程 vs 多线程(并行)
3,使用的垃圾收集算法:即上面说过的标记-清除;标记-复制;标记-整理。一些地方也把“整理”叫做“压缩”

根据不同的收集算法,以及上述概念的不同,存在着不同的垃圾收集器实现。垃圾收集器在向越来越好越来越复杂发展。总体的目标是:更高吞吐量,更高速度,更短stop-the-word时间,既并行又并发发展。

PS:需要注意的是,不存在不需要Stop-the-World,即完全并发的
垃圾收集器,生成并发的CMS(Cocurrent Mark Sweep)收集器:也只是在部分步骤并发,如下图:

几个评估GC性能的指标:
吞吐量 应用花在非GC上的时间百分比
GC负荷 与吞吐量相反,指应用花在GC上的时间百分比
暂停时间 应用花在GC stop-the-world 的时间
GC频率 顾名思义

CMS垃圾回收过程示意图:

这里写图片描述

针对不同场景选择不同垃圾收集器是具有挑战性的工作,总体来说,老生代和新生代是选用不同垃圾收集器的,具体的情况如下图。

垃圾回收器图:

这里写图片描述

G1垃圾回收器是JDK7产生,JDK8完善,有望在JDK9成为默认设置的垃圾回收器,期待它能够完成“大一统”。

内存分配机制

Java内存分配

与垃圾回收器相配合的是内存分配机制:一些内存分配的规则例如:

	1,优先在Eden区分配
	2,大对象直接放老生代区
	3,“长寿”对象放老生代
	4,超出Survivor区大小存入老生代

等等。

(1)对新生代的对象的收集称为minor GC;

(2)对旧生代的对象的收集称为Full GC;

(3)程序中主动调用System.gc()执行的GC为Full GC。

注:System.gc()并不是强制GC而是提醒JVM,应该进行一次FullGC,但是否执行JVM说了算。

Java引用分类

可以发现无论是通过引用计数器还是根搜索法,垃圾标定都与引用相关。

为了赋予GC更大的弹性,引用也有了分类之说。

引用的分类根本上是按照:当被引用的对象和GC遭遇,引用的“强硬”程度区分的。“强硬”程度由高到低,分为“强引用”>“软引用”>“弱引用”>“虚引用”。

强引用:只要强引用还在,GC永远不会回收。

软引用:还有用的引用,内存空间充裕不回收,如果回收发现仍然会超出内存,再把软引用部分回收。

弱引用:没用的引用,GC二话不说,回收!

虚引用(幽灵引用):一个对象是否存在虚引用,对其生存时间不构成影响,无法通过虚引用获得对象实例,只是在垃圾回收时,收到一个系统通知。换句话说,虚引用是辅助垃圾回收机制的设计。

这有点像商贩和城管的关系。

强引用就是证照齐全,固定门点,城管是不会管的。

软引用是也有证照,但是流动摊贩。城管可以不管你,但如果上头来检查,群众有举报,那不好意思,城管要遣走你。

弱引用就是不法小贩,遇到就绝不姑息。

虚引用就是那小贩已经跑了,剩点工具,城管收走,在账上记上一笔,收缴XXX一件,就完了。

实际实现细节不在这里深入,不保证比喻的准确,在Oracle官方博客有文章专门讨论。

相关资料:
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
http://www.cnblogs.com/sunada2005/p/3577799.html
http://blog.csdn.net/fenglibing/article/details/6321453
《深入理解Java虚拟机》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值