Java内存回收机制
1、对象已死?
垃圾回收是对堆中对象的管理,首先就要确定什么是垃圾,即什么情况下堆中的对象可以被回收。
最常用的判定算法是引用计数算法,即每当有一个对象被其它对象所引用,则将对象的引用数+1,当对象的引用数为0时,则认为对象将不再被使用,可以回收。但引用计数算法有一个缺陷,即无法解决对象循环引用的问题。当对象相互引用时,将会给引用的双方的对象的引用计数+1,这样的话对象的引用计数将一直无法被清零,也即是说,GC(Garbage Collection)无法判定对象为可回收对象,该对象将一直占据在内存中无法被释放。
Java中使用的判定算法为根搜索算法(GC Roots Tracing),这个算法的基本思路是通过一系列名为“GC Roots”的对象作为起始点,由该起始点出发向下搜索对象,搜索走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链连接,即此对象不可达时,则认为此对象不可用。
Java中可以作为GC Roots的对象包括以下几种:
a、虚拟机栈(栈帧中的本地变量表)中的引用的对象
b、方法区中的类静态属性引用的对象
c、方法区中的常量引用的对象
d、本地方法栈中JNI的引用的对象
2、常用的垃圾收集算法
a、标记——清除算法(Mark——Sweep)
首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
缺点:
效率问题,标记和清除过程的效率不高;
空间问题,标记清除之后会产生大量的非连续空间碎片,过多的碎片将导致程序在以后的运行中找不到足够大的连续内存空间来分配给较大的对象而不得不提前触发另一次垃圾收集事件。
b、复制算法(Copying)
为了解决标记清除算法的效率问题,复制算法将可用的内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块上,然后再把已使用过的内存空间一次清理掉。
优点:
实现简单,运行高效
缺点:
对内存空间的利用不高,可用内存变成一半,这代价过高
现在的商业虚拟机基本都采用这种收集算法来回收新生代,由于新生代中的对象存活率不高,因此不需要按1:1来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survior空间,每次使用Eden和其中的一块Survior,回收时将Eden和Survior中还存活的对象一次性地拷贝到另外一块Survior上,最后清理掉Eden和刚才用过的Survior空间。(HotSpot虚拟机默认Eden和Survior大小比例是8:1)当然,Survior空间不可能一直够用,此时还需要老年代来进行分配担保(Handle Promotion)(主要是将新生代存活的大对象直接移到老年代,以减少对Survior内存空间的需求)。
c、标记——整理算法(Mark——Compact)
与标记清除算法的标记阶段相同,但标记后会将所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。这种算法一般用于老年代的内存回收上,因为老年代中对象的存活时间都比较长,可能存在100%存活的极端情况,因此不能选择Copying算法来进行回收。
d、分代收集算法(Generational Collection)
这种算法只是根据对象的存活周期的不同将内存划分为几块,一般都划分为新生代和老年代,这样可以根据各个年代的特点采用最适当的收集算法。新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,因此选取复制算法,只需要付出少量存活对象的复制成本就可以完成收集;老年代中因为对象存活率高,没有额外的空间对它进行分配担保,就必须使用“标记——清除”或是“标记——整理”算法来进行回收。在之前的三种算法中已经有所描述。
3、JVM采用的垃圾回收机制
JVM采用分代的内存回收机制:
①Young代 :
- 回收机制:因为对象数量少,所以采用复制回收。
- 组成区域:由1个Eden区和2个Survivor区构成,同一时间的两个Survivor区,一个用来保存对象,另一个是空的;每次进行Young代垃圾回收的时候,就把Eden,From中的可达对象复制到To区域中,一些生存时间长的就复制到了老年代,接着清除Eden,From空间,最后原来的To空间变为From空间,原来的From空间变为To空间。
- 对象来源:绝大多数对象先分配到Eden区,一些大的对象会直接被分配到Old代中。
- 回收频率:因为Young代对象大部分很快进入不可达状态,因此回收频率高且回收速度快。
②Old代 :
- 回收机制 :采用标记压缩算法回收。
- 对象来源 :1.对象大直接进入老年代。
- Young代中生存时间长的可达对象。
- 回收频率 :因为很少对象会死掉,所以执行频率不高,而且需要较长时间来完成。
③Permanent代 :
- 用 途 :用来装载Class,方法等信息,默认为64M,不会被回收。
- 对象来源 :eg:对于像Hibernate,Spring这类喜欢AOP动态生成类的框架,往往会生成大量的动态代理类,因此需要更多的Permanent代内存。所以我们经常在调试Hibernate,Spring的时候经常遇到java.lang.OutOfMemoryError:PermGen space的错误,这就是Permanent代内存耗尽所导致的错误。
- 回收频率 :不会被回收。
- 在JDK8中,Permanent Generation被MetaSpace取代。Metaspace分配在native memory。默认情况下,MetaSpace分配受到可用的本机内存容量的限制(容量依然取决于你使用32位JVM还是64位操作系统的虚拟内存的可用性)。一个新的参数 (MaxMetaspaceSize)可以用来限制用于MetaSpace的本地内存。如果没有特别指定,MetaSpace将会根据应用程序在运行时的需求动态设置大小。 如果MetaSpace的空间占用达到参数“MaxMetaspaceSize”设置的值,将会触发对死亡对象和类加载器的垃圾回收。