jvm学习记录--04 垃圾回收概念与垃圾回收算法

垃圾回收

存在与内存中,但是没有被使用的对象。这些对象如果不清除,就会一直保留到程序结束。如果这些对象占据着内存空间,而其他对象需要使用内存空间的时候发现内存又不够。那么就会出现内存溢出,垃圾回收就是要清理这些没有被使用的对象,释放内存空间。

在c/c++等语言中对象的释放是通过程序员手动释放的。
在Java语言中对象的释放是由垃圾回收机制自动的释放的,不需要程序员来参与。

垃圾回收有三个需要完成的过程:
1 哪些对象需要回收
2 什么时候回收
3 怎么回收

虽然Java虚拟机中垃圾回收是自动化的处理,但是有些时候一样会出现内存溢出,或者垃圾回收成为系统瓶颈,这个时候就需要知道垃圾回收在忙工作的,需要对垃圾回收机制进行调节来解决问题。

如何判断对象是否需要被回收

引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器减1;任何时刻计数器都为0的对象就是不可能再被使用的。
引用计数算法(Reference Counting)的实现简单,判断效率也很高,在大部分情况下它都是一个不错的算法。但是Java语言中没有选用引用计数算法来管理内存,其中最主要的一个原因是它很难解决对象之间相互循环引用的问题。
例如:在testGC()方法中,对象objA和objB都有字段instance,赋值令objA.instance=objB及objB.instance=objA,除此之外这两个对象再无任何引用,实际上这两个对象都已经不能再被访问,但是它们因为相互引用着对象方,异常它们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。

根搜索算法

在主流的商用程序语言中(Java和C#),都是使用根搜索算法(GC Roots Tracing)判断对象是否存活的。这个算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的

在Java语言里,可作为GC Roots对象的包括如下几种:
a.虚拟机栈(栈桢中的本地变量表)中的引用的对象
b.方法区中的类静态属性引用的对象
c.方法区中的常量引用的对象
d.本地方法栈中JNI的引用的对象
这里写图片描述

真正的垃圾:判断可触及性

一般来说如果从根节点无法访问到对象了,那就说么这个对象可以被回收了。但是在特定的情况下,这个对象有可能“复活”自己。那么这样回收这个对象就是不合理的。
可触及性包含三种状态:

  • 可触及的:从根节点开始,可以达到这个对象。
  • 可复活的:对象的所有引用都被释放,但是可能在finalize()函数中复活。
  • 不可触及的:对象在finalize()函数被调用,并且没有复活,那么就会进入不可触及状态。

    以上三种状态:只有在不可触及的时候才能进行回收。

public class CanReliveObj {
    public static CanReliveObj obj;

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("canreliveObj finalize called");
        obj = this;
    }

    @Override
    public String toString() {
        return "I an CanReliveObj";
    }

    public static void main(String[] args) throws InterruptedException {
        obj = new CanReliveObj();
        obj = null;
        System.out.println("第一次GC");
        System.gc();
        Thread.sleep(1000);
        if(obj == null){
            System.out.println("obj is null");
        }else{
            System.out.println("obj 可用");
        }
        System.out.println("第二次GC");
        obj = null;
        System.gc();
        Thread.sleep(1000);
        if(obj == null){
            System.out.println("obj is null");
        }else{
            System.out.println("obj 可用");
        }
    }

}

输出:

第一次GC
canreliveObj finalize called
obj 可用
第二次GC
obj is null

说明:
第一次gc的时候在finalize中复活了自己,所以obj没有被回收。
第二次gc的时候由于finalize只调用一次,所以obj被回收。

引用和可触及性的强度

Java中提供了四个级别的引用:强引用,软引用,弱引用,虚引用,除了强引用之外其他三种都可用在Java.lang.ref包中看到。如图:
这里写图片描述

强引用

强引用是可触及的不会被回收。

假设下面的代码片段在方法内。

StringBuffer str = new StringBuffer("hello world");

这里写图片描述

str分配在栈空间,Stringbuffer被分配到堆空间。str就是StringBuffer的强引用。
如果继续执行一段代码

str1 = str;

这里写图片描述

任何时候强引用都不会被回收,虚拟机宁愿抛出OutOfMemoryError异常也不会回收。
强引用可以直接操作对象。
强引用不会被回收,所以可能导致内存

软引用

软引用是被强类型弱一点的引用,当堆内存不足的时候,就会被回收。
通过java.lang.ref.SoftReference实现

import java.lang.ref.SoftReference;

public class SoftReferenceTest {
    //-Xmx15M 启动
    public static void main(String[] args) {
        User u = new User(1, "张三");
        SoftReference<User> softRef = new SoftReference<User>(u);
        u = null;
        System.out.println(softRef.get());
        System.gc();
        System.out.println("after gc");
        System.out.println(softRef.get());
        byte[] bt = new byte[8*1024*1024];
        System.gc();
        System.out.println(softRef.get());
    }

    static class User{
        private int id;
        private String name;
        private byte[] bt = new byte[5*1024*1024]; 
        public int getId() {
            return id;
        }
        public void setId(int id) {
            this.id = id;
        }
        public byte[] getBt() {
            return bt;
        }
        public void setBt(byte[] bt) {
            this.bt = bt;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public User(int id,String name){
            this.id = id;
            this.name = name;
        }
        @Override
        public String toString() {
            return "id : " + id + " , name : " + name;
        }

    }
}

输出结果:

id : 1 , name : 张三
after gc
id : 1 , name : 张三
null

说明:
这里设置15M最大堆内存,实例化一个User的强引用,然后通过强引用建立一个软引用,去掉强引用。
设置一个内存为8M字节数组之后,总内存15M,字节数组8M,加上软引用中有5M,GC发现内存比较紧张,那么回收软引用。

弱引用

弱引用是比软引用还要低一点的引用,垃圾回收机制发现就会回收它,但是通常垃圾回收机制线程优先级比较低,所以弱引用可以存活一段时间,在下一次垃圾回收之前都是可以存活的。通过java.lang.ref.WeakReference使用。

虚引用

最弱的一种引用,和没有引用时一样的,通过虚引用的get获取强引用时总是失败。虚引用一般用于垃圾回收跟踪,通过java.lang.ref.PhantomReference实现

垃圾回收算法

标记-清除算法

标记清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。
标记阶段:通过根节点(GC Roots)开始,区分对象是否可用回收进行标记。
清除阶段:标记完毕之后,统一进行回收。

这里写图片描述

如果所示:回收完成之后的内存空间是不连续的,当有大对象需要存储的时候,不连续的内存空间效率远远低于连续的内存空间,这是该算法最大的缺点

复制算法

复制算法的是将原有的内存空间分成两块,每次只是用其中一块,在垃圾回收的时候,将正在使用的内存中存活的对象复制到未使用的内存块中,之后清除正在使用的内存块中的所有对象,在交换两个内存的角色,完成垃圾回收。

这里写图片描述

如图所示:将内存分为A和B两块,先使用A,在垃圾回收的时候把存活的对象复制到B,最后情况A内存。把B内存设置为使用中的。这种算法适合于垃圾对象比较多,存活对象比较少,这样复制比较快。然后进行清理效率很高。

在Java新生代串行化垃圾回收器中使用了复制的思想,新生代分为eden,from,to三个空间。其中from和to空间大小相等进行相互复制,因为在新生代垃圾对象通常多于存活对象。

这里写图片描述

标记压缩算法

复制算法适用于新生代,因为新生代比较多垃圾对象。
而标记压缩算法适用于老年代,标记压缩算法在标记清除算法的基础上做了一些优化,它不仅仅是清理了垃圾对象,还把存活的对象进行了整理,使得内存空间连续。

分代算法

将内存按照对象存活周期的不同,划分为几块。
新生代和老年代:新生代由于存活对象少,使用复制算法,老年代由于对象存活率高,无额外空间担保,使用“标记——清除”或“标记——整理”算法

分区算法

分代算法是按照对象的生命周期进行划分。
分区算法是把堆内存空间划分成多个连续的小空间,每一个小空间都是一个区域,每一个小区域单独回收。

堆内存空间越大,进行一次回收会消耗很长的时间。这样就会产生长时间的垃圾回收停顿。
而将堆内存空间分成多个小区域,每次回收不同的小区域,就会减少一次垃圾回收停顿。

垃圾回收停顿现象–stop the world

垃圾回收的任务是识别和回收垃圾对象。为了让垃圾回收正确并且高效的执行,大部分情况下会要求程序进入一个停顿的状态。停顿的状态所有的线程执行暂停,不会产生新的垃圾对象,保证程序的一致性。这个停顿期间程序会卡死,无响应。这个停顿就叫做stop-the-world,垃圾回收调优通常就是改变这个停顿的时间。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值