(4) Java GC概述

来源:互联网 发布:linux 防止dos攻击 编辑:程序博客网 时间:2024/06/06 05:52

GC,垃圾回收,JVM启动的时候除了启动进程-主线程,还有就是GC守护线程(一个JVM实例一个进程一个GC线程),它负责特定时间(运行期间某时刻触发,触发之后会根据相应的触发条件选择相应的算法清除对应空间里面的对象)对JVM内存区中的堆(包括方法区(非堆))的对象进行回收释放,是JVM对Java内存对象的一套管理机制,Java程序员不需要关心内存的分配以及回收的问题,这一切都由JVM来管理,而内存的回收则是由GC来完成。

在说明GC原理之前,先要知道,Java的GC什么时候触发开始的?前面一节说明Java堆内存划分的时候有提到过,当某个堆内存区域达到一定值得时候,会触发JVM的GC处理。具体触发有三个时候:

(1).Minor GC:当Eden区满了或达到指定值,触发Minor GC,清理Eden区和Survivor区;

(2).Major GC:当Old区满了或达到指定值,触发Major GC,清理Old区(一般也会触发Minor GC,但不一定,各个GC回收器策略不一样,耗时一般是Minor GC的10倍,因为采用的回收方法不一样)

(3).Full GC:当前两者触发,但仍内存不足,或者方法区满了,则会触发Full GC,清理内存所有对象空间(包括方法区)。

上面三个定义(貌似还没有官方定义)其实只是概念上的(貌似有的地方就认为Major GC=Full GC,认为Old区或方法区满了就触发全内存对象空间的GC),事实上,这三者并没有那么明确的区分,除了因为各个GC回收器设置的策略不一样,还因为,一般Old区对象都有Young区的引用,所以一般Major GC会引发Minor GC,从而减轻Major GC负担,而Full GC也会触发Major GC,所以你要关心的不是这三者的区别,而是当什么条件下会触发GC,以及相应的GC算法,以及执行这些算法GC的时候对程序的影响。

想要清楚GC原理,除了GC触发时间,还要要搞明白两个问题:

1.哪些对象是被认为是“垃圾”可以进行回收的?具体是如何确定的?

2.采用何种方法去回收“垃圾”对象?即GC算法。

对于第一个问题的答案,哪些对象将被认为是“垃圾”比较好理解:程序中以后无法使用的对象。但如何确定(让程序确定)这些对象是程序以后无法使用的对象呢?一种方法是引用计数法,什么是引用计数法?这就要从Java使用对象的机制来说了,Java程序使用对象都是通过引用来进行的,也就是说,你想操作对象,必须通过引用,这应该很好明白:

ClassA a = new ClassA();

这里的a就是引用,new ClassA()就是一个对象(我们平常说的对象a,指的是这个new ClassA(),这里的对象是实例对象的简称),对象都是通过引用(也就是这里的a)来进行操作的。

然后我们就想,如果一个对象某个时刻没有一个引用指向它,即对象对外没有接口,程序就不能操作这个对象,那么这个对象就是以后无法使用的对象了,例如继上面的代码之后,某个时刻有:

a = null;//或者这个时候a指向了别处,比如 a = a1;a1是ClassA类及子类对象引用。

则之前堆里的对象new ClassA()就成了可回收的“垃圾”对象(注意这里这个对象除了a,没有其他的引用指向它)。

这种方法原理简单,算法实现上也简单,只要给每一个对象添加一个引用计数的信息即可(对象头中添加一个字段,对象头即每个对象在堆里的一段描述信息),每多一个引用,该字段+1,反之-1,然后每次GC扫描这个字段,若为0则回收它。

但这种方法并没有被JVM采用,是因为它存在一个无法解决的问题:循环引用

MyObject object1 = new MyObject();//我们这里称new MyObject()对象为对象xMyObject object2 = new MyObject();//这里的new MyObject()对象称为对象y         object1.object = object2;//object1内部有一个Object引用,这个时候指向了对象yobject2.object = object1;//object2内部有一个Object引用,这个时候指向了对象x,如图所示......        object1 = null;object2 = null;//这个时候虽然这两个引用断掉了,但是对象x、y引用计数并不为0.....class MyObject{    public Object object = null;}


这种情况,如果采用引用计数方法的话,对象x、y将永远不会被回收。

(如果在object1=null;前面先消除object的引用,即增加object1.object=null;object2.object=null;这样,其实是可以消除的,但是,先不说,实际上的循环引用一般不是两两之间的简单循环,而是多个对象相互引用构成一个环,你很难去发现,另外,Java倡导的是不去管内存,也就是说,实际上你不会去刻意去写某一个引用为null,要是每次都要这样写,那Java所谓的GC意义何在?事实上,Java程序员不用去管这些,引用的消除除了某个引用被程序指向了另一个对象,还有就是方法里面局部变量的释放(整个方法帧都释放了,里面的引用都没了,当然所指向的对象引用计数-1),这也是为什么写程序的时候尽量将相关功能写成一个个子方法,再在里面调用这些方法,而不是把一个方法写的很长,这样做可不仅仅是为了方法的简洁明了,功能重用,还是为了更好的GC。)

有人这里可能还会提到,Object的finalize方法,但我要说的是,请忘记这个东西,因为这个方法当时设计出来只是因为一些历史原因(貌似是为了照顾习惯了C++内存管理的那些用户?)。(如果有兴趣的话可以自己查看)

为了解决这种问题,Java采用了一种不同的确认对象为“垃圾”的机制,那就是“GC Roots”可达性分析方法。所谓的“GC Roots”的定义可以参考这里:GC Root(冯立彬)

如果觉得上面看的不是很懂,可以基本上理解GC Roots为以下对象:

(1).栈(包括Java栈和本地栈)中引用的对象;(2).方法区对象(包括Class对象,静态对象,常量对象)

JVM为什么会使用这些对象作为GC Roots呢?我觉得可以从哪些对象在当前是有用的和对象稳定性这两点出发思考,像方法区对象,这个被称为永久代的地方,它们一般情况下是不会去清理的(当要去清理这部分的时候,他们自身就不是GC Roots,其实GC Roots也只是相对本次GC算法来说);而像栈中引用对象是因为,如果存在栈中,那肯定是会被用到的对象,是有用的对象。这里仍然可能出现循环引用,但是这里不用关心这个问题,因为,既然GC Roots本来就是我想要保留下来的对象,即使在上面出现循环引用又如何?

JVM在分析对象是否可达的时候,从GC Roots出发(事实上,GC保存着一张所有对象之间的关系有向图),被它引用及其子引用,即引用的引用,这样就形成一条引用链,这条引用链上的都是可达的,否则就是不可达的,如下图所示:


不过要注意的是被判定为不可达的对象不一定就会成为可回收对象,被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收的“垃圾”对象了(实际上,这一部分还是要看GC回收器自身的实现策略,JVM规范并没有规范这些,事实上,JVM规范对GC这一部分很宽泛)。

另外,Java中引用分为四种:

强引用:常见的引用,不可回收。ClassA a = new ClassA();这里的a就是强引用。软引用:内存足够可不回收,不足够(哪怕在GC Roots链上也会,因为你(程序员)加上这个就是认为后面哪怕回收了也没什么)回收;实现内存敏感的高速缓存。SoftReference<String> sr=new SoftReference<>(str);弱引用:更短暂的生命周期,下一次GC就被回收掉了。WeakReference<T>;虚引用:就和没有引用一样;主要用来跟踪对象被垃圾回收的活动,必须和引用队列联合使用(判断这个虚引用是否加入队列)

本节讨论的是GC的前半部分:确定可回收对象及其方法。这里仅仅讨论理论上的方法,并没有涉及具体的实现,如果有兴趣的话,可以去看各个JVM的相应部分的源码实现。

0 0
原创粉丝点击