Java虚拟机内存划分、垃圾收集算法、垃圾收集器

文章目录

  • 运行时数据区域
    • 程序计数器
    • Java虚拟机栈
    • 本地方法栈
    • Java堆
    • 方法区
      • 运行时常量池
    • 直接内存
  • 对象存活判断
  • 垃圾收集算法
    • 分代收集理论
    • 标记-清除
    • 标记-复制
    • 标记-整理
  • 垃圾收集器
    • Serial
    • ParNew
    • Parallel Scavenge
    • Serial Old
    • Parallel Old
    • CMS
    • G1
  • 参考书籍深入理解Java虚拟机


运行时数据区域

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。具体区域如下图,其中黄色的是线程私有,其他为线程共享区域。
Java虚拟机内存划分、垃圾收集算法、垃圾收集器_第1张图片

程序计数器

内存空间较小,当前线程执行指令的指示器。通过计数器来选取下一条执行的指令,分支、循环、跳转、异常指针、线程恢复等基础功能都需要依赖计数器完成。

Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个时刻一个处理器或一个内核都只会执行一条线程中的指令。

Java虚拟机栈

是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量、操作数栈、动态连接、方法出口等信息。每个方法被调用完成对应一个栈帧在虚拟机中从入栈到出栈的过程。

本地方法栈

和Java虚拟机栈发挥的作用是非常相似的,其区别是虚拟机栈为虚拟机执行Java方法,而本地方法栈则是为虚拟机使用到本地方法服务。

Java堆

是虚拟机管理内存中最大的一块。存放对象实例,几乎所有的对象实例都在这里分配。

是垃圾收集器管理的内存区域,为每个线程分配一定量的线程缓存区(Thread Local Allocation Buffer, TLAB)以提高对象分配的效率。

可以通过-Xms和–Xmx来设定内存空间的大小

方法区

存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码缓存等数据。
在JDK7中HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出,而到JDK8终于废弃永久代的概念,采用本地内存中实现的元空间来代替,把JDK7剩余的内容都移动到元空间中。

运行时常量池

方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译器生成的各种字面量与符号引用,这部分内容将在类加载后放到方法区的运行时常量池中。

直接内存

并不是虚拟机运行时数据区域的一部分,但这部分内存会被频繁使用,而且也能导致OOM异常。

JDK1.4新加入了NIO(New Input/Output)类,引入了一种基于通道与缓冲区的I/O方式,他可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。避免了在Java堆和Native堆来回复制数据。

对象存活判断

Java通过可达性分析算法来判定对象是否存活,通过一系列“GC Roots”的跟对象作为起点集,根据引用关系向下搜索,搜索过程所走过的路径为“引用链”。如果某个对象到GC Roots间没有引用链或是GC Roots到这个对象不可达时,证明此对象不可能再被使用。

固定作为GC Roots的对象包含以下几种:

  1. 虚拟机栈中的引用对象,譬如参数、局部变量、临时变量等。
  2. 方法区类静态属性引用的对象:如 static String
  3. 方法区中常量引用的对象:如: final String
  4. 本地方法栈中的引用对象
  5. Java虚拟机内部的引用:如基本数据类型对应的Class对象,一些常见的异常对象等
  6. 所有被同步(Synchronized关键字)锁持有的对象

对象引用分为如下4中

  1. 强引用:有用且必须,垃圾收集器无论如何都回收不掉的对象
  2. 软引用:有用非必须,在系统发生内存溢出之前,会把这些对象列进回收范围之中进行二次回收,如果内存还不够则抛出内存溢出。
  3. 弱引用:非必须对象,只能生存到下次垃圾收集器发生为止。无论内存是否充足。
  4. 虚引用:无法通过虚引用来获取对象实例,唯一目的是对象被回收时收到一个系统通知。 回收策略同弱引用

垃圾收集算法

分代收集理论

Java一般把堆分为新生代与老年代,在新生代中每次垃圾收集时都会发现大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。

跨代引用假说:存在相互引用的对象应该倾向于同时生存或是同时消亡。很少存在跨代引用对象。在跨带引用只需在新生代建立一个全局的数据结构,把老年代划分成若干小块,标识出老年代的哪一快内存存在跨代。此后当发生Minor GC时,只要包含了跨代引用的小块内存里面的对象才会被加入到GC Roots进行扫描。

部分收集(Partial GC):

  1. 新生代收集(Minor GC/Young GC): 发生在新生代的垃圾收集,对象大部分朝生夕死。
  2. 老年代收集(Major GC/Old GC): 发生在老年代的垃圾收集,只有少量对象可回收。目前只有CMS收集器会单独收集老年代的行为。
  3. 混合收集(Mixed GC): 收集真个新生代以及部分老年代的垃圾收集器,目前只有G1支持这种行为
    整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

标记-清除

算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,标记完成之后统一回收所有被标记的对象。

缺点:第一执行效率不稳定,如果堆中存在大量对象,且大部分需要回收,则需要进行大量的标记和清除动作。第二回收后内存不连续,如果分配大对象时无法找到连续的空间而触发另一次垃圾收集动作。

标记-复制

标记-复制算法也叫半区复杂,将内存划分为两个内存相等的区域,每次只使用其中一块,当这一块用完就讲存活的对象复制到另外一块上面,然后把已用过的内存空间一次性清空。

解决了标记-清除算法面对大量可回收对象执行效率低的问题。

优点:实现简单、运行高效、内存连续
缺点:有一半的空间浪费,且如果存活对象过多,会导致效率低下。

HotSpot虚拟机执行半区复制策略:把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配只是用Eden和一块Survivior。当发生垃圾收集时,将Eden和Survivor中仍然存活的对象复制到另一块Survivor空间上,然后直接清空Eden和已用过的那块Survivor空间。当另一块Survivor空间不足,部分对象会被分配到老年代。默认Eden和Survivor比例:8:1

标记-整理

标记-整理和标记-清除算法一样,但后续步骤不是直接对可回收的对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界外的内存。

标记-清除与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。

垃圾收集器

HotSpot虚拟机垃圾收集器的搭配使用如下图
Java虚拟机内存划分、垃圾收集算法、垃圾收集器_第2张图片
虚拟机列表如下:

名称 回收区域 算法 是否多线程 是否暂停服务 戳发
Serial 年轻代 标记-复制 新生代收集
ParNew 年轻代 标记-复制 新生代收集
Parallel Scavenge 年轻代 标记-复制 新生代收集
Serial Old 老轻代 标记-整理 整堆收集
Parallel Old 老轻代 标记-整理 整堆收集
CMS 老轻代 标记-清除 老年代收集
G1 局部 标记-复制 混合收集

Serial

收集器单线程工作,且在收集期间暂停其他所有工作线程,直到它收集结束。

优势:

  1. 简单高效、对于内存资源受限的环境,它是所有收集器中额外内存消耗最小的。
  2. 对于单核处理器或处理器核心数较小的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

ParNew

是Serial收集器的多线程并行版本,除了同时使用多线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致。

Parallel Scavenge

是新生代收集器,基于标记-复制算法实现的收集器,并行收集的多线程收集器。

其他收集器关注目标是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控的吞吐量(Throughput)。 吞吐量是处理器用于运行用户代码的时间与处理器总消耗时间的比值。

控制吞吐量参数:设置吞吐量大小参数 -XX:GCTimeRatio,控制最大垃圾收集停顿时间参数:-XX:MaxGCPauseMillis

Serial Old

Serial收集器的老年代版本,单线程、标记-整理算法。

Parallel Old

是Parallel Scavenge收集器的老年代版本,多线程、标记-整理算法实现。

CMS

CMS(Concurrent Mark Sweep)收集器是一种已获取最短回收停顿时间为目标的收集器,多线程、标记-清除算法实现。

标记-清除运行过程:

  1. 初始标记: 停顿服务,仅标记GC Roots能直接关联到的对象,速度很快。
  2. 并发标记:不停顿服务,从GC Roots的直接关联对象开始遍历整个对象图过程,耗时较长。
  3. 重新标记:停顿服务,修正并发标记期间,用户线程继续运行导致标记产生变动的那一部分标记记录,耗时比初始标记稍长。
  4. 并发清除:不停顿服务,清理删除掉标记阶段判断的已经死亡对象。

优点:并发收集、低停顿。
缺点:对处理器资源敏感,在并发阶段虽然不会导致用户线程停顿,但会占用一部分线程而导致程序变慢,降低吞吐量。

CMS在并发标记和并发清除阶段,用户线程是继续运行的,程序在运行自然还会伴随有新的垃圾不断产生,但这部分垃圾是在标记过程之后产生的,所以CMS无法在当次手机中处理掉他们,只好留在下次垃圾收集时再清理,这部分垃圾就称为“浮动垃圾”

同时因用户线程是继续运行的,所以就必须预留足够的呢次提供给用户线程使用,因此不能等老年代几乎完全填满在收集。这导致预留空间过大容易导致收集器被激活,而预留空间过小容易内存不足无法分配新对象导致并发失败

CMS收集器无法处理”浮动垃圾“(Floating Garbage)而导致内存不足,有可能出现"并发失败(Concurrent Mode Failure)"是不进而导致另一次完全"Stop The World”的Full GC的产生,临时启用Serial Old收集器来重新进行老年代的垃圾收集。

G1

G1将堆划分多个Region,每个Region可以根据需要,扮演新生代的Eden、Survivor空进,或是老年代空间。收集器能够根据不同角色的Region采用不同的策略去处理。

G1收集器之所以能够建立可预测的停断模型,是因为将Region作为最小回收单位,每次回收都是Region大小的整数倍。避免了Java堆中全区域收集。让G1收集器跟踪各个Region里面垃圾堆积的“价值”大小,价值即回收所获得空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停断时间,优先处理回收价值收益最大的那些Region。

G1收集器运作过程

  1. 初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时段,
  2. 并发标记:从GC Roots开始对堆中对象进行可达性分析,递归扫描整个堆里面的对象图,找出要回收的对象,这个阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB(原始数据快照)记录下的并发时由引用变动的对象。
  3. 最终标记:短暂停顿用户线程、用于处理并发阶段结束后仍然遗留下来的最后那少量的SATB(原始数据快照)记录。
  4. 筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户期望的停顿时间来指定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,在清理掉真个旧Region的全部空间。暂停服务,多线程并发

参考书籍深入理解Java虚拟机

你可能感兴趣的:(架构与规范,java,jvm,算法)