您的位置:首页 > 编程语言 > Java开发

java的垃圾回收

2015-08-20 10:41 323 查看
jvm的垃圾回收是个老生常谈的问题,在这里,我会从以下一个方面来和大家聊聊垃圾回收。

1 在哪里收垃圾?

2 哪些内容可认为是垃圾?

3 怎么回收垃圾?

在哪里收垃圾

这里,我建议大家先读一下拙作: java内存管理



上图中的5部分:

虚拟机栈,本地方法栈,程序计数器三个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(尽管在运行期会由JIT编译器进行一些优化,但在本章基于概念模型的讨论中,大体上可以认为是编译期可知的),因此这几个区域的内存分配和回收都具备确定性,在这几个区域内不需要过多考虑回收的问题,因为方法结束或线程结束时,内存自然就跟随着回收了。

方法区,是各个线程所共享的。它存储已被虚拟机加载的类信息,常亮,静态变量,即时编译器编译后的代码等等。同时里面还包含了运行时常量池, 用于存放编译期间的各种字面量和符号引用。Java虚拟机规范中说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区进行垃圾收集的“性价比”一般比较低。在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70%~95%的空间,而永久代的垃圾收集效率远低于此。

方法区永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。

方法区也成永久代(permanent generation)

最后剩下的就是堆了。

java堆是java虚拟机管理的最大一块内存。且被所有线程共享。这块区域在虚拟机启动时就会创建。它存在的唯一目的就是存放对象。

我们主要就是在堆里回收垃圾

我们先看看java中堆的组成



如上图所示,整个堆被分为两部分,新生代与老生代(Tenured Gen)。

其中新生代又分为三部分,一个eden区(伊甸区,呵呵)和两个Survivor区,分别是from Survivor与to Survivor。

各区域的默认比例在上图中已经给出了。

那些内容可被认为是垃圾?

不能被访问到的对象就是垃圾

那么我们如何判断一个对象已经无法被访问到呢?这里至少有两种算法。

引用计数法

Person a=new Person("name1");
Person b=a;
pserson c=a;
c=new Person("name2");


在上面的代码中,new Person在堆里产生了一个对象,这个对象被引用了三次。后面c=new Person("name2"),原来的那个对象的被引用次数就较少一次,成了2。

我们可以给记录每个对象被引用的次数。如果什么时候,一个对象的被引用次数成了0,那么我们就认为它是垃圾,可以被清除了。

这个方法OK吗?我们看下面的例子

public class ReferenceCountingGC {
public static void main(String[] args) {
testGC();
}
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();
}
}
逻辑上来说,obja与objb已经无法被访问到了,它就是垃圾#应该被清除,但是从引用计数法上来说,obj1与ojb2的被引用数都不为0,他们不应该被清除#

我们读gc报告后,就能知道,这个两个对象都已经被清除了,这就说明java并没有用引用计数法来判定对象是否不可达#

(怎么对gc的报告,一会再说)另外,java不用引用技术法并不能说明这个方法不好,Python就用引用法来管理

跟搜索算法

这个方法逻辑上也很简单



上图中,从根可以到达object1234,但是object5,object6,object7是我们无法到达的。所有我们认为object5,6,7就是垃圾,就是应该回收的。

那么问题来了,跟是什么?

在Java语言里,可作为GC Roots的对象包括下面几种:

    虚拟机栈(栈帧中的本地变量表)中的引用的对象。

    方法区中的类静态属性引用的对象。

    方法区中的常量引用的对象。

    本地方法栈中JNI(即一般说的Native方法)的引用的对象。

怎么回收垃圾(垃圾回收算法)

这里也至少有三种算法

标记-回收算法

第一步:根据上面的跟搜索算法,确定那些对象是应该清除的,并且标记这个对象

第二步:根据第一步做的标记,清除对象。

示意图如下:



这算法的缺陷很明显:有内存碎片的问题。

复制算法

算法的前提是:将内存区域分成两部分,并且每次只使用一部分

回收垃圾时

第一步:把正在使用部分中的存活对象,复制到第二块内存上

第二步:将第一块内存完全擦除。

示意图如下:



这个算法的优势在于:没有碎片,但是劣势但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。而且复制算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

标记整理算法

这个算法与标记-回收算法类似

第一步:标记出存活对象,垃圾对象。

第二步:将存活对象向内存的一端移动。

第三步:清除掉边界外的内存。

示意图如下:



分代回收法

堆的分代情况在上文已经说到了。

在新生代我们采用的是复制算法。

老生代采用的是标记-整理算法或标记清理算法。

这里就牵扯了一个新问题,对象在内存中,是怎么分配的?

如果是小对象,直接优先放到新生代的eden;如果是大对象,就直接放到老生代。

如果在新生代分配内存时,发现不够用了,就使用复制算法,将eden和一个survivor区的存活对象复制到另一个suvivor区。

等于说是,用10%的区域来存放90%的内容。

这个能放下吗?

大部分情况下,答案都是肯定的,因为ibm有研究表明新生代的对象有98%是朝生夕死的。

那如果只占10%的survivor区域真的存不下90%区域中的存活对象呢?

答案是:把这些对象放到老年代中。

如果新生代的某个对象经历了15次垃圾回收都没有死,那把它也放到老生代里。

那如果新生代,老生代都满了呢?

不是还有一个异常叫OutOfMemoryError: Java heap space么?

另外,垃圾收集的动作也有两种:

新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。

老年代 GC(Major GC / Full GC):指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 ParallelScavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程)。MajorGC 的速度一般会比 Minor GC 慢 10倍以上。

那么大对象,小对象的边界是什么?多大算大,多小算小呢?jvm有一个参数来设定这个值,大家有兴趣的百度之。

下一节,我们看看几个gc的实例及gc报告的阅读

参考资料

深入理解java虚拟机 第三章

http://www.cnblogs.com/dolphin0520/p/3783345.html

http://www.th7.cn/Program/java/201409/276272.shtml

http://www.cnblogs.com/gw811/archive/2012/10/19/2730258.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: