Java 垃圾回收机制

来源:互联网 发布:淘宝详情页950 编辑:程序博客网 时间:2024/06/04 18:41

1. 虚拟机内存的管理

Java虚拟机在运行Java程序的过程中将由其所管理的内存分为几个不同的数据区域:
方法区、堆、虚拟机栈、本地方法栈、程序计数器,下面来简单介绍各个数据区的主要功能:

程序计数器:程序计数器是用来指示当前线程执行的字节码的行号。为了使线程切换后能够恢复到正确的位置继续执行,所以每个线程都有一个独立的程序计数器,因此,程序计数器区域是属于线程私有的。

虚拟机栈:虚拟机栈描述的是Java方法执行时的内存模型。每个方法在执行时都会创建一个栈帧用于存储局部变量、方法出口等信息,每个方法从开始执行到结束的过程,对应着栈帧从虚拟机栈进栈到出栈的过程。

本地方法栈:本地方法栈的功能与虚拟机栈的功能非常相似,只不过本地方法栈中执行的是Native方法。

堆:堆是Java虚拟机管理的内存中最大的一块数据区,该内存区域的主要作用就是存放对象实例,几乎所有的对象实例都会在堆上进行分配。堆也是Java垃圾收集器管理的主要区域。

方法区:方法区主要用于存储虚拟机加载的类信息、常量、静态变量等数据。

2. 垃圾对象的判断

2.1 引用计数法

引用计数法的思想:给每一个对象添加一个引用计数器,当在有其他的地方引用该对象的时候,引用计数器加1;当其他地方对它的引用失效的时候,引用计数器减1;当引用计数器的值为0时,就代表这个对象不会再被使用了,可以判定其为垃圾对象。
引用计数法的实现简单,判定高效,但是无法解决对象之间的相互循环引用问题,所以说主流的虚拟机都不是采用引用计数算法来管理内存。
对象的循环引用示例如下:

public class Main3 {    public Main3 instance;    public void testGC(){        Main3 object1=new Main3();        Main3 object2=new Main3();        object1.instance=object2;        object2.instance=object1;        object1=null;        object2=null;        System.gc();    }}

2.2 可达性分析算法

在主流虚拟机的实现中,都是通过可达性分析算法来管理对象。
可达性分析算法的思想为:从一系列称为“GC Roots”的对象出发,开始向下搜索,搜索所走过的路径称为引用链,当一个对象和GC Roots没有任何的引用链相连接的时候,即从GC Roots出发到该对象不可达,那么该对象是不可用的,将会被判定为可回收的对象。
示例如下:

object5, object6, object7到GC Roots是不可达的,将会被标记为可回收。
在Java语言中,可以作为GC Roots对象的有以下几种:
1.虚拟机栈中(本地变量表)引用的对象;
2.方法区中类静态属性引用的对象;
3.方法区中常量引用的对象;
4.本地方法栈中引用的对象;

2.3 引用

无论是通过引用计数法,还是通过可达性分析算法来判断对象是否存活,都与“引用”有关。
在JDK1.2之后,Java将引用分为强引用、软引用、弱引用、虚引用4种,这4种引用强度一次减弱。

强引用:是指普遍存在于程序中,类似于“Object o=new Object()”这样引用,只要强引用还在,那么垃圾回收器都不会回收被引用的对象。

软引用:是指有用但是非必须的对象的引用。软引用的对象,在虚拟机将要发生内存溢出异常之前,会将这些对象作为可回收的对象进行二次回收。

弱引用:也是用来描述非必须对象。弱引用的对象,当垃圾收集器工作时,无论当前内存是否充足,都会被垃圾回收器回收。

虚引用:是最弱的一种引用关系。一个对象上的虚引用并不会影响其生存时间,同时通过虚引用也无法获得相应的对象实例。为一个对象添加虚引用仅仅是为了在该对象被回收时获得一个系统通知。

3. 典型垃圾回收算法

3.1 标记-清除算法

标记-清除算法是最基础的垃圾回收算法。标记-清除算法主要包括”标记“和”清除“两个阶段:首先标记出需要回收的对象,然后统一对所标记的对象进行回收。
标记-清除算法之所以说是最基础的算法,是因为后续的垃圾回收算法都是在标记-清除算法的基础上,改进其不足之处而来。
标记-清除算法主要有两个不足:
一是效率问题,标记和清除过程的效率都不高;
二是空间问题,标记清除后会产生大量不连续的内存空间碎片,使得程序在需要为较大的对象分配空间时找不到足够大的连续内存空间,导致提前再一次发生垃圾回收动作。

3.2 复制算法

复制算法的思想是将内存分为大小相等的两个部分,每次只使用内存的一半,当需要进行垃圾回收时,将还存活的对象复制到另一半内存空间,将已使用的那一半内存空间整体清除回收。这样使得每次都是对内存的整个半区进行回收,为对象分配内存时也只是需要简单地顺序移动另一半内存空间的堆顶指针即可,顺序分配。

但是这样就会使得真正可用的内存空间只有内存空间的一半,代价太高;同时当存活的对象比较多时,将会产生大量的复制,效率不高。

复制算法通常应用于堆内存中新生代的垃圾回收算法。因为新生代中的对象98%都是”朝生夕死“,所以并不需要按照1:1的比例划分新生代的内存中间。通常将新生代内存空间划分为一块较大的Eden区和两块相同的较小的Survivor0和Survivor1区。HotSpot虚拟机默认的Eden区和Survivor区的大小比例为8:1.
每次使用Eden区和一块Survivor0区的空间来为对象分配空间。当进行垃圾回收时,将Eden区和Survivor0区中存活的对象复制到另外一块Survivor1区,然后对Eden区和Survivor0区整体清除回收,将Survivor1区中保存的对象复制到Survivor0区中,保持Survivor1区为空。这样在新生代中真正可用的内存空间为原内存空间的90%(10%+80%),只有10%的空间是被浪费掉的。
当Eden区和Survivor0区中存活下来的对象大于Survivor1区的空间大小时,那么就需要老年代内存区进行分配担保。

3.3 标记-整理算法

标记-整理算法的思想是将存活的对象统一移动到内存空间的一端,然后将端边界以外的空间全部一次性回收。
标记整理算法主要用于老年代的内存管理。

3.4 分代收集算法

现在主流的商业虚拟机都是采用的”分代回收算法“,一般根据对象的存活周期将Java堆分为新生代和老年代,这样便可以根据新生代和老年代的不同特点,选择最佳的垃圾回收算法。新生代中,每次进行垃圾回收时,都会有大批量的对象死亡,只有少量存活,那么可以采用复制算法,只需要花费少量的复制代价便能完成对新生代的垃圾回收;而在老年代中,对象的存活率高,并且没有其他的内存空间作为分配担保,通常采用”标记-整理”算法。

4. 垃圾回收的触发

1)在通常情况下,对象优先在Eden区进行分配。当Eden区没有足够的空间为对象分配空间时,将会发生一次Minor GC;
2)大对象通常直接在老年代中分配,这样可以避免在新生代中产生的大量的复制。典型的大对象有很长的字符串以及数组;
3)虚拟机给每个对象定义一个对象年龄计数器,当每当发生一次Minor GC后仍然存活的对象将会被移动到Survivor区,并将对象年龄计数器加1;当对象的年龄达到一定的岁数后,将会被放置到老年代中;可以通过参数设置这个年龄的上限值;
4)动态年龄判断,通常在Suvivor区中相同年龄的对象所占的空间大小大于等于Survivor区空间一半时,那么大于等于该年龄的对象都将进入老年代中。
5)空间分配担保,在发生Minor GC之前,通常会检查老年代中剩余的连续空间是否大于新生代中所有对象所占用的空间,如果大于,那么Minor GC是安全的;否则需要根据handlePromotionFailure判断是否允许冒险,如果允许冒险,判断老年代中剩余的连续空间是否大于历次晋升到老年代中的对象的平均大小(也就是超过Survivor空间的那部分,因为只有一个Survivor区用来暂时保存存活的对象),如果大于,则说明可以进行一次冒险;否则如果小于或者本来就不允许冒险,那么将会发生一次Full GC。

Minor GC: 从新生代中回收内存被称为Minor GC;
Major GC: 从老年代中回收内存;
Full GC: 清理整个堆包括老年代和新生代;

参考博客:http://blog.csdn.net/jiangwei0910410003/article/details/40709457