阅读深入理解java虚拟机(三)

256 阅读8分钟

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

-Xms:
-Xmx:
//paraell s
-XX:MAXGCPauseMills=
-XX:GCTimeRatio=
-XX:AdaptiveSizePolicy=
//CMS
-XX:UseConcMarkSweepGC
-XX:CMSInitiatingOccuoancyFraction
//G1
-XX:ConcGCThreads=
-XX:MAXGCPauseMills=
-XX:G1HeapRegionSize=
-XX:G1ReservePercent=

根节点枚举

stop the world的原因

所有jvm垃圾收集器在根节点枚举这一步都是需要暂停用户线程的,称之为“stop the world”。可达性分析算法耗时最长的查找引用链的过程可以与用户线程一同工作。
根节点枚举期间必须在一个能保障一致性的状态之中,这种“一致性”指的是在整个枚举期间执行的子系统看起来就像是冻结在了某个时间,根节点集合的引用关系不会再变化。
HotSpot中,使用一组名为oopMap的数据结构来达到这个目的,在类加载即时编译特定位置的时候,会把引用对象的偏移量记录下来,oopMap存储寄存器中哪些地方是对象引用。

safe point

在即时编译的过程中,会在“特定位置”记录下栈里和寄存器里的引用位置。这种策略是为了防止每条指令都生成对应的oopMap造成大量的空间占用,同时也为了防止引用关系不断地变化。这种特殊位置称之为安全点。用户程序执行时,并非在任意代码的位置都能开始垃圾收集,而是只能在安全点才能进行垃圾收集工作。

  1. 抢先式中断
    在垃圾收集进行时,不需要线程执行的代码主动去配合,而是在垃圾回收发生时,系统把全部的用户线程全部中断,如果发现用户线程的中断位置不在安全点上,就恢复执行,直到其运行到安全点上时再次中断线程。
  2. 主动式中断
    不直接对线程进行操作,而是设置一个标志位,在线程运行的过程中轮询这个标志位,一旦发现中断标志位为真时,就在自己最近的安全点上主动挂起。指令为0x01b6d62c: test %eax,0x100100,发送一个自陷异常信号。

记忆集

分代收集理论(3)跨代引用假说:在互相存在引用关系的两个对象倾向于同时生存或者同时死亡。 不必为了少量的跨代引用对象去扫描整个老年代,也不必浪费空间记录每一个对象是否存在跨代引用,只需要维护一个名为数据集的结构,记录老年代哪些区域存在跨代引用即可。

写屏障

有其他分代区域中的对象引用了本区域对象的时候,其对应的卡表元素就会变脏,变脏的时间点原则上应该发生在引用类型字段赋值的那一刻。JVM通过机器码层面的代码来修改记忆集。在HotSpot中是通过写屏障来完成维护卡表的状态的,这个写屏障可以看作是为引用类型字段赋值这个动作的AOP切面。

//re写屏障
post_write_barrier(field,new_value);
*field = new_value;
//after写屏障
post_write_barrier(field,new_value);

并发的可达性分析

可达性分析算法基于一个能保证一致性的快照进行分析。

三色标记法:

  1. 白色球:不可达
  2. 黑色球:可达
  3. 灰色球:至少还有一条链路没有寻找过

假如在初始标记后的并发标记中,一致性快照中的引用发生了改变会存在两种情况:

  1. 本来应该被清除的对象,没有被清除
  2. 本来不该被清除的对象,被清除掉了
    这种情况的发生需要满足两个条件,首先是不止一个黑球对白球有了引用关系,其次是全部灰球到白球的链路被切断了

增量更新

在黑球对白球有了引用关系之后,把黑球的位置标记下来,在并发标记之后再次对黑球进行链路查找,相当于把黑球变成了灰色球,破坏条件1

原始快照

当灰色对象要删除引用对象的时候,会把灰色对象的引用记录下来,并发标记结束后,再对灰色标记的对象进行链路查找,破坏了条件2

Concurrent Mark Sweep收集器

基于标记清除算法的收集器,收集分为四个过程为初始标记、并发标记、重新标记和并发清除。其中初始标记和重新标记阶段需要暂停用户线程,耗时最长的并发标记阶段与用户线程同时进行工作,总体来说可以是与用户线程并发进行工作的。 CMS收集器与parNew和Serial Old搭配使用的,因为在并发清理的过程中会产生浮动垃圾,会造成老年代空间使用增加,如果在CMS的过程中,发生了“并发失败”,也就是虚拟机预留的内存已经不够程序使用了,就会触发一次Full GC,这个参数可以根据-XX:CMSInitiatingOccupancyFraction来调整内存使用达到多少的时候触发CMS收集器活动。另外CMS收集器是基于标记-清除算法,会导致内存空间碎片化,可能导致虽然存在存储空间,但没有连续的内存存放对象的情况。CMS会在连续若干次收集之后,在下一次FullGC开始之前对内存进行一次整理工作。可以用参数-XX:CMSFullGCsBeforeCompaction控制。

G1收集器

G1收集器来时,java的垃圾收集器开始以应付程序的内存分配速率为目标(Allocation Rate),G1建立了一个“停顿时间模型”,期望能够指定在M毫秒的时间内有N毫秒的时间用于了垃圾收集活动。
G1收集器把内存区域分为了不同的Region区域,每个区域可以根据需要充当eden、survivor和Old区域,此外还有一个专门存储大对象的Humongous区域,如果对象的大小超过了Region的一半就会被认为是一个大对象。Region的大小为2的整数倍,取值范围为1MB~32MB,每个Region的大小可以根据参数-XX:G1HeapRegionSize来设置。
G1的收集过程和CMS一样,同样分为四个阶段,分别是初始标记、并发标记、最终标记和标记清理,其中只有并发标记是和用户进程同时进行的,其他的三个阶段都会发生STW。因为最终标记阶段是STW的,所以不会和CMS一样产生浮动垃圾。

eden 区域移动至survivor区,survivor区域不够则移动到Old区

survivor区移动至新的survivor区,部分数据会晋升至Old区

eden区域收拾干净之后,GC结束,用户的应用程序继续执行

并发标记阶段如何与用户线程同时工作?

最终标记阶段:原始快照和TAMS来实现的 CMS收集器通过增量更新算法实现,G1收集器通过原始快照来实现。在并发标记和用户线程同时工作的过程中,由于用户线程仍然在产生新的引用,G1为每个Region设计了两个TAMS(top at mark start)指针,可以认为把一部分区域划分出来专门用来存储新分配的对象。

停顿预测模型

和paraellS一样,通过设置-XX:MAXGCPauseMills来设置期望的停顿时间。
G1收集器的停顿预测模型是以衰减均值为理论基础实现的,在垃圾收集的过程中,G1收集器会分析每一个Region的回收耗时、每个Region的记忆卡的数量和各个可以测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等信息,使用平均值来代表整体的状态,使用衰减平均值来代表最近的平均状态,确定由哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。
可以把不同的Region看成不同的副本,通关不同的Region可以获得不同的经验值和金币,想要通关Region的耗时和时间花费也不同,选择通关哪些Region可以在最短的时间内获得一个最大的收益。

CMS和G1的对比情况

CMS基于标记-清除算法,从局部来看G1是基于标记-复制算法,整体来看G1是一种标记-整理算法,G1不会产生内存碎片化的情况,CMS会因为内存区域碎片化没有足够的空间存放大对象造成Full GC,G1收集器更加有利于程序的长时间运行。
G1在垃圾收集过程中的内存占用和执行负载比CMS更高,G1收集器的记忆表更加复杂,堆中的每个Region的维护一个卡表,结构更加复杂需要的内存更多,同时G1在最终标记阶段,使用的原草初始快照算法,这种算法相较于增量更新,更加复杂,需要用到pre写屏障来记录灰球的指针变化,由于G1的复杂性,写屏障变成了一个异步操作。一般来说小内存的应用使用CMS收集器会更好,6GB~8GB内存的应用使用G1的效果更好,生产环境中通过测试决定收集器的使用。