jvm垃圾收集器与内存分配策略
垃圾收集器与内存分配策略:
- 以下参考周志明的<<深入理解jvm高级特性与最佳实践>>。
判断对象是否存活:
- 引用计数:通过判断对象被引用的次数(为0,则表示不可被使用),但这很难解决对象相互循环引用的问题。
- 根搜索算法:即采用有向图的方式,判断从GC Roots到某个对象是否可达。
上图中obj1,obj2,obj3,obj4可达,obj5,obj6不可达(可被GC回收)。
- 可作为GC Roots的对象:
1. 虚拟机栈中局部变量引用的对象。
2. 类静态属性引用的对象。
3. 常量引用的对象。
4. JNI中引用的对象。
java中的引用类型:
java中有四种引用类型(由强到弱):
- 强引用(StrongReference): 只要对象有被强引用,对象就不会被GC回收,如Object o = new Object()中的o就是强引用。
- 软引用(SoftReference): 软引用的对象为不必须回收的对象,但在内存不足发生溢出之前,GC会在回收时,将软引用的对象视作可可回收的对象,所以SoftReference可以用来做一些高速缓存。
- 弱引用(WeakReference):软引用的对象只能存活到下一次GC的时候,无论内存是否足够,如WeakHashMap类。
- 虚引用(PhantomReference): 虚引用对对象存活无影响,唯一的用处,可以与ReferenceQueue结合使用,在对象被回收时,得以通知。
方法区(Permanent Generation)回收:
- java8中已经移除了Permanent Generation, 取而代之的是MetaSpace区。
- 方法区回收的对象:
1. 废弃常量(如无引用的常量字符串)。
2. 无用的类。
- 类什么条件下可以被回收:
1. 该类的所有实例已经被回收。
2. 加载该类的ClassLoader已经被回收。
3. 该类的java.lang.Class对象没有被引用,无法任何地方通过反射访问该类。
垃圾回收算法:
- 标记-清除(Mark-Sweep): 先标记要回收的对象,再统一进行回收操作。
简单方便,内存碎片化严重
- 复制算法: 将堆分为2份,1份空白(A),1份存放对象(B),GC后,将B中存活对象复制到A中,然后将B变成空白。
无内存碎片化, 浪费可用内存
- 标记-整理(Mark-Compact): 先标记可回收对象,将存活对象都移向一端,再回收对象。
无内存碎片化,充分利用可用内存
- 分代回收:将堆分为年轻代和老年代,年轻代(存活对象少)采用复制算法,老年代(对象存活长)可采用标记-清除或标记-整理。
根据对象存活特性,合理使用不同回收算法,商业JVM都使用该回收算法。
垃圾收集器:
- 既然jvm采用分代回收,那么年轻代和老年代各自使用的垃圾收集器也不一样。
- 年轻代使用的垃圾收集器:Serial,ParNew,Parallel Scavenge。
- 老年代使用的垃圾收集器:CMS, Serial Old(MSC), Parallel Old。
- 通用的垃圾收集器:G1。
Serial/Serial Old收集器:
- 单线程回收,回收期间暂停其他所有工作线程。(jvm -client模式下年轻代默认的收集器)
ParNew收集器:
- 多线程同时回收,回收期间暂停其他所有工作线程。
Parallel Scavenge收集器:
- 和ParNew类似,但其重在控制cpu吞吐量大小。
Serial Old收集器:
- Serial老年代版本。使用单线程,标记-整理的算法。
Parallel Old收集器:
- Parallel Scavenge收集器的老年代版本。使用多线程,标记-整理算法。
CMS(Concurrent Mark Sweep)收集器:
- 以获得最短回收暂停时间为目标。这是一款真正意义的并发收集器(可允许垃圾收集线程与用户线程并行)。其主要有四个阶段:
1. 初始标记:标记GC Roots可关联的对象,会暂停用户线程。
2. 并发标记:GC Roots Tracing过程,用户线程并行。
3. 重新标记:重新标记由于在并发标记过程中用户线程导致的对象状态变化,会暂停用户线程。
4. 并发清理:清理可回收对象,用户线程并行。
G1(Garbage First)收集器:
- 一款先进的垃圾收集器。它针对整个堆,将其分为大小相等的区域,记录每个区占用信息,优先回收回收价值更高的区域。
- 详解介绍:http://my.oschina.net/indestiny/blog/214160
这些收集器都一些可控参数,根据实际场景来调整,这里是官方文档:
http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html
内存分配与回收策略:
- jvm内存分配基本就是堆上分配,也有可能栈上(如通过逃逸分析,可以在栈上分配)。
- 看看整个堆分区:
对象优先分配在Eden区:
内存分配回收实例:
/** * 内存分配回收 * -XX:+PrintGCDetails 打印GC信息 * -Xms12M 起始堆大小 * -Xmx12M 最大堆大小 * -Xmn6M 年轻代大小 * -XX:SurvivorRatio=6 Eden:S0:S1 = 4:1:1 * * jvm args: * -XX:+PrintGCDetails -Xms12M -Xmx12M -Xmn6M -XX:SurvivorRatio=4 */ public class MemoryAllocate { private static final int _1M = 1024*1024; public static void main(String[] args) { byte[] b1 = new byte[_1M]; // allocate 1M byte[] b2 = new byte[_1M*2]; // allocate 2M byte[] b3 = new byte[_1M]; // allocate 1M, 发生年轻代GC(Minor GC) } }gc信息如下:
- 垃圾回收方式:
1.Minor GC:发生在年轻代,速度快,频繁。
2.Full GC/Major GC:发生在老年代, 一般也伴随Minor GC, 速度一般比Minor GC慢10倍以上。
大对象直接进入老年代:
- 可以通过设定-XX:PretenureSizeThreshold来判断当分配的对象大于这个阈值时,则直接将其分配到老年代。
- PretenureSizeThreshold只对Serial和ParNew收集器有效。
/** * PretenureSizeThreshold * -XX:+PrintGCDetails 打印GC信息 * -Xms12M 起始堆大小 * -Xmx12M 最大堆大小 * -Xmn6M 年轻代大小 * -XX:SurvivorRatio=6 Eden:S0:S1 = 4:1:1 * -XX:PretenureSizeThreshold 对象超过该值,直接进入老年代,单位B * -XX:UseParNewGC 使用UserParNew收集器 * jvm args: * -XX:UseParNewGC -XX:+PrintGCDetails -Xms12M -Xmx12M -Xmn6M -XX:SurvivorRatio=4 -XX:PretenureSizeThreshold=2097152 */ public class PretenureSizeThreshold { private static final int _1M = 1024*1024; public static void main(String[] args) { // 分配2M, 大于PretenureSizeThreshold, 直接进入老年代 byte[] b1 = new byte[_1M * 3]; } }
其他参数介绍:
- MaxTenuringThreshold: 在Eden区,对象在每次Minor GC后存活下来,则其年龄+1, 当存活对象年龄大于MaxTenuringThreshold时,就进入老年代。
- HandlePromotionFailure: 空间担保机制。在发生Minor GC时,虚拟机会检测比较每次晋升到老年代的平均大小与老年代剩下的空间大小,如果大于,则进行一次Full GC;如果小于,则查看HandlePromotionFailure是否允许担保失败,若允许只进行Minor GC;若不允许,还是会进行一次Full GC。
上面就基本介绍了jvm分配回收内存策略。
不吝指正。