一、前言
这是我的JVM
系列博客第三篇,这篇博客来讲一讲JVM
中,对象的分配与回收策略。即对象是如何在堆中存放,以及垃圾回收何时被触发,如何触发等内容。以下内容是建立在已经知道Java
垃圾回收算法的基础上描述的,如果对垃圾回收算法不了解,应该先去看看这一部分的内容,可以看看这篇博客:Java中的垃圾回收算法详解。
二、正文
2.1 堆内存的划分以及使用的垃圾回收算法
在介绍如何回收之前,我们先需要了解堆内存的划分。堆内存是Java
内存模型中最大的一块(Java
的内存模型可以参考这篇博客:浅析Java的内存模型 ),它存在的唯一目的就是存放对象。JVM
为了更好地管理对象以及进行垃圾回收,将对堆内存划分为两大区域,即新生代和老年代:
- 新生代:用来存放生命周期短的对象。由于这一块内存中的对象存活时间较短,且对象首先在这里被分配,所以频繁发生垃圾回收,而且每次回收一般都能释放大量空间;
- 老年代:用来存放生命周期长的对象。新生代中存活了较长时间的对象会被迁移到这里(当然,对象进入老年代不仅仅只有这一个方法),所以这里存放的对象生命周期一般较长,所以这一块区域发生垃圾回收的频率较低,每次回收释放的空间也较少;
对于新生代而言,这一块区域中的对象存活时间短,每一次垃圾回收都能回收大部分内存,所以适合使用复制算法进行垃圾回收,同时以老年代作为这个算法的担保空间;对于老年代而言,每次垃圾回收只能释放小部分空间,若使用复制算法,每次将需要做大量复制,而且此时Survivor
需要较大的空间,所以不适合使用复制算法,因此在老年代中,一般使用标记—清除或者标记—整理算法进行垃圾回收。
2.2 对象的分配策略
由于新生代使用的是复制算法,所以新生代的内存被划分为三个空间,即一个较大的Eden
空间用来为新对象分配空间,两个较小的Survivor
空间复制垃圾回收后存活的对象。下面就来介绍一下对象的分配原则。
(1)对象优先在Eden空间分配
当我们创建一个对象时,若没有特殊情况,这个对象会被分配在新生代的Eden
空间。此时,若Eden
空间不足,无法提供这个对象所需的空间时,将会触发垃圾回收机制,清理Eden
空间,为新对象腾出更多空间。在进行垃圾回收时,会将Eden
中仍然存活的对象放入一个空闲的Survivor
中,但是若Survivor
空间不足以存放这些存活的对象,则由于担保机制的存在,这些对象会被放入到老年代中。
(2)大对象直接在老年代分配
假设我们在代码中创建了一个很大的对象(比如数组或较长的字符串),而这个对象在很长一段时间都要使用,不会轻易被当作垃圾回收。这时候将面临一个问题:若将这个对象分配在新生代的Eden
中,每次进行垃圾回收时,都需要将这个大对象复制到Survivor
中保留,这个复制过程是一笔很大的开销,而且由于大对象占用了大量空间,垃圾回收将会频繁发生。所以,为了避免这种情况的发生,对于较大的对象,将会被直接分配到老年代中,原因是老年代发生垃圾回收的频率较低,而且不是使用复制算法进行垃圾回收。
那如何判断一个对象是否属于大对象呢?JVM
提供一个参数-XX:PretenureSizeThreshold
,通过这个参数来设定多大属于大对象。当需要为一个对象分配空间时,若此对象所需的空间大于这个参数的值,就会被判定为一个大对象,从而在老年代中为其分配空间。
(3)长期存活的对象将进入老年代
这个原则合情合理,老年代存在的首要目的,就是存放生命周期较长的对象,所以对于新生代中存活了很长时间的对象,就应该把他们移入老年代,而不是一直留在新生代中占用空间。每一个对象都有一个自己的年龄计数器,记录了自己的存活周期。对于新生代中的对象,初始时刻它的年龄计数器为0
,每经历一次垃圾回收后,年龄计数器+1
。当计数器的值到达设定好的阈值时(默认是15
),就证明这个对象是一个”老油条“,于是将它转入到老年代中。
对于生命周期长的对象,由于不会轻易死亡,所以每一次垃圾回收都会被它拖慢,而且垃圾回收对于短时间内不会死亡的对象也没有意义,所以不应该将它留在垃圾回收频繁的新生代占用空间,这就是需要将这种对象转让老年代的理由。JVM
中也提供了一个参数-XX:MaxTenuringThreshold
来设置对象进入老年代的阈值,当对象的年龄计数器超过这个值时将进入老年代。
(4)动态年龄判断
对于新生代中的对象,并不一定需要年龄计数器到达阈值才被放入老年代,有一种特殊情况会导致对象直接进入老年代。当新生代的Survivor
空间中,某一个年龄的对象相加,所占空间总和超过了Survivor
空间的一半,则大于或等于这个年龄的对象将直接进入老年代,而不需要到达阈值。比如说,在Survivor
空间中,年龄为5
的对象相加,所占空间超过了Survivor
总空间的一半,则所有年龄>=5
的对象,会被直接转入老年代。
2.3 空间分配的担保机制
前面我们提到了,老年代为新生代的垃圾回收提供了担保,即当新生代发生垃圾回收后,Survivor
空间不足以容纳所有存活的对象,则这些对象需要放入到老年代中,下面就来说说老年代如何最大效率地提供这种服务。
在新生代发生垃圾回收前,老年代会统计一下它的剩余内存,看看是否装下新生代中的所有对象,若能够装下,说明新生代可以直接进行垃圾回收,老年代完全能够保证有空间为他做担保;但是,若老年代所剩空间不足以装下新生代的所有对象,则就需要深思熟虑了:老年代剩下的空间虽然不足以装下新生代的全部对象,但是只要能够装下新生代垃圾回收后剩下的对象就行了。然而,这里并不确定新生代在垃圾回收后,会剩下多少对象,可能很多也可能很少,老年代此时如果直接允许新生代进行垃圾回收,将会面临需要做担保时无法装下其剩余对象的风险。这里JVM
会通过概率学进行判断:即JVM
统计之前发生担保时,需要提供的空间的平均值,若老年代当前剩余空间大于平均值,表示大概率能够担保成功,于是新生代可以直接进行垃圾回收;若小于这个平均值,则老年代自己会进行一次垃圾回收,腾出一部分空间,再让新生代进行垃圾回收。当老年代发生了垃圾回收后,还是无法满足空间的分配,JVM就会抛出内存溢出异常。
通过上面的步骤可以看出,老年代在提供担保的同时,尽力避免自己发生垃圾回收,只有在自己无法保证能够担保的情况下,才会进行垃圾回收。这样做的目的就是为了提高新生代垃圾回收的速度。
三、总结
以上就是对象在堆中的分配与回收策略,理解这一部分内容,对我们日常对创建对象的理解有很大的帮助。
四、参考
- 《深入理解Java虚拟机》