JAVA垃圾回收
时间:20180307
问题:
-
如何判定对象为垃圾对象
- 引用计数法
- 可达性分析法
-
如何回收
- 回收策略
- 标记-清除
- 复制算法
- 标记-整理算法
- 分代收集算法
- 垃圾回收器
- Serial
- Parnew
- Cms
- G1
- 回收策略
-
何时回收
引用计数法
很多教科书给出如下定义:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
但是,至少主流的JAVA虚拟机里面没有选用引用计数法来管理内存,其中最主要的原因就是它很难解决对象之间的循环引用的问题。
打印详细的GC日志信息参数(VM arguments):
-verbose:gc
-XX:+PrintGCDetails
测试堆中多个对象之间存在循环引用,外部没有引用,垃圾回收情况:
public class Main {
public Object instance = null;
public static void main(String[] args) {
Main m1 = new Main();
Main m2 = new Main();
m1.instance = m2;
m2.instance = m1;
m1 = null;
m2 = null;
System.gc();
}
}
结果:
[GC (System.gc()) [PSYoungGen: 1996K->936K(38400K)] 1996K->944K(125952K), 0.0012261 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 936K->0K(38400K)] [ParOldGen: 8K->565K(87552K)] 944K->565K(125952K), [Metaspace: 2735K->2735K(1056768K)], 0.0054020 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 38400K, used 333K [0x00000000d5900000, 0x00000000d8380000, 0x0000000100000000)
eden space 33280K, 1% used [0x00000000d5900000,0x00000000d59534a8,0x00000000d7980000)
from space 5120K, 0% used [0x00000000d7980000,0x00000000d7980000,0x00000000d7e80000)to space 5120K, 0% used [0x00000000d7e80000,0x00000000d7e80000,0x00000000d8380000)
ParOldGen total 87552K, used 565K [0x0000000080a00000, 0x0000000085f80000, 0x00000000d5900000)
object space 87552K, 0% used [0x0000000080a00000,0x0000000080a8d770,0x0000000085f80000)
Metaspace used 2741K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 304K, capacity 386K, committed 512K, reserved 1048576K
public class Main {
public Object instance = null;
public Main() {
byte [] m = new byte[20 * 1024 * 1024];
}
public static void main(String[] args) {
Main m1 = new Main();
Main m2 = new Main();
m1.instance = m2;
m2.instance = m1;
m1 = null;
m2 = null;
System.gc();
}
}
结果:
[GC (System.gc()) [PSYoungGen: 22476K->872K(38400K)] 42956K->21360K(125952K), 0.0010626 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 872K->0K(38400K)] [ParOldGen: 20488K->565K(87552K)] 21360K->565K(125952K), [Metaspace: 2735K->2735K(1056768K)], 0.0052890 secs] [Times: user=0.14 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 38400K, used 333K [0x00000000d5900000, 0x00000000d8380000, 0x0000000100000000)
eden space 33280K, 1% used [0x00000000d5900000,0x00000000d59534a8,0x00000000d7980000)
from space 5120K, 0% used [0x00000000d7980000,0x00000000d7980000,0x00000000d7e80000)
to space 5120K, 0% used [0x00000000d7e80000,0x00000000d7e80000,0x00000000d8380000)
ParOldGen total 87552K, used 565K [0x0000000080a00000, 0x0000000085f80000, 0x00000000d5900000)
object space 87552K, 0% used [0x0000000080a00000,0x0000000080a8d770,0x0000000085f80000)
Metaspace used 2741K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 304K, capacity 386K, committed 512K, reserved 1048576K
图解:
可达性分析算法
这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起点,从这个节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链时,则证明此对象是不可用的。就是从GCRoots到这个对象是不可达的。
可作为G'C'Roots的对象
- 虚拟机栈(局部变量表)
- 方法区的类静态属性所引用的对象
- 方法区中常量所引用的对象
- 本地方法栈中引用的对象
垃圾收集算法
标记-清楚算法
最基础的收集算法是“标记-清除(Mark-Sweep)”算法,如同它的名字一样,算法分为“标记”和"清除"两个阶段:首先标记出所有需要回收的对象,在标记后统一回收所有被标记的对象。标记过程就是(可达性分析算法)。
不足之处
1.效率问题:标记和清除两个过程的效率都不高。
2.空间问题:标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法
复制算法:它将可用内存按容量划分为大小相等的凉快,每次只使用其中的一块,当这一块的内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
堆
1.新生代
- Eden 伊甸园
- Survivor 存活区
- Tenured Gen
从
2.老年代
标记 -整理算法
标记-整理算法有可以叫做标记-整理-清除算法。其中标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存(被标记需要清除的对象)。
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,已应对被使用的内存中对象都100%存活的极端情况,所以在老年代一般不能直接用这种算法。
分代收集算法
当前商业虚拟机的垃圾收集都采用“分代收集Generational Collection算法”,只是根据对象存活的周期的不同将内存划分为几块。一般把JAVA堆分为新生代和老年代。这样就可以根据各个年代的特点采用最适合的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就使用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外的空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。