25讲答疑课堂:模块四热点问题解答 - 图325讲答疑课堂:模块四热点问题解答

你好,我是刘超。

25讲答疑课堂:模块四热点问题解答 - 图4本周我们结束了“JVM性能监测及调优”的学习,这⼀期答疑课堂我精选了模块四中 11 位同学的留⾔,进⾏集中解答,希望也能对你有所帮助。另外,我想为坚持跟到现在的同学点个赞,期待我们能有更多的技术交流,共同成⻓。

第20讲

25讲答疑课堂:模块四热点问题解答 - 图5

很多同学都问到了类似“⿊夜⾥的猫”问到的问题,所以我来集中回复⼀下。JVM的内存模型只是⼀个规范,⽅法区也是⼀个规
范,⼀个逻辑分区,并不是⼀个物理空间,我们这⾥说的字符串常量放在堆内存空间中,是指实际的物理空间。

25讲答疑课堂:模块四热点问题解答 - 图6

⽂灏的问题和上⼀个类似,⼀同回复⼀下。元空间是属于⽅法区的,⽅法区只是⼀个逻辑分区,⽽元空间是具体实现。所以类
的元数据是存放在元空间,逻辑上属于⽅法区。

第21讲

25讲答疑课堂:模块四热点问题解答 - 图7

Liam同学,⽬前Hotspot虚拟机暂时不⽀持栈上分配对象。W.LI同学的留⾔值得参考,所以这⾥⼀同贴出来了。
25讲答疑课堂:模块四热点问题解答 - 图8

25讲答疑课堂:模块四热点问题解答 - 图9

25讲答疑课堂:模块四热点问题解答 - 图10

25讲答疑课堂:模块四热点问题解答 - 图11第22讲

25讲答疑课堂:模块四热点问题解答 - 图12

25讲答疑课堂:模块四热点问题解答 - 图13

25讲答疑课堂:模块四热点问题解答 - 图14

⾮常赞,Region这块,Jxin同学讲解得很到位。这⾥我再总结下CMS和G1的⼀些知识点。

CMS垃圾收集器是基于标记清除算法实现的,⽬前主要⽤于⽼年代垃圾回收。CMS收集器的GC周期主要由7个阶段组成,其中有两个阶段会发⽣stop-the-world,其它阶段都是并发执⾏的。
25讲答疑课堂:模块四热点问题解答 - 图15
G1垃圾收集器是基于标记整理算法实现的,是⼀个分代垃圾收集器,既负责年轻代,也负责⽼年代的垃圾回收。

跟之前各个分代使⽤连续的虚拟内存地址不⼀样,G1使⽤了⼀种 Region ⽅式对堆内存进⾏了划分,同样也分年轻代、⽼年代,但每⼀代使⽤的是N个不连续的Region内存块,每个Region占⽤⼀块连续的虚拟内存地址。

在G1中,还有⼀种叫 Humongous 区域,⽤于存储特别⼤的对象。G1内部做了⼀个优化,⼀旦发现没有引⽤指向巨型对象,

则可直接在年轻代的YoungGC中被回收掉。
25讲答疑课堂:模块四热点问题解答 - 图16
G1分为Young GC、Mix GC以及Full GC。

G1 Young GC主要是在Eden区进⾏,当Eden区空间不⾜时,则会触发⼀次Young GC。将Eden区数据移到Survivor空间时, 如果Survivor空间不⾜,则会直接晋升到⽼年代。此时Survivor的数据也会晋升到⽼年代。Young GC的执⾏是并⾏的,期间会发⽣STW。

当堆空间的占⽤率达到⼀定阈值后会触发G1 Mix GC(阈值由命令参数-XX:InitiatingHeapOccupancyPercent设定,默认值45),Mix GC主要包括了四个阶段,其中只有并发标记阶段不会发⽣STW,其它阶段均会发⽣STW。
25讲答疑课堂:模块四热点问题解答 - 图17

G1和CMS主要的区别在于:

CMS主要集中在⽼年代的回收,⽽G1集中在分代回收,包括了年轻代的Young GC以及⽼年代的Mix GC;
G1使⽤了Region⽅式对堆内存进⾏了划分,且基于标记整理算法实现,整体减少了垃圾碎⽚的产⽣; 在初始化标记阶段,搜索可达对象使⽤到的Card Table,其实现⽅式不⼀样。

这⾥我简单解释下Card Table,在垃圾回收的时候都是从Root开始搜索,这会先经过年轻代再到⽼年代,也有可能⽼年代引⽤到年轻代对象,如果发⽣Young GC,除了从年轻代扫描根对象之外,还需要再从⽼年代扫描根对象,确认引⽤年轻代对象的情况。

这种属于跨代处理,⾮常消耗性能。为了避免在回收年轻代时跨代扫描整个⽼年代,CMS和G1都⽤到了Card Table来记录这些引⽤关系。只是G1在Card Table的基础上引⼊了RSet,每个Region初始化时,都会初始化⼀个RSet,RSet记录了其它
Region中的对象引⽤本Region对象的关系。

除此之外,CMS和G1在解决并发标记时漏标的⽅式也不⼀样,CMS使⽤的是Incremental Update算法,⽽G1使⽤的是SATB
算法。

⾸先,我们要了解在并发标记中,G1和CMS都是基于三⾊标记算法来实现的:

⿊⾊:根对象,或者对象和对象中的⼦对象都被扫描;
灰⾊:对象本身被扫描,但还没扫描对象中的⼦对象;
⽩⾊:不可达对象。

基于这种标记有⼀个漏标的问题,也就是说,当⼀个⽩⾊标记对象,在垃圾回收被清理掉时,正好有⼀个对象引⽤了该⽩⾊标记对象,此时由于被回收掉了,就会出现对象丢失的问题。

为了避免上述问题,CMS采⽤了Incremental Update算法,只要在写屏障(write barrier)⾥发现⼀个⽩对象的引⽤被赋值到
⼀个⿊对象的字段⾥,那就把这个⽩对象变成灰⾊的。⽽在G1中,采⽤的是SATB算法,该算法认为开始时所有能遍历到的对象都是需要标记的,即认为都是活的。

G1具备Pause Prediction Model ,即停顿预测模型。⽤户可以设定整个GC过程中期望的停顿时间,⽤参数-
XX:MaxGCPauseMillis可以指定⼀个G1收集过程的⽬标停顿时间,默认值200ms。

G1会根据这个模型统计出来的历史数据,来预测⼀次垃圾回收所需要的Region数量,通过控制Region数来控制⽬标停顿时间的实现。

25讲答疑课堂:模块四热点问题解答 - 图18

Liam提出的这两个问题都⾮常好。

不管什么GC,都会发送stop-the-world,区别是发⽣的时间⻓短。⽽这个时间跟垃圾收集器⼜有关系,Serial、PartNew、Parallel Scavenge收集器⽆论是串⾏还是并⾏,都会挂起⽤户线程,⽽CMS和G1在并发标记时,是不会挂起⽤户线程的,但其它时候⼀样会挂起⽤户线程,stop the world 的时间相对来说就⼩很多了。

Major Gc 在很多参考资料中是等价于 Full GC的,我们也可以发现很多性能监测⼯具中只有Minor GC 和 Full GC。⼀般情况
下,⼀次Full GC将会对年轻代、⽼年代、元空间以及堆外内存进⾏垃圾回收。触发Full GC的原因有很多:

当年轻代晋升到⽼年代的对象⼤⼩,并⽐⽬前⽼年代剩余的空间⼤⼩还要⼤时,会触发Full GC;
当⽼年代的空间使⽤率超过某阈值时,会触发Full GC;
当元空间不⾜时(JDK1.7永久代不⾜),也会触发Full GC; 当调⽤System.gc()也会安排⼀次Full GC。
25讲答疑课堂:模块四热点问题解答 - 图19

25讲答疑课堂:模块四热点问题解答 - 图20

接下来解答 ninghtmare 的提问。我们可以通过 jstat -gc pid interval 查看每次GC之后,具体每⼀个分区的内存使⽤率变化情
况。我们可以通过JVM的设置参数,来查看垃圾收集器的具体设置参数,使⽤的⽅式有很多,例如 jcmd pid VM.flags 就可以查看到相关的设置参数。
25讲答疑课堂:模块四热点问题解答 - 图21
这⾥附上第22讲中,我总结的各个设置参数对应的垃圾收集器图表。

25讲答疑课堂:模块四热点问题解答 - 图22

25讲答疑课堂:模块四热点问题解答 - 图23第23讲

25讲答疑课堂:模块四热点问题解答 - 图24

我⼜不乱来同学的留⾔真是没有乱来,细节掌握得很好!

前提是⽼年代有⾜够接受这些对象的空间,才会进⾏分配担保。如果⽼年代剩余空间⼩于每次Minor GC晋升到⽼年代的平均值,则会发起⼀次 Full GC。
25讲答疑课堂:模块四热点问题解答 - 图25

25讲答疑课堂:模块四热点问题解答 - 图26

25讲答疑课堂:模块四热点问题解答 - 图27

看到这⾥,我发现爱提问的同学始终爱提问,⾮常⿎励啊,技术是需要交流的,也欢迎你有任何疑问,随时留⾔给我,我会知
⽆不尽。

现在回答W.LI同学的问题。这个会根据我们创建对象占⽤的内存使⽤率,合理分配内存,并不仅仅考虑对象晋升的问题,还会综合考虑回收停顿时间等因素。针对某些特殊场景,我们可以⼿动来调优配置。

第24讲

25讲答疑课堂:模块四热点问题解答 - 图28

下⾯解答Geek_75b4cd同学的问题。

我们知道,ThreadLocal是基于ThreadLocalMap实现的,这个Map的Entry继承了WeakReference,⽽Entry对象中的key使⽤了WeakReference封装,也就是说Entry中的key是⼀个弱引⽤类型,⽽弱引⽤类型只能存活在下次GC之前。

如果⼀个线程调⽤ThreadLocal的set设置变量,当前ThreadLocalMap则会新增⼀条记录,但由于发⽣了⼀次垃圾回收,此时的key值就会被回收,⽽value值依然存在内存中,由于当前线程⼀直存在,所以value值将⼀直被引⽤。.

这些被垃圾回收掉的key就会⼀直存在⼀条引⽤链的关系:Thread —> ThreadLocalMap–>Entry–>Value。这条引⽤链会导致
Entry不会被回收,Value也不会被回收,但Entry中的key却已经被回收的情况发⽣,从⽽造成内存泄漏。我们只需要在使⽤完该key值之后,将value值通过remove⽅法remove掉,就可以防⽌内存泄漏了。

25讲答疑课堂:模块四热点问题解答 - 图29

最后⼀个问题来⾃于WL同学。

内存泄漏是指不再使⽤的对象⽆法得到及时的回收,持续占⽤内存空间,从⽽造成内存空间的浪费。例如,我在第03讲中说到的,Java6中substring⽅法就可能会导致内存泄漏。

当调⽤substring⽅法时会调⽤new string构造函数,此时会复⽤原来字符串的char数组,⽽如果我们仅仅是⽤substring获取⼀
⼩段字符,⽽在原本string字符串⾮常⼤的情况下,substring的对象如果⼀直被引⽤,由于substring⾥的char数组仍然指向原字符串,此时string字符串也⽆法回收,从⽽导致内存泄露。

内存溢出则是发⽣了OutOfMemoryException,内存溢出的情况有很多,例如堆内存空间不⾜,栈空间不⾜,还有⽅法区空间
不⾜等都会导致内存溢出。

内存泄漏与内存溢出的关系:内存泄漏很容易导致内存溢出,但内存溢出不⼀定是内存泄漏导致的。

今天的答疑就到这⾥,如果你还有其它问题,请在留⾔区中提出,我会⼀⼀解答。最后欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他加⼊讨论。
25讲答疑课堂:模块四热点问题解答 - 图30

  1. 精选留⾔

25讲答疑课堂:模块四热点问题解答 - 图31nightmare
⽼师cms和g1能不能加餐讲详细⼀点 因为互联⽹公司 cms和g1问的⾮常多
2019-07-20 01:24
作者回复
好的,可以考虑。
2019-07-22 10:00

25讲答疑课堂:模块四热点问题解答 - 图32 java的垃圾回收使⽤的是复制算法和标记整理算法,这样对象的内存是变化的吧?那么引⽤它的栈上的地址也会变掉吗?如果 是的话如果hashmap的key如果没有⾃⼰实现hashcode的话,是不是就会引起了内存泄漏和程序错乱
2019-07-22 10:06
25讲答疑课堂:模块四热点问题解答 - 图33明翼
超哥,有问题请教下:
1)曾经被问到⼀个问题,就是java多线程分配内存的时候是如何控制并发冲突的那?
2)能不能结合代码把java内存创建的过程讲⼀次,⽐如成员变量的引⽤是在哪⾥分配的(我理解是堆上),堆上还是栈上,临 时变量那,通过这种整体的讲解会对我们印象⽐较深刻。
2019-07-22 09:26

25讲答疑课堂:模块四热点问题解答 - 图34Jxin
抛砖引⽟了,感谢⽼师的知⽆不尽。( ´◔‸◔`)
2019-07-20 20:59

25讲答疑课堂:模块四热点问题解答 - 图35 -W.LI-
25讲答疑课堂:模块四热点问题解答 - 图36 ⽼师好!最近正好在看多线程编程指南。有个东⻄没搞明⽩。
我⾃⼰写了个demo把所有线程都在临界区调⽤wait⽅法,wait⽅法后是sleep⽅法。我在主线程调⽤了notifyall(),在临界区内
打印了所有线程的状态,notifyall()之前都是waiting,之后都是blocked。出了临界区之后⼜打印了⼀次,发现有⼀个是timed_w
aiting,别的还是blocked。从表现来看
notifyall():wait->blocked
调⽤notifyall()的线程出临界区释放锁锁:
竞争到锁定blocked->runnable,别的还是blocked。
之前⽼师说notifyall()在出临界区的时候调⽤⽐较好,可以防⽌被唤醒的阻塞状态线程,竞争不到锁再次阻塞。
notifyall是本地⽅法看不到实现。我想确认下
notifyall的逻辑是:唤醒waiting线程->尝试获取锁->获取不到blocked?
还是:所有waiting状态线程->blocked状态进去锁池队列。(只有在有线程释放锁的时候(出临界区)才会从锁池队列拿⼀个线程尝试获取锁)。我⽐较倾向于第⼆种。没看源码希望⽼师帮忙解惑下,我特意翻了之前的课在那边也留⾔了,⽼师在这回复就好了谢谢⽼师
2019-07-20 10:30