理解GC
作为Java开发者,一般不需要专门编写内存回收和垃圾清理代码,对内存泄露和溢出的问题,也不必过度担心,因为java提供了自动内存管理以及垃圾清理机制。
一 JVM运行时内存布局
方法区
是一块各线程共享的内存区域
存储已经被虚拟机加载的类信息,常量,静态变量等等数据。
在方法区有一个运行时常量池,存放的是编译器生成的各种字面量以及符号引用
堆
是一块各线程共享的内存区域
是java虚拟机管理的内存中最大的一块,存放的是对象实例。
是垃圾收集器管理的主要区域
程序计数器
每个线程都有一个独立的程序计数器
可视作当前线程所执行的字节码的行号指示器,记录当前执行到哪一条指令,下一条该取哪一条指令
虚拟机栈
线程私有的区域
其生命周期与线程相同,在编译期间就完成了分配。
虚拟机栈实际上是一个java方法执行的内存模型,每个方法在执行时,会创建一个栈帧,存储局部变量表,操作数栈方法的出入口等等。一个方法的调用执行直至结束即对应着一个栈帧在虚拟机入栈到出栈的过程。
本地方法栈
Native方法
二 垃圾回收
判断是否对象已死
- 引用计数算法
- 可达性分析算法
引用计数算法
给对象添加一个引用计数器,当该对象被引用时,计数值+1,当引用失效时,计数值-1。
当任何时候一个对象的计数值为0时,那么这个对象已死(即不可能被使用)。
这种算法在一些特定的情况下不适用,难以解决对象之间的相互引用。
public class ReferenceCountingGC {
public Object ins=null;
private static final int _1MB=1024*1024;
private byte []ki=new byte[_1MB*2];
public static void testGC(){
ReferenceCountingGC oA=new ReferenceCountingGC();
ReferenceCountingGC oB=new ReferenceCountingGC();
oA.ins=oB;
oB.ins=oA;
oA=null;
oB=null;
//以上表明这两个对象相互引用,但是在之后的GC中,依然被回收了。
System.gc();
}
public static void main(String[] args) {
testGC();
}
}
可达性分析算法
jvm使用这种算法进行判断是否对象已死。
这种算法通俗来讲就是有一系列称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,他们走过的路径称为引用链,他们没有走过的点即为不可达,那么这个对象就会不可用的。
那么可以作为GC Roos的对象是什么?
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区常量引用的对象
- 本地方法栈中JNI引用的对象
真正确认一个对象死亡是需要两次标记的,第一次是发现一个对象没有与GC Roots相连接的引用链,第二次是判断这个对象是否有比较执行finalize()
方法
GC的区域
线程私有的内存区域是无需GC的
在编译期即已经知道内存占用的大小,其内存的分配与回收都具有确定性,其内存生命周期伴随线程的生命周期。
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享的区域是需要GC的
在运行期间动态分配内存,内存的分配与回收具有不确定性
-
堆内存
-
方法区(常量池)
在方法区重点回收的是废弃的常量以及无用的类
如果没有String对象访问某个常量,也没有地方引用这个常量,那么这个常量将会被清理。
如果一个类的所有实例都被回收,以及该类的对象没有被引用,那么将会被卸载
常见的GC算法
-
标记清除算法
将待清除的对象做标记,直接清空。
该方法简单快速,但是会产生大量的空间碎片
-
复制算法
将内存对半,总留有一半空的内存区域,在标记之后,将不被清理的对象复制到空的内存区域,直接将原内存区域清空。
-
标记整理算法
标记并清理垃圾对象之后,将剩下的对象进行整理挪动,使其内存区域连续,避免了空间碎片问题,但拉低了效率。
-
分代收集算法
当前的商业虚拟机都采用此算法垃圾收集,将堆内存根据对象的存活周期的不同分为新生代和老年代,对不同的年代采用不同的算法。
新生代中对象生命周期短,每次垃圾回收都有大量的对象死去,使用复制算法,只需要付出较小的复制成本
老年代中,对象存活率高,采用标记整理或者标记清理算法。
三 垃圾回收器
Serial 收集器(串行收集器)
当进行垃圾回收时,会Stop The World 即暂停其他所有的工作线程,直至收集完成。
简单高效,没有线程交互的开销。
复制算法
适用于新生代较小的Client模式
ParNew
是Serial的多线程版本( STW时采用多线程并发执行回收操作)
复制算法
是很多虚拟机在Server模式下的默认选项
Parallel收集器
是一个新生代收集器,目标是达到一个可控制的吞吐量。
采用复制算法
以上都是新生代收集器。
以下是老生代收集器
Serial Old
Serial的老生代版本,适用于Client模式。
单线程,标记整理。
Parallel Old 收集器
Parallel收集器的老年代版本。
CMS收集器
目标:最短回收停顿时间。
标记清除,可以设置参数进行内存整理。
-
初始标记
只标记GC Roots直接关联的对象。
-
并发标记
和用户线程并发执行可达性分析
-
重新标记
STW,修正并发标记因线程变动的部分
-
并发清除
和用户线程一起并发执行清除
会造成空间碎片问题,并发操作会占用CPU资源,以及无法清除浮动垃圾。
G1收集器
G1管理整个GC堆,分成不同的Region,维护一个优先列表,记录每个Region的价值,价值是由之前这个区域GC所获空间大小和GC所需时间来决定的。整体是标记整理,局部是复制算法。
-
初始标记:STW标记GC Roots直接引用的对象。
-
并发标记:和用户线程并发执行可达性分析标记,这个过程中的引用变化会记录到Remebered Set Logs。
-
最终标记:STW修正并发标记过程中改变的引用,合并Remebered Set Logs到Rememebered Set。
C堆,分成不同的Region,维护一个优先列表,记录每个Region的价值,价值是由之前这个区域GC所获空间大小和GC所需时间来决定的。整体是标记整理,局部是复制算法。
-
初始标记:STW标记GC Roots直接引用的对象。
-
并发标记:和用户线程并发执行可达性分析标记,这个过程中的引用变化会记录到Remebered Set Logs。
-
最终标记:STW修正并发标记过程中改变的引用,合并Remebered Set Logs到Rememebered Set。
-
筛选回收:对所有Region进行价值排序,根据用户决定的GC耗时来取价值高的Region进行回收工作。这个部分是STW,因为只对部分区域回收,不会太耗时,并且耗时时间也是用户自己决定的。