《深入理解Java虚拟机》学习笔记 GC

JVM 之 GC(Garbage Collection)基础

《深入理解Java虚拟机》学习笔记

java与C++的垃圾管理

​ 我们都知道Java与C++最大的区别就在于对内存的管理。

​ C++有析构函数,所以程序员能很明确的清除类所占用的空间。

​ Java有自己的垃圾回收机制,所以一般不需要程序员自己主动去释放空间。但是为了适应C++程序员的转型,Java增加了finalize()方法,在GC回收之前调用,可以让程序员主动去清除类所占据的内存空间。但是finalize()的执行又具有不准确性

public class Main {
    public static Main SAVE_TEST=null;

    public void isAlive(){
        System.out.println("我还活着!");
    }

    @Override
    protected void finalize()throws Throwable{
        super.finalize();
        System.out.println("内存清除方法运行");
        Main.SAVE_TEST=this;
    }

    public static void main(String[] args) throws InterruptedException {
        SAVE_TEST=new Main();
        int n=3;
        while(true) {
            if (n == 0) {
                break;
            }
            SAVE_TEST = null;
            System.gc();//主动清理内存
            //因为finalize()方法优先级很低,所以暂停一会儿等他先运行
            Thread.sleep(500);
            if (SAVE_TEST != null) {
                SAVE_TEST.isAlive();
            } else {
                System.out.println("我没了");
            }
            n--;
        }
    }
}
/**
 *内存清除方法运行
 *我还活着!
 *我没了
 *我没了
 */

​ 明明我们第一次已经调用System.gc();但是我们看到清除方法执行之后,Main类又复活过来了。

​ 这里就体现了finalize()的方法执行原理,以下部分引至《深入理解Java虚拟机》第三章的部分文字:

	如果一个对象被判定为有必要执行finalize()方法,那么这个对象将被放置在一个叫做F-Queue的队列中,
	并在稍后由一个由虚拟机自动建立的、 低优先级 的FInalizer线程去执行它,
	这里的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。

这里得出两点:

  1. Java虚拟机不保证finalize方法会被及时调用(低优先级)
  2. 不会保证它会被完全执行,甚至不保证其执行

而且finalize方法至多由GC执行一次,第一次标记时将其加入F-Queue队列。第二次再标记到,就直接将其加入“即将回收”的集合。这也是为什么之前程序,第一次Main活力,后面两次都死了。所以,在垃圾回收的时候,更多的应该依赖各个“代”的回收算法,而不是finalize()甚至书中建议,大家忘了有finalize()这个方法。

清除前的准备 判断对象是否存活

引用计数法

​ 之前我老师上课一直讲的Java的判断对象是否存活的方法就是这个方法。

原理

​ 引自《深入理解JAVA虚拟机》3.2

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;
当失去引用时,计数器值就减1;任何时刻该计数器为0的对象就是不可能再被使用的。

​ 但是,Java并不是这样做的。因为引用计数法不能解决两个对象互相引用的问题。

public class Main {
    public Object instance=null;
    //加入这个为了方便观察内存中的变化
    private byte[] bigSize=new byte[2*1024*1024];
    public static void main(String[] args) throws InterruptedException {
        Main a=new Main();
        Main b=new Main();
        a.instance=b;
        b.instance=a;
        a=null;
        b=null;
        System.gc();
    }
}
//没有a=null;b=null时GC情况
//[GC (System.gc()) [PSYoungGen: 8030K->4888K(57344K)] 8030K->4896K(188416K) 4M多
//a,b置null
//[GC (System.gc()) [PSYoungGen: 8030K->808K(57344K)] 8030K->816K(188416K) 816K

​ 上面的例子,我们看到对象a和b的instance属性都互相引用对方的Main实例。当主动将对象a和b的引用置null的时候,其在堆中的Main实例引用还被对方的instance所用。即计数器为1,按理GC的时候,这两个Mian实例并不会被释放。然而却被释放了,堆中并没有。所以证明Java并不是用这个方式判断对象是否存活。

可达性分析算法

​ 主流的Java虚拟机都是用可达性分析来判断Java对象是否还存活的。

原理

​ 通过一系列的“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链的时候,证明这个对象不可用(死了)。

能作为GC Roots的对象包括
  • 虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JIN引用的对象

准备清除 GC算法

​ 主流虚拟机采用分代搜集法来处理垃圾回收。大体分为新生代、存活代、老年代。

​ 存活代存活在新生代内存中(自己理解的不知道对不对)。

标记-清除算法

​ 最基础的回收算法。是后面所有算法的基础。虽然不怎么用。

原理

​ 在堆中将内存划分为等大的块。每个块存中三种可能:对象存活、对象可回收、未使用。

​ 首先虚拟机对所以需要回收的块标记(前面所的两次标记法)。再标记完成后统一回收所被标记的对象。

回收前:(空表示未使用)

存活回收
回收回收存活
回收回收
回收存活回收

回收后:

存活
存活
存活

这样的方法存在两个 问题

  • 标记和清除的效率都不高
  • 如果运气不好只有一个各自未使用,又来了要两个格子的对象。不得不再GC一次。

复制算法

新生代存活代处理回收算法。

原理

​ **最开始的复制算法 **:将内存分为大小相等的两块A,B。如果其中一块(A)内存用完了。就把上面存活的对象复制到另一半的内存(B)中,然后清空当前这内存块(A),然后使用另外一半内存块(B)。一直循环。因为只需要复制并且是按顺序分配内存,并且也不用担心内存连续问题。效率高了很多,但是却一直要空着一半的内存。花销太大。

改良后的复制算法 :IBM公司研究表明新生代有98%的对象都是经常死亡的对象。所以完全可以不用浪费一半。将新生代内存按8:1:1的大小分别分配给新生代、存活代A、存活代B。A,B之间还是用最初的复制算法循环使用。当回收时,直接将 新生代 和 存活代 中的 存活对象 全部复制到另一个存活代中。

​ 理论上理想情况下改良后的复制算法已经足以支撑所有的垃圾回收。但是如果运气不好,存活的对象超过存活代的大小。就需要依赖其他的内存来分担压力了。所以有了老年代。

标记-整理算法

​ 复制算法,在对象存活率较高的情况下,会进行很多的复制操作。而到了老年代的对象。进过多次回收都还坚挺。其上的对象死亡率会很低。所以有了标记整理算法。

原理

​ 标记依旧。当回收的时候,直接将存活的对象向一端移动。然后清空最后一个存活对象后面的空间。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值