最新美团滴滴 Java 岗虚拟机面经:2020 下半年你还想不想涨薪?
####问题 1.1jvm 内存模型
栈区:
栈分为 java 虚拟机栈和本地方法栈
重点是 Java 虚拟机栈,它是线程私有的,生命周期与线程相同。
每个方法执行都会创建一个栈帧,用于存放局部变量表,操作栈,动态链接,方法出口等。每个方法从被调用,直到被执行完。对应着一个栈帧在虚拟机中从入栈到出栈的过程。
通常说的栈就是指局部变量表部分,存放编译期间可知的 8 种基本数据类型,及对象引用和指令地址。局部变量表是在编译期间完成分配,当进入一个方法时,这个栈中的局部变量分配内存大小是确定的。
会有两种异常 StackOverFlowError 和 OutOfMemoneyError。当线程请求栈深度大于虚拟机所允许的深度就会抛出 StackOverFlowError 错误;虚拟机栈动态扩展,当扩展无法申请到足够的内存空间时候,抛出 OutOfMemoneyError。
本地方法栈为虚拟机使用到本地方法服务(native)
堆区:
堆被所有线程共享区域,在虚拟机启动时创建,唯一目的存放对象实例。
方法区:
被所有线程共享区域,用于存放已被虚拟机加载的类信息,常量,静态变量等数据。被 Java 虚拟机描述为堆的一个逻辑部分。习惯是也叫它永久代(permanment generation)
垃圾回收很少光顾这个区域,不过也是需要回收的,主要针对常量池回收,类型卸载。
常量池用于存放编译期生成的各种字节码和符号引用,常量池具有一定的动态性,里面可以存放编译期生成的常量;运行期间的常量也可以添加进入常量池中,比如 string 的 intern()方法。
程序计数器:
当前线程所执行的行号指示器。通过改变计数器的值来确定下一条指令,比如循环,分支,跳转,异常处理,线程恢复等都是依赖计数器来完成。
Java 虚拟机多线程是通过线程轮流切换并分配处理器执行时间的方式实现的。为了线程切换能恢复到正确的位置,每条线程都需要一个独立的程序计数器,所以它是线程私有的。
唯一一块 Java 虚拟机没有规定任何 OutofMemoryError 的区块。
####1.2 jvm 堆空间是怎么划分的
通常情况下分为两个区块年轻代和年老代。更细一点年轻代又分为 Eden 区最要放新创建对象,From survivor 和 To survivor 保存 gc 后幸存下的对象,默认情况下各自占比 8:1:1。
不过很多文章介绍分为 3 个区块,把方法区算着为永久代。这大概是基于 Hotspot 虚拟机划分,然后比如 IBM j9 就不存在永久代概论。不管怎么分区,都是存放对象实例。
1.3 jvm 内存有哪些初始化参数
1.JVM 运行时堆的大小
-Xms 堆的最小值-Xmx 堆空间的最大值
2.新生代堆空间大小调整
-XX:NewSize 新生代的最小值-XX:MaxNewSize 新生代的最大值-XX:NewRatio 设置新生代与老年代在堆空间的大小-XX:SurvivorRatio 新生代中 Eden 所占区域的大小
3.永久代大小调整
-XX:MaxPermSize
问题 2 jvm 垃圾回收机制有了解吗?
在 java 中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在 JVM 中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
问题 2.1 java 中垃圾收集的方法有哪些?
标记-清除: 这是垃圾收集算法中最基础的,根据名字就可以知道,它的思想就是标记哪些要被回收的对象,然后统一回收。这种方法很简单,但是会有两个主要问题:1.效率不高,标记和清除的效率都很低;2.会产生大量不连续的内存碎片,导致以后程序在分配较大的对象时,由于没有充足的连续内存而提前触发一次 GC 动作。
复制算法: 为了解决效率问题,复制算法将可用内存按容量划分为相等的两部分,然后每次只使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清楚完第一块内存,再将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一般的内存。 于是将该算法进行了改进,内存区域不再是按照 1:1 去划分,而是将内存划分为 8:1:1 三部分,较大那份内存交 Eden 区,其余是两块较小的内存区叫 Survior 区。每次都会优先使用 Eden 区,若 Eden 区满,就将对象复制到第二块内存区上,然后清除 Eden 区,如果此时存活的对象太多,以至于 Survivor 不够时,会将这些对象通过分配担保机制复制到老年代中。(java 堆又分为新生代和老年代)
标记-整理 该算法主要是为了解决标记-清除,产生大量内存碎片的问题;当对象存活率较高时,也解决了复制算法的效率问题。它的不同之处就是在清除对象的时候现将可回收对象移动到一端,然后清除掉端边界以外的对象,这样就不会产生内存碎片了。
分代收集 现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代和老年代。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理 或者 标记-清除。
问题 2.2 jvm 垃圾收集器有哪几种?
Serial 收集器: 单线程的收集器,收集垃圾时,必须 stop the world,使用复制算法。
ParNew 收集器: Serial 收集器的多线程版本,也需要 stop the world,复制算法。
Parallel Scavenge 收集器: 新生代收集器,复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量。如果虚拟机总共运行 100 分钟,其中垃圾花掉 1 分钟,吞吐量就是 99%。
Serial Old 收集器: 是 Serial 收集器的老年代版本,单线程收集器,使用标记整理算法。
Parallel Old 收集器: 是 Parallel Scavenge 收集器的老年代版本,使用多线程,标记-整理算法。
CMS(Concurrent Mark Sweep) 收集器: 是一种以获得最短回收停顿时间为目标的收集器,标记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除,收集结束会产生大量空间碎片。
G1 收集器: 标记整理算法实现,运作流程主要包括以下:初始标记,并发标记,最终标记,筛选标记。不会产生空间碎片,可以精确地控制停顿。
问题 2.3 CMS 收集器和 G1 收集器的区别:
CMS 收集器是老年代的收集器,可以配合新生代的 Serial 和 ParNew 收集器一起使用;
G1 收集器收集范围是老年代和新生代,不需要结合其他收集器使用;
CMS 收集器以最小的停顿时间为目标的收集器;
G1 收集器可预测垃圾回收的停顿时间
CMS 收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
G1 收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。
问题 2.4 JVM 中一次完整的 GC 流程是怎样的,对象如何晋升到老年代
Java 堆 = 老年代 + 新生代 新生代 = Eden + S0 + S1 1、当 Eden 区的空间满了, Java 虚拟机会触发一次 Minor GC,以收集新生代的垃圾,存活下来的对象,则会转移到 Survivor 区。 2、大对象(需要大量连续内存空间的 Java 对象,如那种很长的字符串)直接进入老年态; 3、如果对象在 Eden 出生,并经过第一次 Minor GC 后仍然存活,并且被 Survivor 容纳的话,年龄设为 1,每熬过一次 Minor GC,年龄+1,若年龄超过一定限制(15),则被晋升到老年态。即长期存活的对象进入老年态。 4、老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行 Full GC,Full GC 清理整个内存堆 – 包括年轻代和年老代。 5、Major GC 发生在老年代的 GC,清理老年区,经常会伴随至少一次 Minor GC,比 Minor GC 慢 10 倍以上。
问题 2.5 什么情况下会触发 fullgc
老年代空间不足 老年代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行 Full GC 后空间仍然不足,则抛出如下错误: java.lang.OutOfMemoryError: Java heap space 为避免以上两种状况引起的 Full GC,调优时应尽量做到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
永生区空间不足 JVM 规范中运行时数据区域中的方法区,在 HotSpot 虚拟机中又被习惯称为永生代或者永生区,Permanet Generation 中存放的为一些 class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation 可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么 JVM 会抛出如下错误信息: java.lang.OutOfMemoryError: PermGen space 为避免 Perm Gen 占满造成 Full GC 现象,可采用的方法为增大 Perm Gen 空间或转为使用 CMS GC。
CMS GC 时出现 promotion failed 和 concurrent mode failure 对于采用 CMS 进行老年代 GC 的程序而言,尤其要注意 GC 日志中是否有 promotion failed 和 concurrent mode failure 两种状况,当这两种状况出现时可能 会触发 Full GC。 promotion failed 是在进行 Minor GC 时,survivor space 放不下、对象只能放入老年代,而此时老年代也放不下造成的;concurrent mode failure 是在 执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间不足”是 CMS GC 时当前的浮动垃圾过多导致暂时性的空间不足触发 Full GC)。 对措施为:增大 survivor space、老年代空间或调低触发并发 GC 的比率,但在 JDK 5.0+、6.0+的版本中有可能会由于 JDK 的 bug29 导致 CMS 在 remark 完毕 后很久才触发 sweeping 动作。对于这种状况,可通过设置-XX: CMSMaxAbortablePrecleanTime=5(单位为 ms)来避免。 统计得到的 Minor GC 晋升到旧生代的平均大小大于老年代的剩余空间 这是一个较为复杂的触发情况,Hotspot 为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行 Minor GC 时,做了一个判断,如果之 前统计所得到的 Minor GC 晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发 Full GC。 例如程序第一次触发 Minor GC 后,有 6MB 的对象晋升到旧生代,那么当下一次 Minor GC 发生时,首先检查旧生代的剩余空间是否大于 6MB,如果小于 6MB, 则执行 Full GC。 当新生代采用 PS GC 时,方式稍有不同,PS GC 是在 Minor GC 后也会检查,例如上面的例子中第一次 Minor GC 后,PS GC 会检查此时旧生代的剩余空间是否 大于 6MB,如小于,则触发对旧生代的回收。 除了以上 4 种状况外,对于使用 RMI 来进行 RPC 或管理的 Sun JDK 应用而言,默认情况下会一小时执行一次 Full GC。可通过在启动时通过- java - Dsun.rmi.dgc.client.gcInterval=3600000 来设置 Full GC 执行的间隔时间或通过-XX:+ DisableExplicitGC 来禁止 RMI 调用 System.gc。
堆中分配很大的对象 所谓大对象,是指需要大量连续内存空间的 java 对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发 JVM 进行 Full GC。 为了解决这个问题,CMS 垃圾收集器提供了一个可配置的参数,即-XX:+UseCMSCompactAtFullCollection 开关参数,用于在“享受”完 Full GC 服务之后额外免费赠送一个碎片整理的过程,内存整理的过程无法并发的,空间碎片问题没有了,但提顿时间不得不变长了,JVM 设计者们还提供了另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数用于设置在执行多少次不压缩的 Full GC 后,跟着来一次带压缩的。
问题 2.6 怎么判断一个对象是否存活
jvm 中有两种方式判断对象是否存活
引用计数:每个对象有一个引用计数属性,新增一个引用时计数加 1,引用释放时计数减 1,计数为 0 时可以回收。此方法简单,无法解决对象相互循环引用的问题。
可达性分析(Reachability Analysis):从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的,不可达对象。
问题 2.7 哪些对象可作为 GC Roots 对象?
虚拟机栈中应用的对象
方法区里面的静态对象
方法区常量池的对象
本地方法栈 JNI 应用的对象
问题 3 jvm 类加载原理
问题 3.1 说一下类的生命周期
java 类加载过程:加载-->验证-->准备-->解析-->初始化,之后类就可以被使用了。绝大部分情况下是按这
样的顺序来完成类的加载全过程的。但是是有例外的地方,解析也是可以在初始化之后进行的,这是为了支持
java 的运行时绑定,并且在一个阶段进行过程中也可能会激活后一个阶段,而不是等待一个阶段结束再进行后一个阶段。
1.加载
加载时 jvm 做了这三件事:
1)通过一个类的全限定名来获取该类的二进制字节流
2)将这个字节流的静态存储结构转化为方法区运行时数据结构
3)在内存堆中生成一个代表该类的 java.lang.Class 对象,作为该类数据的访问入口
2.验证
验证、准备、解析这三步可以看做是一个连接的过程,将类的字节码连接到 JVM 的运行状态之中
验证是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,不会威胁到 jvm 的安全
验证主要包括以下几个方面的验证:
1)文件格式的验证,验证字节流是否符合 Class 文件的规范,是否能被当前版本的虚拟机处理
2)元数据验证,对字节码描述的信息进行语义分析,确保符合 java 语言规范
3)字节码验证 通过数据流和控制流分析,确定语义是合法的,符合逻辑的
4)符号引用验证 这个校验在解析阶段发生
3.准备 为类的静态变量分配内存,初始化为系统的初始值。对于 final static 修饰的变量,
直接赋值为用户的定义值。如下面的例子:这里在准备阶段过后的初始值为 0,而不是 7
public static int a=7 4.解析
解析是将常量池内的符号引用转为直接引用(如物理内存地址指针)
5.初始化
到了初始化阶段,jvm 才真正开始执行类中定义的 java 代码
1)初始化阶段是执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译器自动收集
类中的所有类变量的赋值动作和静态语句块(static 块)中的语句合并产生的。
评论