jvm内存管理-垃圾收集

java垃圾收集

文章开始前考虑一下java程序中什么才是所谓的“垃圾”,回想我们之前说的详见上篇博客 java程序运行时jvm内存分配,其中程序计数器、虚拟机栈、本地方法区都是线程私有的,随线程生和灭;栈中的栈帧随方法的开始和退出进行着入栈和出栈操作,每一个栈帧中分配多少内存基本在类结构确定下来后就已知了,这几个区域的内存分配和回收具备确定性。而java堆和方法区则不一样,一个接口中的多个实现类需要的内存不同,每个方法需要的内存也不一样,我们只有在程序处于运行期间才知道创建的对象,这部分对象的内存分配和回收都是动态的,垃圾收集器所关注的就是这部分内存。

1. 对象死了吗?

在垃圾收集前,垃圾收集器首先要判断对象是否已经“死了”(不可能再通过任何途径使用的对象)。判断对象是否存活的方式有以下几种常见的。

1.1 引用计数法(Reference Counting

给对象添加一个引用计时器,每当有一个地方引用它,计数器值加一;当引用失效时,计数器值减一;当计数器为0时,认为该对象是不可能再被使用的。
优点: 实现简单,判定效率高;
缺点: java对象的引用关系复杂,很难解决对象之间互相循环引用的问题。如下代码所示:

public class ReferCountGCTest{
    public Object instance = null;
    private static final int _1MB = 1024*1024;
    //用于占内存
    private byte[] bigSize = new byte[2 * _1MB];
    public static void main(String []args){
    ReferCountGCTest a = new ReferCountGCTest();
    ReferCountGCTest b = new ReferCountGCTest();
    a.instance = b;
    b.instance = a;

    a = null;
    b = null;
    //gc,a和b能否被回收?
    System.gc();
    }
}

这段测试代码里面,令a.instance = b;b.instance = a;除此之外这两个对象再无引用,之后令a和b都为null,实际上这两个对象都不可能再被访问,但是它们互相引用着对方,因此各自的引用计数都不为0,则无法被gc。因此在java虚拟机中不是使用的引用计数法来管理内存。

1.2 可达性分析算法(Reachability Analysis)

主流的商用程序语言都是通过可达性分析来判断对象是否存活。算法思想为:通过一系列被称为“GC Roots”的对象作为起始点,从这些根节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链时,此对象不可达即认为此对象是不可用的。如图:
GC Roots引用链
其中对象5、6、7虽然相互之间有关联,但是都是不可达的,所以会被判定为可回收的对象。

在java语言中,可作为GC Roots的对象包括:
1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
2. 方法区中类静态属性引用的对象。
3. 方法区中常量引用的对象。
4. 本地方法栈中JNI引用的对象。

2. 垃圾收集算法

判断完什么是垃圾之后,在进行垃圾收集时候主要有有如下算法:

2.1 标记-清除算法

标记-清除算法(Mark-Sweep)是最基础的算法,后续算法都是基于其思路进行改良的。与其名字一样,算法分为标记清除两个阶段:首先标记出所有需要回收的对象,标记完成后统一回收被标记的对象。它的不足主要有两个:一个是效率低,标记和清除的效率都不高,另一个问题是清除之后会产生大量的不连续的内存碎片。空间碎片多了之后当需要给一个较大的对象分配内存时,会因为无法找到足够的连续内存而提前触发一次垃圾收集。但实际上,剩下的内存总量够这个对象分配。
优点:实现方便
缺点:效率低,会产生内存碎片

2.2 复制算法

为了解决上述问题,复制(Copying)算法出现了,它将内存按容量划分为大小相等的两块。每次只使用其中一块内存,当一块内存用完了,就将还存活的对象复制到另一块内存上,然后将已使用的内存一次清理掉,这样每次只要对半个区进行内存回收,也解决了内存碎片问题(只需要在转移时移动堆顶指针,按顺序分配内存即可),实现简单,运行高效。缺点就是将内存缩小为原来的一半,代价太大了!
优点:不会产生内存碎片,效率高于标记-清除。
缺点:牺牲了内存容量

现在的商用虚拟机都采用这种算法思想回收新生代,因为新生代的对象98%的都是“短命”的,是“朝生夕死”的。因此并不需要按1:1的比例划分内存空间,而是将新生代内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor,在回收时,将Eden和该Survivor中还存活的对象一次性复制到另一块Survivor区上,然后清理掉Eden和用过的Survivor空间。HotSpot虚拟机默认的Eden和Survivor大小比例为8:1,即新生代可用内存空间占整个新生代容量的80%+10%=90%,只有10%暂时被浪费掉。

这里插入一个说明,我们没法保证每次回收只有不多于10%的对象存活,当Survivor空间不够转移的时候,老年代空间会进行分配担保。即另一块Survivor不够存放上一次新生代收集后存活的对象时,,这些对象会进入老年代。正常情况下,对象每在Survivor区中“躲过”一次回收,“岁数”就会加一,直到一个我们设定的值后就会进入老年代

复制算法在对象存活率较高的情况下就不适用了,因为较高的存活率意味着更多的复制操作和更多的额外担保空间,因此老年代一般不直接使用复制算法。

2.3 标记-整理算法

根据老年代的特点,有人提出了“标记-整理”(Mark-Compact)算法,标记过程与“标记-清除”算法一样,但是之后不是直接对可回收对象进行清理,而是让存活的对象向一段移动,然后直接清理掉端边界以外的内存。这样不会产生碎片,也避免了大量复制。
优点:不会产生内存碎片,比复制算法节约空间
缺点:整理内存空间需要额外开销。

2.4 分代收集算法

分代收集(Generational Collection)算法本身不是新的算法思想,只是按对象各自的特点奖内存划分为几块,一般是java堆分为新生代和老年代。对于新生代,其中的对象都比较“短命”,存活率低,使用复制算法,只需要复制少量存活对象即可完成收集;对于老年代,其中对象存活率高、没有额外空间进行分配担保,需要使用“标记-清除”或者“标记-整理”算法进行回收。现在的虚拟机一般都采用“分代收集”算法进行垃圾收集。

本文讲述了java虚拟机判断对象是否死亡(成为“垃圾”)的方法,和常用的垃圾收集算法。参考《深入理解java虚拟机》

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值