关于Java GC(垃圾回收机制)的博文
链接:https://www.zhihu.com/question/35164211/answer/68265045
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
在公司当技术面试官几年间,从应届生到工作十几年的应聘者都遇到过。先表达一下我自己对面试的观点:
1.笔试、面试去评价一个人肯定是不够准确的,了解一个人最准确的方式就是“路遥知马力,日久见人心”。通过一、二个小时内的做题、交流,只是没有其他办法下进行的无奈之举,所以通过了面试不代表有多成功,没通过也不代表有多失败。
2.好的面试官本身交谈的时候就不应当把自己一个居高临下的角色上,应当把自己和应聘者当做两个做技术的人平等的交流,把自己当作权威往往就会受到观点的角度、语言表达、工作领域的惯性的制约。
3.好的考察题目则是大家能经常接触,不同层次的人能有不同层次的答案,能从问题引申出后面继续讨论的话题。
举个例子抛砖引玉,下面这个问题是我以前常问的,从应届生到工作十几年的人都问过:
“地球人都知道,Java有个东西叫垃圾收集器,它让创建的对象不需要像c/cpp那样delete、free掉,你能不能谈谈,GC是在什么时候,对什么东西,做了什么事情?”
我自己分析一下这个问题,首先是“什么时候”,不同层次的回答从低到高排列:
1.系统空闲的时候。
分析:这种回答大约占30%,遇到的话一般我就会准备转向别的话题,譬如算法、譬如SSH看看能否发掘一些他擅长的其他方面。
2.系统自身决定,不可预测的时间/调用System.gc()的时候。
分析:这种回答大约占55%,大部分应届生都能回答到这个答案,起码不能算错误是吧,后续应当细分一下到底是语言表述导致答案太笼统,还是本身就只有这样一个模糊的认识。
3.能说出新生代、老年代结构,能提出minor gc/full gc
分析:到了这个层次,基本上能说对GC运作有概念上的了解,譬如看过《深入JVM虚拟机》之类的。这部分不足10%。
4.能说明minor gc/full gc的触发条件、OOM的触发条件,降低GC的调优的策略。
分析:列举一些我期望的回答:eden满了minor gc,升到老年代的对象大于老年代剩余空间full gc,或者小于时被HandlePromotionFailure参数强制full gc;gc与非gc时间耗时超过了GCTimeRatio的限制引发OOM,调优诸如通过NewRatio控制新生代老年代比例,通过MaxTenuringThreshold控制进入老年前生存次数等……能回答道这个阶段就会给我带来比较高的期望了,当然面试的时候正常人都不会记得每个参数的拼写,我自己写这段话的时候也是翻过手册的。回答道这部分的小于2%。
PS:加起来不到100%,是因为有确实少数直接说不知道,或者直接拒绝回答的= =#
分析第二个问题:“对什么东西”:
1.不使用的对象。
分析:相当于没有回答,问题就是在问什么对象才是“不使用的对象”。大约占30%。
2.超出作用域的对象/引用计数为空的对象。
分析:这2个回答站了60%,相当高的比例,估计学校教java的时候老师就是这样教的。第一个回答没有解决我的疑问,gc到底怎么判断哪些对象在不在作用域的?至于引用计数来判断对象是否可收集的,我可以会补充一个下面这个例子让面试者分析一下obj1、obj2是否会被GC掉?
class C{
public Object x;
}
C obj1、obj2 = new C();
obj1.x = obj2;
obj2.x = obj1;
obj1、obj2 = null;
3.从gc root开始搜索,搜索不到的对象。
分析:根对象查找、标记已经算是不错了,小于5%的人可以回答道这步,估计是引用计数的方式太“深入民心”了。基本可以得到这个问题全部分数。
PS:有面试者在这个问补充强引用、弱引用、软引用、幻影引用区别等,不是我想问的答案,但可以加分。
4.从root搜索不到,而且经过第一次标记、清理后,仍然没有复活的对象。
分析:我期待的答案。但是的确很少面试者会回答到这一点,所以在我心中回答道第3点我就给全部分数。
最后由一个问题:“做什么事情”,这个问发挥的空间就太大了,不同年代、不同收集器的动作非常多。
1.删除不使用的对象,腾出内存空间。
分析:同问题2第一点。40%。
2.补充一些诸如停止其他线程执行、运行finalize等的说明。
分析:起码把问题具体化了一些,如果像答案1那样我很难在回答中找到话题继续展开,大约占40%的人。
补充一点题外话,面试时我最怕遇到的回答就是“这个问题我说不上来,但是遇到的时候我上网搜一下能做出来”。做程序开发确实不是去锻炼茴香豆的“茴”有几种写法,不死记硬背我同意,我不会纠语法、单词,但是多少你说个思路呀,要直接回答一个上网搜,我完全没办法从中获取可以评价应聘者的信息,也很难从回答中继续发掘话题展开讨论。建议大家尽量回答引向自己熟悉的,可讨论的领域,展现给面试官最擅长的一面。
3.能说出诸如新生代做的是复制清理、from survivor、to survivor是干啥用的、老年代做的是标记清理、标记清理后碎片要不要整理、复制清理和标记清理有有什么优劣势等。
分析:也是看过《深入JVM虚拟机》的基本都能回答道这个程度,其实到这个程度我已经比较期待了。同样小于10%。
4.除了3外,还能讲清楚串行、并行(整理/不整理碎片)、CMS等搜集器可作用的年代、特点、优劣势,并且能说明控制/调整收集器选择的方式。
分析:同上面2个问题的第四点。
最后介绍一下自己的背景,在一间不大不小的上市软件公司担任平台架构师,有3年左右的面试官经验,工作主要方向是大规模企业级应用,参与过若干个亿元级的项目的底层架构工作。
-------------------------------------------------------------------------------------------------------------------------
先提出一些问题在这里吧!
①Java和C++在内存分配和管理上有什么区别?
Java与C++之间有一堵由动态内存分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人想出来。
对于从事C和C++程序开发的开发人员来说,在内存管理领域,他们既是拥有最高权利的皇帝,也是从事最基础工作的劳动人民-----既拥有每一个对象的所有权,又担负着每一个对象从生命开始到终结的维护责任。
对于Java程序员来说,虚拟机的自动内存分配机制的帮助下,不再需要为每一个new操作去写配对的delete/free代码,而且不容易出现内存泄露和内存溢出问题,看起来由虚拟机管理内存一切都很美好。不过,也正是因为Java程序员把内存控制的权利交给Java虚拟机,一旦出现内存泄露和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那排查错误将会是一项异常艰难的工作。
并且好的Java程序在编写的时候肯定要考虑GC的问题,怎样定义static对象,怎样new对象效率更高等等问题,简称面向GC的编程
也可以说Java的内存分配管理是一种托管的方式,托管于JVM。
C++经过编译时直接编译成机器码,而Java是编译成字节码,由JVM解释执行。
C++是编译型语言,而Java兼具编译型和解释型语言的特点。
②Java虚拟机规范将JVM虚拟机所管理的内存分为几部分?<img src="https://pic4.zhimg.com/a30c88d8d3c43f75cb73ce0fe113a78b_b.png" data-rawwidth="549" data-rawheight="357" class="origin_image zh-lightbox-thumb" width="549" data-original="https://pic4.zhimg.com/a30c88d8d3c43f75cb73ce0fe113a78b_r.png">
1. 程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行字节码的行号指示器。是线程私有,生命周期与线程相同。
2. Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。
Java虚拟机栈描述的是Java方法(区别于native的本地方法)执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动作链接、方法出口等信息。每个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
3. 本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机所使用到的Native方法服务。
4. 方法区
(3)有哪些方法可以判断一个对象已经可以被回收,JVM怎么判断一个对象已经消亡可以被回收?①引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。
Java语言没有选用引用计数法来管理内存,因为引用计数法不能很好的解决循环引用的问题。
②根搜索算法
在主流的商用语言中,都是使用根搜索算法来判定对象是否存活的。
GC Root Tracing 算法思路就是通过一系列的名为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连,即从GC Roots到这个对象不可达,则证明此对象是不可用的。
<img src="https://pic1.zhimg.com/cb06b4bd6d62cf310b7f3014ab5cb2fc_b.png" data-rawwidth="705" data-rawheight="377" class="origin_image zh-lightbox-thumb" width="705" data-original="https://pic1.zhimg.com/cb06b4bd6d62cf310b7f3014ab5cb2fc_r.png">
比如上图,左边的对象都是存活的,右边的都是可以回收的。
(4)那些对象可以作为GC Roots?
虚拟机栈(栈帧中的本地变量表)中的引用的对象
方法区中的类静态属性引用的对象
方法区中的常量引用的对象
本地方法栈中JNI(Native方法)的引用对象
(5)Java代码编译的结果是什么?
是字节码文件.class
(6) 怎么理解Java语言的平台无关性?语言无关性?通过什么方法实现的?
(7)Java中的static变量和static方法在JVM运行中内存的分配管理有什么不同和一般变量方法?
静态对象 非静态对象
拥有属性: 是类共同拥有的 是类各对象独立拥有的
内存分配: 内存空间上是固定的 空间在各个附属类里面分配
分配顺序: 先分配静态对象的空间 继而再对非静态对象分配空间,也就是初 始化顺序是先静态
(8)Java类的加载过程?
(9)在heap中没有类实例的时候,类信息还存在于JVM吗? 存在于什么地方?
----------------------------------------------------------------------------------------------------------------------------
HotSpot JVM (下简称JVM)的内存管理
JVM将堆分成了 二个大区 Young 和 Old 如下图:
关于为什么要这样区分Young(将Young区分为Eden、Servivor1、Servivor2以及相对的From和To ),这要牵涉到JVM的垃圾回收算法的讨论。
1)因为引用计数法无法解决循环引用问题,JVM并没有采用这种算法来判断对象是否存活。
2)JVM一般采用GCRoots的方法,只要从任何一个GCRoots的对象可达,就是不被回收的对象
3)判断了对象生死,怎么进行内存的清理呢?
4)标记-清除算法,先标记那些要被回收的对象,然后进行清理,简单可行,但是①标记清除效率低,因为要一个一个标记和清除②造成大量不连续的内存碎片,空间碎片太多可能会导致当程序在以后的运行过程中需要分配较大对象的时候无法找到足够的连续内存而不得不触发另一次垃圾收集动作。
5)采用复制收集算法:将可用内存按照容量分为大小相等的两块,每次只是使用其中的一块。当这一块的内存用完了,就将可用内存中
<img src="https://pic3.zhimg.com/55e852328b19760c09569f1f90637376_b.png" data-rawwidth="836" data-rawheight="146" class="origin_image zh-lightbox-thumb" width="836" data-original="https://pic3.zhimg.com/55e852328b19760c09569f1f90637376_r.png">
<img src="https://pic1.zhimg.com/212cd1165d947e182f1a71bcdd40ab60_b.png" data-rawwidth="832" data-rawheight="510" class="origin_image zh-lightbox-thumb" width="832" data-original="https://pic1.zhimg.com/212cd1165d947e182f1a71bcdd40ab60_r.png"><img src="https://pic1.zhimg.com/2ae24424f47c4830dad9734772aa6e28_b.png" data-rawwidth="841" data-rawheight="374" class="origin_image zh-lightbox-thumb" width="841" data-original="https://pic1.zhimg.com/2ae24424f47c4830dad9734772aa6e28_r.png">
上面的过程就解释了为什么我们的Yong内存需要分为三块,Eden,Survivor1,Survivor2以及FROM TO相对使用的用法
<img src="https://pic2.zhimg.com/618110e6a18cac3f20f2b3837b188d4d_b.png" data-rawwidth="573" data-rawheight="306" class="origin_image zh-lightbox-thumb" width="573" data-original="https://pic2.zhimg.com/618110e6a18cac3f20f2b3837b188d4d_r.png"> 因此当Eden区满的时候 GC执行,这时会将 Eden 区和 From 区中还被引用的对象会被移到 To区 ,个别大对象和部分From对象在To已满的情况下会被放到Old区,如下图: 因此当Eden区满的时候 GC执行,这时会将 Eden 区和 From 区中还被引用的对象会被移到 To区 ,个别大对象和部分From对象在To已满的情况下会被放到Old区,如下图:
<img src="https://pic2.zhimg.com/35ed96d1c1635b589883094b82459791_b.png" data-rawwidth="543" data-rawheight="298" class="origin_image zh-lightbox-thumb" width="543" data-original="https://pic2.zhimg.com/35ed96d1c1635b589883094b82459791_r.png">HotSpot VM 内存堆的两个Servivor区
HotSpot VM 内存堆的两个Servivor区
----------------------上面的部分内容来自
http://www.iteye.com/topic/894148
帖子里面题主提了一个问题:
第一次发贴,有什么问题请大家指正一下。有个地方不太清楚 ,为什么需要From 和 To 两个平行的区呢,为什么不直接从Survivor 移到 Old? 这样设计的好处是什么?难道是因为在移动对象的时候需要压缩调整对象空间,所以这种整体移动的设计会快一点吗?希望大家一起来讨论一下 ^_^