第3章 垃圾收集器与内存分配策略

垃圾收集器与内存分配策略

JVM垃圾回收行为的并行与并发

1,概述

2,对象已死?

2.1 引用计数算法
  • 在对象中添加一个引用计数器,每当有一个地方引用它时,计数器的值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
2.2 可达性分析算法
  • 当前主流的商用程序语言(Java,C#)的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。
  • 这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为”引用链“,如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
2.3 再谈引用
  • jdk1.2之后,java对引用的概念进行了扩充,将引用分为强引用,软引用,弱引用和虚引用。
2.4 生存还是死亡
  • 即使在可达性分析算法中判定为不可达的对象,也不是”非死不可的“,这时候它们暂时还处于”缓刑“阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为”没有必要执行“。
  • 任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会再被执行。
2.5 回收方法区
  • 方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。
  • 关于是否对类型进行回收,HotSpot虚拟机提供了 --Xnoclassgc参数进行控制,还可以使用-verbose:class以及–XX:+TraceClassLoading,–XX:+TraceClassUnLoading查看类加载和卸载信息。

3,垃圾收集算法

  • 从如何判定对象消亡的角度出发,垃圾收集算法可以划分为”引用计数式垃圾收集“和”追踪式垃圾收集“两大类,这两类也被称作”直接垃圾收集“和”间接垃圾收集“。本文所介绍的所有算法均属于追踪式垃圾收集的范畴。
3.1 分代收集理论
  • 分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:
  • 1,弱分代假说:绝大多数对象都是朝生夕灭的
  • 2.强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。
  • 这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。
  • 设计者一般至少会把Java堆划分为新生代和老年代两个区域,顾名思义,在新生代中,每次垃圾收集时都会发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。
  • 为了解决跨代引用的问题,就需要对分代收集理论添加第三条经验法则:
  • 3,跨代引用假说:跨代引用相对于同代引用来说仅占极少数
  • 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为新生代收集,老年代收集,混合收集
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集
3.2 标记-清除算法
  • 最早出现也是最基础的垃圾收集算法是”标记-清除“算法。
  • 算法分为两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
  • 它主要有两个缺点:第一个是执行效率不稳定,第二个是内存空间的碎片化问题。
3.3 标记复制算法
  • 常被简称为复制算法。为了解决标记-清除算法面对大量可回收对象时执行效率低的问题
  • 半区复制的垃圾收集算法,它将内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
  • 这种复制回收算法id代价是将可用内存缩小为了原来的一半,空间浪费未免太多了一点。
  • 针对具备”朝生夕灭“特点的对象,提出了”Appel式回收“。任何人没办法百分百保证每次回收都只有不多于10%的对象存活,因此Appel式回收还有一个充当罕见情况的”逃生门“的安全设计,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保。
3.4 标记-整理算法
  • 标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率就会降低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保以应对极端情况,在老年代一般不能直接选用这种算法。
  • 针对老年代对象的存亡特征,提出了”标记–整理“算法,其中的标记过程仍然与"标记-清除"算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。

CMS收集器,(Concurrent Mark Sweep) 并发收集,低停顿,适合响应时间敏感的应用使用

4,HotSpot的算法细节实现

4.1 根节点枚举
  • 目前主流的Java虚拟机使用的都是准确式垃圾收集。
  • 在用户线程停顿下来之后,其实并不需要一个不漏的检查完所有执行上下文和全局的引用位置,虚拟机是有办法直接得到哪些地址存放着对象引用的。在HotSpot的解决方案里,是使用一组称为OopMap的数据结构来达到这个目的。
4.2 安全点
  • 实际上HotSpot没有为每一条指令都生成OopMap,只是在“特定的位置”记录了这些信息,这些位置被称为安全点
  • 有了安全点,也就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到安全点后才能够暂停。
  • 安全点位置的选取基本上是以”是否具有让程序长时间执行的特征“为标准来选定的。
  • 对于安全点,另一个需要考虑的问题是,如何在垃圾收集发生时让所有线程(这里其实不包括执行JNI调用的线程)都跑到最近的安全点,然后停顿下来。这里有两种方案可供选择:抢先式中断和主动式中断。
  • 现在几乎没有虚拟机实现采用抢先式中断来暂停线程响应GC事件。
  • 由于轮询操作在代码中会频繁出现,这要求它必须足够高效。HotSpot使用内存保护陷阱的方式,把轮询操作精简至只有一条汇编指令的程度。
4.3 安全区域
  • 安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入垃圾收集过程的安全点。但是程序”不执行“的时候呢,所谓的程序不执行就是没有分配处理器时间,典型的场景便是用户线程处于Sleep状态或者Blocked状态,这时候线程无法响应虚拟机的中断请求,不能再走到安全的地方去中断挂起自己,对于这种情况,就必须引入安全区域来解决
  • 安全区域是指能够确保在一段代码片段中,引用关系不会发生变化,因此在这个区域中任意地方开始垃圾收集都是安全的。
4.4 记忆集与卡表
  • 记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构
  • 可供选择的记录精度:字长精度,对象精度,卡精度,卡精度
  • 卡精度所指的是用一种称为”卡表“的方式去实现记忆集。
4.5 写屏障
  • 我们已经解决了如何使用记忆集来缩减GC Roots扫描范围的问题,但还没有解决卡表元素如何维护的问题,例如它们何时变脏,谁来把它们变脏等等。
  • 在HotSpot虚拟机里是通过写屏障技术来维护卡表状态的

编译执行和解释执行的区别:

  • 计算机并不能直接地接受和执行用高级语言编写的源程序,源程序在输入计算机时,通过"翻译程序"翻译成机器语言形式的目标程序,计算机才能识别和执行。这种"翻译"通常有两种方式,即编译方式和解释方式。编译方式是指利用事先编好的一个称为编译程序的机器语言程序,作为系统软件存放在计算机内,当用户将高级语言编写的源程序输入计算机后,编译程序便把源程序整个地翻译成用机器语言表示的与之等价的目标程序,然后计算机再执行该目标程序,以完成源程序要处理的运算并取得结果。解释方式是指源程序进入计算机后,解释程序边扫描边解释,逐句输入逐句翻译,计算机一句句执行,并不产生目标程序

伪共享

  • 为了避免伪共享的问题,一种简单的解决方案是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表元素未被标记过时才将其标记为变脏,

在JDK7之后,HotSpot虚拟机增加了一个新的参数 -XX:+UseCondCardMark,用来决定是否开启卡表更新的条件判断。

4.6 并发的可达性分析

当且仅当以下两个条件同时满足时,会产生”对象消失”的问题

  1. 赋值器插入了一条或多条从黑色对象到白色对象的新引用
  2. 赋值器删除了全部从灰色对象到该白色对象的直接或者间接引用

因此要解决并发扫描时对象的消失问题,只需破坏这两个条件中的任意一个即可。由此产生了两种解决方案:增量更新(破坏第一个条件)和原始快照(破坏第二个条件)

5,经典垃圾收集器

5.1 Serial收集器
  • 迄今为止,它依然是HotSpot虚拟机运行在其他客户端模式下的默认新生代收集器,有着优于其他收集器的地方,那就是简单而高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器额外内存消耗最小的。
5.2 ParNew收集器
  • 新生代收集器
  • ParNew收集器实质上是Seial收集器的多线程并行版本
  • ParNew收集器除了支持多线程并行收集之外,其他与Serial收集器相比并没有太多创新之处,但它却是不少运行在服务器端模式下的HotSpot虚拟机,其中有一个与功能,性能无关但其实很重要的原因:除了Serial收集器外,目前只有它能与CMS收集器配合工作。
  • ParNew收集器是激活CMS后(使用-XX:UseConcMarkSweepGC选项)的默认新生代收集器,也可以使用-XX:+/-UseParNewGC选项来强制指定或者禁用它。
5.3 Parallel Scavenge 收集器
  • 也是一款新生代收集器
  • 它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量就是处理器用于用户代码的时间与处理器总消耗时间的比值。
  • 自适应调节策略
5.4 Serial Old收集器
  • 是Serial收集器的老年代版本,同样是一个单线程收集器
5.5 CMS收集器
  • CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。

  • 基于标记–清除算法实现的,整个过程分为四个步骤。包括:

  • 1,初始标记

  • 2,并发标记

  • 3,重新标记

  • 4,并发清除

CMS至少存在以下三个明显的缺点:
  • 首先,CMS收集器对处理器资源非常敏感。
  • 由于CMS收集器无法处理“浮动垃圾”,有可能出现“Concurrent Mode Failure”失败而导致另一次完全“Stop The World”的Full GC的产生(CMS的另一个缺点是它需要更大的堆空间。因为CMS标记阶段应用程序的线程还是在执行的,那么就会有堆空间继续分配的情况,为了保证在CMS回 收完堆之前还有空间分配给正在运行的应用程序,必须预留一部分空间。也就是说,CMS不会在老年代满的时候才开始收集。相反,它会尝试更早的开始收集,已 避免上面提到的情况:在回收完成之前,堆没有足够空间分配!默认当老年代使用68%的时候,CMS就开始行动了。 – XX:CMSInitiatingOccupancyFraction =n 来设置这个阀值。)
  • 还有最后一个缺点,CMS是一款基于“标记-清除”算法实现的收集器,收集结束时会有大量空间碎片产生。
5.6 Garbage First收集器
  • 简称G1,开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。
  • 是一款面向服务端应用的垃圾收集器
  • 停顿时间模型,能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾手机上的时间大概率不超过N毫秒这样的目标,这几乎已经是实时Java(RTSJ)的中软实时垃圾收集器特征了。
  • Mixed GC模式,可以面向堆内的任何部分来组成回收集(Collection Set,一般简称Cset)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大。
  • 每个Region的大小可以通过参数 -XX:G1HeapRegionSize设定
  • G1的记忆集在存储结构的本质上是一种哈希表,Key是别的Region的起始地址,Value是一个集合,里面存储的是卡表的索引号
  • G1收集器的运作过程大致可划分为以下四个步骤
    1,初始标记
    2,并发标记
    3,最终标记
    4,筛选回收
    以上阶段中,G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的。

G1从整体上来看是基于“标记-整理”算法实现的收集器,但从局部(两个region之间)上看又是基于“标记—复制”算法实现

5.7 低延迟垃圾收集器

  • 衡量垃圾收集器的三项最重要的指标是:内存占用,吞吐量和延迟
5.7.1 Shenandoah 收集器
  • Shenandoah更像是G1的下一代继承者,它们两者有着相似的堆内存布局,在初始标记,并发标记等许多阶段的处理思路上都高度一致。
  • 在管理堆内存方面,它与G1至少有三个明显的不同之处,最重要的当然是支持并发的整理算法,G1的回收阶段是可以多线程并行的,但却不能与用户线程并发。其次,Shenandoah(目前)是默认不使用分代收集器的;最后Shenandoah摒弃了在G1中耗费大量内存和计算资源去维护的记忆集,改用名为“连接矩阵”的全局数据结构来记录跨Region的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题的发生概率。
  • 对于并发回收阶段遇到的困难,Shenandoah将会通过读屏障和被称为“Brooks Pointers”的转发指针来解决。
  • “引用访问屏障”是指内存屏障只拦截对象中数据类型为引用类型的读写操作,而不去管原生数据类型等其他非引用字段的读写,这能够省去大量对原生类型,对象比较,对象加锁等场景中设置内存屏障所带来的消耗
5.7.2 ZGC收集器
  • ZGC收集器是一款基于Region内存布局的,(暂时)不分代的,使用了读屏障,染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器
  • ZGC的Region具有动态性----动态创建和销毁,以及动态的区域容量大小。
  • ZGC收集器有一个标志性的设局是它采用的染色指针技术
  • ZGC的染色指针是最直接,最纯粹的,它直接把标记信息记在引用对象的指针上,这时,与其说可达性是遍历对象图来标记对象,还不如说是遍历“引用图”来标记“引用”了
  • 染色指针是一种直接将少量的额外信息存储再指针上的技术
  • 尽管Linux的64位指针的高18位不能用来寻址,但剩余的46位指针所能支持的64TB内存在今天仍然能够充分满足大型服务器的需要,鉴于此,ZGC染色指针技术将这46位中的高四位提取出来存储四个标志信息。

线性地址

  • ZGC的运作过程大致可划分为以下四个大的阶段
  • 并发标记,标记阶段会更新染色指针中的Marked0,Marked1标志位
  • 并发预备重分配
  • 并发重分配,重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表,记录从旧对象到新对象的转向关系
  • 并发重映射,重映射所做的就是修正整个堆中间指向重分配集中旧对象的所有引用,但是这一阶段并不是一个必须要“迫切”去完成的任务,因为即使是旧引用,它也是可以自愈的。因此,ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成。

如果应用必须运行Windows操作系统下,那ZGC就无缘了。

摩尔定律是英特尔创始人之一戈登·摩尔的经验之谈,其核心内容为:集成电路上可以容纳的晶体管数目在大约每经过24个月便会增加一倍。换言之,处理器的性能每隔两年翻一倍。

6,选择合适的垃圾收集器

如何选择一款适合自己应用的收集器,主要受以下三个因素影响:

  1. 应用程序的主要关注点是什么
  2. 运行应用的基础设施如何
  3. 使用jdk的发行商是什么?版本号是多少?
6.1 虚拟机及垃圾收集器日志

7307K->480K(9216K)"的含义是:GC前该内存区域已使用容量 -> GC后该内存区域已使用容量(该内存区域的总容量)
jvm中的日志详解

  • 到JDK9的时候,HotSpot所有功能的日志都收归到“-Xlog”参数上
  • 命令行中最关键的参数是选择器(Selector),它由标签(Tag)和日志级别(Level)共同组成,标签可以理解为虚拟机中某个功能模块的名字,它告诉日志框架用户希望得到虚拟机哪些功能的日志输出。垃圾收集器的名称为“gc”
  • 日志级别从低到高,共有Trace,Debug,Info,Warning,Error,Off六种级别,日志级别决定了输出信息的详细程度,默认级别为Info.
  • 查看GC基本信息,在JDK9之前使用-XX:+PrintGC,之后使用-Xlog:gc:
  • 查看GC详细信息,在JDK9之前使用-XX:+PrintGCDetails,之后使用-Xlog:gc*
6.2 实战:内存分配与回收策略
  • Java技术体系的自动内存管理,最根本的目标是自动化地解决两个问题:自动给对象分配内存以及自动回收分配给对象的内存
  • 对象优先在Eden分配;大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
    JVM调优总结 -Xms -Xmx -Xmn -Xss
  • 大对象直接进入老年代,HotSpot虚拟机提供了-XX:PretenureSizeThreshold参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在Eden区及两个Survivor区之间来回复制,产生大量的内存复制操作。
  • 注意-XX:PretenureSizeThreshold参数只对Serial和ParNew两款新生代收集器有效。
  • 长期存活的对象将进入老年代,对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置
  • 动态对象年龄判断
    jvm日志中的jvm中desired Survivor size
    jvm中的日志中的user,sys,real
  • 空间分配担保,在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值