JVM之垃圾回收

Stella981
• 阅读 444

文章目录

写时复制

写时复制(copy-on-write)是众多 UNIX 操作系统用到的内存优化的方法。比如在 Linux 系统中使用 fork() 函数复制进程时,大部分内存空间都不会被复制,只是复制进程,只有在内存中内容被改变时才会复制内存数据。 但是如果使用标记清除算法,这时内存会被设置标志位,就会频繁发生不应该发生的复制。

另外,关于标记清除的变形,还有一种叫做标记压缩(Mark and Compact)的算法,它不是将被标记的对象清除,而是将它们不断压缩。

Hotspot虚拟机

HotSpot的正式发布名称为"Java HotSpot Performance Engine",是Java虚拟机的一个实现,包含了服务器版和桌面应用程序版。

Snapshot-At-The-Beginning (SATB)

G1 使用的是 SATB 标记算法,主要应用于垃圾收集的并发标记阶段,解决了CMS 垃圾收 集器重新标记阶段长时间 Stop The World(STW) 的潜在风险。其算法全称是 Snapshot At The Beginning,由字面理解,是垃圾回收器开始时活着的对象的一个快照。

它是通过 “根集合”穷举可达对象得到的,穷举过程中采用了三色标记法:

  • 白:对象没有被标记到,标记阶段结束后,会被当做垃圾回收掉。
  • 灰:对象被标记了,但是它所在的Field还没有被标记或标记完(可达对象还未被标记)。
  • 黑:对象被标记了,且它的所有Field也被标记完了。

所以,漏标的情况只会发生在白色对象中,且满足以下任意一个条件:

  • 并发标记时,应用线程给一个黑色对象的引用类型字段赋值了该白色对象
  • 并发标记时,应用线程删除所有灰色对象到该白色对象的引用

SATB 利用 write barrier 将所有即将被删除的引用关系的旧引用记录下来,最后以这些旧引用为根 Stop The World 地重新扫描一遍即可避免漏标问题。 因此 G1 Remark阶段 Stop The World 与 CMS 了的remark有一个本质上的区别,那就是这个暂停只需要扫描有 write barrier 所追中对象为根的对象, 而 CMS 的remark 需要重新扫描整个根 集合,因而CMS remark有可能会非常慢。

Remembered Set(RSet)概念

RSet全称是Remembered Set,是辅助GC过程的一种结构,典型的空间换时间工具,和Card Table有些类似。

G1收集器中,Region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用是使用Remembered Set来避免扫描全堆。

G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序对Reference类型数据进行写操作时,会产生一个Write Barrier(写屏障)暂时中断写操作(虽然写屏障使得应用线程增加了一些性能开销,但Minor GC变快了许多,整体的垃圾收集效率也提高了许多,通常应用的吞吐量也会有所改善),检查Reference引用的对象是否处于不同的Region之间(在分代中就是检查老年代中的对象是否引用了新生代的对象),如果是便通过CardTable(卡表)把相关引用信息记录到被引用对象所属的Region的Remembered Set中。

当内存回收时,在GC根节点的枚举范围加入Remembered Set即可保证不对全局堆扫描也不会有遗漏。

卡表(Card Table)

为了支持高频率的新生代的回收,虚拟机使用一种叫做卡表(Card Table)的数据结构,卡表作为一个比特位的集合,每一个比特位可以用来表示年老代的某一区域中的所有对象是否持有新生代对象的引用。当老年代中的某个区域持有了新生代对象的引用时,JVM就把这个区域对应的Card所在的位置标记为dirty(bit位设置为1)。

JVM之垃圾回收

这样新生代在GC时,可以不用花大量的时间扫描所有年老代对象,来确定每一个对象的引用关系,而可以先扫描卡表,只有卡表的标记位为1时,才需要扫描给定区域的年老代对象。而卡表位为0的所在区域的年老代对象,一定不包含有对新生代的引用。这样子可以提高效率减少MinorGC的停顿时间。

JVM之垃圾回收
如上图,卡表中每一个位表示年老代4K的空间,卡表记录未0的年老代区域没有任何对象指向新生代,只有卡表位为1的区域才有对象包含新生代引用,因此在新生代GC时,只需要扫描卡表位为1所在的年老代空间。使用这种方式,可以大大加快新生代的回收速度。

配置每次扫描的Card数量

在JVM中,一个Card覆盖的默认大小是512字节,在多个线程并行收集时,JVM通过ParGCCardsPerStrideChunk参数设置每个线程每次扫描的Card数量,默认是256。

相当于是把老年代分成许多strides,每个线程每次扫描一个stride,每个stride大小为512*256 = 128K。

如果你的老年代大小为4G,那总共有4G/128K=32K个Strides。多线程在扫描这么多的strides时就涉及到调度和分配的问题,stride数量太多就会导致线程在stride之间切换的开销增加,进而导致GC暂停时间增长。

因此JVM提供了ParGCCardsPerStrideChunk这个参数来配置每个stride对应的card数量,这个数量要根据实际的业务场景进行调优,网上一般流传3个魔术数字:32768、4K和8K。

例如,配置每次扫描的Card数量:
(UnlockDiagnosticVMOptions:解锁任何额外的隐藏参数)

-XX:+UnlockDiagnosticVMOptions
-XX:ParGCCardsPerStrideChunk=4096

这个值不能设置的太大,因为GC线程需要扫描这个stride中老年代对象持有的新生代对象的引用,如果只有少量引用新生代的对象那就导致浪费了很多时间在根本不需要扫描的对象上。
(在JVM调优过程中,没有一个参数的值完美的,只有经过不断的调优过程,慢慢的摸索到适合自己应用的最佳参数范围,除非应用对YGC的耗时特别敏感,不到万不得已,不用优化该参数,默认的256也适合大部分情况。但是随着现在机器内存的扩大,适当的增大该参数值(4K),也是可以的)

"空闲列表"内存分配&"指针碰撞"内存分配

虚拟机遇到一条new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过,如果没有,那必须先执行相应的类加载过程。

在类加载检查通过后为新生对象分配内存。内存分配方式有两种:

(1)指针碰撞

指针碰撞的前提是Java堆是绝对规整的,有用的和空闲各自放在一边,中间放着一个指针作为分界点指示器,所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。在使用Serial,和ParNew等收集器时候使用的是指针碰撞。

(2)空闲列表

如果Java堆不是规整的,已使用的内存和空闲的内存相互交错,虚拟机就必须维护一个列表,记录了哪些是可用的内存,在分配的时候从列表中找到一块足够大的空间分配给对象实例,并更新列表上的记录。在使用CMS这种基于Mark-Sweep(标志-清除)算法的收集器时,通常采用空闲列表。

Promotion Failure

在minor gc过程中,survivor(幸存者)的剩余空间不足以容纳eden(伊甸园)及当前在用的survivor区间存活对象,只能将容纳不下的对象移到年老代(promotion),而此时年老代满了无法容纳更多对象,通常伴随full gc,因而导致的promotion failure。
这种情况通常需要增加年轻代大小,尽量让新生对象在年轻代的时候尽量清理掉。

Concurrent Mode Failure

如果CMS预留内存空间无法满足程序需要,就会出现一次"Concurrent Mode Failure"失败;

这时JVM启用后备预案:临时启用Serail Old收集器(为什么不启用Parallel old?这样并行回收的停顿时间会更短。 https://stackoverflow.com/questions/39569649/why-is-cms-full-gc-single-threaded#39573243 ),而导致另一次Full GC的产生;

这样的代价是很大的,此时JVM将采用停顿的方式进行Full gc,整个gc时间会相当可观,完全违背了采用CMS GC的初衷,所以CMSInitiatingOccupancyFraction不能设置得太大。

出现该现象的原因主要是由于cms的无法处理浮动垃圾(Floating Garbage)引起的。
(出现此现象的原因主要有两个:一个是在年老代被用完之前不能完成对无引用对象的回收;一个是当新空间分配请求在年老代的剩余空间中无法得到满足(比如在年老代申请大空间对象))

这个跟cms的机制有关。cms的并发清理阶段,用户线程还在运行,因此不断有新的垃圾产生,而这些垃圾不在这次清理标记的范畴里头,cms无法在本次gc清除掉,这些就是浮动垃圾。

由于这种机制,cms年老代回收的阈值不能太高,否则就容易预留的内存空间很可能不够(因为本次gc同时还有浮动垃圾产生),从而导致concurrent mode failure发生。

-XX:CMSInitiatingOccupancyFraction

要避免此现象,可以降低触发CMS的阀值,即参数-XX:CMSInitiatingOccupancyFraction的值,该值代表老年代堆空间的使用率,通常JDK默认值是68;可以选择调低到50或者以下(指设定CMS在对老年代占用率达到50%的时候开始GC),让CMS GC尽早执行,以保证有足够的空间

-XX:+UseCMSInitiatingOccupancyOnly

有一个需要注意的点,仅仅设置CMSInitiatingOccupancyFraction的值的值表示第一次CMS收集按照该比例收集,后面JVM将会自动进行调节。配置该参数使用的还有一个参数。
-XX:+UseCMSInitiatingOccupancyOnly可以设置true和false,默认为false。

将-XX+UseCMSInitiatingOccupancyOnly 值设置为true来命令 JVM 不基于运行时收集的数据来启动 CMS 垃圾收集周期。而是,当该标志被开启时,JVM 通过 CMSInitiatingOccupancyFraction 的值进行每一次 CMS 收集,而不仅仅是第一次。

然而,请记住大多数情况下,JVM 比我们自己能作出更好的垃圾收集决策。因此,只有当我们充足的理由 (比如测试) 并且对应用程序产生的对象的生命周期有深刻的认知时,才应该使用该标志。
不建议开启该属性值。

垃圾收集器期望的目标

(1)、停顿时间

  • 停顿时间越短就适合需要与用户交互的程序;
  • 良好的响应速度能提升用户体验;

(2)、吞吐量

  • 高吞吐量则可以高效率地利用CPU时间,尽快完成运算的任务;
  • 主要适合在后台计算而不需要太多交互的任务;

(3)、覆盖区(Footprint)

  • 在达到前面两个目标的情况下,尽量减少堆的内存空间;
  • 可以获得更好的空间局部性;

吞吐量(Throughput)

  • CPU用于运行用户代码的时间与CPU总消耗时间的比值;
    • 即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间);
    • 比如,虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
  • 高吞吐量即减少垃圾收集时间,让用户代码获得更长的运行时间;

Stop TheWorld说明

  • JVM在后台自动发起和自动完成的,在用户不可见的情况下,把用户正常的工作线程全部停掉,即GC停顿;
  • 会带给用户不良的体验;

Jvm的client&server版本

Jvm有client和server两个版本,分别针对桌面应用程序和服务端应用做了相应的优化,client版本加载速度较快,server版本加载速度较慢但运行起来较快。

简而言之:client版本启动快,server版本运行快。

由于服务器的CPU、内存和硬盘都比客户端机器强大,所以程序部署后,都应该以server模式启动,获取较好的性能。

小提示:可以通过运行:java -version来查看jvm默认工作在什么模式。

Minor GC & Full GC & Major GC

Minor GC

  • 又称新生代GC(Young GC),指发生在新生代的垃圾收集动作;
  • 因为Java对象大多是朝生夕灭,所以Minor GC非常频繁,一般回收速度也比较快;

Full GC

  • 一般很多人称Major GC或老年代GC,指发生在老年代的GC;但是实际上,Full GC指的是一次完整GC。
  • 出现Full GC经常会伴随至少一次的Minor GC,原因就是减轻Full GC 的负担。(不是绝对,Parallel Sacvenge收集器就可以选择设置Major GC策略);
  • Major GC速度一般比Minor GC慢10倍以上;

Minor GC和Major GC是俗称,在Hotspot JVM实现的Serial GC, Parallel GC, CMS, G1 GC中大致可以对应到某个Young GC和Old GC算法组合;

Major GC通常是跟full GC是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说“Major GC”的时候一定要问清楚他想要指的是上面的Full GC还是Old GC。

程序中主动调用System.gc()强制执行的GC为Full GC。

新生代收集器&老年代收集器

  • 新生代收集器:Serial、ParNew、Parallel Scavenge;
  • 老年代收集器:Serial Old、Parallel Old、CMS;
  • 整堆收集器:G1;
    JVM之垃圾回收

并行和并发的概念

如果某个系统支持两个或者多个动作(Action)同时存在,那么这个系统就是一个并发系统。
如果某个系统支持两个或者多个动作同时执行,那么这个系统就是一个并行系统。

在并发程序中可以同时拥有两个或者多个线程。这意味着,如果程序在单核处理器上运行,那么这两个线程将交替地换入或者换出内存。
这些线程是同时“存在”的——每个线程都处于执行过程中的某个状态。
如果程序能够并行执行,那么就一定是运行在多核处理器上。
此时,程序中的每个线程都将分配到一个独立的处理器核上,因此可以同时运行。

可以得出结论:“并行”概念是“并发”概念的一个子集。 也就是说,编写一个拥有多个线程或者进程的并发程序,但如果没有多核处理器来执行这个程序,那么就不能以并行方式来运行代码,只能称为并发。
看下图来进行理解:
(Erlang 之父 Joe Armstrong 用该图解释了并发与并行的区别)
JVM之垃圾回收
并发是两个队列交替,但是只能使用一台咖啡机,并行是两个队列可以同时使用两台咖啡机
(任何属于冯诺依曼结构体系的计算机(经典计算机,目前应该除了实验中的量子计算机,都是属于该范畴的),其中的CPU(或者说核)必然是串行执行指令的。所以在任何单CPU机器上,是不存在严格意义上,或者说狭义上的并行的--在指令级别的严格意义上的并行,是指在一个足够小的时刻,可以允许大于一条的指令在执行。)

要很好的理解这个图,请记住下面几点:
一定要把图中的一个queues来对应一个任务,队列中的每一个人对应这个任务的步骤,并发和并行的共同前提肯定是有多个任务要处理(也就是图中的队列数量大于等于2)
再进一步区分:

并发强调了其实现的前提是:要处理的任务必须是可分步骤的(队列中的人数大于等于2);
并行强调了其实现的前提是:必须是多个处理器(咖啡机的数量大于等于2).

生活中的一个例子

  • 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
  • 你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
  • 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。

并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力。
在这里,将吃饭和接电话理解为两个任务。

并发垃圾收集和并行垃圾收集的区别

并行(Parallel)

  • 指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;
  • 如ParNew、Parallel Scavenge、Parallel Old;

并发(Concurrent)

  • 指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行);
  • 户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;
  • 如CMS、G1(也有并行);

没关注公众号的朋友,可以关注一波,干货多多
JVM之垃圾回收

本文同步分享在 博客“谙忆”(CSDN)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Opencv中Mat矩阵相乘——点乘、dot、mul运算详解
Opencv中Mat矩阵相乘——点乘、dot、mul运算详解2016年09月02日00:00:36 \牧野(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fme.csdn.net%2Fdcrmg) 阅读数:59593
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这