JVM垃圾收集回收策略

一、jVM垃圾收集概述

在Java内存运行时区域中,程序计数器、虚拟机栈、本地方法栈随着线程的创建和销毁而被分配和清除,栈中的栈帧随着方法的进入和退出而出栈和入栈,每一个栈帧中分配多少内存基本上是在类结构确定下来时就已经知晓的。因此Java垃圾回收中,主要关注的是堆和方法区这两个区域。

垃圾回收主要需要考虑三个问题:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

二、哪些内存需要回收

在程序中,当前没有被使用、在未来也不会被用到的对象实例毫无疑问就是需要被回收的,这种需要被回收的对象实例可被称为已死对象。因此垃圾回收的第一步就是找出已死对象,目前判断对象已死的算法有引用计数法和可达性分析法两种。

1、引用计数法

引用计数法的算法思路为:在对象中添加一个引用计数器,每当有一个地方引用它时,这个计数器的值就加一,当有引用失效时,计数器值就减一,只要计数器的值不为0,就表明这个对象尚且有被使用。但是在主流的Java虚拟机中都没有使用引用计数法,因为这个看似简单的算法还有许多例外情况需要单独考虑处理,例如对象之间的循环依赖引用问题。两个对象相互引用,但是并无其他的对象引用它们,实际上这两个对象就是不会再被使用的,但由于它们相互引用,因此未被判断为需要回收的已死对象。

2、可达性分析算法

可达性分析算法的基本思路是:通过一系列称为GC Roots的根对象作为起始结点集,从这些结点开始,根据引用关系向下搜索,搜索过程中所走过的路径称为引用链,如果某个对象到GC Roots间没有任何引用链相连,则证明该对象是不会再被使用的已死对象。在Java中,可以作为GC Roots的对象包括以下几种:

  • 在虚拟机栈的栈帧中的本地变量表所引用的对象;
  • 在方法区中类静态属性所引用的对象;
  • 在方法区中常量引用的对象,如字符串常量池中的引用;
  • 在本地方法栈中引用的对象;
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象和系统类加载器;
  • 所有被同步锁持有的对象;

3、Java中的引用

在jdk1.2之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种,这四种引用的强度依此递减。不同的引用强度描述了在垃圾收集时什么样的场景下才会对其进行回收。

强引用是指类似Object obj = new Object() 这种引用关系,在任何情况下,只要强引用关系还在,垃圾收集器就永远不会回收被强引用的对象。

软引用描述的是一些还有用,但非必须的对象。以软引用关联着的对象在系统将要发生内存溢出异常时,会把这些对象列入回收范围之中进行第二次回收,第二次回收还没有足够的内存,才会抛出内存溢出异常。

弱引用也是用来描述那些非必须的对象,与软引用的区别是,无论内存是否充足,在垃圾回收时就会对其进行回收。

虚引用是最弱的一种引用关系,虚引用对对象的生存时间完全不构成影响,也不能通过虚引用获得一个对象实例,为一个对象设置虚引用唯一的目的就只是为了能在这个对象被收集器回收时收到一个通知。

4、对象并不是标记一次即判为死亡

要真正宣告一个对象死亡,至少要经过两次标记过程:如果对象在进行可达性分析后发现没有与其相连的引用链,那它将会被第一次标记,随后会进行一次筛选,筛选的条件是此对象是否有必要执行它的finalize()方法,假如对象没有覆盖finalize()方法,或者finalize()方法已经被调用过了,就会判定为不需要执行;
如果需要执行finalize()方法,那么虚拟机会将该对象放置在一个名为F-Queue的队列中,并在稍后由一条虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。finalize方法是对象摆脱初步已死标记的最后一次机会,收集器会在稍后对F-Queue中的对象进行第二次小规模的标记,如果对象在finalize方法中将自己与引用链上的任何一个对象重新建立关联关系,那它在第二次标记时就会被移出即将回收的集合。

三、垃圾收集算法

1、分代收集理论

分代收集理论的设计原则是将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。分代收集理论建立在两个分代假说之上:

  • 弱分代假说:绝大多数对象都是朝生夕灭的;
  • 强分代假说:熬过多次垃圾收集过程的对象就越难以消亡,即熬过多次垃圾回收的对象在程序中还会使用到。

因此,如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低的回收代价回收到大量空间;如果把难以消亡的对象集中放在一起,虚拟机就可以使用较低的频率来回收这个区域。

将Java堆划分出不同的区域后,垃圾收集器就可以每次只回收其中某一个或者某些部分的区域,如Minor GC(新生代收集),Major GC(老年代收集),Full GC(收集整个Java堆和方法区);根据不同区域里存储对象的存亡特征发展出了一系列相匹配的垃圾收集算法,如标记清除,标记复制,标记整理等。

基于分代收集理论的收集器一般会将Java堆至少划分为新生代和老年代两个区域。在新生代中,每次垃圾收集时通常都会有大量的对象死去,每次回收后存活的少量对象,将会逐步晋升到老年代中存放。但分代收集并非至是简单的划分内存,因为还存在对象之间的跨代引用等问题。新生代中的对象是是完全有可能被老年代引用的,为了找出该区域中的存货对象,就不得不在固定的GC Roots之外,再额外遍历整个老年代中所有对象来确保可达性分析结果的正确性,但这样无疑会为内存回收带来很大的性能负担。

因此,分代收集理论还有第三条经验法则:跨代引用假说,跨代引用相对于同代引用来说仅占极少数。根据这条假说,只需新生代中建立一个被称为记忆集的数据结构,这个结构把老年代划分为若干小块,标识出老年代哪一块内存会存在跨代引用,此后发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。

2、标记清除算法

标记清除算法是最早出现也是最基础的垃圾收集算法,算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来标记存活的对象,统一回收所有未被标记的对象。
标记清除的优点是简单,但主要有两个缺点:

  • 第一个是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要回收的,这时必须进行进行大量标记和清除动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;
  • 第二个是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发一次垃圾收集。

3、标记复制算法

标记复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中一块,当这一块的内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用过内存空间一次清理掉。
标记复制算法的优点是分配内存时不需要考虑有空间碎片的问题,因为分配内存时只需移动堆顶指针,按顺序分配即可;不过缺点也很明显,可用内存只有原来的一半,空间浪费较大;而且存活对象较多时,需要进行大量复制,降低效率。

标记复制算法只将存活的对象进行复制保留,因此非常适合回收朝生夕灭的新生代,同时也由于新生代的这个特点,并不需要按照1比1的比例来划分新生代的内存空间,只需留较少部分内存来承接存活的对象。

新生代的基本划分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor,发生垃圾收集时,将Eden和使用的Survivor中仍然存活的对象一次性复制到另一块Survivor上,然后直接清理掉Eden和使用的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8 :1。针对垃圾回收时,存活对象大于百分之10,剩余一个Survivor无法容纳时,就会依赖其他内存区域(基本上是老年代)进行分配担保,如直接进入老年代。

4、标记整理算法

标记整理算法先对已死对象或存活对象进行标记,然后让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。在老年代中,通常大量对象都为存活对象,因此标记整理算法适合对老年代进行收集。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值