共计 4143 个字符,预计需要花费 11 分钟才能阅读完成。
面试官:我还记得上次你讲到 JVM 内存构造(运行时数据区域)提到了「堆」,而后你说是分了几块区域嘛
面试官:过后感觉再讲下去那我可能就得加班了
面试官 : 明天有点空了,持续聊聊「堆」那块吧
候选者:嗯,后面提到了堆分了「新生代」和「老年代」,「新生代」又分为「Eden」和「Survivor」区,「survivor」区又分为「From Survivor」和「To Survivor」区
候选者:说到这里,我就想聊聊 Java 的垃圾回收机制了
面试官:那你开始你的表演吧
候选者:咱们应用 Java 的时候,会创立很多对象,但咱们未曾「手动」将这些对象进行革除
候选者 :而如果用 C /C++ 语言的时候,用完是须要本人 free(开释) 掉的
候选者:那为什么在写 Java 的时候不必咱们本人手动开释”垃圾”呢?起因很简略,JVM 帮咱们做了(主动回收垃圾)
面试官:嗯…
候选者:我集体对垃圾的定义:只有对象不再被应用了,那咱们就认为该对象就是垃圾,对象所占用的空间就能够被回收
面试官 : 那是怎么判断对象不再被应用的呢?
候选者:罕用的算法有两个「援用计数法」和「可达性分析法」
候选者:援用计数法思路很简略:当对象被援用则 +1,但对象援用失败则 -1。当计数器为 0 时,阐明对象不再被援用,能够被可回收
候选者:援用计数法最显著的毛病就是:如果对象存在循环依赖,那就无奈定位该对象是否应该被回收(A 依赖 B,B 依赖 A)
面试官:嗯…
候选者:另一种就是可达性分析法:它从「GC Roots」开始向下搜寻,当对象到「GC Roots」都没有任何援用相连时,阐明对象是不可用的,能够被回收
候选者:「GC Roots」是一组必须「沉闷」的援用。从「GC Root」登程,程序通过间接援用或者间接援用,可能找到可能正在被应用的对象
面试官 : 还是不太懂,那「GC Roots」个别是什么?你说它是一组沉闷的援用,能不能举个例子,太形象了。
候选者:比方咱们上次不是聊到 JVM 内存构造中的虚拟机栈吗,虚拟机栈里不是有栈帧吗,栈帧不是有局部变量吗?局部变量不就存储着援用嘛。
候选者:那如果栈帧位于虚拟机栈的栈顶,是不是就能够阐明这个栈帧是沉闷的(换言之,是线程正在被调用的)
候选者:既然是线程正在调用的,那栈帧里的指向「堆」的对象援用,是不是肯定是「沉闷」的援用?
候选者:所以,以后沉闷的栈帧指向堆里的对象援用就能够是「GC Roots」
面试官:嗯…
候选者:当然了,能作为「GC Roots」也不单单只有下面那一小块
候选者:比方类的动态变量援用是「GC Roots」,被「Java 本地办法」所援用的对象也是「GC Roots」等等…
候选者:回到了解的重点:「GC Roots」是一组必须「沉闷」的「援用」,只有跟「GC Roots」没有间接或者间接援用相连,那就是垃圾
候选者:JVM 用的就是「可达性剖析算法」来判断对象是否垃圾
面试官:懂了
候选者:垃圾回收的第一步就是「标记」,标记哪些没有被「GC Roots」援用的对象
候选者:标记完之后,咱们就能够抉择间接「革除」,只有不被「GC Roots」关联的,都能够干掉
候选者:过程非常简单粗犷,但也存在很显著的问题
候选者:间接革除会有「内存碎片」的问题:可能我有 10M 的空余内存,但程序申请 9M 内存空间却申请不下来(10M 的内存空间是垃圾革除后的,不间断的)
候选者:那解决「内存碎片」的问题也比较简单粗犷,「标记」完,不间接「革除」。
候选者:我把「标记」存活的对象「复制」到另一块空间,复制完了之后,间接把原有的整块空间给干掉!这样就没有内存碎片的问题了
候选者 :这种做法毛病又很显著:内存利用率低,得有一块新的区域给我复制(挪动) 过来
面试官:嗯…
候选者:还有一种「折中」的方法,我未必要有一块「大的残缺空间」能力解决内存碎片的问题,我只有能在「以后区域」内进行挪动
候选者:把存活的对象移到一边,把垃圾移到一边,那再将垃圾一起删除掉,不就没有内存碎片了嘛
候选者:这种业余的术语就叫做「整顿」
候选者:扯了这么久,咱们把思维再次回到「堆」中吧
候选者:通过钻研表明:大部分对象的生命周期都很短,而只有少部分对象可能会存活很长时间
候选者:又因为「垃圾回收」是会导致「stop the world」(利用进行拜访)
候选者:了解「stop the world」应该很简略吧:回收垃圾的时候,程序是有短暂的工夫不能失常持续运作啊。不然 JVM 在回收的时候,用户线程还持续调配批改援用,JVM 怎么搞(:
候选者:为了使「stop the world」继续的工夫尽可能短以及进步并发式 GC 所能应酬的内存调配速率
候选者:在很多的垃圾收集器上都会在「物理」或者「逻辑」上,把这两类对象进行辨别,死得快的对象所占的区域叫做「年老代」,活得久的对象所占的区域叫做「老年代」
候选者:但也不是所有的「垃圾收集器」都会有,只不过咱们当初线上用的可能都是 JDK8,JDK8 及以下所应用到的垃圾收集器都是有「分代」概念的。
候选者:所以,你能够看到我的「堆」是画了「年老代」和「老年代」
候选者:要值得注意的是,高版本所应用的垃圾收集器的 ZGC 是没有分代的概念的(:
候选者:只不过我为了好阐明现状,ZGC 的话有空咱们再聊
面试官:嗯…好吧
候选者:在后面更后面提到了垃圾回收的过程,其实就对应着几种「垃圾回收算法」,别离是:
候选者:标记革除算法、标记复制算法和标记整顿算法【「标记」「革除」「复制」「整顿」】
候选者:通过下面的铺垫之后,这几种算法应该还是比拟好了解的
候选者:「分代」和「垃圾回收算法」都搞明确了之后,咱们就可以看下在 JDK8 生产环境及以下常见的垃圾回收器了
候选者:「年老代」的垃圾收集器有:Seria、Parallel Scavenge、ParNew
候选者:「老年代」的垃圾收集器有:Serial Old、Parallel Old、CMS
候选者:看着垃圾收集器有很多,其实还是十分好了解的。Serial 是单线程的,Parallel 是多线程
候选者:这些垃圾收集器实际上就是「实现了」垃圾回收算法(标记复制、标记整顿以及标记革除算法)
候选者:CMS 是「JDK8 之前」是比拟新的垃圾收集器,它的特点是可能尽可能减少「stop the world」工夫。在垃圾回收时让用户线程和 GC 线程可能并发执行!
候选者:又能够发现的是,「年老代」的垃圾收集器应用的都是「标记复制算法」
候选者:所以在「堆内存」划分中,将年老代划分出 Survivor 区(Survivor From 和 Survivor To),目标就是为了有一块残缺的内存空间供垃圾回收器进行拷贝(挪动)
候选者:而新的对象则放入 Eden 区
候选者:我上面从新画下「堆内存」的图,因为它们的大小是有默认的比例的
候选者:图我曾经画好了,应该就不必我再阐明了
面试官 : 我还想问问,就是,新创建的对象个别是在「新生代」嘛,那在什么时候会到「老年代」中呢?
候选者:嗯,我认为简略能够分为两种状况:
候选者:1. 如果对象太大了,就会间接进入老年代(对象创立时就很大 || Survivor 区没方法存下该对象)
候选者:2. 如果对象太老了,那就会降职至老年代(每产生一次 Minor GC,存活的对象年龄 +1,达到默认值 15 则降职老年代 || 动静对象年龄断定 能够进入老年代)
面试官 : 既然你又提到了 Minor GC,那 Minor GC 什么时候会触发呢?
候选者:当 Eden 区空间有余时,就会触发 Minor GC
面试官:Minor GC 在我的了解就是「年老代」的 GC,你后面又提到了「GC Roots」嘛
面试官 : 那在「年老代」GC 的时候,从 GC Roots 登程,那不也会扫描到「老年代」的对象吗?那那那.. 不就相当于全堆扫描吗?
候选者:这 JVM 里也有解决办法的。
候选者:HotSpot 虚拟机「老的 GC」(G1 以下)是要求整个 GC 堆在间断的地址空间上。
候选者:所以会有一条分界线(一侧是老年代,另一侧是年老代),所以能够通过「地址」就能够判断对象在哪个分代上
候选者:当做 Minor GC 的时候,从 GC Roots 登程,如果发现「老年代」的对象,那就不往下走了(Minor GC 对老年代的区域毫无趣味)
面试官 : 但又有个问题,那如果「年老代」的对象被「老年代」援用了呢?(老年代对象持有年老代对象的援用),那时候必定是不能回收掉「年老代」的对象的。
候选者:HotSpot 虚拟机下 有「card table」(卡表)来防止全局扫描「老年代」对象
候选者:「堆内存」的每一小块区域造成「卡页」,卡表实际上就是卡页的汇合。当判断一个卡页中有存在对象的跨代援用时,将这个页标记为「脏页」
候选者:那晓得了「卡表」之后,就很好办了。每次 Minor GC 的时候只须要去「卡表」找到「脏页」,找到后退出至 GC Root,而不必去遍历整个「老年代」的对象了。
面试官:嗯嗯嗯,还能够的啊,要不持续聊聊 CMS?
候选者:这面试快一个小时了吧,我图也画了这么多了。下次?下次吧?有点儿累了
本文总结:
- 什么是垃圾:只有对象不再被应用,那即是垃圾
- 如何判断为垃圾:可达性剖析算法和援用计算算法,JVM 应用的是可达性剖析算法
- 什么是 GC Roots:GC Roots 是一组必须沉闷的援用,跟 GC Roots 无关联的援用即是垃圾,可被回收
- 常见的垃圾回收算法:标记革除、标记复制、标记整顿
- 为什么须要分代:大部分对象都死得早,只有少部分对象会存活很长时间。在堆内存上都会在物理或逻辑上进行分代,为了使「stop the world」继续的工夫尽可能短以及进步并发式 GC 所能应酬的内存调配速率。
- Minor GC:当 Eden 区满了则触发,从 GC Roots 往下遍历,年老代 GC 不关怀老年代对象
- 什么是 card table【卡表】:空间换工夫(相似 bitmap),可能防止扫描老年代的所有对应进而顺利进行 Minor GC(案例:老年代对象持有年老代对象援用)
- 堆内存占比:年老代占堆内存 1 /3,老年代占堆内存 2 /3。Eden 区占年老代 8 /10,Survivor 区占年老代 2 /10(其中 From 和 To 各站 1 /10)
欢送关注我的微信公众号【Java3y】来聊聊 Java 面试,对线面试官系列继续更新中!
【对线面试官 - 挪动端】系列 一周两篇继续更新中!
【对线面试官 - 电脑端】系列 一周两篇继续更新中!
原创不易!!求三连!!