GC分析(一)

    最近项目老是出现GC,所以稍微研究了一下java层的GC,GC就是垃圾收集器自动回收生命周期结束的对象,释放内存。GC分析是优化了系统性能,涉及到 CPU、CACHE、IO各个方面都要综合进行考虑,今天只讲一小部分,GC分析。

一、GC判断

首先要确定对象是否存活,现在在java中的主流的算法主要是可达性分析算法来判断对象是否需要进行垃圾回收。

1)可达性分析算法思路:是通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。

GC Roots对象指:

虚拟机栈中引用的对象

元数据区中静态变量引用的对象

元数据区中常量引用的对象

本地方法栈中native方法引用的对象


2)从可达性算法中可以看出,判断对象是否可达时,与“引用”有关。那么什么情况下可以说一个对象被引用,引用到底代表什么?

在JDK1.2之后,Java对引用的概念进行了扩充,可以将引用分为以下四类:

强引用:就是指在程序代码中普遍存在的,类似Object obj = new Object()这类似的引用,只要强引用在,垃圾回收器永远不会回收被引用的对象。也就是说,即使出现内存溢出,也不会回收这些对象。

软引用 :是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。

SoftReference sr = new SoftReference(new String("hello"));

弱引用 :也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。

WeakReference sr = new WeakReference(new String("hello"));

虚引用:不能单独使用,主要是用于追踪对象被垃圾回收的状态,为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。通过PhantomReference类和引用队列ReferenceQueue类联合使用实现。

ReferenceQueue queue = new ReferenceQueue();

PhantomReference pr = new PhantomReference(new String("hello"), queue);

二、垃圾回收算法

    现在大多数回收算法将堆内存分为新生代和老年代。因为新生代和老年代结构上的不同,所以产生了分代回收算法,即新生代的垃圾回收和老年代的垃圾回收采用的是不同的回收算法。针对新生代,主要采用复制算法,而针对老年代,通常采用标记-清除算法或者标记-整理算法来进行回收。

1、标记—清除算法

以可达性分析算法遍历所有引用,给活着的对象打上标记。遍历结束后,统一清除需要回收的对象。该算法有两个问题:

1)标记和清除过程效率不高。主要由于垃圾收集器需要从GC Roots根对象中遍历所有可达的对象,并给这些对象加上一个标记,表明此对象在清除的时候被跳过,然后在清除阶段,垃圾收集器会从Java堆中从头到尾进行遍历,如果有对象没有被打上标记,那么这个对象就会被清除。显然遍历的效率是很低的;

2)会产生很多不连续的空间碎片,所以可能会导致程序运行过程中需要分配较大的对象的时候,无法找到足够的内存而不得不提前出发一次垃圾回收。

初始状态:

图 2-1-1

运行一段时间:

图 2-1-2

基于标记-清除算法后:

图 2-1-3

2、标记—整理算法

与标记-清除算法过程一样,只不过在标记后不是对未标记的内存区域进行清理,二是让所有的存活对象都向一端移动,然后清理掉边界外的内存

初始状态:

图 2-2-1

运行一段时间后:

图 2-2-2

基于标记-整理后:

图 2-2-3

3、复制算法

复制算法是为了解决标记-清除算法的效率问题的,其思想如下:将可用内存的容量分为大小相等的两块,每次只使用其中的一块,当这一块内存使用完了,就把存活着的对象复制到另外一块上面,然后再把已使用过的内存空间清理掉。这样当垃圾收集器进行回收的时候就不用考虑空间碎片的问题,缺点在于把内存缩小为原来的一半,代价未免有点大。

初始状态:

图 2-3-1

运行一段时间:

图 2-3-2

基于复制算法后:

图 2-3-2

当然正是由于其缩小内存为原来的一半代价大的问题,现代的JVM并不是按照1:1划分内存空间的,二是将内存分为一块较大的Eden区和两块较小的Survivor区,每次使用其中的Eden和一块Survivor区。当回收的时候,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor中,最后把Eden和Survivor的空间清理出来。其实这里还有一个问题:就是如果垃圾回收后,存活的对象需要的空间大于剩余一块Survivor的空间怎么办?答案是需要依赖其他内存进行分配(这里主要指的是老年代)。

注:HotSpot虚拟机默认Eden和Survivor的大小比例是8:1:1,也就是说新生代中牺牲掉10%的空间而不是一半的空间。

4.分代收集算法(Generation Collection)

我们将内存按照 Java 生存时间分为 新生代(Young) 和 老年代(Old),前者存放时间短,后者存放时间长,当然存放时间长也是由存放时间短升级上来的。然后针对两者可以采用不同的回收算法,比如对于新生代采用复制算法会比较高效,而对老年代可以采用标记-清除或者标记-整理算法。这种算法也是最常用的。JVM Heap 分代后的划分一般如下所示,新生代一般会分为 Eden、Survivor0、Survivor1区,便于使用复制算法。

图 2-4-1

1.对象一般都是先在 Eden区创建

2.当Eden区满,触发 Young GC,此时将 Eden中还存活的对象复制到 S0中,并清空 Eden区后继续为新的对象分配内存

3.当Eden区再次满后,触发又一次的 Young GC,此时会将 Eden和S0中存活的对象复制到 S1中,然后清空Eden和S0后继续为新的对象分配内存

4.每经过一次 Young GC,存活下来的对象都会将自己存活次数加1,当达到一定次数后,会随着一次 Young GC 晋升到 Old区

5.Old区也会在合适的时机进行自己的 GC

你可能感兴趣的:(GC分析(一))