Java 的垃圾回收策略

栏目: Java · 发布时间: 7年前

内容简介:Java 的垃圾回收策略

当程序 创建对象,数组引用类型实体 时,系统都会在 堆内存 中为之分配一块内存区,对象就保存在这块内存区,当这块内存不再被任何变量引用时,这块内存就变成垃圾,系统就要回收。

  • 只回收堆内存中对象,不会回收物理资源
  • 程序无法精确控制回收时机。
  • 在垃圾回收机制回收任何对象之前,总会先调用它的 finlize() 方法,可能导致 垃圾回收机制取消

如何判断对象是否已经死亡?

引用计数算法

古老的判断对象是否存活的算法是:给对象添加一个计算器,一旦有地方引用该对象,则计数器加一,当引用失效时,就减一。任何计数器为 0 的对象,就不能再使用了。这种算法虽然经典,但是其并不能解决一个对象之间相互循环引用的问题。

public class RefreenceCount{
  
  class GcObject{
  	public Object instance = null;
}
  
  public static void main(String[] args){
	GcObject o1 = new GcObject(); 
    GcObject o2 = new GcObject();
    
    o1.instance = o2; // 1
    o2.instance = o1; // 2
    
    o1 = null;
    o2 = null;
}
}

如上我们在最后注释一和注释二处还是无法释放对方。这样会造成堆溢出。

可达性分析算法

主流的语言中都是通过可达性算法分析对象是否存活的:通过一系列称为 “GC Roots” 对象作为起始点,从这些节点开始向下搜索,其所走过的路径称为引用链,当一个对象到 GC Roots 不可达时,则该对象是不可用的,也就是判断这些对象为可回收的。

Java 语言中,如下可作为 GC Roots 对象:

  • 虚拟机栈中引用的本地对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI 引用的对象

而可达性算法,我们可以看一个非常好的示例。 垃圾回收机制中,引用计数法是如何维护所有对象引用的

再谈引用

前面所说的两种都是跟引用有关系。在 JDK1.2 之前,引用的定义很狭隘,一个对象只有被引用和未被引用的两种状态。在 JDK1.2 之后,对引用的定义进行了扩充,我们希望:当内存空间还足够时候,则对象能够保留在内存中;如果内存空间在进行垃圾回收之后,内存空间还是非常紧张,则抛弃这些对象。适合于很多缓存功能的应用场景。分成四种强度递减的引用:

  • 强引用:这种引用很常见,类似于 :

    A a = new A()// 强引用

    只要引用存在,就不会回收引用对象。如果显示的将对象置为 null 或者其超出对象的生命范围,则被回收。当内存空间不足,Java虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题

  • 软引用:如果一个对象具有软引用,内存足够时,gc不会回收它;内存不足时,gc就会回收这个对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。 软引用可用来实现内存敏感的高速缓存。

  • 弱引用:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象, 不管当前内存空间足够与否,都会回收它的内存

    public class WeakRefrenceDemo {
    
        public static void main(String[] args) {
    
            String s = new String("hello");// 1
            WeakReference<String> wrf = new WeakReference<String>(s);
            s = null;// 2
            System.out.println(wrf.get());// 3
            System.gc();// 4
            System.runFinalization();
            System.out.println(wrf.get());// 5
        }
    }

    如上,我们逐行分析。在注释 1 处,我们不能自以为聪明的用:

    String s = "hello"// 6

    来替代 1 中的语句,因为按照 6 中的做法,系统会采用常量池来管理这个字符串直接量,会采用强引用来引用它。同时在 1 处我们用 s 引用变量引用 “hello” 字符串对象,接下来创建一个弱引用对象来引用 s 引用的同一个对象。执行到 2 处时候,切断了 s 和引用对象的之间的联系。而我们不切断的话,会发生什么呢?自然 gc 机制无法发挥出实质性的作用,导致并没回收任何引用对象,故输出的还是字符串对象。

    执行到 3 处时候,由于系统内存还足够,不会回收 wrf 对象,因此会输出 “hello” 对象。而后通知程序进行强制垃圾回收,自然弱引用对象 wrf 就会被系统回收了,此时输出的就为 null 了。

    下面再看一个例子,弱引用在 Android 中的应用,我们希望在 Activity 中新建一个线程获取数据:

    public class MainActivity extends Activity {
    
        //...
        private int page;
        private Handler handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                if (msg.what == 1) {
                    //...
                    page++;
                } else {
                    //...
                }
            };
        };
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            //...
            new Thread(new Runnable() {
                @Override
                public void run() {
                    //..
                    Message msg = Message.obtain();
                    msg.what = 1;
                    //msg.obj = xx;
                    handler.sendMessage(msg);
                }
            }).start();
            //...
        }
    }

    Activity 具有自身的生命周期,Activity 中新开启的线程运行过程中,可能此时用户按下了Back键,或系统内存不足等希望回收此 Activity, 由于 Activity 中新起的线程并不会遵循 Activity 本身的什么周期,也就是说,当 Activity 执行了onDestroy,由于线程以及 Handler 的 HandleMessage 的存在, 使得系统本希望进行此Activity 内存回收不能实现,因为非静态内部类中隐性的持有对外部类的引用,导致可能存在的内存泄露问题。

    因此,在 Activity 中使用 Handler 时,一方面需要将其定义为静态内部类形式,这样可以使其与外部类(Activity)解耦,不再持有外部类的引用, 同时由于 Handler 中的 handlerMessage 一般都会多少需要访问或修改 Activity 的属性,此时,需要在 Handler 内部定义指向此 Activity 的 WeakReference, 使其不会影响到 Activity 的内存回收同时,可以在正常情况下访问到 Activity 的属性。

    google 官方推荐的一个示例写法如下:

    public class MainActivity extends Activity {
    
        //...
        private int page;
        private MyHandler mMyHandler = new MyHandler(this);
        private static class MyHandler extends Handler {
            private WeakReference<MainActivity> wrActivity;
            public MyHandler(MainActivity activity) {
                this.wrActivity = new WeakReference<MainActivity>(activity);
            }
    
            @Override
            public void handleMessage(Message msg) {
                if (wrActivity.get() == null) {
                    return;
                }
                MainActivity mActivity = wrActivity.get();
                if (msg.what == 1) {
                    //...
                    mActivity.page++;
                } else {
                    //...
                }
            }
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            //...
            new Thread(new Runnable() {
                @Override
                public void run() {
                    //..
                    Message msg = Message.obtain();
                    msg.what = 1;
                    //msg.obj = xx;
                    mMyHandler.sendMessage(msg);
                }
            }).start();
            //...
        }
    }

  • 虚引用:是一种最弱的引用关系,其存在的必要就是在该虚引用对象被垃圾回收器回收时候,系统能够接收到一个通知。

生存还是死亡

当一个对象被创建之后,根据它是否被引用变量所引用的状态,可以将其所处状态分为如下三种:

  • 可达状态:当一个变量被创建之后,有一个或者以上的变量引用它,其就处于可达状态。
  • 可恢复状态:没有任何对象来引用它,就处于可恢复状态。这种状态下,系统的垃圾回收机制准备来回收该对象所占用的内存,在回收该对象之前,系统会调用所有可恢复对象的 finalize() 方法来进行资源的清理,如果系统在清理资源的同时,重新让一个引用变量引用该对象,则该对象会再次变为可达对象,反之则该对象进入不可达状态。
  • 不可达状态:没用引用变量指向该对象,并且不能被恢复,则成为不可达状态。只有一个对象在不可达状态时候,垃圾回收器才会回收该对象。

然而即使某个对象被标记为不可达的情况下,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行根搜索后发现没有与 GC Roots 相连接的引用链,那它会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法,或 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。如果该对象被判定为有必要执行 finalize() 方法,那么这个对象将会被放置在一个名为 F-Queue 队列中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行finalize()方法。finalize() 方法是对象逃脱死亡命运的最后一次机会(因为一个对象的 finalize() 方法最多只会被系统自动调用一次),稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果要 在 finalize() 方法中成功拯救自己,只要在 finalize() 方法中让该对象重引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。接下来,我们顺便看一下 finalize() 方法:

在垃圾回收机制回收某个对象之前,通常会调用 finalize() 方法来回收一些资源。在没有明确指定回收方法之前,调用默认的 finalize() 放法,只有在调用了该方法之后,垃圾回收机制才真正的开始执行。但是我们始终要注意该方法不一定会得到执行,故我们不要指望在该方法中去执行垃圾清理的工作。finalize() 方法具有以下特点:

  • 不要主动调用某个对象的 finalize() 方法
  • finalize() 方法是否被调用具有不确定性
  • JVM 执行可恢复对象的 finalize() 方法时候,可能会是该方法重新变为可达状态
  • JVM 执行 finalzie() 方法出现异常时,不会抛出异常

接下来,我们看一个对象自我拯救的示例:

public class FinalizeEscapeGC {

    public static FinalizeEscapeGC SAVE_HOOK = null;

    public void isAlive(){
        System.out.println("yes i am still live");
    }

    // 留个心眼,这个方法每个对象只会被系统自动调用一次
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method called");
        FinalizeEscapeGC.SAVE_HOOK = this;// 注释一
    }

    public static void main(String[] args) throws Throwable {

        SAVE_HOOK = new FinalizeEscapeGC();

        // 对象第一次自我拯救成功
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);
        if (SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("no,i am dead");
        }

        // 对象第二次拯救自己失败
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);
        if (SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("no,i am dead");
        }
    }
}

输出结果:

finalize method called // 标注 finalize 被调用
yes i am still live // 逃脱成功
no,i am dead // 逃脱失败

为什么会出现对象的复活?又为什么会出现同样的代码,而对象只会复活一次?首先解决第一个问题,对象复活的奥秘完全在注释一处,我们在 finalize() 方法中执行了这样一句:

FinalizeEscapeGC.SAVE_HOOK = this;// 注释一

上面就将一个可恢复的对象变成了可达对象。而后面只能复活一次的原因是因为一个对象的 finalize() 方法只能被调用一次。

如何去回收垃圾

  • 标记-清除算法

    首先标记所有需要被清除的对象,然后进行回收,至于判定哪些对象该被回收,前面已经说过了。由于该算法是比较基础的算法,所以不可避免的带来两个问题:标记和清除效率不高的问题、空间碎片化的问题。

  • 复制算法

    复制算法是针对标记—清除算法的缺点,在其基础上进行改进而得到的,它讲 可用内存按容量 分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面,然后再把已使用过的内存空间一次清理掉。其缺点很明显,将可用内存缩小到了一半。复制算法有如下优点:

    • 每次只对一块内存进行回收,运行高效。
    • 只需移动栈顶指针,按顺序分配内存即可,实现简单。
    • 内存回收时不用考虑内存碎片的出现。
  • 标记-整理算法

    其标记过程仍然跟标记-清除算法一样。但是该算法不会直接对可回收对象进行清理,而是让所有存活的对象都向一段移动,然后直接清理端边界以外的内存。

  • 分代收集算法

    当前用的比较多的垃圾收集算法都是分代收集。根据对象存活周期的不同,将内存分为几块。一般是将 Java 堆分为新生代和老生代,根据各个年代的特点采用合适的垃圾回收算法。在新生代中每次都有大批对象死去,只有少量存活,故而采用复制算法。只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高,没有额外空间对其进行分配担保,就必须使用标记-整理或者标记-清理算法来进行回收。

内存分配策略

  • 对象优先在 Eden 分配

    大多数情况下,对象优先在新生代 Eden 区中分配,当该区域没有足够空间时候,将发生一次 MinorGC。其中我们在复制算法中提到了:新生代 98% 对象是朝生夕死,所以没必要 1:1 分布内存空间,从而产生了一块较大的 Edge 和两块 Survivor 空间。

  • 大对象直接进入老年代

  • 长期存活的对象将进入老年代

    虚拟机采用了分代会回收的思想来管理内存,故判断新老对象的标准就很重要。虚拟机给每个对象定义了一个对象年龄,如果对象在 Eden 区 出身,并且经过第一个 Minor GC 之后仍然存活,并且能被 Survivor 区域容纳的话,将被移动到 Survivor 区域中,并将对象年龄设置为1.对象在 S 区域中每熬过一次 MInor GC,年龄就增加1.当年龄增加到一定 15岁时候,就会被存入到老年代。年龄阈值可以通过设置指定参数来确定。

  • 动态对象年龄判定

  • 空间分配担保


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

构建之法(第二版)

构建之法(第二版)

邹欣 / 人民邮电出版社 / 2015-7 / 59

软件工程牵涉的范围很广, 同时也是一般院校的同学反映比较空洞乏味的课程。 但是软件工程的技术对于投身IT 产业的学生来说是非常重要的。作者邹欣有长达20年的一线软件开发经验,他利用业余时间在数所高校进行了长达6年的软件工程教学实践,总结出了在16周的时间内让同学们通过 “做中学 (Learning By Doing)” 掌握实用的软件工程技术的教学计划,并得到高校师生的积极反馈。在此基础上,作者对......一起来看看 《构建之法(第二版)》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具