讨论对象
主要是对堆内存的垃圾回收,栈内存根据栈帧,其他线程不共享的程序计数器,虚拟机栈,本地方法栈,回收起来比较方便。但是对堆和方法区回收起来就比较复杂了,是我们着重需要考虑的
对象的存活
1.引用计数法
通过引用计数器是不是0来判断是不是存活
/**
* testGC()方法执行后,objA和objB会不会被GC呢?
* @author zzm
*/
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
/**
* 这个成员属性的唯一意义就是占点内存,以便在能在GC日志中看清楚是否有回收过
*/
private byte[] bigSize = new byte[2 * _1MB];
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();
}
}
//结果还是回收了,证明JVM不是使用指针计数器的
2. 可达性分析算法
GC Roots的对象作为起始点,组成一个引用链
Java中包括
- 虚拟机栈(栈帧中本地变量表)中引用的对象
- 方法去中类静态属性引用对象
- 方法区中常量引用对象
- 本地方法栈Native引用的对象
3. 引用概念
强引用:代码中普遍存在的,Object obj=new Object();
软引用: 有足够的内存就不回收
弱引用:下一次垃圾回收就回收
虚引用:
4. Dead Or Alive
没有GC Roots可达,会先看看是否覆盖对象的finalize()方法,或者是否没有执行过finalize()方法,不符合其中之一就要被干掉了,不建议使用这种方法。。。
/**
* 此代码演示了两点:
* 1.对象可以在被GC时自我拯救。
* 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
* @author zzm
*/
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive() {
System.out.println("yes, i am still alive :)");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize mehtod executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
//对象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
// 下面这段代码与上面的完全相同,但是这次自救却失败了
SAVE_HOOK = null;
System.gc();
// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
}
}
5. 回收方法区
废弃常量和无用的类
无用的类的判断比较复杂
- 该类所有的实例都被回收,java堆中不存在实例
- 加载该类的ClassLoader已经被回收
- java.lang.Class对象没有任何地方被引用,无法通过反射访问该类的方法
在大量使用反射,动态代理,CGLib,ByteCode框架,动态生成JSP及OSGi
垃圾收集算法
1. 标记清除算法
标记所有需要回收的,统一清除。首先这两个过程,效率不高,另外会产生大量不连续内存碎片,导致后续无法分配大对象。
回收前: XXOOXXXOXOXOXO
回收后: OO O O O O
2.复制算法
强行控制连续的内存空间,每次只使用内存的一半,然后把需要回收的回收掉,不需要的放到预留的部分
回收前: XXOOXO|
回收后: |OOO
如果不想一直空着1/2的空间,可以分配两个Survivor空间来担保,就是上次留下来的在Survivor,新来的在Eden,然后一块滚去另一个Survivor,不够需要老年代来担保
3.标记整理算法
存活较多复制算法会带来非常多的复制,100%存活也不是不可能。老年代一般用标记整理
回收前:XXOOXO
回收后:OOO
移动不需要回收的往前,然后扫尾
4.分代收集算法
上面思想的结合,新生代大量死去用复制算法,老年代存活率高,用1或3
垃圾收集器
- Serial 收集器:最古老的收集器,其他线程Stop The World,单线程手机,现在在Client端运行还行。。
- ParNew收集器:就是Serial收集器的多线程版
- CMS收集器:Concurrent Mark Sweep,Stop The World去初始标记(GC-Roots)和重新标记,然后和用户线程并发标记和并发清理
- G1收集器:后台维护一个优先列表,根据时间先回收加支醉倒的Region(Garbage-First)
- ParallelScavenge,Serial Old,Parallel Old(Old是老年代版本)
内存分配与回收策略
1.对象优先在Eden分配
minor GC发生在新生代回收速度快
private static final int _1MB = 1024 * 1024;
/**
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
*/
public static void testAllocation() {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[4 * _1MB]; // 出现一次Minor GC
}
2.大对象直接进入老年代
full GC发生在老年代,比minor慢10倍
最好避免一群短命的大对象。不然会经常触发Full GC
-XX:PretenureSizeThreshold=3145728 对Serial和ParNew收集器有效,代表超过这个Size直接进入老年代
private static final int _1MB = 1024 * 1024;
/**
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
* -XX:PretenureSizeThreshold=3145728
*/
public static void testPretenureSizeThreshold() {
byte[] allocation;
allocation = new byte[4 * _1MB]; //直接分配在老年代中
}
3.长期存活对象进入老年代
-XX:MaxTenuringThreshold=1 代表经历几次Minor GC进入老年代
private static final int _1MB = 1024 * 1024;
/**
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1
* -XX:+PrintTenuringDistribution
*/
@SuppressWarnings("unused")
public static void testTenuringThreshold() {
byte[] allocation1, allocation2, allocation3;
allocation1 = new byte[_1MB / 4]; // 什么时候进入老年代决定于XX:MaxTenuringThreshold设置
allocation2 = new byte[4 * _1MB];
allocation3 = new byte[4 * _1MB];
allocation3 = null;
allocation3 = new byte[4 * _1MB];
}
4.动态对象年龄判定
同年龄大于等于survivor的一半,都进入
private static final int _1MB = 1024 * 1024;
/**
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15
* -XX:+PrintTenuringDistribution
*/
@SuppressWarnings("unused")
public static void testTenuringThreshold2() {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[_1MB / 4]; // allocation1+allocation2大于survivor空间一半
allocation2 = new byte[_1MB / 4];
allocation3 = new byte[4 * _1MB];
allocation4 = new byte[4 * _1MB];
allocation4 = null;
allocation4 = new byte[4 * _1MB];
}
5.空间分配担保
jdk6以后,老年代的连续空间小于新生代对象总大小或者历次晋升的平均大小进行Full GC,否则就Minor GC就行
1.6以前是有参数可以控制的,先看看是不是小于总大小,再看是不是小于平均晋升大小,如果可以进行一次Minor GC尝试。尝试失败也要Full GC