java垃圾回收
在java开发中,每个运行程序都会产生对象,JVM分配这些对象都需要一定的内存空间。由于java拥有垃圾收集器(GC),让我们不必专门去写内存回收代码。java的垃圾回收指的是回收内存,针对的java对象,所以涉及到JVM内存结构。此外,这些也是值得注意的问题:
- GC是怎么判断哪些对象需要回收
- 采取哪些措施回收,什么时候回收
- 落实到GC中是什么样子
首先看下JVM的内存结构,主要分为堆、栈、PC和方法区(在java 8后方法区不复存在,取而代之的是元数据区mataSpace,存放在本地内存,所以与之相关的Klass也被移除了,以下分析基于HotSpot虚拟机)。因为对象主要存在堆和方法区中(利用逃逸技术可以将一些对象标量分解后在栈上分配),GC关注的也是这些区域。
JVM内存结构这里是一张更详细的图片
堆又分为年轻代和老年代,而年轻代中分为三部分Eden
:FromSpace
:ToSpace
=8:1:1,FromSpace和ToSpace是等价的,两者身份是可以互换的。老年代空间比年轻代的要大。在创建对象时首先被分配到新生代,一般来说,大部分对象都是创建完后不久就不再使用,很快会被清理掉,如果经历了GC还在使用的就移到更持久的区域,年轻代内存划分成这样和回收的策略有关。
JVM为了区分哪些对象是需要回收的有如下方法:
- 引用计数法
- 可达性分析算法
在hotSpot中采用的是可达性分析算法。引用计数法是指当对象创建时,如果有新的引用指向它则加一,否则减一,当持有的引用数量为0时则标记为清理对象。这种方法在对象相互引用时无法识别出是否为待清理对象,比如:
public class Person {
public Person friend;
// ……
}
Person 阿珍 = new Person(); // 1
Person 阿强 = new Person(); // 2
阿珍.person = 阿强; // 3
阿强.person = 阿珍; // 4
阿珍 = null; // 5
阿强 = null; // 6
按照常理,阿珍和阿强这两个对象的引用已经没有了,应该是要被清理的,但引用计数法则判定不出。1和4操作后阿珍对象引用数为2,同理阿强也为2,5和6操作都只释放了1个引用,此时已经不能通过引用来访问到阿珍和阿强两个对象了,但由于两者相互引用导致引用数不为0,GC也就不会回收这两个对象,造成内存泄漏。
可达性分析采用的是图论的方法,对象之间的引用可记录为一张图,从根节点(GC Roots)出发,遍历这些对象,如果有不连通的对象,则标记为可回收对象。在JVM中,这些引用可以作为GC Roots:
- 栈中的对象引用
- 方法区中的静态对象引用和常量对象引用
- 本地方法栈JNI的对象引用
刚才的例子如果采用可达性分析则如下图所示:
内存分配1 内存分配2 GC将对象引用设为null
后:
寻找到可回收的对象后,GC就可以进行回收工作了,以下是回收算法:
- 标记-清除
- 标记-整理
- 复制
标记-清除
将标记为可回收的对象直接清除 ,但这样会造成内存碎片产生,像老鼠打洞一样,这里空一点那里空一点,不利于内存再分配。
标记-整理
相当于在标记-清除的基础上添加了整理,把存活下来的对象往一个方向堆,类似于消消乐,消失的那部分会有其他对象从上面掉下来填充(这里的方向就是向下)。
复制
复制算法将内存划为大小相同的两部分,先用A内存,对A内存进行清除时遍历存活对象,将存活的对象全部复制到B,然后一口气将A内存清空,下一次对B内存清理时亦是如此。
因为大部分对象是朝生夕死,所以JVM的垃圾收集器根据其存活时间长短将堆划分为年轻代和老年代。可以预见,年轻代对象回收发生频率很高,所以采取复制算法比较好,一次性复制存活对象和清除回收对象,简单粗暴。而老年代是存活的对象比较多,需要回收的对象少,采用复制算法开销会比较大,所以采用标记-整理算法。不同区域采用不同的算法,这就是java的分代回收机制。
正是如此,年轻代才会将内存划分为8:1:1的布局。当Eden
快满的时候,进行一次Minor GC,将存活对象复制到FromSpace
,下次Minor GC的时候将FromSpace
和Eden
的存活对象复制到ToSpace
。这两个Survivor
总有一个是空的,如果在复制的过程中发现有些对象达到了晋升老年代的条件(当这两个存活去切换了一定次数之后,默认是15次,可以使用-XX:MaxTenuringThreshold
控制)则将其移动到老年代。为了加快Eden
的对象回收和内存分配,HotSpot JVM采用了bump-the-point和TLAB(Thread-Local Allocation Buffers)两种技术。由于Eden
是连续的, bump-the-point跟踪最后创建的一个对象,查看其后面是否有充足的内存,TLAB针对的是多线程,它将Eden
划分为若干段,每个线程使用独立的一段。这两种技术结合起来可以快速地分配内存。如果对象比较大(比如大数组),可能会直接分配到老年代。当老年代快满的时候会触发一次Major GC。
这些都是对象回收的策略,根据这些策略,GC使用如下几种收集器对其进行管理。
HotSpot JVM1.6的垃圾收集器相连收集器的可以配合使用。
-
新生代收集器:Serial、ParNew、Parallel Scavenge
-
老年代收集器:CMS、Serial Old、Parallel Old
根据并行和串行以及年轻代和老年代的划分,看下各个收集器的特点:
收集器 | 串行、并行 or 并发 | 新生代/老年代 | 算法 | 目标 | 使用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制 | 响应速度优先 | 单CPU环境下的Client模式 |
ParNew | 并行 | 新生代 | 复制 | 响应速度优先 | 多CPU环境时在Server模式下与CMS配合 |
Parallel Scavenge | 并行 | 新生代 | 复制 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
Serial Old | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单CPU环境下的Client模式、CMS的后备预案 |
Parallel Old | 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
CMS | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或B/S系统服务端上的java应用 |
G1 | 并发 | both | 标记-整理 + 复制 | 响应度优先 | 面向服务端应用,将来替换CMS |
Serial收集器
单线程运行,进行GC的时候其他线程暂停,简单高效,是Hospot运行在Client模式下默认使用的新生代垃圾收集器。
ParNew
是Serial收集器的多线程版,是在Server模式下新生代的首选收集器,目前除了Serial收集器仅有它能和CMS(Concurrent Mark Sweep)配合工作。
Parallel Scavenge
多线程,关注GC时尽可能缩短用户线程的停顿时间,提高吞吐量(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + GC时间)),适用于不需要太多交互的后台运算。
Serial Old
单线程,使用标记-整理算法,在工作是其他线程需要停止。
Parallel Old
多线程,关注吞吐量,在注重吞吐量和CPU资源敏感的场合可以与Parallel Scavenge配合使用。
CMS
多线程并发运行,使用标记-清除算法,致力于获取最短停顿时间(缩短垃圾回收时间)。
CMS执行过程为:初始化标记->并发标记->预清理->可控清理->重新标记->并发清除->并发重设状态等待下次CMS的触发(先2次标记,1次预处理,1次重新标记,1次清除)
- 初始化标记:仅标记GC Roots能关联到的对象
- 并发标记:进行GC Roots Tracing,整个过程耗时最长
- 重新标记:为了修正用户在运行时导致标记变动那一部分对象的标记记录
CMS运行需要额外的CPU和内存资源,所以在CPU内存紧张的情况下会采用Serial Old收集。同时因为采用的标记-清除算法,导致空间碎片产生。在CMS并发处理阶段由于用户线程还在运行,产生的新垃圾在本次清理中无法被清理,称为浮动垃圾,只能等待下CMS回收。
G1
多线程并行,采用标记-整理算法,运行在Server模式下
连续内存空间 RegionG1将整个堆内存划分为大小相同的独立区域(region),新生代和老年代在物理上不再分隔。当对象分配到一个region后它可以与整个堆的任意对象发生引用关系,进行扫描时需扫描整个堆,消耗很大。为了避免全堆扫描的发生,G1中每个Region会与一个Remembered Set关联,当对对象数据进行写操作时会产生一个Write Barrier暂时中断写操作,检查对象引用的对象是否处于不同的Region中,是则通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中,扫描从GC Roots出发也就不会扫描全堆和有所遗漏了。
G1可以建立可预测的时间模型,它跟踪各个Region中垃圾的价值大小(回收获得的空间与所付出的时间),在后台维护一个优先列表,优先回收价值最大的Region。
G1的运行过程大致如下:
- 初始标记:仅标记GC Roots能直接到达的对象
- 并发标记:开始对heap中的对象标记,标记线程与应用程序线程并行执行,并收集各个Region的存活对象。
- 最终标记:并发标记会将对象的修改记录到Remmembered Set Logs中,在本阶段将RSL整合到RS中。
- 筛选回收:对Region中的回收价值进行排序,此阶段时间用户可控制。
参考自:
深入理解JVM(3)——7种垃圾收集器
Java系列笔记(3) - Java 内存区域和GC机制
Java Hotspot G1 GC的一些关键技术