JVM 内存模型垃圾回收

Posted ~无关风月~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM 内存模型垃圾回收相关的知识,希望对你有一定的参考价值。

JVM 内存模型

JVM运行时数据区是一种规范,而JVM内存模式是对该规范的实现。

重点存储数据的是堆和方法区(非堆),(注意这两块区域都是线程共享的)。

一块是非堆区,一块是堆区
堆区分为两大块,一个是Old区,一个是Young区
Young区分为两大块,一个是Survivor区(S0+S1),一块是Eden区
S0和S1一样大,也可以叫From和To

一般情况下,新创建的对象都会被分配到Eden区,一些特殊的大的对象会直接分配到Old区。

如何理解Minor/Major/Full GC?
Minor GC:新生代
Major GC:老年代
Full GC:新生代+老年代

为什么需要Survivor区?只有Eden不行吗?
如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。 这样一来,老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。

老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。频发的Full GC消耗的时间很长,会影响大型程序的执行和响应速度。

Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。

为什么需要两个Survivor区?
最大的好处就是解决了碎片化。

假设现在只有一个Survivor区:
刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor 区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些 存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的, 也就导致了内存碎片化。
永远有一个Survivor space是空的,另一个非空的Survivor space无碎片。

新生代中Eden:S1:S2为什么是8:1:1?
新生代中的可用内存:复制算法用来担保的内存 = 9:1
可用内存中Eden:S1 = 8:1
即新生代中Eden:S1:S2 = 8:1:1
现代的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象大概98%是 “朝生夕死”的

堆内存中都是线程共享的区域吗?
JVM默认为每个线程在Eden上开辟一个buffer区域,用来加速对象的分配,称之为TLAB,全称:Thread Local Allocation Buffer。
对象优先会在TLAB上分配,但是TLAB空间通常会比较小,如果对象比较大,那么还是在共享区域分配。

JVM 什么时候会GC

JVM 调用 GC 的频度还是很高的,主要两种情况下进行垃圾回收:

  • 当应用程序线程空闲;
  • java 内存堆不足时,会不断调用 GC 。
  • 显示调用 System.GC() 只能建议 JVM 需要在内存中对垃圾对象进行回收,但不是必须马上回收。

GC 有两个线程:
新创建的对象被分配到 New 区,当该区被填满时会被 GC 辅助线程移到 Old 区,当 Old 区也填满了会触发 GC 主线程遍历堆内存里的所有对象。 Old 区的大小等于 -Xmx 减去 -Xmn

GC垃圾回收算法

对象存活判断:
1、引用计数法:每一个对象有一个引用计数属性,每新增一个引用,该计数加1,引用释放时,计数减1,计数为0时,表示可回收。
弊端:无法解决对象相互引用的问题,两对象相互引用,他们的计数属性都大于1,但两对象都不再使用,却无法被回收。

2、可达性分析:从GCRoot开始,向下搜索,当一个对象到GCRoot有引用链相连时,是可达的,不用回收。
可作为GC Roots的对象包括:

  • JVM桟(桟帧中的本地变量表)中引用的对象;
  • 本地方法桟中引用的对象;
  • 方法区中类静态属性引用的对象;
  • 方法区中常量引用的对象。

标记-清除(Mark-Sweep)

  • 标记:扫描堆内所有对象,需要回收的全部标记出来
  • 清理:清除掉被标记的所有对象,释放内存空间

缺点:

  • 效率低,需要遍历堆内存
  • 会产生大量内存碎片


标记-复制(Mark-Copying)

将内存划分为两块相等的区域,每次只使用其中一块,当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次清除掉。

特点:无内存碎片,较高效
缺点:但使用的内存减为一半,持续复制长生存期的对象则导致效率降低。不适用于有较多较持久的对象。

标记-整理(Mark-Compact)

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

分代收集算法(Generational Collection)

把Java堆分为新生代和老年代,根据其不同特点使用不同的垃圾回收算法。
新生代:对象存活率低,复制算法。
老年代:对象存活率高,标记-整理算法。

堆分代的唯一理由就是优化GC性能。

垃圾收集器

Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。

为什么要stop the world?
对于复制算法,如果一块内存满了,需要将存活的对象全部移动到另一块内存,将这块内存全部清空,如果不stop the world,新产生的对象无处安放,会扰乱算法的执行。对于标记-整理算法,会扰乱整理过程。标记的时候,多线程会影响可达性分析。

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

Serial

一种单线程收集器

优点:简单高效,拥有很高的单线程收集效率
缺点:收集过程需要暂停所有线程
算法:复制算法
适用范围:新生代 应用:Client模式下的默认新生代收集器

-XX:+UseSerialGC 使用串行收集器

Serial Old

是Serial收集器的老年代版本,也是一个单线程收集器,不同的是采用"标记-整理算法",运行过程和Serial收集器一样。

-XX:+UseParallelOldGC

ParNew

Serial收集器的多线程版本。

优点:在多CPU时,比Serial效率高。
缺点:收集过程暂停所有应用程序线程,单CPU时比Serial效率差。
算法:复制算法
适用范围:新生代
应用:运行在Server模式下的虚拟机中首选的新生代收集器

-XX:+UseParNewGC 使用ParNew收集器
-XX:ParallelGCThreads 限制线程数量

Parallel Scavenge

类似ParNew收集器,但更关注于系统的吞吐量。可以通过参数打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最适合的停顿时间或最大的吞吐量。新生代复制算法。

吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)
比如虚拟机总共运行了100分钟,垃圾收集时间用了1分钟,吞吐量=(100-1)/100=99%。
若吞吐量越大,意味着垃圾收集的时间越短,则用户代码可以充分利用CPU资源,尽快完成程序的 运算任务。

-XX:+UseParallelGC 使用Parallel收集器+老年代串行
-XX:MaxGCPauseMillis控制最大的垃圾收集停顿时间
-XX:GCRatio直接设置吞吐量的大小。

Parallel Old

Parallel Old 收集器是Parallel Scavenge收集器的老年代版本,使用多线程 和 标记-整理算法进行垃圾回收,也是更加关注系统的吞吐量。

-XX:+UseParallelOldGC

CMS(Concurrent Mark Sweep)

是一种以获取 最短回收停顿时间 为目标的收集器。为了提高服务响应速度,降低STW时间。

步骤:基于 标记-清除 算法

  • 初始标记(CMS initial Mark):需要STW。标记一下GCRoots能直接关联到的对象,速度很快;
  • 并发标记(CMS concurrent Mark):进行GC Roots Tracing的过程:从GCRoots开始对堆中对象进行可达性分析,找出存活的对象,耗时较长但可与用户程序并发执行。
  • 重新标记(CMS remark):需要STW。为了修正并发标记期间,因用户程序运作而导致标记产生变动的那一部分对象的标记记录,停顿时间比初始标记稍长,但远比并发标记时间短。
  • 并发清除(CMS concurrent sweep):清除不可达对象回收空间,同时有新垃圾产生,留着下次清理称为 浮动垃圾

耗时最长的并发标记和并发清除可以与用户线程并行,所以总体来说,CMS收集器内存回收过程与用户线程并发地执行。

是老年代收集器(新生代用ParNew)

优点:并发收集、低停顿
缺点:产生大量内存碎片,并发阶段会降低吞吐量

-XX:+UseConcMarkSweepGC 使用CMS收集器
-XX:+UseCMSCompactAtFullCollection FullGC 后进行一次碎片整理,整理过程是独占的,会STW
-XX:+CMSFullGCsBeforeCompaction 设置进行几次FullGC 后,进行一次碎片整理
-XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)

G1(Garbage-First)

JDK 7开始使用,JDK 8非常成熟,JDK 9默认的垃圾收集器,适用于新老生代。

G1收集器将整个java堆划分为多个相等的独立区域,新生代和老年代不再是物理上的分隔,他们都是可以是一部分不连续的Region集合。
每个Region大小都是一样的,可以是1M到32M之间的数值,但是必须保证是2的n次幂。
所谓Garbage-Frist,其实就是优先回收垃圾最多的Region区域。

与CMS收集器相比G1收集器有如下特点:
1.空间整合,采用标记整理算法,不会产生内存空间碎片。
2.可预测停顿,能建立可预测停顿时间模型,能够让使用者明确制定在N毫秒时间片段内,消耗在垃圾收集上的时间不能超过N毫秒。
3.并行与并发,能充分利用CPU,多核环境优势,其他收集器需要停顿java线程执行GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
4.不用和其他收集器配合,可以独立管理整个GC堆。

步骤:

  • 初始标记:需要STW,只标记GCRoots能直接关联到的对象,耗时短。
  • 并发标记:从GCRoot开始对堆中对象进行可达性分析,找出存活对象,耗时较长,但可与用户线程并发执行。
  • 最终标记:需要STW,修正在并发标记阶段因用户程序继续运作而导致标记产生变动的那一部分标记记录。
  • 筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户期望的GC停顿时间来定制回收计划。

-XX:+UseG1GC

ZGC

JDK11新引入的ZGC收集器,不管是物理上还是逻辑上,ZGC中已经不存在新老年代的概念了 会分为一个个page,当进行GC操作时会对page进行压缩,因此没有碎片问题 只能在64位的linux上使用,目前用得还比较少

(1)可以达到10ms以内的停顿时间要求
(2)支持TB级别的内存
(3)堆内存变大后停顿时间还是在10ms以内

垃圾收集器分类

串行收集器 :Serial和Serial Old
只能有一个垃圾回收线程执行,用户线程暂停。
适用于内存比较小的嵌入式设备 。

并行收集器[吞吐量优先] : Parallel Scanvenge、Parallel Old
多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
适用于科学计算、后台处理等弱交互场景。

并发收集器[停顿时间优先] :CMS、G1
用户线程和垃圾收集线程同时执行(但并不一定是并行的,可能是交替执行的),垃圾收集线程在执行的时候不会停顿用户线程的运行。
适用于相对时间有要求的场景,比如Web 。

常见问题

评价垃圾回收器好处的标准?
吞吐量和停顿时间
停顿时间:垃圾收集器 进行 垃圾回收终端应用执行响应的时间
吞吐量:运行用户代码时间/(运行用户代码时间+垃圾收集时间)

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

如何选择合适的垃圾收集器?
优先调整堆的大小让服务器自己来选择
如果内存小于100M,使用串行收集器
如果是单核,并且没有停顿时间要求,使用串行或JVM自己选
如果允许停顿时间超过1秒,选择并行或JVM自己选
如果响应时间最重要,并且不能超过1秒,使用并发收集器

以上是关于JVM 内存模型垃圾回收的主要内容,如果未能解决你的问题,请参考以下文章

深入探究JVM之垃圾回收器

深入理解JVM:内存结构垃圾回收类加载内存模型

深入理解JVM:内存结构垃圾回收类加载内存模型

深入理解JVM:内存结构垃圾回收类加载内存模型

JVM内存模型及垃圾回收机制

直通BAT必考题系列:JVM的4种垃圾回收算法垃圾回收机制与总结