最近在拉钩课程上学习Android进阶课程,跟着老师上课的内容再整理一遍笔记,理一下思路。
简介
与C语言相比,Java中不需要手动释放对象内存,JVM中的垃圾回收器会自动回收。在JVM中程序计数器、虚拟栈和本地方法栈不需要考虑回收问题,它们的生命周期依赖线程的生命周期。而堆和方法区需要在程序运行期间动态分配和回收内存,所以垃圾回收器主要关注这部分内存。
可达性分析
JVM中通过可达性分析决定对象是否需要被回收;
可达性分析是从离散数学的图论引入的,JVM把内存中所有对象之间的引用关系看成一张图,通过一组名为“GC Root”的对象作为起始点,从这些节点开始向下搜索,搜索所经过的路径称为引用链,最后通过对象的引用链是否可达来判断对象是否需要被回收。如图所示:
虽然JKM直接存在引用,但与GC Root之间不存在路径,所以当GC进行垃圾回收时,遍历到JKM对象时就会将它们回收。
注:虽然上图圆形图标和GC Root标记的是对象,但实际上代表的是对象在内存中的引用
一)GC Root
可作为GC Root的对象:
- Java虚拟机栈的栈帧中局部变量表引用的对象;
- 方法区中静态引用指向的对象;
- 仍处于存活状态中的线程对象;
- Native方法中JNI引用的对象;
二)何时触发回收
- Allocation Failure:在堆内存分配时,如果因为可用剩余空间不足导致对象内存分配失败,系统会触发一次GC;
- System.gc():开发者可用主动调用此API请求一次GC;
三)验证GC Root的几种方式
1、使用 -Xms 初始分配JVM运行时的内存大小,如果不指定默认为物理内存的1/64;
//运行HelloWorld程序时,从物理内存中分配200M空间给JVM
java -Xms200m HelloWorld
2、验证虚拟机栈(栈帧的局部变量)中引用的对象作为GC Root
public class GCRootLocalVariable {
private int _10MB = 10 * 1024 * 1024;
private byte[] memory = new byte[8 * _10MB];
public static void main(String[] args){
System.out.println("开始时:");
printMemory();
method();
System.gc();
System.out.println("第二次GC完成时:");
printMemory();
}
public static void method(){
GCRootLocalVariable gc = new GCRootLocalVariable();
System.gc();
System.out.println("第一次GC完成时:");
printMemory();
}
public static void printMemory() {
System.out.println("free is " + Runtime.getRuntime().freeMemory()/1024/1024 + "M, ");
System.out.println("total is " + Runtime.getRuntime().totalMemory()/1024/1024 + "M, ");
}
}
打印结果如下:
开始时:
free is 244M,
total is 245M,
第一次GC完成时:
free is 163M,
total is 245M,
第二次GC完成时:
free is 243M,
total is 245M,
gc作为局部变量,引用了new出的对象(80M),并且把它作为GC Root,当第一次GC时,并不会被回收;
当method()方法执行完成后,局部变量消失,没有引用指向gc对象,所以第二次GC时gc被回收,内存被释放。
3、验证方法区中静态变量引用的对象作为GC Root
public class GCRootStaticVariable {
private static int _10MB = 10 * 1024 * 1024;
private byte[] memory;
private static GCRootStaticVariable staticVariable;
public GCRootStaticVariable(int size){
memory = new byte[size];
}
public static void main(String[] args){
System.out.println("开始时:");
printMemory();
GCRootStaticVariable gc = new GCRootStaticVariable(4 * _10MB);
gc.staticVariable = new GCRootStaticVariable(8 * _10MB);
gc = null;
System.gc();
System.out.println("GC完成");
printMemory();
}
public static void printMemory() {
System.out.println("free is " + Runtime.getRuntime().freeMemory()/1024/1024 + "M, ");
System.out.println("total is " + Runtime.getRuntime().totalMemory()/1024/1024 + "M, ");
}
}
打印结果如下:
开始时:
free is 244M,
total is 245M,
GC完成
free is 163M,
total is 245M,
程序开始时运行内存是242M,创建gc对象(40M),初始化gc内部的静态变量staticVariable(80M)。当调用GC时,只有gc对象被回收,staticVariable作为GC Root,内存并没有被回收。
4、验证活跃线程作为GC Root
public class GCRootThread {
private int _10MB = 10 * 1024 * 1024;
private byte[] memory = new byte[8 * _10MB];
public static void main(String[] args) throws Exception{
System.out.println("开始时:");
printMemory();
AsyncTask at = new AsyncTask(new GCRootThread());
Thread thread = new Thread(at);
thread.start();
System.gc();
System.out.println("main方法执行完毕,完成GC");
printMemory();
thread.join();
at = null;
System.gc();
System.out.println("线程代码执行完毕,完成GC");
printMemory();
}
private static class AsyncTask implements Runnable{
private GCRootThread gc;
public AsyncTask(GCRootThread gcRootThread) {
this.gc = gcRootThread;
}
@Override
public void run() {
try{
Thread.sleep(500);
}catch(Exception ex){
}
}
}
}
打印结果如下:
开始时:
free is 244M,
total is 245M,
main方法执行完毕,完成GC
free is 163M,
total is 245M,
线程代码执行完毕,完成GC
free is 243M,
total is 245M,
第一次调用GC时,线程没有结束,作为GC Root不会被回收。thread.join();保证线程结束再调用后续代码,所以当第二次调用GC时,线程已经执行完毕并被置为null,此时线程已经被销毁,内存被回收。
5、验证成员变量作为GC Root
public class GCRootClassVariable {
private static int _10MB = 10 * 1024 * 1024;
private byte[] memory;
private GCRootClassVariable classVariable;
public GCRootClassVariable(int size){
memory = new byte[size];
}
public static void main(String[] args){
System.out.println("开始时:");
printMemory();
GCRootClassVariable gc = new GCRootClassVariable(4 * _10MB);
gc.classVariable = new GCRootClassVariable(8 * _10MB);
gc = null;
System.gc();
System.out.println("GC完成");
printMemory();
}
}
打印结果如下:
开始时:
free is 244M,
total is 245M,
GC完成
free is 243M,
total is 245M,
四)如何回收垃圾
1、标记清除算法
从GC Root集合开始,将内存整个遍历一遍,保留所有可以被GC Root直接或间接引用到的对象,剩下的对象当成垃圾并进行回收。
- Mark标记阶段:找到内存中的所有GC Root,和GC Root对象直接或间接相连的标记为灰色,否则标记为黑色;
- Sweep清除阶段:遍历完所有GC Root之后,将被标记为垃圾的对象直接清除;
优点:实现简单,不需要将对象进行移动;
缺点:需要中断进程内其他组件的执行,并且可能产生内存碎片,提高了垃圾回收的频率;
2、复制算法
将现有的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中。清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片;
缺点:可用内存大小缩小为原来的一半,对象存活率高时会频繁进行复制;
3、标记—压缩算法
需要先从根节点开始对所有可达对象做一次标记,标记后将所有的存活对象压缩到内存的一端,然后清理边界外所有的空间。
- Mark标记阶段:找到内存中的所有GC Root,和GC Root对象直接或间接相连的标记为灰色,否则标记为黑色;
- Compact压缩阶段:将剩余存活对象压缩到内存某一端;
优点:避免了碎片的产生,也不需要两块相同的内存空间,性价比极高;
缺点:压缩操作需要移动局部对象,一定程度上降低了效率。
JVM分代回收策略
JVM根据对象存活周期不同把堆内存划分为新生代和老年代;
分代回收的中心思想是:新创建的对象在新生代中分配内存,此区域的对象生命周期一般较短。如果经过多次回收仍然存活下来,则将它们转移到老年代中。
一)新生代
常规应用进行一次垃圾收集一般可以回收70%~95%的空间,回收率高,一般采用复制算法回收;
新生代可分为Eden、Survivor0(简称S0),Survivor1(简称S1),按照8:1:1的比例划分。
工作过程
1、绝大多数刚被创建的对象会存放在Eden区,如图:
2、当Eden区第一次满的时候会进行垃圾回收,清除Eden区并将存活对象复制到S0,S1为空,如图:
3、下一次Eden区满时,再进行一次垃圾回收,此时将Eden和S0区中的所有垃圾对象清除,并将存活对象复制到S1,S0为空。如图:
4、如此反复切换15次(默认)后,如果还有存活对象说明这些对象的生命周期较长,则转移到老年代中,如图:
二)老年代
老年代的内存大小一般比新生代大,能存放更多对象,如果对象比较大,新生代的剩余空间不足,则这个大对象会直接被分配到老年代中。
老年代中的对象有时候会引用新生代对象,这时如果要执行新生代GC,可能需要查询整个老年代上可能存在引用新生代的情况,这样做很低效。所以,老年代中维护了一个512byte的card table,所有老年代对象引用新生代对象的记录都被记录下来,每当新生代发送GC时,只需要检查这个card table即可,大大提高了性能。
引用类型
JVM中通过GC Root的引用可达性判断对象是否存活,根据引用强度由强到弱,可分为:强引用、软引用、弱引用、虚引用;