JVM垃圾回收机制与内存分配策略
Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。
垃圾回收机制与内存分配策略
一、对象已死吗?
我们需要对堆中的对象回收,首先要判断这个对象是不是已经不需要了。可以采用:
1.引用计数法
给对象添加一个引用的计数器,当被引用的时候,这个计数器+1,反之则-1,当计数器为0的时候,对象就是不可能在被使用的。
这个方法实现简单,效率高,但是java虚拟机中却不采取这个方法。
public static void testGC(){
ReferenceCountingGC objA=new ReferenceCountingGC();
ReferenceCountingGC objB=new ReferenceCountingGC();
objA.instance=objB;
objB.instance=objA;
objA=null;
objB=null;
//假设在这行发生GC,objA和objB是否能被回收?
System.gc();
}
在这段代码中,objA和objB相互引用,虽然这两个对象最后都设置为null,但是在虚拟机中的对象内部,还是存在着互相引用。显然在最后都不会使用到了这两个对象,但是他们的引用并没有消除,引用计数法无法通知虚拟机回收这两个对象。
2.可达性分析算法
这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如图3-1所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。
在Java语言中,可作为GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如
NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。 - 所有被同步锁(synchronized关键字)持有的对象。
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
3.引用
所以在jvm中,对象的“生死”都是通过判断引用来进行实现,在jdk1.2之后,Java对引用的概念进行了扩充,将引用分为:
- 强引用:代码之中普遍存在的Object o = new Object()
- 软引用:用来描述一些还有用但并非必须的对象,SoftReference类
- 弱引用:WeakReference类
- 虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。PhantomReference类
4.方法区的回收
很多人认为方法区不需要回收垃圾,Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区中进行垃圾收集的“性价比”一般比较低:在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70%~95%的空间,而永久代的垃圾收集效率远低于此。
- 常量的回收,与堆中对象的回收一致,只需要判断是否存在引用。
- 类的回收需要满足三个条件
- 该类在堆中已经没有实例
- 该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象已经没有在任何地方被引用。
当满足了这三个条件,并不一定需要回收对象,只是提供可以“回收”的信息
二、垃圾回收算法
1.标记-清除算法
如同它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
这个算法存在两个问题:
- 空间问题:但这个算法容易产生内存碎片问题,碎片太多可能会导致后续无法分配大内存对象。
- 效率问题:标记和清除的效率都不高
2.复制算法
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
这种算法的代价是将内存缩小为了原来的一半,未免太高了一点。
3.标记-整理算法
在标记清除算法的基础上,加上了存活对象的移动,将他们整理成有序的排列,然后直接清除指针(有序对象的分割点)后所有内存空间,使得空间重新变为有序。
4.分代收集算法
当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。
三、垃圾收集器
1.Serial收集器
会停止所有线程进行垃圾收集,优点是简单高效,是一个单线程的,没有线程交互的开销。
2.ParNew收集器
Serial收集器的增强版本,是多线程并行的。
并发和并行:
在垃圾收集器上,并行代表多个垃圾收集线程共同运行,协同工作。此时默认用户是等待状态
并发代表垃圾收集线程可以和用户程序在同一时间内工作,用户进程未被冻结。
3.Parallel Scavenge收集器
Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器。
特点在于他可控制吞吐量。
4.CMS收集器
CMS(Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。
从名字就可以看出CMS收集器是基于标记-清除算法的实现,他的运作过程分为四步
- 初始标记 ,需要暂停用户进程,标记GC Root能直接关联到的对象
- 并发标记 ,遍历整个对象图,可以并发
- 重新标记,需要暂停用户进程,修正并发期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
- 并发清除,清除标记阶段判断死亡的对象。
CMS收集器优缺点: - 优点:并发收集,低停顿
- 缺点:
- 导致用户进程变慢
- 无法处理浮动垃圾,意思为并发过程中用户新添的,没有标记到的对象。
- 碎片空间问题
5.Garbage First收集器(G1)
可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。
四、内存分配回收策略
Java技术体系的自动内存管理,最根本的目标是自动化地解决两个问题:自动给对象分配内存以及自动回收分配给对象的内存。
1.对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
2.大对象直接进入老年代
大对象就是指需要大量连续内存空间的Java对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组。在Java虚拟机中要避免大对象的原因是,在分配空间时,它容易导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们,而当复制对象时,大对象就意味着高额的内存复制开销。
3.长期存活的对象将进入老年代
HotSpot虚拟机中多数收集器都采用了分代收集来管理堆内存,那内存回收时就必须能决策哪些存活对象应当放在新生代,哪些存活对象放在老年代中。为做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。对象通常在Eden区里诞生,如果经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
4.动态对象年龄判定
为了能更好地适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的年龄必须达到XX:MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄。
5.空间分配担保
每次gc的时候,都会检查当前新生代总对象大小是否小于老生代的最大连续可用空间。可以的话则说明这个gc是安全的。
否则则要进行担保,判断以往历史中晋升到老生代的对象平均大小,如果成功则进行一次有风险的minor gc。如果没有的话,则必须要进行Full gc。
不过这种行为是存在风险的,不能够确保此次晋升到老生代的对象就一定小于历史平均值。