Java虚拟机学习记录(五)-垃圾收集器

一、前言

众所周知,Java与C的一个显著的区别在于c需要手动的去管理内存,而Java几乎不需要去做这样的处理。原因在于Java的虚拟机有一套自己的内存回收策略。

在Java虚拟机运行过程中,虚拟机栈、程序计数器、本地方法栈随着线程的生命周期的变化而变化,因此这一部分的内存是不需要额外的去回收。但是对于Java堆来说,几乎所有的对象的创建(这里之所以说几乎,是因为随着JIT编译器、对象逃逸分析和栈上分配的技术发展,部分对象不需要在堆中分配内存)都是在堆中进行,如果不作内存回收,很快就会被占满内存,那么怎么样去分析得到那些对象已经不再需要则是问题的关键。所以,要实现垃圾回收,先要判断对象是否已死,然后再对已死对象执行回收算法。下面记录几种常见的在垃圾回收中的算法

二、对象是否已死的判定——引用计数法

这种方法原理很简单,就是说每个对象增加一个引用就给他的计数器加1,当引用失效时(这里的引用失效,我觉得是说当程序的执行离开了对象的作用域,那么这个引用就算是失效了),计数器就减一。当计数器再次变为0时,这个对象就判定它已经可以回收了。

但是这种方法也有一个严重的问题,当ObjectA.instance = ObjectB;ObjectB.instance = ObjectA时,这里两个对象互相引用着导致引用计数一直不为0。因此在JVM的主流实现中,都不采用这种方法。据说python的GC算法就是引用计数加上辅助算法完成的。

三、对象是否已死的判定——可达性分析算法

可达性算法是借助树的数据结构的一个算法,通过判定一个对象是否可以通过树的根到达来确定其是否需要回收。引用一张来自网上的图片图片引用地址

可达性分析

在这里,虽然object 8,object 9,object 10,object 11,object 12都互相持有引用,但是因为从GC Roots中无法到达,所以可以判定为可回收对象。很好的解决了引用计数法的弊端。
在Java中,可作为GC Roots的对象包括:
虚拟机栈(栈帧中的本地变量表)中的引用对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI引用的对象

四、垃圾回收算法——标记-清除算法

标记-清除算法共有两个阶段。标记和清除

标记是将内存空间扫描一遍,对所有可回收的对象做标记。清除是对内存空间再做一遍扫描,清除可回收的对象。这种方法有两个问题:一是要做两遍扫描,效率不高,二是会产生大量的内存碎片(回收的对象随机分布造成),当分配一个很大的对象时很有可能找不到这样的连续空间而提前触发一次垃圾回收。因此这种算法大多数的JVM的实现都不使用。

五、垃圾回收算法——复制算法

复制算法最大的特点是将内存空间分为大小相同的两部分。当开始垃圾回收时,只需将存活的对象移到另一块没有使用的内存空间,然后将使用的一边全部回收,这样的做法简单高效,但是对内存的浪费实在是太大了。就像是你买了8G的内存条只能使用4G,你肯定是不愿意的。

但是实际上,现在的商业虚拟机都采用这种算法的优化版本来回收新生代。因为在新生代中(Java堆会分为新生代[Young Generation]和年老代[Old Generation])中的对象绝大多数都是可回收的,那么实际上每次做垃回收时,新生代中存活的对象并不多。所以并不需要按照1:1进行空间分割,一般情况下,会将新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和一块Survivor空间,当进行垃圾回收时,将这两块空间中的存活对象移到另一块空闲的Survivor空间。然后将对象全部清除。这样只有10%的内存会被浪费。

经历了几次垃圾回收都依然存活的对象会被放置到年老代中,因此年老代中的对象都是不容易被回收的。

因为没有办法保证每次的垃圾回收过程存活的对象都不超过10%,所以当Survivor空间不够用时,需要依赖其他内存(这里指年老代)进行分配担保(Handle Promotion)。

六、垃圾回收算法——标记-整理法

上面的复制算法很明显不适用于年老代,因为年老代中的对象特点是存活率大。标记-整理算法与标记-清除算法类似,但是它不是直接对对象进行清除,而是将存活的对象向一端移动,然后直接清理掉端边界意外的内存。引用一张来自网络上的图片图片引用地址

此处输入图片的描述

七、扩展一下引用的知识

在四、五的判定对象是否已死中,均涉及到了引用。在上面的表述中,似乎只有引用和死亡两种状态。但是事实上,Java规定了四种引用状态来帮助Java程序获得更好的性能。怎么去理解这个,一般来说,当Java虚拟机的内存足够时,有的对象虽然已经不需要了,但是完全没有必要将它丢弃,只有在进行垃圾回收后内存仍然不足时才将这些对象回收。这样就增加了这些对象的复用。免去了下一次使用这些对象又要重新创建的问题。很多系统的缓存功能都符合这样的应用场景。

在JDK1.2后,Java扩充了引用的概念,分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)。

a.强引用
Object obj = new Object()这种就是强引用,只要这个引用存在,无论如何JVM都不会回收这些对象

b.软引用
软引用用来描述一些还有用但并非必需的对象。用软引用的对象在系统进行过垃圾回收仍然内存不足时才会进行回收。在JDK1.2之后,提供了SoftReference类来实现软引用。

c.弱引用
弱引用也是用来描述非必需的对象,但是强度比软引用还弱一点。在下一次GC时必定会回收。

d.虚引用
虚引用对对象的生存时间构不成影响,就和没有引用与之关联一样,所以任何时候的GC都会将其回收。对一个对象设置虚引用的唯一目的就是这个对象被回收时会收到一个系统通知。在JDK1.2以后,提供了PhantomReference类来实现虚引用。

用代码来检验一下软引用、弱引用和虚引用。

package test;

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;

public class TestReference {
    public static boolean run = true;
    public static void main(String[] args){
        WeakReference<String> weakReference = new WeakReference<String>(new String("weak Reference"));
        SoftReference<String> softReference = new SoftReference<String>(new String("soft Reference"));
        final ReferenceQueue<String> queue = new ReferenceQueue<String>();
        new Thread(new Runnable() {

            @Override
            public void run() {
                while(run){
                    Object object = queue.poll();
                    if(object != null){
                        try {
                            Field referent = Reference.class.getDeclaredField("referent");
                            referent.setAccessible(true);
                            Object result = referent.get(object);
                            System.out.println("即将回收"+result.getClass()+(String)result);
                        } catch (NoSuchFieldException | SecurityException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        } catch (IllegalArgumentException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        } catch (IllegalAccessException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                }

            }
        }).start();
        PhantomReference<String> phantomReference = new PhantomReference<String>(new String("phantom Reference"), queue);

        System.out.println(weakReference.get());
        System.out.println(softReference.get());
        System.out.println(phantomReference.get());
        System.out.println("*****************下面开始GC******************");
        System.gc();        //System.gc只是建议系统进行垃圾回收,并不是立刻执行
        System.out.println(weakReference.get());
        System.out.println(softReference.get());
        System.out.println(phantomReference.get());
    }
}

上面这段代码的输出为

weak Reference
soft Reference
null
*****************下面开始GC******************
null
soft Reference
null
即将回收class java.lang.Stringphantom Reference

可以看到,软引用在gc之后仍然是存在的,而弱引用gc之后变成null了,虚引用一直为null。并且我们通过新开一个线程来检测虚引用被回收的通知。所以正确的使用soft Reference和weak Reference可以实现缓存和防止内存泄露,而虚引用一般用来实现细粒度的内存控制。比如下面代码实现一个缓存。程序在确认原来的对象要被回收之后,才申请内存创建新的缓存。Java幽灵引用的作用

八、总结

可以看出,不论是对java堆中内存空间进行分代,还是对引用进行四种类型的划分,都是为了解决在java程序运行过程中因为存在各种各样的对象,单一的垃圾回收算法没有办法高效的进行。事实上垃圾回收算法再不断的变化,每一种当前存在的垃圾回收算法都有它适应的运行环境。因此在什么时候使用什么样的垃圾回收算法,对于程序的性能来说也是至关重要的。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据