java内存模型与垃圾回收机制

一、java内存模型

Eden  so  4—JVM Heap

图源

java内存模型

如图所示,java内存模型包含三部分:

  1. Java heap:包含新生代老年代两个内存空间,几乎所有的创建的对象都存储在这里
  2. Perm Gen:包含永久代内存空间,用于存储运行时的类、常量、静态变量以及方法代码、ClassLoader
  3. Java Stack:每个线程启动时,都会创建一个自己的java Stack,用于存储运行堆栈,以及原始类型临时变量,以及线程中用到的对象的引用

二、内存管理中的各类GC

Eden  Survivor I  Obj«ts Oder than 'S GC Cycles  Tenured

图源

三类GC:

  1. Minor GC:清理年轻代内存空间中的短生命周期对象,在年轻代空间中的短生命周期的对象在被Minor GC一定轮数(默认是15)后,会被转入老年代空间,判断为长生命周期对象。这类GC由于绝大多数都是短生命周期的对象,因此通常处理是很快的,因此其Stop the World操作带来的影响很小。在Eden内存空间满时被促发。
  2. Major GC:清理老年代内存空间,老年代空间中的对象都是相对长生命周期的对象,因此处理起来相对耗时。
  3. Full GC:将清理年轻代、老年代、永久代所有内存空间的对象,这也是为什么静态对象可能被回收。

三、垃圾回收算法

垃圾回收具体流程一般分成三步:

  1. 标记处需要被回收的对象
  2. 回收被标记的对象空间
  3. 将回收的对象空间移动为连续的内存块便于分配给新的对象。

对象已死吗?

引用计数算法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

存在的问题:很难解决对象循环引用的问题。主流的Java虚拟机实现中没有使用该算法。

可达性分析算法:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Rootsmeiyou 没有任何引用链相连时,则证明此对象是不可用的。

可作为根节点的对象:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI(即一般说的Native方法)引用的对象

垃圾收集算法

标记清除算法:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

不足:标记和清除的效率不高。标记清除后会产生大量的不连续的内存碎片。空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法:将可用内存按容量划分成大小相等的两块,每次只使用其中的的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

商业使用:新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才使用过的Survivor空间。默认是8:1.

标记-整理算法:标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉断边界意外的内存。

四、垃圾回收器

i

上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。Hotspot实现了如此多的收集器,正是因为目前并无完美的收集器出现,只是选择对具体应用最适合的收集器。

新生代

1、Serial(串行)收集器是最基本、发展历史最悠久的收集器,它是采用复制算法的新生代收集器,曾经(JDK 1.3.1之前)是虚拟机新生代收集的唯一选择。它是一个单线程收集器,只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直至Serial收集器收集结束为止(“Stop The World”)。这项工作是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉,这对很多应用来说是难以接收的。

im

2、ParNew收集器就是Serial收集器的多线程版本,它也是一个新生代收集器。除了使用多线程进行垃圾收集外,其余行为包括Serial收集器可用的所有控制参数、收集算法(复制算法)、Stop The World、对象分配规则、回收策略等与Serial收集器完全相同,两者共用了相当多的代码。

ParNew收集器的工作过程如下图(老年代采用Serial Old收集器):

im

3、Parallel Scavenge收集器也是一个并行多线程新生代收集器,它也使用复制算法。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput)

停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务

老年代

1、Serial Old 是 Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”(Mark-Compact)算法。

此收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,它还有两大用途:

  • 在JDK1.5 以及之前版本(Parallel Old诞生以前)中与Parallel Scavenge收集器搭配使用。
  • 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

2、Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程“标记-整理”算法。前面已经提到过,这个收集器是在JDK 1.6中才开始提供的,在此之前,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old以外别无选择,所以在Parallel Old诞生以后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。

3、CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度。从名字上(“Mark Sweep”)就可以看出它是基于“标记-清除”算法实现的。

CMS收集器工作的整个流程分为以下4个步骤:

  • 初始标记(CMS initial mark):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
  • 并发标记(CMS concurrent mark):进行GC Roots Tracing的过程,在整个过程中耗时最长。
  • 重新标记(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。
  • 并发清除(CMS concurrent sweep)

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

优点

CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集低停顿,因此CMS收集器也被称为并发低停顿收集器(Concurrent Low Pause Collector)。

缺点

  1. 对CPU资源非常敏感

  2. 无法处理浮动垃圾

  3. 标记清除算法导致的空间碎片

G1

G1 GC 是 JDK 1.7 中正式投入使用的用于取代 CMS 的压缩回收器,它虽然没有在物理上隔断新生代与老生代,但是仍然属于分代垃圾回收器;G1 GC 仍然会区分年轻代与老年代,年轻代依然分有 Eden 区与 Survivor 区。G1 GC 首先将堆分为大小相等的 Region,避免全区域的垃圾收集,然后追踪每个 Region 垃圾堆积的价值大小,在后台维护一个优先列表,根据允许的收集时间优先回收价值最大的Region;同时 G1 GC 采用 Remembered Set 来存放 Region 之间的对象引用以及其他回收器中的新生代与老年代之间的对象引用,从而避免全堆扫描。G1 GC 的分区示例如下图所示:

G!

总结而言,G1 GC 的特性如下:

  • 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力;
  • 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况;
  • 分代GC:G1依然是一个分代回收器,但是和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代;
  • 空间整理:G1在回收过程中,会进行适当的对象移动,不像CMS只是简单地标记清理对象。在若干次GC后,CMS必须进行一次碎片整理。而G1不同,它每次回收都会有效地复制对象,减少空间碎片,进而提升内部循环速度;
  • 可预见性:为了缩短停顿时间,G1建立可预存停顿的模型,这样在用户设置的停顿时间范围内,G1会选择适当的区域进行收集,确保停顿时间不超过用户指定时间。

G1 GC 的工作步骤

  • 初始标记(标记一下GC Roots能直接关联的对象并修改TAMS值,需要STW但耗时很短)
  • 并发标记(从GC Root从堆中对象进行可达性分析找存活的对象,耗时较长但可以与用户线程并发执行)
  • 最终标记(为了修正并发标记期间产生变动的那一部分标记记录,这一期间的变化记录在Remembered Set Log 里,然后合并到Remembered Set里,该阶段需要STW但是可并行执行)
  • 筛选回收(对各个Region回收价值排序,根据用户期望的GC停顿时间制定回收计划来回收)

参考文章:

  1. Java(JVM) 内存模型与内存管理
  2. 《深入理解java虚拟机》第二版
  3. 深入理解JVM(3)——7种垃圾收集器
  4. 垃圾回收算法与 JVM 垃圾回收器综述

results matching ""

    No results matching ""