JVM内存分配策略及垃圾收集器简介

Java的内存分配策略

1.对象优先分配在Eden区

大多数情况下,对象都是优先分配在新生代eden区的,当eden区内存不够的时候就会发生Minor GC。

当没有eden区内存不够,开始进行minorGC时,若在这期间又发现已经分配了内存的空间allocation1无法存入存活区Survivor区,只能通过空间分配担保机制(后面第五点会讲到) 把新生代提前转移到老年代,若老年代上的空间足够存放这个allocation1分区就不会出现fullGC

2.大对象直接进入老年代

什么是大对象?

大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。

大对象经常出现会带来什么问题?

经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。

如何设置大对象的阈值?

-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。

3.长期存活的对象进入老年代

为对象定义年龄计数器,对象在 Eden 出生 并经过 Minor GC 依然存活,将移动到 Survivor 中 ,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。

如何设置年龄阈值?

-XX:MaxTenuringThreshold 用来定义年龄的阈值。

4.动态对象年龄判断

虚拟机并不是永远都要求对象的年龄必须达到-XX:MaxTenuringThreshold所配置的数字才能晋升老年代,如果在survivor中相同年龄所有对象的大小的总和大于survivor分区的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到满足MaxTenuringThreshold要求的年龄。

5.空间分配担保

在JDK6 Update24后,规则变成------ 只要老年代连续的内存空间大于新生代对象的总大小 或者 历次晋升的平均大小就会进行MinorGC,否则就会进行FullGC。

Java垃圾收集器组合

Serial收集器+Serial Old收集器

Serial (串行) 收集器是最基本、历史最悠久的垃圾收集器了。

新生代才用标记-复制 算法,老年代标记-整理算法

Serial收集器

优点:简单高效,因为是单线程,没有线程交互的开销,因此拥有最高的单线程收集效率

Serial Old收集器

Serial收集器的老年代版本

ParNew收集器+CMS收集器

parNew收集器

负责新生代的收集器

serial收集器的多版本线程,线程的数量跟cpu数量相同(默认)

可以通过-XX:ParallelGCThreads 参数来设置线程数。

新生代才用复制算法,老年代才用标记-整理算法,只能和cms垃圾收集器配合工作

CMS收集器

负责老年代的收集

重视响应速度、低停顿 的一个垃圾收集器(使用"标记-清除"算法

CMS收集器是一种以获取最短回收停顿时间为目标的收集器(针对尤其重视服务的响应速度的情况,挺短时间越短越适合需要与用户交互的程序,良好的相应速度能给用户带来较好的体验)

优点:

  • 并发收集
  • 低停顿

缺点:

  • 使用的回收算法"标记-清除"算法会导致收集结束时产生大量的空间碎片,往往出现老年代空间剩余,但无法找到足够大的连续空间来分配当前而不得不提前触发一次Full GC。
  • 吞吐量低:CMS的低停顿时间是以牺牲吞吐量为代价的,导致CPU利用率不够高。
  • 并发收集阶段会降低吞吐量
  • 对CPU资源敏感
  • 无法处理浮动垃圾,可能出现Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次GC时才能进行回收。(由于浮动垃圾的存在,因此需要预留出一部分内存,意味着CMS手机不能像其他收集器那样等待老年代快满的时候回收。)【若预留的内存不够存放浮动垃圾,就会出现Concurrent Mode Failure,这时虚拟机将临时启用Serial Old来替代CMS】

控制参数

  • -XX:+UseConcMarkSweepGC ------ 使用CMS收集器
  • -XX:+UseCMSCompactAtFullCollection ------ Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
  • -XX:+CMSFullGCsBeforeCompaction ------ 设置进行几次Full GC后,进行一次碎片整理
  • -XX:ParallelCMSThreads ------ 设定CMS的线程数量(一般情况约等于可用CPU数量)

执行流程

  • 初始标记(STW):

    仅仅只是标记一下GC Roots能直接关联到的对象,速度非常快,但是需要停顿。

  • 并发标记:

    同时开启GC和用户线程,用一个闭包结构去记录可达对象。(耗时长,但是不需要停顿)

  • 重新标记(STW):

    重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录(这个阶段的停顿时间一般会比初始阶段的标记时间长,但远远比并发标记的时间短,且需要停顿)

  • 并发清除

    开启用户线程,同时GC线程开始对未标记的区域做清扫(耗时长,不需要停顿)

在整个标记过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿

所以,总体上来说,CMS收集器的内存回收过程与用户线程一起并发地执行。

Parallel Scavenge收集器+ Parallel Old收集器

Java1.8版本的默认垃圾收集器,被称为"吞吐量优先"收集器

和ParNew一样是多线程收集器

新生代才用标记-复制算法,老年代才用标记-整理算法

适用场景:

高吞吐可以高效率地利用cpu时间,尽快完成程序的运算任务,主要是适合在后台运算而不需要太多交互的任务。

缩短停顿时间是以牺牲吞无量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降

在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。

G1收集器

G1是一款面向服务端应用的垃圾收集器,在多cpu和大内存的场景下有很好的性能。(通常是4GB或更大)它能够在不牺牲太多吞吐量的情况下,提供更可控的停顿时间(建议的内存大小为8GB或更大,并且至少具有4个CPU核心。)

堆被分为新生代和老年代,其他收集器进行收集的范围都是整个新生代或者老年代,而G1可以直接堆新生代和老年代一起回收

G1垃圾收集器适合是什么时候使用?

  1. 大内存的情况下适合G1
  2. 对象分配和晋升速度变化很大。
  3. 垃圾回收时间特别长,超过1s
  4. 8g以上堆内存可以用。
  5. 停顿时间500ms以上。

G1垃圾收集器的特点

  • 空间整合:

    从整体来看是基于"标记-整理"算法实现的收集器,但是从局部来看,实际上是基于两个Region之间的基于"复制"算法实现的,这意味这运行期间不会产生内存空间碎片。【标记复制算法】

  • 可预测的停顿:

    能让使用者明确制定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒

标记-复制算法

下面以G1为例,通过G1中标记-复制算法过程(G1的Young GC和Mixed GC均采用该算法)

标记复制算法的三个阶段:

标记阶段: 即从GC Roots集合开始,标记活跃对象。

转移阶段: 即把活跃对象复制到新的内存地址上。

重定位阶段: 因为转移导致对象的地址发生了变化,在重定位阶段,所有指向对象旧地址的指针都要调整到对象新地址上。

G1的混合回收过程分为:标记阶段、清理阶段、复制阶段

G1把堆划分成多个大小相同的独立区域(Region),新生代和老年代不一定的是物理隔离(只是在逻辑上是连续的)【分区概念】

启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。

好处:通过引入Region的概念,从而将原来的一整块内存空间划分为多个小空间,使得每个小空间可以单独进行垃圾回收。

这种划分方式带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个Region垃圾回收时间 以及回收所获得的空间 (这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。

每个Region都有一个Remembered Set(已记忆集合), 用来记录该Region对象的引用法对象所在的Region。(通过Remembered Set,在做可达性分析的时候就可以避免全堆扫描)

G1垃圾收集器的收集步骤

  1. 标记阶段(STW)

    首先初始标记这个阶段是会停顿的,并且会触发一次普通的Mintor GC(对应GC log:GC pause (young) (inital-mark))

  2. Root Region Scaning

    程序运行过程中会回收survivor区,这一过程必须在young GC之前完成

  3. Concurrent Marking

    在整个堆中进行并发标记(和应用程序是并发执行的),此过程可能被young GC打断。

    (在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那个区域就会被立即回收,如图中打×的地方)

    同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)

  4. Remark,再标记(STW)

    会有短暂的停顿。再标记阶段用来收集并发阶段产生的垃圾(并发阶段和应用程序一同运行);G1才用了比CMS更快的初始快找算法(SATB)

  5. Copy/Clean up,多线程清除失活对象(STW)

    会有停顿。G1将回收区域的存过对象拷贝到新区域,清除Remember Sets,并发清空回收区域并把它返回到空闲区域链表中。

  6. 复制/清除过程后

    回收区域的活性对象已经被集中回收到社蓝色和深绿色区域。

G1收集器的运作流程

如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:

  1. 初始标记

  2. 并发标记

  3. 最终标记(STW)

    为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。

  4. 筛选回收

    首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。(此阶段其实也可以做到与用户程序一起并发执行的,但是因为只回收一部分的Region,时间是用户可控制的【停顿时间可控】,而且停顿用户线程将大幅度提高收集效率)

ZGC收集器

ZGC作为下一代垃圾回收器,性能非常优秀。ZGC垃圾回收过程几乎全部是并发,实际STW停顿时间极短,不到10ms。

这得益于其采用的着色指针和读屏障技术

ZGC垃圾收集器特点

  • ZGC是单代垃圾收集器,而CMS是分代垃圾收集器。单代垃圾回收期每次处理的对象更多,更耗费cpu资源
  • ZGC使用读屏障,读屏障操作更需耗费额外的计算资源

ZGC垃圾收集器适合什么时候使用?

ZGC垃圾收集器是一种低延迟的垃圾收集器,适用于需要非常短的停顿时间的应用程序。

它适用于大内存容量(通常是16GB或更大)和多核CPU的环境。 (建议的内存大小为16GB或更大,并且至少具有8个CPU核心。)

全并发的ZGC

与CMS中的ParNew和G1类似,ZGC也才用了标记-复制算法(改进过的) ,不过ZGC对该算法进行了重大改进:ZGC在标记。转移和重定位阶段几乎都是并发的,这是ZGC实现挺短时间小于10ms目标的最关键原因

ZGC垃圾回收周期

ZGC只有三个STW的阶段:初始标记、再标记、初始转移

其中,初始标记和初始转移分别都只需要扫描所有的GC Roots,其处理时间和GcRoots的数量成正比,一般情况耗时非常短

再标记阶段STW时间很短,最多1ms,超过1ms则再次进入并发标记阶段。(即,ZGC几乎所有暂停都只依赖于GC Roots集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。)

相关推荐
man20171 小时前
【2024最新】基于springboot+vue的闲一品交易平台lw+ppt
vue.js·spring boot·后端
hlsd#1 小时前
关于 SpringBoot 时间处理的总结
java·spring boot·后端
路在脚下@1 小时前
Spring Boot 的核心原理和工作机制
java·spring boot·后端
幸运小圣1 小时前
Vue3 -- 项目配置之stylelint【企业级项目配置保姆级教程3】
开发语言·后端·rust
前端SkyRain2 小时前
后端Node学习项目-用户管理-增删改查
后端·学习·node.js
提笔惊蚂蚁2 小时前
结构化(经典)软件开发方法: 需求分析阶段+设计阶段
后端·学习·需求分析
老猿讲编程3 小时前
Rust编写的贪吃蛇小游戏源代码解读
开发语言·后端·rust
黄小耶@3 小时前
python如何使用Rabbitmq
分布式·后端·python·rabbitmq
宅小海4 小时前
Scala-List列表
开发语言·后端·scala
蔚一5 小时前
Javaweb—Ajax与jQuery请求
前端·javascript·后端·ajax·jquery