就在昨天,小码仔和同事蛋哥面试了一个前来求职的小姐姐。回顾整个面试过程,小姐姐的表现能够说是可圈可点。因此小码仔忙里偷闲把对小姐姐的面试过程整理出来,分享给你们。java
事情的整个过程是这样子的,就在昨天阳光明媚的午后,我和蛋哥一如从前同样处理着社畜的平常工做,被hr小姐姐通知进行Java候选人面试。当我和蛋哥抱着吃饭的小本本进了面试接待室。啊,一个眉清目秀的小姐姐。程序员
我老脸一红,不不不,同是女孩纸,我要矜持。我和蛋哥礼貌的冲妹子笑了笑说“很差意思,让你久等了”,而后我示意妹纸坐下,说:“咱们开始吧,我看你的简历有作过JVM调优,那咱们今天就来讨论下JVM……”。web
JVM简单来讲它是一个虚构出来的计算机,是经过在实际的计算机上仿真模拟各类计算机功能来实现的。面试
JVM屏蔽了与具体操做系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码也就是字节码,就能够在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终仍是把字节码解释成具体平台上的机器指令执行。算法
Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。数组
PC寄存器是用于存储每一个线程下一步将执行的JVM指令,如该方法为native的,则PC寄存器中不存储任何信息。安全
JVM栈是线程私有的,每一个线程建立的同时都会建立JVM栈,JVM栈中存放的为当前线程中局部基本类型的变量(java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double)、部分的返回结果以及Stack Frame,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址。并发
它是JVM用来存储对象实例以及数组值的区域,能够认为Java中全部经过new建立的对象的内存都在此分配,Heap中的对象的内存须要等待GC进行回收。jvm
(1)在Sun JDK中这块区域对应的为PermanetGeneration,又称为持久代。编辑器
(2)方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当开发人员在程序中经过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在必定的条件下它也会被GC,当方法区域须要使用的内存超过其容许的大小时,会抛出OutOfMemory的错误信息。
存放的为类中的固定的常量信息、方法和Field的引用信息等,其空间从方法区域中分配。
JVM采用本地方法堆栈来支持native方法的执行,此区域用于存储每一个native方法调用的状态。
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、装换解析和初始化,最终造成能够被虚拟机直接使用的 Java 类型。
虚拟机遇到一条new指令时,首先将去检查这个指令的参数可否在常量池中定位到一个类的符号引用,并检查这个符号引用表明的类是否已经被加载、解析和初始化过,若是没有,那么必须先执行相应的类加载过程。
在类加载检查经过后,接下来虚拟机将会为新生的对象分配内存。对象所须要的内存大小在类加载完成后即可彻底肯定,为对象分配空间等同于把一块肯定大小的内存从java堆中划分出来。
内存分配完成后,虚拟机须要将分配到的内存空间都初始化为零值(不包括对象头),这一步操做保证了对象的实例字段在 Java 代码中能够不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。若是使用TLAB,这一工做过程也能够提早到TLAB分配时进行。
接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪一个类的实例,如何才能找到类的元数据信息,对象的哈希吗,对象的GC分代年龄等信息,这些信息存放在对象的对象头中。根据虚拟机当前的运行状态的不一样,对象头会有不一样的设置方式。
在上面工做都完成以后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象建立才刚开始, 方法尚未执行,全部的字段都还为零。因此通常来讲,执行 new 指令以后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算彻底产生出来。
在建立对象的时候有一个很重要的问题,就是线程安全,由于在实际开发过程当中,建立对象是很频繁的事情,例如正在给A对象分配内存,可是指针还没修改,这时候对象B可能使用原来的指针来分配内存的状况。做为虚拟机来讲,必需要保证线程是安全的,一般来说,虚拟机采用两种方式来保证线程安全:
CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操做,若是由于冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操做的原子性。
TLAB: 为每个线程预先在 Eden 区分配一块内存。JVM 在给线程中的对象分配内存时,首先在各个线程的TLAB 分配,当对象大于TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配。虚拟机是否启用TLAB,能够经过-XX:+/-UseTLAB参数来设定。
简而言之就是已经再也不存活的对象即为垃圾。
断定对象是否存活的算法有两种:
引用计数算法:给对象中添加一个引用计数器,每当一个地方应用了对象,计数器加1;当引用失效,计数器减1;当计数器为0表示该对象已死、可回收。可是它很难解决两个对象之间相互循环引用的状况。
可达性分析算法:经过一系列称为“GC Roots”的对象做为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即对象到GC Roots不可达),则证实此对象已死、可回收。Java中能够做为GC Roots的对象包括:虚拟机栈中引用的对象、本地方法栈中Native方法引用的对象、方法区静态属性引用的对象、方法区常量引用的对象。
因为引用计数算法很难解决两个对象之间相互循环引用的状况,所以在咱们的平常开发过程当中,一般经过可达性分析算法来断定对象是否存活的。
使对象变为垃圾主要有如下状况:
例如:
(1)改变对象的引用,如置为null或者指向其余对象。
Object x=new Object();//object1
Object y=new Object();//object2
x=y;//object1 变为垃圾
x=y=null;//object2 变为垃圾
(2)超出做用域
if(i==0){
Object x=new Object();//object1
}//括号结束后object1将没法被引用,变为垃圾
(3)类嵌套致使未彻底释放
class A{
A a;
}
A x= new A();//分配一个空间
x.a= new A();//又分配了一个空间
x=null;//将会产生两个垃圾
(4)线程中的垃圾
class A implements Runnable{
void run(){
//....
}
}
//main
A x=new A();//object1
x.start();
x=null;//等线程执行完后object1才被认定为垃圾
复制代码
最基础的算法,分标记和清除两个阶段:首先标记处所须要回收的对象,在标记完成后统一回收全部被标记的对象。
它有两点不足:一个效率问题,标记和清除过程都效率不高;一个是空间问题,标记清除以后会产生大量不连续的内存碎片(相似于咱们电脑的磁盘碎片),空间碎片太多致使须要分配大对象时没法找到足够的连续内存而不得不提早触发另外一次垃圾回收动做。
为了解决效率问题,出现了“复制”算法,他将可用内存按容量划分为大小相等的两块,每次只须要使用其中一块。当一块内存用完了,将还存活的对象复制到另外一块上面,而后再把刚刚用完的内存空间一次清理掉。这样就解决了内存碎片问题,可是代价就是能够用内容就缩小为原来的一半。
复制算法在对象存活率较高时就会进行频繁的复制操做,效率将下降。所以又有了标记-整理算法,标记过程同标记-清除算法,可是在后续步骤不是直接对对象进行清理,而是让全部存活的对象都向一侧移动,而后直接清理掉端边界之外的内存。
当前商业虚拟机的GC都是采用分代收集算法,这种算法并无什么新的思想,而是根据对象存活周期的不一样将堆分为:新生代和老年代,方法区称为永久代(在新的版本中已经将永久代废弃,引入了元空间的概念,永久代使用的是JVM内存而元空间直接使用物理内存)。
这样就能够根据各个年代的特色采用不一样的收集算法。
新生代中的对象“朝生夕死”,每次GC时都会有大量对象死去,少许存活,使用复制算法。新生代又分为Eden区和Survivor区(Survivor from、Survivor to),大小比例默认为8:1:1。
老年代中的对象由于对象存活率高、没有额外空间进行分配担保,就使用标记-清除或标记-整理算法。
新产生的对象优先进去Eden区,当Eden区满了以后再使用Survivor from,当Survivor from 也满了以后就进行Minor GC(新生代GC),将Eden和Survivor from中存活的对象copy进入Survivor to,而后清空Eden和Survivor from,这个时候原来的Survivor from成了新的Survivor to,原来的Survivor to成了新的Survivor from。复制的时候,若是Survivor to 没法容纳所有存活的对象,则根据老年代的分配担保将对象copy进去老年代,若是老年代也没法容纳,则进行Full GC(老年代GC)。
大对象直接进入老年代:JVM中有个参数配置-XX:PretenureSizeThreshold,令大于这个设置值的对象直接进入老年代,目的是为了不在Eden和Survivor区之间发生大量的内存复制。
长期存活的对象进入老年代:JVM给每一个对象定义一个对象年龄计数器,若是对象在Eden出生并通过第一次Minor GC后仍然存活,而且能被Survivor容纳,将被移入Survivor而且年龄设定为1。没熬过一次Minor GC,年龄就加1,当他的年龄到必定程度(默认为15岁,能够经过XX:MaxTenuringThreshold来设定),就会移入老年代。可是JVM并非永远要求年龄必须达到最大年龄才会晋升老年代,若是Survivor 空间中相同年龄(如年龄为x)全部对象大小的总和大于Survivor的一半,年龄大于等于x的全部对象直接进入老年代,无需等到最大年龄要求。
CMS收集器是一种以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。
基于“标记清除”算法,并发收集、低停顿,运做过程复杂,分4步:
1)初始标记:仅仅标记GC Roots能直接关联到的对象,速度快,可是须要“Stop The World”
2)并发标记:就是进行追踪引用链的过程,让垃圾回收器和用户线程同时运行,并发工做。
3)从新标记:修正并发标记阶段因用户线程继续运行而致使标记发生变化的那部分对象的标记记录,比初始标记时间长但远比并发标记时间短,须要“Stop The World”
4)并发清除:清除标记为能够回收对象,能够和用户线程并发执行
因为整个过程耗时最长的并发标记和并发清除均可以和用户线程一块儿工做,因此整体上来看,CMS收集器的内存回收过程和用户线程是并发执行的。
可是CMS收集器有3个缺点:
1)对CPU资源很是敏感
并发收集虽然不会暂停用户线程,但由于占用一部分CPU资源,仍是会致使应用程序变慢,总吞吐量下降。
CMS的默认收集线程数量是=(CPU数量+3)/4;当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更大,可能没法接受。
2)并发清理阶段用户线程还在运行,这段时间就可能产生新的垃圾,新的垃圾在这次GC没法清除,只能等到下次清理。
并发清除时须要预留必定的内存空间,不能像其余收集器在老年代几乎填满再进行收集;若是CMS预留内存空间没法知足程序须要,就会出现一次"Concurrent Mode Failure"失败;这时JVM启用后备预案:临时启用Serail Old收集器,而致使另外一次Full GC的产生;
3)产生大量内存碎片:CMS基于"标记-清除"算法,清除后不进行压缩操做产生大量不连续的内存碎片,这样会致使分配大内存对象时,没法找到足够的连续内存,从而须要提早触发另外一次Full GC动做。
还会形成“对象消失”。
举个例子,咱们先看一下一次正常的标记过程:
再来看一下这个,如图对象7和对象10原本就是原引用链(根节点->5->6->7->8->11->10)的一部分。修改后的引用链变成了(根节点->5->6->7->10)。
因为蓝色对象不会从新扫描,这将致使扫描结束后对象10和对象11都会回收了。他们都是被修改以前的原来的引用链的一部分。
当且仅当如下两个条件同时知足时,会产生"对象消失"的问题,原来应该是蓝色的对象被误标为了白色:
条件一:赋值器插入了一条或者多条从蓝色对象到白色对象的新引用。
条件二:赋值器删除了所有从蓝色对象到该白色对象的直接或间接引用。
咱们结合前面形成“对象消失”的图能够看到:
蓝色对象7到白色对象10之间的引用是新建的,对应条件一。
蓝色对象8到白色对象11之间的引用被删除了,对应条件二。
因为两个条件之间是当且仅当的关系。因此,咱们要解决并发标记时对象消失的问题,只须要破坏两个条件中的任意一个就行。
因而产生了两种解决方案:增量更新和原始快照。
增量更新要破坏的是第一个条件(赋值器插入了一条或者多条从蓝色对象到白色对象的新引用),当蓝色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束以后,再将这些记录过的引用关系中的蓝色对象为根,从新扫描一次。
这样对象9又被扫描成为了蓝色。也就不会被回收,因此不会出现对象消失的状况。
原始快照要破坏的是第二个条件,即赋值器删除了所有从蓝色对象到该白色对象的直接或间接引用,当蓝色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束以后,再将这些记录过的引用关系中的蓝色对象为根,从新扫描一次。
这个能够简化理解为:不管引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照开进行搜索。
哇~你竟然看到了这里!好了各位小可爱,以上就是这篇文章的所有内容了,也是JVM最多见的一些面试题,能看到这里的人呀,都是最胖的!哦不,都是最棒哒~
最近忙里偷闲更了一篇JVM相关的文章,很是感谢小可爱们能看到这里,若是以为这个文章写得还不错, 求点赞👍 求关注❤️ 求分享👥 没错,本少女就是这么的虚荣!嘻嘻~
若是本篇文章有任何错误,请批评指教,不胜感激 !
对啦~最后蛋哥问小姐姐,“是什么让你如此优秀”???
小姐姐微微一笑拿出手机,“由于我一直在关注【小码仔】呀~”