JVM在面试中的高频考点
1.介绍一下Java运行时数据区域,并说一下每个部分都存哪些内容?
回答:Java的运行时区主要包含堆、方法区、虚拟机栈、程序计数器和本地方法栈,其中堆和方法区是所有线程所共有的。而且虚拟机栈、程序计数器和本地方法栈是线程所私有的。
堆:存放对象实例
方法区:用来存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
虚拟机栈:(生命周期与线程相同)Java中每个方法执行的时候,Java虚拟机都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
程序计数器:保存下一条需要执行的字节码指令,是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都是依赖程序计数器。
本地方法栈:与虚拟机栈类似
追问1:程序计数器可以为空吗?
回答:可以为空,当执行的是本地方法时。
追问2:堆中又怎么细分的?
回答:堆中可以细分为新生代和老年代,其中新生代又分为Eden区,From Survivor和To Survivor区,比例是8:1:1。
追问3:哪些区域会造成OOM
回答:除了程序计数器不会产生OOM,其余的均可以产生OOM。
2.Java中对象的创建过程是什么样的?
回答:Java中对象的创建过程为5步
(1)当遇到new关键字的时候,首先坚持这个指令的参数是否可以在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化。
(2)在类加载检查后,接下来需要为新对象分配内存。
(3)需要将分配到的内存空间都初始化为零。
(4)需要对对象进行相关的设置,比如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的GC分代年龄等信息。
(5)执行<init>()方法。
追问1:内存分配的策略有哪些?
回答:Java中的内存分配策略主要有两种,分别是指针碰撞和空闲列表。
指针碰撞:假设Java堆中的内存都是规整的,所有被使用过的放在一边,未使用过的放在一边,中间有一个指针作为分界,分配内存仅仅需要把这个指针向空闲空间方向移动一段即可。
空闲列表:如果Java堆中的内存不是规整的,已使用过的和空闲的交错,虚拟机就需要维护一个列表,记录哪些内存是可用的,在分配的时候找到足够大的一块内存进行分配。
追问2:对象头包含哪些?
回答:虚拟机中对象头包含两类信息,第一类是用于存储对象自身运动时数据、如哈希码、GC分代年龄、线程持有的锁、偏向线程ID、偏向时间戳。对象的另外一部分是类型指针,即对象指向它的类型元数据的指针。
追问3:对象的访问定位方法有几种,各有什么优缺点?
回答:Java虚拟机中对象的访问方式有①使用句柄和②直接指针两种。
句柄: 如果使用句柄的话,那么Java堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;
直接指针: 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference 中存储的直接就是对象的地址。
总结:使用句柄最大的好处就是reference中存储的是稳定句柄地址,在对象移动时只会改变句柄中的实例数据指针,而reference本身不需要被修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
3.如何判断对象已死?
回答:Java中判断对象死亡的方法有引用计数法和可达性分析。
引用计数法:对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。
可达性分析:通过一系列的GC Roots的根对象作为 起始节点,从这些节点开始,根据引用关系向下搜索,如果某个对象到GC Roots间没有任何引用链相连。
追问1:GCroot可以是哪些?
回答:在Java中可以作为GC Roots的比较多,分别有
(1)在虚拟机栈中引用的对象,比如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
(2)在方法区中类静态属性引用的对象,比如Java类的引用类型静态变量。
(3)在方法区中常量引用的对象,比如字符串常量池里的引用。
(4)在本地方法栈中JNI引用的对象。
(5)Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象。
(6)所有被同步锁持有的对象。
追问2:被标志为GC的对象一定会被GC掉吗?
回答:不一定,还有逃脱的可能。真正宣告一个对象死亡至少经历两次标记的过程。
如果对象进行可达性分析后没有与GC Roots相连,那么这是第一次标记,之后会在进行一次筛选,筛选的条件是是否有必要执行finalize()方法。【详细可以看课本《深入理解Java虚拟机》】
4.垃圾回收算法有哪些?详细叙述一下。
回答:垃圾回收算法主要有三种,分别标记清除、标记整理和标记复制。
标记清除:算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它的主要不足空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记复制:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。
标记整理:首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
追问1:新生代和老年代一般使用什么算法?
回答:新生代一般使用标记复制和标记整理算法,老年代一般使用标记清除算法。
追问2:为什么新生代不使用标记清除算法?
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。
5.垃圾回收器有哪些?
回答:垃圾回收器可以在新生代和老年代都有,在新生代有Serial、ParNew、Parallel Scavenge;老年代有CMS、Serial Old、Parallel Old;还有不区分年的G1算法。
追问1:CMS垃圾回收器的过程是什么样的?会带来什么问题?
回答:CMS回收过程可以分为4个步骤。
(1)初试标记:初试标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,但需要暂停所有其他的工作线程。
(2)并发标记: GC 和用户线程一起工作,执行GC Roots跟踪标记过程,不需要暂停工作线程。
(3)重新标记:在并发标记过程中用户线程继续运作,导致在垃圾回收过程中部分对象的状态发生了变化,未来确保这部分对象的状态的正确性,需要对其重新标记并暂停工作线程。
(4)并发清除:清理删除掉标记阶段判断的已经死亡的对象,这个过程用户线程和垃圾回收线程同时发生。
带来的问题:
(1)CMS收集器对处理器资源非常敏感。
(2)CMS无法处理“浮动垃圾”。
(3)CMS是基于标记-清除算法,会产生大量的空间碎片。
追问2:G1垃圾回收器的改进是什么?相比于CMS突出的地方是什么?
回答:G1垃圾回收器抛弃了分代的概念,将堆内存划分为大小固定的几个独立区域,并维护一个优先级列表,在垃圾回收过程中根据系统允许的最长垃圾回收时间,优先回收垃圾最多的区域。(G1算法是可控STW的一种算法,GC收集器和我们GC调优的目标就是尽可能的减少STW的时间和次数。)
G1突出的地方:
基于标记整理算法,不产生垃圾碎片。
可以精确的控制停顿时间,在不牺牲吞吐量的前提下实现短停顿垃圾回收。
追问3:现在jdk默认使用的是哪种垃圾回收器?
回答:(被问到过好几次)
jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9 默认垃圾收集器G1
6.内存分配策略是什么样的?
对象优先在Eden分配,如果说Eden内存空间不足,就会发生Minor GC/Young GC
大对象直接进入老年代,大对象:需要大量连续内存空间的Java对象,比如很长的字符串和大型数组,1、导致内存有空间,还是需要提前进行垃圾回收获取连续空间来放他们,2、会进行大量的内存复制。
-XX:PretenureSizeThreshold 参数 ,大于这个数量直接在老年代分配,缺省为0 ,表示绝不会直接分配在老年代。
长期存活的对象将进入老年代,默认15岁,-XX:MaxTenuringThreshold调整
动态对象年龄判定,为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
空间分配担保:新生代中有大量的对象存活,survivor空间不够,当出现大量对象在MinorGC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代.只要老年代的连续空间大于新生代对象的总大小或者历次晋升的平均大小,就进行Minor GC,否则FullGC。
追问1:内存溢出与内存泄漏的区别?
内存溢出:实实在在的内存空间不足导致;
内存泄漏:该释放的对象没有释放,多见于自己使用容器保存元素的情况下。
7.jvm调优了解过吗?常用的命令和工具有哪些?
回答:Linux中有top、vmstat、pidstat,jdk中的jstat、jstack、jps、jmap等。(建议详细去看看这些命令的区别和作用,都可能会被问到)
追问1:内存持续上升,如何排查?
回答: CPU100%那么一定有线程在占用系统资源, 找出哪个进程cpu高(top),该进程中的哪个线程cpu高(top -Hp) , 导出该线程的堆栈 (jstack) , 查找哪个方法(栈帧)消耗时间 (jstack) 工作线程占比高 | 垃圾回收线程占比高 。【详细可以到网络搜索,最好是自己清楚这个排查思路!】
(1)通过top找到占用率高的进程
(2)通过top -Hp pid找到占用CPU高的线程ID
(3)把线程ID转化为16进制,得到线程IDxx
(4)通过命令jstack 找到有问题的代码
追问2:jstack和jsp的区别是什么?
jstack:(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照。
线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。
在代码中可以用java.lang.Thread类的getAllStackTraces()方法用于获取虚拟机中所有线程的StackTraceElement对象。使用这个方法可以通过简单的几行代码就完成jstack的大部分功能,在实际项目中不妨调用这个方法做个管理员页面,可以随时使用浏览器来查看线程堆栈。
jps : 列出当前机器上正在运行的虚拟机进程
-p :仅仅显示VM 标示,不显示jar,class, main参数等信息.
-m:输出主函数传入的参数. 下的hello 就是在执行程序时从命令行输入的参数
-l: 输出应用程序主类完整package名称或jar完整名称.
-v: 列出jvm参数, -Xms20m -Xmx50m是启动程序指定的jvm参数
8.虚拟机的加载机制是什么样的?
回答:JVM的类加载分为7个阶段:分别是加载、验证、准备、解析、初始化、使用和卸载。
加载:读取Class文件,并根据Class文件描述创建对象的过程。
验证:确保Class文件符合当前虚拟机的要求。
准备:在方法区中为类变量分配内存空间并设置类中变量的初始值。
解析:JVM会将常连池中的符合引用替换为直接引用。
初始化:执行类构造器<client>方法为类进行初始化。
追问1:类加载有哪些?
回答:JVM提供了三种类加载器,分别启动类加载器(Bootstrap Classloader)、扩展类加载器(Extention Classloader)和应用类加载器(Application Classloader)
追问2:什么叫双亲委派机制?
回答:双亲委派机制是指一个类在收到类加载请求后不会尝试自己加载这个类,而且把这该类加载请求委派给其父类去完成,父类在接收到该加载请求后又会将其委派给自己的父类,以此类推,这样所有的类加载请求都被向上委派到启动类加载器中。若父类加载器在接收到类加载请求后发现自己也无法加载该类,则父类会将该请求反馈给子类向下委派子类加载器加载该类,直到被加载成功,若找不到会曝出异常。
追问3:如何打破双亲委派机制?
回答:重写一个类继承ClassLoader,并重写loadClass方法。(Tomcat是不支持双亲委派机制的)