Java虚拟机GC机制
引言
程序中分配在堆上的内存,当不再需要的时候,需要及时回收以便后续能申请到内存可使用。像C、C++等语言,如果需要释放无用内存空间需要由编程人员自己来处理。而Java语言的虚拟机支持管理内存生命周期、自动释放无用内存,即GC机制。
1 相关基础概念
1.1 运行时数据分区
java代码运行在虚拟机,分为两类:一类是线程共享数据区,即一个进程中所有的线程都可访问的区域;一类是线程隔离数据区,即进程中各个线程是相互隔离的。整个内存分区结构如下图:
- 方法区
存储已被虚拟机加载的类信息、常量、静态变量等,类似C程序的代码区; - 堆
存放动态分配的内存,诸如new出来的对象的内存。它是GC管理的年轻代和老年代区域; - 虚拟机栈
它是每个线程执行java代码的函数栈。用于存储局部变量表、操作栈、动态链接、方法返回地址等信息; - 本地方法栈
类似于虚拟机栈,它是每个线程执行native代码的函数栈; - 程序计数器
记录当前线程运行方法执行到了第几行。如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。
1.2 可达性分析
了解C++智能指针的朋友,可能会提出基于引用计数可以实现:当不再需要时,释放内存。但是,基于引用计数无法解决环形持有的问题。同样,这时有人会说可以基于弱引用方式破解环形引用,但是弱引用的前提是写代码的人事先知道有环行引用、然后用弱引用去破除,故而弱引用任然无法解决问题。
Java虚拟机采用一种可达性策略去根除基于引用计数存在的问题。基本思想就是:在GC过程中,标记是否能被GC Roots对象引用可达,可达是指对象的上级对象、再上级、。。。,最终能到达GC Roots对象。没有标记为GC Roots可达的对象,就是可释放的对象。
1.2 GC Roots
GC Roots对象又称根对象,在虚拟机规范中以下对象作为GC Roots:
- 系统类加载器(system class loader)加载的对象;
- 活着的线程,包括等待或阻塞的线程;
- Java方法栈和native方法栈中的一些参数/局部变量;
- 静态变量、常量引用;
- Held by JVM - JVM由于特殊目的为GC保留的对象,但实际上这个与JVM的实现是有关的。
2 常见GC算法
2.1 标记-清除算法
算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。
优缺点:
- 算法比较简单、容易实现
- 容易产生内存碎片
2.2 拷贝算法
算法将内存分为等大两块,基本思想是:每次使用一块,当GC的时候,把存活对象拷贝到另一块;下次GC的时候,又把存活对象拷贝回来;。。。循环使用。
优缺点:
- 算法简单高效、也避免了内存碎片
- 每次只使用了一半内存,浪费极大
2.3 标记-整理算法
算法核心思想是:当标记完存活对象后,将存活对象移动到一端,使得所有的空闲内存连续。
优缺点:
- 避免了内存碎片、也不会浪费额外空间
- 整个算法相对复杂、而且性能耗时也大
3 GC过程概括
3.1 分代GC
Java虚拟机GC将内存分为新生代、老年代和永久代。新生代和老年代内存对应着堆区;而永久代对应着方法区。
- 新生代
新生代默认占三分之一的堆空间。整个新生代又分为两个Survivor分区和一个Eden,默认占比为1:1:8。任何时刻,都有一个Survivor分区空闲。 - 老年代
老年代默认占三分之二的堆空间。它是所有大对象和长久存活对象的存留区域。 - 永久代
永久代是HotSpot虚拟机对虚拟机规范中方法区的一种实现方式。所以在HotSpot虚拟机中,永久代就是方法区。
3.2 Minor GC与Full GC
堆内存的GC过程分为Minor GC和Full GC。当对象构建的时候,会优先在新生代分配内存,若新生代不满足分配空间大小就会触发Minor GC;若Minor GC仍然不满足空间大小,就会触发Full GC,然后在老年代分配内存;若老年代不满足分配空间大小,就会触发异常。特别地,Minor GC和Full GC绝不是只在申请对象空间不够的时候才触发。
- Minor GC
当对象在新生代分配内存后,存在于新生代的Eden分区和一块Survivor分区(假设这块称为 Survivor A,另一块称为Survivor B)。每当经过一次Minor GC,便会将存活对象拷贝到Survivor B、同时将对象年龄+1;当下次Minor GC时,又会将Eden分区和Survivor B中的对象拷贝到Survivor A、同样将对象年龄+1;。。。依次往复,当对象年龄默认达到15(其值可以通过虚拟机参数XX:MaxTenuringThreshold修改)后,就会将对象拷贝到老年代。特别地,当Minor GC过程中,空闲的Survivor无法容纳的对象将提前进入老年代。 - Full GC
Full GC一般采用标记整理算法,所以频繁的Full GC会导致系统卡顿。
3.3 永久代GC
原则上,方法区的对象会尽力避免GC,因为GC方法区的性价比特低。只回收废弃常量和无用类。必须满足如下所有条件就是废弃常量和无用类:
- 该类对应的Class对象没有在任何地方被引用;
- 加载该类的所有ClassLoader已经被回收;
- 该类所有的实例都被回收, Java堆中不存在该类的任何实例。
3.4 Minor GC优化
- 问题
当进行Minor GC时,所有线程必须同步阻塞,必然影响性能。 - TLAB
为了解决上述问题,每个线程在初始化时都会在Eden中申请一块内存作为线程独占内存分配区域,称为TLAB。类似于Linux内核中的CPU变量。
当TLAB中剩余内存小于要分配的对象内存大小时,就会在线程共享的新生代中申请;或者废弃当前TLAB、重新申请TLAB空间然后再次进行内存分配。两种方案各有利弊。为了解决这个问题:虚拟机定义了一个阈值,TLAB中剩余内存小于要分配的对象内存大小的情况下,若申请对象内存大于该阈值,就会在线程共享的堆中申请;反之,就会废弃当前TLAB、重新申请TLAB空间然后再次进行内存分配。
例如,TLAB总空间100KB,使用了90KB,剩余10KB,如果设置的阈值为20KB,那么如果新对象的内存大于20KB,则直接在线程共享分配的堆中分配,如果小于20KB,则会废弃掉之前的那个TLAB,重新分配一个TLAB空间来给新对象分配内存。