聊的不止技术。跟着小帅写代码,还原和技术大牛一对一真实对话,剖析真实项目筑成的一砖一瓦,了解最新最及时的资讯信息,还可以学到日常撩妹小技巧哦,让我们开始探索主人公小帅的职场生涯吧!
(PS:本系列文章以幽默风趣风格为主,较真侠和杠精请绕道~)
江华:“老胡,有人来面试啦,怎样?”
老胡:“中规中矩,没有太大的亮点,甚至可以说他表现出来的深度、广度跟他的工龄不匹配。”
小帅:“这么讲究的啊,欸,老胡,你在面试人的时候都会考虑哪些方面呀?”
老胡:“无非就几个方面啦,人品、实力、潜力。首先是人品,这里指的不是说你要多么圣母,最起码人是正面的,有良好的心态,不会存心搞破坏,不是极端分子。虽然在短短一个小时内可能看出来什么,甚至被有些人蒙骗过去,但一直是招人的核心点。”
老胡:“其次就是能力了,能力又分为硬实力和软实力。硬实力当然指的就是你的基础技能和项目经验是不是跟岗位匹配的。软实力的话会侧重于你的沟通能力、处事能力和总结能力了。”
老胡:“最后会考察你的潜力,你积极上进和面对挑战或挫折的态度、对知识的深度和广度,对未知的好奇和征服,这些都能体现出你的上限在哪里,也就是说你值不值得去培养。”
老胡:“当然不同的岗位要求可能会有些出入,大体上是相同的,如果这些你都符合面试官的口味,那么恭喜,你被录取了。”
小帅:“哦哦,原来这样的啊。我好像刚才听到你面试的时候问了JVM垃圾回收的问题欸,你都问些啥啦?给我们讲讲呗。”
江华:“垃圾回收来来回回不就是几个问题么?有什么好问的?”
老胡:“呵,这么膨胀的么,我来考下你们。”
老胡:“你们想过为什么要进行垃圾回收?”
小帅:“为了内存能重复利用呗。”
老胡:“为什么进行垃圾回收,内存就能重复利用呢?”
小帅:“因为对象的分配需要占用内存,那些没用的对象被回收了,这样就有内存空间重新分配对象了。”
老胡:“既然垃圾回收的是没用的对象,那么什么样的对象才是没用的对象?”
小帅:“顾名思义,没有被引用的对象就是没用的对象吧。”
老胡:“那么我们怎样才能知道对象有没有被引用?”
小帅:(挠头表情)
江华:“引用计数法或者可达性分析算法?”
小帅:“哦哦,记起来了,书上有提到。引用计数法:对象头处维护一个计数器,每增加一次对该对象的引用计数器自加,如果对该对象的引用失联,则计数器自减。当计数器为0时,表明该对象已经被废弃,不处于存活状态。”
老胡:“引用计数法有缺陷,这种方式一方面无法区分软、虛、弱、强引用类别。另一方面,会造成死锁,假设两个对象相互引用始终无法释放计数器,循环引用,永远不能被回收。”
江华:“可达性分析算法可以,通过一系列称为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为“引用链”,当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的。”
老胡:“OK,那么什么样的对象可以作为GC Roots呢?”
小帅:(挠头表情)
江华:“静态变量,常量引用的对象?”
老胡:“这样说不够严谨,其实可以这样推导,垃圾回收主要集中在堆,非堆区域的对象是不是更加可能作为GC Roots呢?”
在Java语言中,可作为GC Roots的对象包括下面几种:
1、虚拟机栈(栈帧中的本地变量表)中引用的对象。
2、方法区中类静态属性引用的对象。
3、方法区中常量引用的对象。
4、本地方法栈中JNI(即一般说的Native方法)引用的对象。
老胡:“对象的生命周期有长有短,如果一视同仁来处理肯定会影响效率,那么怎样处理这种情况呢?”
小帅:“这个我知道,分代处理。新生代、老年代、永久代......”
(图2-317-1 JDK1.8之前内存划分)
老胡:“为什么这样划分就能提高回收的效率呢?”
小帅:(挠头表情)
江华:“因为不划分的话,所有对象都在一起,每次做可达性分析岂不是要扫描全部对象?而且很多对象都是朝生夕死的,把生命周期短的集中到一个小区域,没用了就马上回收。周期长的放到另外的区域,因为可能还会被再次使用,满了再回收,这样效率会提高很多。”
老胡:“差不多说到点子上。根据分配策略,大多数情况下,对象在新生代的Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC,也就是Young GC。由于大部分对象生命周期都比较短,用完一次就不用了。所以理论上Eden区的垃圾回收率非常高,回收一次,又有很多空间可以利用了。扫描面积减少,空间周转率提高,所以这样高效。当然这些还跟使用什么垃圾回收器有关。”
老胡:“刚才提到Eden区垃圾回收率高,那为什么新生代还需要一个Survivor区呢?”
小帅:(挠头表情)
江华:“小帅,没你的事了,可以一边玩蛋去了。”
江华:“Eden区虽然垃圾回收率高,但每次总会有剩余的垃圾吧。如果每次都把这些垃圾扔到老年代,老年代会很快就会被填满,触发Major GC,也就是Full GC。老年代回收成本高且回收率较低,所以用Survivor区缓冲一下,说不定下次就能回收了。”
老胡:“那为什么Survivor区要设置两个,而不是一个或者三个呢?”
小帅:(挠头表情)
江华:(挠头表情)
小帅:“......”
老胡:“我们要学会用已知去推导未知。先回忆一下,垃圾回收算法有哪些?”
江华:“标识—复制、标识—清理、标识—整理这些吧。”
老胡:“那新生代一般用的回收算法是哪个?”
江华:“标识—复制,因为这个算法比较符合新生代对象存活的特性。等等,我好像明白了。”
老胡:“不错。”
小帅:“啥呀?”
江华:“因为碎片化啊!”
老胡:“是的。标识—复制算法:它将内存按容量划分成大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后把已使用的内存空间一次清理掉。这样使得每次都是对这个半区进行内存回收,就不会出现碎片的情况,简单高效。”
老胡:“假如只有一块Survivor区,当发生Minor GC时,Eden区和Survivor区都会有一些存活的对象,如果把Eden区的存活的对象硬塞到Survivor区,必定造成空间不连续,甚至造成每次分配一个大一点的对象,都要担保进入老年区,可想而知后果是怎样。而且假如和Eden区按1:1划分未免代价太高了。”
老胡:“假设有两块Survivor区,按1:1划分,刚开始,Eden区和两块Survivor区都没有对象。”
(图2-317-2 JDK1.8前)
老胡:“当Eden区空间不足,触发Minor GC,存活的对象被复制到一块Survivor From区,Eden区、To区为空。”
(图2-0317-3 JDK1.8前)
老胡:“当Eden区和Survivor From区都满时,复制Eden区和From区存活的对象到To区,清理掉Eden区和From区,然后交换From区和To区,看下这样是不是很合理多了。可以看出,只有在 Eden 区快满的时候才会触发 Minor GC 。而 Eden 区占新生代的绝大部分,所以 Minor GC 的频率得以降低。当然,使用两个 Survivor 这种方式我们也付出了一定的代价,如 10% 的空间浪费、复制对象的开销等。”
(图2-317-4 JDK1.8前)
小帅:“厉害了。”
江华:“厉害了。”
老胡:“那你们还知道有哪些垃圾回收器吗?”
小帅:“我知道,这个我知道,HotSpot虚拟机中的7种垃圾收集器:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1。”
(7种垃圾回收器 图片来源网络 )
老胡:“哟,不错嘛。那么你们可以说下各种垃圾回收器的原理吗?”
小帅:“......(变态)”
江华:“......(变态)”
老胡:“咳咳,开玩笑的啦。本来还想问CMS垃圾回收器的标记过程,G1垃圾回收器Region、Card Table、Remember Set、三色标记法这些呢,不过书上和网上对于这种知识点说得非常深入,你们可以先自行回去深入学习,后面我们再讨论。”
老胡:“我们继续,Major GC的时候说的Stop the World 指的是什么?”
小帅:“我记得书本说过,启动一个Main方法的时候,除了主线程,还会启动GC线程。Major GC为什么这么多人讨厌就是因为Stop the World。Stop the World 指的就是挂起了主线程,执行GC线程。”
老胡:“顺便提一下,Minor GC也会Stop the World的。是的,Stop the World 指的是挂起用户线程,但用户线程总不能随便挂起吧,是主动挂起的还是被动挂起的?”
江华:“被动挂起的话还得引入监控线程,轮询GC状态,成本太高。应该是主动挂起的吧,我记得还有个叫安全点的东西。”
老胡:“不错,那什么是安全点呢?”
江华:“小帅,去挠头了。”
小帅:“......”
老胡:“......”
老胡:“程序运行期间,无时无刻都可能有对象生成。如果没有一个地方让用户线程暂停下来,前面提到判断对象存活的GC Roots 可达性分析就很难进行了。就好像收拾你小孩的玩具,刚捡起,他又扔其它玩具出来一样。但程序执行时并非在所有地方都能停顿下来开始GC,只有在达到合适的地方才能暂停,这个合适的地方就是预先设置的安全点。”
老胡:“是不是所有地方都可以设置为安全点?不是的。Safepoint 的选定既不能太少以致于GC等待时间太长,也不能过于频繁以致于过分增大运行时的负荷。所以,安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的。例如方法调用、循环跳转、异常跳转等。”
江华:“老胡,那么安全点怎样实现?”
老胡:“你刚才不是说用户线程是自己主动停下来的么,所以推出,安全点就像标志位,代表是否需要挂起。JVM在编译期间,在上面提到合适的地方会加入一个test指令。当需要GC需要中断线程的时候,把内存页设置为不可读,线程执行到test指令时就会产生一个自陷异常信号,在预先注册的异常处理器中暂停线程实现等待了。当然还有安全区域的说法,有兴趣可以去了解一下。”
老胡:“HR召唤,溜了。”
小帅:“哇塞,老胡真厉害。我发现他说的我都看过,但他刚才问我的时候,一脸懵逼,完全搭不上话。”
江华:“那是因为你没有理解,只是死记硬背。不过的确有种醍醐灌顶的感觉,我觉得有必要顺着类似他那样的思路重新看JVM的相关知识了。”
小帅:“嗯嗯,马上回去看书。等等,小檬要下班了,我要去目送一下啦,告辞。”
江华:“......”
参考资料
《深入理解Java虚拟机:JVM高级特性与最佳实践(最新第二版)》
《JVM源码分析之安全点safepoint》https://www.jianshu.com/p/c79c5e02ebe6
《Java虚拟机垃圾回收(三) 7种垃圾收集器》https://blog.csdn.net/tjiyu/article/details/53983650