深入理解Java虚拟机_学习笔记_第三章
学习<<深入理解Java虚拟机>>第三版时做的笔记
文章目录
垃圾标记算法
引用计数算法
缺点在于两个对象互相引用,但是又不可被访问时,引用计数不为0,无法被回收
Java虚拟机没有采用这种算法
可达性分析算法
通过GC Roots根对象作为起始节点集,引用链搜索.
如果对象到GC Roots不可达,则不可能再被使用
可作为GC Roots的对象
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象,例如字符串常量池里的引用
- Native方法引用的对象
- Java虚拟机内部的引用
- 被synchronized持有的对象
引用
强引用
最传统的引用,形如:Object obj=new Object()
只要强引用关系还存在,则永远不会回收掉被引用的对象
软引用
描述一些还有用,但是非必须的对象。在系统将要发生内存溢出异常前,会把这些对象列入回收范围内进行二次回收。
弱引用
也是描述一些还有用,但是非必须的对象。比软引用更弱,只能生存到下一次垃圾收集发生为止。垃圾收集器工作时,不管内存是否充足,都会回收掉弱引用关联的对象
虚引用
无法通过虚引用得到一个对象实例。设置虚引用关联的唯一目的是能在这个对象被收集器回收时收到一个系统通知
不可达对象的回收过程
- 进行可达性分析后,发现没有于GC Roots相连接的引用链,进行第一次标记
- 随后查看是否有必要执行finalize()方法,如果没有重写它,或者已经被调用过,则没有必要执行,则回收
- 如果有必要执行,则放置在F-Queue队列中
- finalize方法是对象最后的逃脱机会,如果它在finalize方法中重新与引用链上任何一个对象建立关联,则不会被回收。如果还没逃脱,则被回收了。
- 任何一个对象finalize方法只会被执行一次
尽量避免使用finalize()方法
回收方法区
- 主要回收废弃的常量和不再使用的类型
- 回收类型很苛刻,得同时满足三个条件:该类所有实例被回收,加载该类的类加载器被回收,该类对应的Class对象没有被引用
- 在框架等自定义类加载器的场景中,通常需要虚拟机具有类型卸载的能力
垃圾收集算法
分代收集理论
- 根据弱分代假说,强分代假说。分为新生代,老年代
- 跨代引用假说:跨带引用相对于同代引用占比很小。所以在新生代建立全局的数据结构(记忆集),标识出老年代哪一块内存会存在跨带引用。之后的Minor GC中,只有包含在记忆集的对象才会加入GC Roots中进行扫描
- Minor GC/Young GC 目标只是新生代的垃圾收集
- Major GC/Old GC 目标只是老年代的垃圾收集,只有CMS收集器有
- Full GC 收集整个Java堆和方法区的垃圾收集
标记清除算法
先标记所有要回收的对象,标记完成后统一回收所有被标记对象
缺点:
1. 执行效率不稳定,随对象数量的增长而下降
2. 存在内存空间碎片化的问题
标记复制算法
半区复制
将内存容量划分成大小相等的两块,每次只使用其中一块。当其中一块用完后,把还存活的对象复制到另一块,再把已经使用的清空。
实现简单,运行高效,但是可用内存缩小为原来的一半。
优化后的半区复制
- 把新生代分为Eden,Survivor1,Survivor2区,空间为8:1:1
- 每次只使用Eden区和其中一块Survivor区
- 发生垃圾回收时,将Eden区和使用的Survivor区仍然存活的对象复制到另一块Survivor区,清空Eden区和之前使用的Survivor区
- 如果另一块Survivor区没有足够的空间存放存活对象,这些对象可以通过担保机制直接进入老年代
标记整理算法
- 针对老年代的算法
- 先标记,再让所有存活的对象都向内存空间的一端移动,然后直接清理掉边界以外的内存
- 是移动式算法,负载较重,需要全程暂停用户应用程序,即Stop The World.但优点是不会产生碎片.总的来说提高了吞吐量.因为HotShot虚拟机关注吞吐量,所以采用了标记整理.
HotSpot虚拟机细节
根节点枚举
- 固定可作为GC Roots的主要是:1.全局性引用(常量和类静态属性)2.执行上下文(栈帧中的本地变量表)
- 根节点枚举必须STW,因为它必须要再一个能保障一致性的快照上才得以进行
- 通过OopMap直接得到哪个地方存放着对象引用.不需要一个不漏的检查所有执行上下文和全局的引用位置
安全点
- 只有在特定的位置才记录下OopMap信息,即安全点
- 用户进程必须执行到安全点才能够暂停
- 安全点位置选取以"是否具有让程序长时间执行的特征"为标准,最明显的特征就是有指令序列的复用
- 还需要在垃圾收集发生时让所有线程都跑到最近的安全点,然后停顿下来
抢先式中断
当垃圾收集时,把所有线程中断,再让不在安全点的继续跑到安全点
没有虚拟机采用
主动式中断
简单设置标志位,每个线程执行时不断轮询它,一旦中断标志位为真,则自己在最近的安全点主动中断挂起。
轮询操作精简到只有一条汇编指令–完成安全点轮询和触发线程中断
安全区域
- 处于Sleep或Block状态的用户线程无法响应虚拟机的中断请求,不能走到安全的地方去中断挂起自己。
- 所以安全区域确保在某一段代码中,引用关系不会发生改变,在这个区域内进行垃圾收集是安全的。可以把安全区域看作是被扩展拉伸的安全点
- 用户线程到了安全区域,会标识已经进入安全区域。而要离开时,如果虚拟机已经完成了根节点枚举,则继续执行,否则要一直等待直到收到可以离开安全区的信息
记忆集和卡表
- 记忆集是用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。
- 收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向收集区域的指针即可
- 所以可以采用更粗的记录粒度来节省记忆集的开销。
- 目前最常采用的是卡表,卡表是记忆集的具体实现。卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨带指针。
- 卡表最简单是实现是字节数组,每一个元素都对应着其标识的内存区域中一块特定大小的内存块,即卡页。大小为2的9次方,512字节。
- 一个卡页通常不止一个对象。只要卡也中其中一个对象存在了跨代指针,就标识为1,即变脏。
- 垃圾收集时,就把卡表中变脏的元素对应的内存块加入GC Roots中一并扫描
写屏障
- 有其他分代区域中对象引用了本区域对象时,对应的卡表元素应该变脏.时间点应该发生在引用类型字段赋值的那一刻。
- HotSpot虚拟机通过写屏障技术维护卡表状态,是在虚拟机层面上对"引用类型字段赋值"这个动作的AOP切面,即在引用对象赋值时会产生一个环绕通知,供程序执行额外操作。
- 应用写屏障后,虚拟机会为所有赋值操作生成相应的指令。每次对引用进行更新,就会产生额外的开销。
- 卡表在高并发场景下的“伪共享”问题:缓存系统以缓存行为单位存储,当多线程互相修改独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响而导致性能下降。
- 为避免伪共享问题,可以先检查卡带标记,只有当该卡表元素未被标记过时才将其标记为变脏。
并发的可达性分析
- 可达性分析算法理论上要求全过程都STW。在根节点枚举过程中,通过OopMap技术,停顿时间已经比较短暂了。但是从GC Roots遍历图花费的时间与堆容量成正比,所以需要优化。
- 如果用户线程与收集器并发工作。收集器在对象图上标记颜色,同时用户线程在修改引用关系,即修改对象图的结构。会产生两种后果:1.把原本消亡的对象标记为存活,这可以容忍 2.把存活对象标记为消亡,则肯定会出错
对象消失
即原本是黑色的对象被误标记为白色,需要下述条件同时满足:
- 新增了黑色到白色对象的新引用。
- 删除了灰色到该白色对象的所有引用
解决方案
破坏两个条件之一即可。都是通过写屏障实现
- 增量更新。理解为当黑色对象一旦插入了指向白色对象的引用后,它就变回灰色对象。
- 原始快照。无论引用对象删除与否,都会按照刚刚开始扫描的那一刻的对象图快照来进行搜索。
经典垃圾收集器
简介
如果两个收集器之间存在连线,则可以搭配使用
Serial收集器
- 单线程新生代收集器,必须STW
- 是客户端模式下默认的新生代收集器,简单而高效
- 对于内存资源受限,单核处理器或处理器核心数较少的环境表现良好
ParNew收集器
- 是Serial收集器的并行版本,其他与Serial大致相同
- JDK9后,ParNew与CMS只能互相搭配使用
- 在单核心处理器不会有比Serial更好的效果
Parallel Scavenge
- 同样是基于标记-复制算法,并行收集的多线程收集器。被认为是吞吐量优先收集器。
- 目标是达到一个可控制的吞吐量,即运行用户代码的时间/(运行用户代码的时间+运行垃圾收集时间)
- 停顿时间短适合用户交互,高吞吐量适用于后台运算,能最高效率的利用处理器资源,尽快完成任务。
- -XX:MaxGCPauseMillis控制最大垃圾收集停顿时间。设置吞吐量大小:-XX:GCTimeRatio
- -XX:+UseAdaptiveSizePolicy 根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大吞吐量。自适应的调节策略。
Serial Old收集器
单线程收集器,使用标记整理算法,供客户端模式使用的。
Parallel Old收集器
支持多线程并发收集。
“吞吐量优先”收集器搭配组合:Parallel Scavenge与Parallel Old
CMS收集器
- 并发收集,低停顿,以获取最短回收停顿时间为目标
- 基于标记-清除算法
四个阶段
- 初始标记。需要STW,仅仅标记一下GC Roots直接关联到的对象,速度很快。
- 并发标记。从GC Roots直接关联到的对象开始遍历整个对象图,耗时较长但是不需要停顿用户线程,可以并发执行。
- 重新标记。修改并发标记期间,因用户线程继续运行而导致的标记发生变动的那一部分对象的标记记录。参考之前的增量更新。需要STW,但是时间仍然比较短。
- 并发清除。删除掉标记阶段判断为已经死亡的对象,不需要移动对象,所以可以和用户线程并发执行。
三个问题
- 对处理器资源敏感。即在并发阶段,虽然不会导致用户线程停顿,但因为占用一部分线程,而导致应用程序变慢,降低总吞吐量。
- 无法处理浮动垃圾。可能因失败而导致另一次完全的STW的Full GC产生。浮动垃圾即用户线程在并发执行时还会伴随着新的垃圾对象不断产生,但CMS无法在当次收集中处理他们。而且用户线程持续运行,就需要预留足够的内存空间提供给用户线程使用。如果预留的空间无法满足程序分配新对象的需要,就会出现并发失败(Concurrent Mode Failure)就会冻结用户线程的执行,临时启用Serial Old收集器重新进行老年代的垃圾收集。
- 有大量的空间碎片。如果无法找到足够大的连续空间来分配给当前对象,则会提前触发一次Full GC。
Garbage First收集器
- 面向局部收集,基于Region内存布局
- JDK8后,被称为全功能的垃圾收集器。主要面向服务端。
- JDK9后完全取代了Parallel Scavenge加Parallel Old组合,成为服务端模式下默认垃圾收集器。
- 建立了停顿时间模型。即能够支持指定的一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒。
- 它可以面向堆内存的任何部分来组成回收集。衡量标准不再是属于哪个分代,而是哪块内存存放的垃圾数量最多,回收收益最大。即为Mixed GC模式。
- 把连续的Java堆划分为多个大小相等的独立区域(Region),每个Region可以扮演Eden,Survivor或者老年代空间。对扮演不同角色的Region采取不同的策略进行处理。
- 还有专门存储大对象的Humongous区域。只要大于Region容量的一半即认为是大对象。而大于一个Region的大对象就会存放在N个连续的Humongous Region区域。将Humongous Region区域作为老年代的一部分看待。
- 将Region作为单位回收的最小单元,每次收集的内存空间是Region大小的整数倍。
- 按照各个Region垃圾堆积的价值大小,即回收所获得的空间大小和回收所需时间的经验值。在后台维护优先队列,根据用户运行的收集停顿时间,优先回收价值收益最大的Region。
细节
- 跨Region引用对象的解决。每个Region都维护自己的记忆集。记录下别的Region指向自己的指针,并且标记这些指针分别在哪些卡页的范围内。记忆集为哈希表,key为别的Region的起始地址,value是卡表索引号的集合。大致需要10%-20%的Java堆内存维持收集器工作。
- 并发标记阶段采用原始快照保证收集线程与用户线程互不干扰的运行。因为程序继续运行会有新对象的创建。所以每个Region都设计了两个指针,划分出一块用于并发回收过程中新对象的分配。这个地址内的对象默认是存活的,不纳入回收范围。如果内存回收速度赶不上分配速度,则也会Full GC,STW
- 建立可靠的停顿预测模型。根据每个Region的回收耗时等信息分析得出平均值等统计信息。衰减平均值比普通的平均值更加容易受到新数据的影响,代表“最近的”平均状态。即Region的统计状态越新,越能决定其回收价值。通过这些信息进行预测,就能在不超过期望停顿时间的约束下获得最高的收益。
实现步骤
- 初始标记。需要STW,仅仅标记一下GC Roots直接关联到的对象,并且修改TAMS指针,让下一阶段用户线程并发执行时,能正确的在可用Region中分配新对象.需要停顿线程,但耗时很短。
- 并发标记。从GC Roots直接关联到的对象开始遍历整个对象图,耗时较长但是不需要停顿用户线程,可以并发执行。扫描结束后,还要重新处理原始快照记录下在并发有引用变动的对象。
- 最终标记。对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍然遗留下来的最后那少量的原始快照(SATB)记录。
- 筛选回收。根据Region的回收价值和成本进行排序,再根据用户所期望的停顿时间来制定回收计划。把决定回收的Region存活对象复制到空的Region,再清理掉整个旧Region的全部空间。必须暂停用户线程。
其他
目标是在延迟可控的情况下获得尽可能高的吞吐量。
可以由用户指定期望的停顿时间
停顿时间设置100-200ms之间会比较合适,太低则导致选出来的回收集只占堆内存很小一部分。
追求能应付用于的内存分配速率,而不是追求一次把整个Java堆清理干净。
G1整体是基于标记整理算法的,而局部即两个Region之间,则是基于标记复制算法的。使得不会产生内存碎片。
相对于CMS的缺点
- 内存占用率高,因为卡表
- 对写屏障有复杂操作,不得不实现为类似消息队列的结构,先放到队列,再异步处理。
- 在小内存上CMS大概率优于G1平衡点大致是6-8GB内存
低延迟垃圾回收器
理论
衡量垃圾收集器指标:内存占用,吞吐量,延迟。最多能同时达到其中两项
CMS,G1分别采用增量更新,原始快照方式实现了标记阶段的并发,但是对于标记后的处理仍未得到妥善解决。
Shenandoah和ZGC几乎整个工作过程都是并发的,只有在初始标记,最终标记这些阶段有短暂的停顿。
ZGC收集器
基于Region内存布局的,不设分代的,使用了读屏障,染色指针和内存多映射来实现的可并发的标记整理算法,以低延迟为首要目标的一款垃圾收集器。
内存布局
Region具有动态性,动态创建和销毁,有动态的区域容量大小
- 小型Region:容量固定2MB
- 中型Region:容量固定32MB
- 大型Region:容量不固定,但是必须为2MB的整数倍,用于放置4MB或以上的大对象。每个大型Region只会存放一个大对象。
染色指针技术
把标记信息直接记录在引用对象的指针上。标志信息包括引用对象的三色标记状态,是否进入重分配集(即被移动过)等。
优势:
- 一旦某个Region的存活对象被移走之后,该Region立即就能被释放和重用。
- 大幅度减少内存屏障的使用数量,只使用了读屏障。
- 可扩展,之后可以把指针前18位开发使用。
运行流程
分为4个大阶段,都可以并发执行,只在两个阶段中间会存在短暂的停顿小阶段
并发阶段。
遍历对象图做可达性分析。标记只在染色指针上进行。
并发预备重分配。
得出本次收集过程需要清理哪些Region,将他们组成重分配集。ZGC每次都会扫描所有的Region,换取G1中记忆集的维护成本。
并发重分配。
- 把重分配集中存活对象复制到新的Region中。并且为重分配集的每个Region维护转发表,记录从旧对象到新对象的转向关系。
- 仅从引用就明确得知一个对象是否处于重分配集中。
- 自愈能力:在ZGC中,当读取处于重分配集的对象时,会被读屏障拦截,通过转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象。只慢一次。
并发重映射
修正整个堆中指向重分配集中旧对象的所有引用。不是迫切任务,因为可以旧引用通过自愈,最多是第一次比较慢。
特点
能承受的对象分配速率不会太高。
支持“NUMA-Aware”(非统一的内存访问架构。为多处理器计算机所设计的内存架构)的内存分配。
令人震惊的,革命性的ZGC
Epsilon收集器
不能够进行垃圾收集,无操作的垃圾收集器。对于微服务等,只要Java虚拟机能正确分配内存,在堆耗尽之前就会退出,那么运行负载极小,没有任何回收行为的Epsilon是恰当的选择。
内存分配与回收策略
目标
自动给对象分配内存
自动回收分配给对象的内存
策略
- 对象内存分配应该都是堆上分配(但也有可能栈上分配)
- 新生对象通常分配在新生代中,少数情况(如对象大小超过一定阈值)可能直接分配到老年代
对象优先分配Eden区域
- 大多数情况下在Eden区分配
- 新生代总可用空间为Eden区+1个Survivor区的总容量
- 当Eden没有足够的空间时发生一次Minor GC,如果对象放不进Survivor区,则通过分配担保机制提前把这些对象转移到老年代
大对象直接进入老年代
大对象需要大量连续的内存空间,例如很长的字符串,很长的数组。
容易导致提前垃圾收集,以获取足够的连续空间安置大对象
大对象直接在老年代分配,以避免在Eden与Survivor区来回复制,付出高额内存复制开销
长期存活对象进入老年代
每个对象头有对象年龄计数器。如果经过一次MinorGC仍然存活,并且能被Survivor容纳,则计数器+1,当到15时,则晋身为老年代。
动态对象年龄判断
如果Survivor区中相同年龄所有对象的总和大于Survivor区的一半,那么年龄大于等于该年龄‘的对象直接进入老年代
空间分配担保
在Minor GC前,检查老年代最大可用连续空间是否大于新生代的所有对象空间,如果成立,则这次Minor GC安全。负责要看是否允许担保失败。允许,则检查老年代最大可用连续空间是否大于历次晋升到老年代的平均大小,如果小于,或者不允许担保,则Full GC。
新生代中,把Survivor无法容纳的对象直接送入老年代,担保前提是老年代本身有容纳这些对象的剩余空间。
期存活对象进入老年代
每个对象头有对象年龄计数器。如果经过一次MinorGC仍然存活,并且能被Survivor容纳,则计数器+1,当到15时,则晋身为老年代。
动态对象年龄判断
如果Survivor区中相同年龄所有对象的总和大于Survivor区的一半,那么年龄大于等于该年龄‘的对象直接进入老年代
空间分配担保
在Minor GC前,检查老年代最大可用连续空间是否大于新生代的所有对象空间,如果成立,则这次Minor GC安全。负责要看是否允许担保失败。允许,则检查老年代最大可用连续空间是否大于历次晋升到老年代的平均大小,如果小于,或者不允许担保,则Full GC。
新生代中,把Survivor无法容纳的对象直接送入老年代,担保前提是老年代本身有容纳这些对象的剩余空间。
在JDK7及之后。不能设置参数影响空间分配担保策略。只要老年代最大可用连续空间大于新生代的所有对象空间,或者大于历次晋升到老年代的平均大小,就进行Minor GC,负责Full GC