深入理解JVM之垃圾回收与内存分配策略


title: 深入理解JVM之垃圾回收与内存分配策略
author: Mutong
cover: true
categories:
tags: JVM


概述

  • 在Java内存运行时区域的各个部分,其中程序计数器,虚拟机栈,本地方法栈3个区域与线程有着相同的生命周期.
  • 栈中的栈帧随着方法的进入和退出而有条不紊地执行者出战和入栈操作,每一个栈帧中分配多少内存基本上时在类结构确定下来时就已知地.因此这几个区域地内存分配和回收都具备了确定性,不需要考虑回收地问题
  • 但是Java堆和方法区则不一样,这部分内存地分配和回收都是动态地,垃圾回收器所关注地这部分内存.

可达性分析算法

1. 算法思想

  • 在Java语言时通过可达性分析来判断对象是否存活地的,这个算法的基本思想就是通过一系列的称为"GCroot"的对象为启动,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到"GCroot"没有任何引用链相连,说明此对象是不可达的.


    image
  • 如图,对象567虽然是有关联的,但是他们直接没有到GCroot的引用链,那么他们就会被回收
  • 但是对于对象5,6,7,可不是非死不可的,如果对象在进行可达性分析之后发现没有与GCroot相连接的引用链,那么它将会被第一次标记并且进行一次筛选,筛选的条件就是否有必要执行finalize()方法,当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么这个对象才会被销毁.
  • 如果对象被判断有必要执行finalize()方法,那么会将这个方法防止在一个叫做F-Queue的队列种,并在稍后由一个虚拟机自动建立的,低优先级的finalizer线程去执行它.
  • 任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,他的finalize()方法不会被执行,也就面临死亡.
  • 在Java语言中,可作为GCroot的对象包括下面的几种:
    • 虚拟机栈(栈帧中的本地变量表)中引用的对象
    • 方法区中静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈(Native方法)引用的对象

2. 引用

  • 引用的概念分为4种
    • 强引用:类似Object obj = new Object()这类引用,就是强引用,垃圾收集器引用不会回收被强引用的对象
    • 软引用是用来描述一种还有用但并非必须的对象,在系统发送内存溢出的异常之前,将会把这些对象列入回收范围之中进行第二次回收.如果此次回收还没有足够的内存,才会抛出溢出异常,RoftReference类来实现软引用
    • 弱引用是用来描述非必需对象的,但是它的强度比软引用更弱一点,被弱引用关联的对象只能生存到下一次垃圾收集器发生之前,当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象,WeakReference类来实现弱引用.
    • 虚引用最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例,为一个对象设置虚引用的唯一目的就是能在这个对象被垃圾收集器回收时收到一个系统通知,PhantomReference来实现虚引用

垃圾收集算法

1. 标记-清除算法

  • 算法分为"标记"和"清除"两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象.这是一种最基础的收集算法,
  • 他又两个不足,一个是效率问题,标记和清除两个过程的的效率都不高;另一个是空间问题,标记清除之后会产生大量的不连续的内存片段,空间碎片他多可能会导致以后在程序运行的过程中需要分配较大对象时候,无法找到足够的连续内存二不得不提前触发一次垃圾收集动作

2. 复制算法

  • 它将课用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活的对象复制到另外一块上面,然后再把已经使用郭的内存空间一次清理掉,
  • 这样使得每次都是对整个半区的内存回收,内存分配也就不用考虑内存碎片等复杂情况

3. 标记-整理算法

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

4. 分代收集算法

  • 根据对象存活周期的不同将内存划分为几块,一般是把Java堆分为年轻代和老年代,这样就可以根据各个年代的特点采用最适当的是收集算法.
  • 在新生代中,每次垃圾收集都会发现大批对象死去,只有少量存活,那就选用复制算法,只要付出少量存活对象的复制成本就可以完成收集,
  • 在老年代中,因为对象存活率比较高,没有额外的空间对他进行分配,就必须使用"标记-清理"或者"标记-整理"算法来进行回收.

4. HotSpot的算法实现

1. 枚举根节点

5. 垃圾收集器

  • 垃圾回收器一直与内存大小息息相关
    image
  • 如图是作用于不同分代的收集器,如果两个收集器之间存在连线,就说明他们可以搭配使用,

1. Serial收集器

  • 新生代收集器,Serial收集器是一个单线程的收集器,它的单线程的意义不仅仅是说明它只会使用一个个CPU或者是一条收集线程去完成垃圾收集工作,它在进行垃圾收集的时候,必须暂停其他所有的工作线程,知道它收集结束
  • 简单高效,仅仅是与其他的单线程收集器相比.Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择

2. ParNew收集器

  • 新生代收集器,ParNew收集器是Serial收集器的多线程版本,多运行在Server模式下的虚拟机上

3. Parallel Scavenge收集器

  • 新生代收集器,Parallel Scavenge收集器的特点是达到一个可控制的吞吐量,所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,其他的收集器关注点尽可能地缩短垃圾收集时用户线程地停顿时间.
  • 停顿时间越短越适合需要与用户交互的程序,良好的相应速度能够提升用户体验,而高吞吐量可以高效率的利用CPU的时间尽快完成程序的运算工作.
  • Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数

4. Serial Old收集器

  • Serial收集器的老年版本,同样是一个单线程收集器,使用"标记整理"算法
  • 一个用途是与Parallel Scavenge收集器搭配使用,另一个用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用

5. Parallel Old收集器

  • Parallel收集器的老年版本,使用多线程和标记整理算法

6. CMS收集器

  • CMS收集器是一种以获取最短回收停顿时间为目标的收集器,应用在B/S系统的服务器上

  • CMS收集器时基于标记清除算法实现的,他的运作过程分为4个步骤:

    • 初始标记:标记仅仅只是标记一下GC root能够直接关联到的对象
    • 并发标记:这个阶段就是进行GC root Tracing的过程
    • 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象
    • 并发清除
  • CMS收集器对CPU资源非常敏感,因为时并发标记,虽然不会导致应用程序变慢,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低.

  • CMS收集器无法处理浮动垃圾,可能出现"oncurrent Mode Failure"失败而导致另外一次Full GC的产生,在并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集处理他们,之一部分垃圾就被称为浮动垃圾.

  • CMS基于"标记-清除"算法实现的收集器,会产生大量的碎片,空间碎片过多,将会给大对象的分配带来麻烦,往往无法找到足够大的连续空间来分配当前对象,不得不提前触发一次GC Full

7. G1收集器

  • 并行和并发
  • 空间整合:采用"标记-整理"算法实现的收集器,从局部来看是基于"复制"算法实现的,但不会产生碎片
  • 可预测的停顿:G1除了可以追求地停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒
  • 它不再是将收集的范围划分为新生代或者老年代,而是物理上分块.逻辑上分代,G1跟踪各个块里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的,保证了G1收集器在有限时间内可以获取尽可能高的收集效率
  • 在G1收集器中,块与块之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用,虚拟机都是通过使用Remembered Set来避免全堆扫描的,G1中每一个块都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的块之间.
  • 如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:
    • 初始标记:标记G从 root能够直接关联的对象,然后修改TAMS的值,让下一阶段用户程序并发运行时,能在正确可用的块中创建对象,这阶段需要停顿线程,但是耗时很短
    • 并发标记:从GC root开始对堆中对象进行可达性分析,找出存活的对象,可与用户程序并发执行
    • 最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,需要停顿线程,但是可以并行执行.
    • 筛选回收:对各个块的回收价值和成本排序,根据用户所期望的GC停顿时间来指定回收计划可以与用户程序一起并发执行.

内存分配与回收策略

  • 对象的内存分配,其实就是在堆上分配,对象主要分配在新生代的Eden区,如果启动了本地线程分配缓冲,将按线程有限在TLAB上分配,极少数的情况下直接分配在老年代中,
  • 大对象直接进入老年代,所谓的大对象,就是需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组
  • 长期存活的对象就是老年代,虚拟机给每个对象都定义了一个对象年龄计数器,如果对象在eden出生并且经过第一次Minor GC仍然存活,并且能被Survivor容纳的话,将被移动到Serivior空间中,年龄为1,对象在Survivor中每熬过依次Minor GC,年龄就增加1,当年龄到15岁,就进入老年代

你可能感兴趣的:(深入理解JVM之垃圾回收与内存分配策略)