JVM重要概念2

原文地址 https://www.cnblogs.com/yescode/p/13961190.html

一 JVM的内存申请

  一般而言生成对象需要向堆中的新生代申请内存空间,而堆又是全局共享的,像新生代内存又是规整的,是通过一个指针来划分的。

  

  内存是紧凑的,新对象创建指针就右移对象大小 size 即可,这叫指针加法(bump [up] the pointer)。

可想而知如果多个线程都在分配对象,那么这个指针就会成为热点资源,需要互斥那分配的效率就低了。

二 什么是TLAB

  TLAB(Thread Local Allocation Buffer),为一个线程分配的内存申请区域。

  这个区域只允许这一个线程申请分配对象,允许所有线程访问这块内存区域。

  TLAB 的思想其实很简单,就是划一块区域给一个线程,这样每个线程只需要在自己的那亩地申请对象内存,不需要争抢热点指针。

  当这块内存用完了之后再去申请即可。

  这种思想其实很常见,比如分布式发号器,每次不会一个一个号的取,会取一批号,用完之后再去申请一批。

可以看到每个线程有自己的一块内存分配区域,短一点的箭头代表 TLAB 内部的分配指针。

如果这块区域用完了再去申请即可。

三 为什么 G1 不维护年轻代到老年代的记忆集?

  对于G1来说,一次mixed gc一定伴随着一次young gc。而一次young gc后会留下什么呢?会留下survivor,而survivor是mixed gc流程初始标记的根引用之一

  所以,如果真的有young 到old的引用,一定会扫描出来的,所以就没有必要维护young到old的remembered set的情况。

四 G1 为了维持并发的正确性用了什么手段  

  G1 用了 SATB(snapshot-at-the-beginning),打破了第二个条件,会通过写屏障把旧的引用关系记下来,之后再把旧引用关系再扫描过。

这个从英文名词来看就已经很清晰了。讲白了就是在 GC 开始时候如果对象是存活的就认为其存活,等于拍了个快照。

而且 gc 过程中新分配的对象也都认为是活的。每个 region 会维持 TAMS (top at mark start)指针,分别是 prevTAMS 和 nextTAMS 分别标记两次并发标记开始时候 Top 指针的位置。

  Top 指针就是 region 中最新分配对象的位置,所以 nextTAMS 和 Top 之间区域的对象都是新分配的对象都认为其是存活的即可。

  

而 g1 通过 SATB 的话在最终标记阶段只需要扫描 SATB 记录的旧引用即可,从这方面来说会比 cms 快,但是也因为这样浮动垃圾会比 cms 多

什么是 logging write barrier

  有些书上的说法,管SATB叫前置写屏障,RSET就后置写屏障,后置写屏障也叫作logging write barrirer。

  把写屏障要执行的一些逻辑搬运到后台线程执行,来减轻对应用程序的影响。

  在写屏障里只需要记录一个 log 信息到一个队列中,然后别的后台线程会从队列中取出信息来完成后续的操作,其实就是异步思想。

  像 SATB write barrier ,每个 Java 线程有一个独立的、定长的 SATBMarkQueue,在写屏障里只把旧引用压入该队列中。满了之后会加到全局 SATBMarkQueueSet。

  后台线程会扫描,如果超过一定阈值就会处理,开始 tracing。

  在维护记忆集的写屏障也用了 logging write barrier 。

简单说下 G1 回收流程

G1 从大局上看分为两大阶段,分别是并发标记和对象拷贝。

并发标记是基于 STAB 的,可以分为四大阶段:

1、初始标记(initial marking),这个阶段是 STW 的,扫描根集合,标记根直接可达的对象即可。在G1中标记对象是利用外部的bitmap来记录,而不是对象头。

2、并发阶段(concurrent marking),这个阶段和应用线程并发,从上一步标记的根直接可达对象开始进行 tracing,递归扫描所有可达对象。 STAB 也会在这个阶段记录着变更的引用。

3、最终标记(final marking), 这个阶段是 STW 的,处理 STAB 中的引用。

4、清理阶段(clenaup),这个阶段是 STW 的,根据标记的 bitmap 统计每个 region 存活对象的多少,如果有完全没存活的 region 则整体回收。

对象拷贝阶段(evacuation),这个阶段是 STW 的。

根据标记结果选择合适的 reigon 组成收集集合(collection set 即 CSet),然后将 CSet 存活对象拷贝到新 region 中。

G1 的瓶颈在于对象拷贝阶段,需要花较多的瓶颈来转移对象。

GC 调优的两大目标是啥?

分别是最短暂停时间和吞吐量。

最短暂停时间:因为 GC 会 STW 暂停所有应用线程,这时候对于用户而言就等于卡顿了,因此对于时延敏感的应用来说减少 STW 的时间是关键。

吞吐量:对于一些对时延不敏感的应用比如一些后台计算应用来说,吞吐量是关注的重点,它们不关注每次 GC 停顿的时间,只关注总的停顿时间少,吞吐量高。

举个例子:

方案一:每次 GC 停顿 100 ms,每秒停顿 5 次。

方案二:每次 GC 停顿 200 ms,每秒停顿 2 次。

两个方案相对而言第一个时延低,第二个吞吐高,基本上两者不可兼得。

所以调优时候需要明确应用的目标。

GC 如何调优

这个问题在面试中很容易问到,抓住核心回答。

现在都是分代 GC,调优的思路就是尽量让对象在新生代就被回收,防止过多的对象晋升到老年代,减少大对象的分配。

需要平衡分代的大小、垃圾回收的次数和停顿时间。

需要对 GC 进行完整的监控,监控各年代占用大小、YGC 触发频率、Full GC 触发频率,对象分配速率等等。

然后根据实际情况进行调优。

比如进行了莫名其妙的 Full GC,有可能是某个第三方库调了 System.gc。

Full GC 频繁可能是 CMS GC 触发内存阈值过低,导致对象分配不过来。

还有对象年龄晋升的阈值、survivor 过小等等,具体情况还是得具体分析,反正核心是不变的。

posted on 2020-11-15 16:10  MaXianZhe  阅读(86)  评论(0)    收藏  举报

导航