JVM虚拟机

  • 1. JVM的主要组成部分?
  • 2. JVM的运转流程及每个部分的作用?
  • 3. 请完整描述JVM类加载过程?
  • 4. 类加载顺序(父类、子类等的)过程?
  • 5. JVM虚拟机分派与重载(Overload)与重写(Override)?
  • 6. 什么是类加载器?以及类加载器的种类?
  • 7. 什么是双亲委派机制?
  • 8. JVM运行时数据区的内存模型?
  • 9. 运行时常量池与包装类?
  • 10. 运行时常量池与字符串类(String)?
  • 11. JVM虚拟机栈(执行方法)的内存模型?
  • 12. JVM虚拟机方法调用的底层实现原理?
  • 13. JVM虚拟机分代垃圾回收思想下的堆的内存模型?
  • 14. JVM为什么需要Survivor空间?只有Eden空间可以吗?
  • 15. JVM为什么需要两个Survivor空间?
  • 16. 请完整描述出:JVM在分代垃圾回收思想下的内存分配流程?
  • 17. JVM如何判断对象是否存活?
  • 18. JVM的垃圾回收算法用哪些?各自算法的实现原理?
  • 19. JAVA中的强引用、软引用、弱引用、虚引用?各种类型引用的回收策略?
  • 20. JVM垃圾回收器有哪些?详细谈谈各个收集器的实现原理和应用场景?
  • 21. CMS垃圾回收器垃圾收集过程及缺点?
  • 22. G1垃圾回收器垃圾收集过程及优缺点?
  • 23. JVM有哪些核心参数,如何作调整?
  • 21. JVM常用的调优工具有哪些?
  • 24. 内存泄露与内存溢出?
  • 25. 线上发生JVM内存泄露,谈谈你的性能调优思路和步骤?


1. JVM的主要组成部分?

  1. 类加载器(ClassLoader)
  2. 运行时数据区(Runtime Data Area)
  3. 执行引擎(Execution Engine)
  4. 本地库连接(Native Interface)

2. JVM的运转流程及每个部分的作用?

首先通过类加载器(ClassLoader)将Java代码装换成字节码文件,运行时数据区(Runtime Data Area)将字节码加载到内存中,而字节码文件只是JVM的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由CPU去执行,而在这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

3. 请完整描述JVM类加载过程?

  1. 加载:类加载的一个过程,在加载阶段JVM虚拟机需要完成以下3件事:
    1.1 通过一个类的全限定类名获取此类的二进制字节流;
    1.2 将这个字节流代表的静态存储结构转化为方法区的运行时数据结构;
    1.3 在内存中生成一个代表此类的java.lang.Class对象,作为方法区这个类各种数据的访问对象。
  2. 验证:是”链接阶段“的第一步,这一阶段的目的是为了确保class文件的字节流包含的信息符合当前虚拟机的需求,并且不会危害到虚拟机自身安全,验证阶段大致分为下面四个阶段的验证动作:
    2.1 文件格式验证:字节流是否符合class文件格式的规范;
    2.2 元数据验证:对字节码描述的信息进行语义分析;
    2.3 字节码验证:数据流和流程控制语义是否合法;
    2.4 符号引用验证:对自身以外的信息进行匹配性校验。
  3. 准备:是正式为变量分配内存并设置类变量初始值的阶段;
  4. 解析:是虚拟机将常量池内的符号引用替换为直接引用的过程:
    4.1 类或接口的解析;
    4.2 字段的解析;
    4.3 类方法的解析。
  5. 初始化:对实例变量分配内存并对变量赋初始值;
  6. 使用;
  7. 卸载。

4. 类加载顺序(父类、子类等的)过程?

虚拟化VCP面试 虚拟化面试题及答案_垃圾回收器

  1. 静态代码块和构造函数执行先后顺序:
    1.1 父类静态代码块;
    1.2 子类静态代码块;
    1.3 初始化父类成员变量;
    1.4 初始化父类语句块;
    1.5 父类构造函数;
    1.6 初始化子类成员变量;
    1.7 初始化子类语语句块;
    1.8 子类构造方法。
  2. 静态代码块只在加载时执行一次;
  3. 子类构造函数调用时,一定会先调用父类构造函数,显示调用则会调用父类指定的构造函数,隐式调用则会调用父类无参构造函数。
    所以该题的答案是:ABA。

5. JVM虚拟机分派与重载(Overload)与重写(Override)?


6. 什么是类加载器?以及类加载器的种类?

虚拟机将类记载阶段中”通过一个类的全限定类名来获取描述此类的二进制字节流“这个动作放到了Java虚拟机外部实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块成为”类加载器“;从Java虚拟机的角度来看,只有两种不通的类加载器,一种是启动类加载器,这个类加载器有C++语言实现,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器有Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.classLoader:

  1. 启动类加载器:负责将存放在JAVA_HOME\lib目录中的并且是虚拟机能识别的(rt.jar)类库加载到内存中;
  2. 扩展类加载器:负责将存放在JAVA_HOME\ext目录中的所有类库加载到内存中,开发者可以直接使用扩展类加载器;
  3. 应用程序类加载器:负责加载用户类路径上所指定的类库加载到内存中,开发者可以直接使用这个类加载器。

7. 什么是双亲委派机制?

类加载器的双亲委派机制是在JDK1.2中被引入的,它的工作机制是:如果一个类加载器收到类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有父类加载器反馈无法完成加载请求时(它的搜索范围内没有找到所需要的类),子类加载器才会尝试自己去加载。
使用双亲委派机制组织类加载之间的关系,有一个好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,相反,如果没有使用双亲委派机制,有各个类加载器自行去加载的话,那系统可能出现多个相同全限定类名的类,Java类型体系中基础的行为也无法保证,应用程序也将变得一片混乱。(破坏双亲委派的例子:Tomcat;自定义类加载器,继承ClassLoader类,重写loadClass方法和findClass方法)

8. JVM运行时数据区的内存模型?

  1. 程序计数器:线程私有;字节码行号指示器,通过改变程序计数器的值选取下一条执行指令,保证切换后回复到正确的执行位置;Java方法记录正在执行字节码指令地址;Native方法为Undefined,此区域会不存在内存溢出异常。
  2. 本地方法栈:线程私有;为虚拟机使用Native方法提供支持,HotSpot直接将本地方法栈和虚拟机栈合二为一了,此区域会抛出内存溢出异常和内存泄露异常。
  3. 非堆(方法区、永久代):线程共享;存储类信息、静态变量、常量、即时编译器编译后的代码数据、运行时常量池(存放字面值常量、符号引用),此区域会抛出内存溢出异常。
  4. 堆:线程共享;JVM所管理的最大的一块内存,存放实例对象和数组,物理上可以是不连续的内存空间,只要逻辑上连续即可,此区域会抛出内存溢出异常。
  5. 虚拟机栈:线程私有

9. 运行时常量池与包装类?

一道关于Integer的笔试题

虚拟化VCP面试 虚拟化面试题及答案_虚拟化VCP面试_02

10. 运行时常量池与字符串类(String)?


11. JVM虚拟机栈(执行方法)的内存模型?

  1. 局部变量表:编译器确定,局部变量存储空间,存放方法参数和方法内部定义的局部变量,容量单位为Slot(32位物理内存);通过索引定位使用局部变量表(从0开始),完成参数值到参数列表的传递过程,0位索引代表方法所属对应实例引用,用this可以访问。
  2. 操作数栈:编译器确定,“后入先出”的“栈”,可以是任意Java数据类型,32为虚拟机容量是1,64为虚拟机容量是2;执行方法时字节码是在操作数栈中完成内容写入和内容提取的,也就是出栈和入栈;局部变量表共享区域和操作数栈共享区域数据共享。
  3. 动态连接:栈帧中包含指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接;运行时常量池中的符号引用转化为直接引用被称为动态连接。
  4. 方法返回地址:返回方法调用的位置;在栈帧中保存一些信息(PC计数器/异常处理表),用来帮助恢复它的上层方法的执行状态;实际过程为:当前方法出栈,恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈,调用PC计数器中的值以指向方法调用指令后面的一条指令。
  5. 附加信息:

12. JVM虚拟机方法调用的底层实现原理?

Java中每一次方法的调用,都是虚拟机栈中的栈帧从入栈到出栈的过程,而栈帧是方法调用和方法执行的基本结构,在活动线程中,只有栈顶的栈帧才有效,该方法为当前方法。

13. JVM虚拟机分代垃圾回收思想下的堆的内存模型?

为了提高内存分配效率和垃圾回收效率,JVM虚拟机采用分代思想对堆进行垃圾回收。所以根据对象的存活周期不同,JVM虚拟机将堆内存分为:

  1. 新生代:新创建对象在新生代分配内存,大部分对象“朝生夕死”,存活时间较短,垃圾回收效率高,新生代采用“复制算法”对垃圾进行回收,所以新生代又分为Eden空间和Survivor空间,默认比例为8:1:1(可通过SurvivorRatio参数进行调节)。
  2. 老年代:新生代多次垃圾回收后存活下来的对象,生命周期较长,垃圾回收频率较低,且回收速度比较慢,老年代采用“标记-整理”算法进行垃圾回收。
  3. 永久代:存放类信息、静态变量、常量、字面值常量、即时编译器编译后的代码和符号引用。一般不进行垃圾回收。

14. JVM为什么需要Survivor空间?只有Eden空间可以吗?

如果没有Survivor空间空间的话,Eden空间进行一次GC后,就会将所有存活的对象全部晋升到老年代,即便它在接下来的几次GC过程中极有可能被新生代垃圾回收器收集掉。这样老年代很快被填满,Full GC的频率大大增加。而老年代的空间要比新生代大很多,对它进行垃圾收集会消耗更长的时间;如果老年代垃圾收集的频率很高的话,就会严重影响性能,基于这种考虑,虚拟机引进了Survivor空间。设置Survivor空间的目的是让那些中等寿命的对象尽量在Minor GC(新生代垃圾收集过程)时被干掉,最终在整体上减少虚拟机垃圾收集过程对用户的影响。

15. JVM为什么需要两个Survivor空间?

新生代一般采用”复制“算法进行垃圾收集,原始的复制算法是把内存一份为二,垃圾收集时把存活的对象从一块空间(From space)复制到另一块空间(To space),再把原先的那块内存(From space)清理干净,最后调换From space和To space的逻辑角色。
根据“复制”算法的特性,如果设置一个Survivor空间的话,所有“存活”对象会频繁的在这个Survivor空间分配空间,这个Survivor空间很快就会被填满,使新生代不得不进行一次Minor GC(新生代垃圾收集过程),导致新生代垃圾收集频率升高,导致严重影响性能;而2个Survivor空间,只会在Eden空间快满的时候才会触发Minor GC,而Eden空间占新生代空间的绝大部分,所以Minor GC(新生代垃圾收集过程)的频率得以降低。当然,使用两个Survivor空间的方式也付出了一定的代价,如10%的空间浪费、复制对象的开销等等。

16. 请完整描述出:JVM在分代垃圾回收思想下的内存分配流程?

  1. JDK1.6之后,编译器通过逃逸分析确定对象是在栈上分配内存还是在堆上分配内存,如果在堆上分配内存在执行第2步;
  2. 在Eden区加锁,如果eden_top + size(对象大小) <= eden_end,则将对象存放在Eden区,并增加eden_top,如果Eden区域不足以存放该对象,则执行一次Minor GC;
  3. 经过Minor GC后,如果Eden区仍不足以存放该对象,则直接分配到老年代;
  4. 如果老年代不足以存放该对象,则执行Full GC;
  5. 如果执行完Full GC仍不足以存放该对象,则抛出内存泄露异常。

17. JVM如何判断对象是否存活?

  1. 引用计数算法:给对象添加一个引用计数器,每当一个地方引用它计数器加一;引用失效计数器减一,计数器为0说明不在引用,这种算法时间简单,判定效率高,但是无法解决对象循2环引用问题。
  2. 可达性分析法:当一个对象到GC Roots没有引用链相连时,证明此对象不可用;可以作为GC Roots有:虚拟机栈(栈帧中的本地变量表)、方法区中的静态属性引用对象、方法区中的常量引用对象和本地方法栈中的引用对象。

18. JVM的垃圾回收算法用哪些?各自算法的实现原理?

  1. 标记-清除:算法分为标记阶段和清除阶段;首先标记出所有需要回收的对象,在标记完成后,统一回收所有被标记的对象,该算法有两个缺点:一个是效率问题,标记和清除两个过程效率都不高,另一个是空间问题,标记清除之后,会产生大量的不连续的内存碎片,内存碎片过多,可能导致在以后的程序运行过程中需要分配大对象时,无法找到足够的连续内存,而不得不提前触发一次垃圾收集动作。
  2. 复制算法:为了解决效率问题,一种成为“复制”的收集算法出现了,它将可用的内存按容量划分为大小相等的两块,每次只使用一块,当被使用的内存被用完了,就将仍存货的对象复制到另外一块未使用的内存上面,然后再把已使用的内存空间一次性清理掉,这样使得每次都是对整个半区进行垃圾收集,内存分配时不需要考虑内存碎片问题,只要移动堆顶的指针,按顺序分配内存即可,但是这种算法的代价是将内存缩小为原来的一半,代价太高了一点。该算法一般适用于“新生代”内存的垃圾收集。
  3. 标记-整理: 标记过程仍和“标记-清除”算法一样,但是“清除”过程不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接对边界以外的内存进行垃圾收集,该种算法一般适用于“老年代”的垃圾收集。

19. JAVA中的强引用、软引用、弱引用、虚引用?各种类型引用的回收策略?

  1. 强引用:通过new关键字创建的引用,只要该对象存活,指向该存活对象的引用就不会被垃圾回收器收集。
  2. 软引用:new SoftReference< Object>(new Object()),主要功能类似与缓存,在内存充足的情况下直接通过软引用取值,无需从繁忙的真是来源查询数据,提升速度;当内存不足时,垃圾回收器会优先回收软引用所指向对象的内存空间。
  3. 弱引用:new WeakReference< Object>(new Object()),主要用于监控对象是否已经被垃圾回收器标记为“即将回收”的垃圾,软引用指向的对象会在下次垃圾回收时被垃圾回收器进行垃圾收集。(ThreadLocal)
  4. 虚引用:new PhantomReference< Object>(new Object()),主要用于检测对象是否已经被垃圾收集;只要进行垃圾收集时,虚引用指向的对象的内存空间就会被收集掉。

20. JVM垃圾回收器有哪些?详细谈谈各个收集器的实现原理和应用场景?

  1. 新生代垃圾回收器:
    1.1 Serial垃圾回收器:采用“复制”算法,曾经是“新生代”垃圾收集的唯一选择,这个垃圾回收器是一个单线程收集器,单线程的意义不仅说明它只会使用一个CPU或一个线程去完成垃圾收集,更重要的是它进行垃圾收集时,必须暂停其他所有的工作线程,直到它垃圾收集结束。
    1.2 ParNew垃圾回收器:采用“复制”算法,其实就是Serial垃圾回收器的多线程版本,它是许多运行在Server模式下的虚拟机中首选的新生代垃圾回收器,其中一个与性能无关的重要原因是,除了Serial垃圾回收器之外,目前只有它能与CMS垃圾回收器配合工作。
    1.3 Parallel Scavenge垃圾回收器:采用“复制”算法,并行的多线程垃圾回收器,该收集器的特点是:可以形成一个可控的吞吐量(CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码的时间/(运行用户代码的时间 + 垃圾收集时间)),吞吐量越高,停顿时间越短,越适合交互性强的程序,良好的响应速度能提高用户体验,同时高吞吐量也可以高效的利用CPU时间,尽快完成程序的运行任务,所以Parallel Scavenge垃圾回收器经常被称为“吞吐量优先”的垃圾回收器。
  2. 老年代垃圾回收器:
    2.2 Serial Old垃圾回收器:采用“标记-整理”算法,Serial垃圾回收器的老年代版本,主要意义在于给Clint模式下的虚拟机使用,进行老年代的垃圾收集,它主要有两大用途:一个是在JDK1.5之前与Parallel Scavenge垃圾回收器搭配使用,另一种用途是CMS垃圾回收器并发收集失败时,提供后备预案。
    2.3 Parallel Old垃圾回收器:采用“标记-整理”算法,Parallel Scavenge垃圾回收器的老年代版本。
    2.4 CMS垃圾回收器:采用“标记-清除”算法,是一种以活动最短回收停顿时间为目标的收集器,目前很大一部分Java应用集中在互联网上或者B/S架构的服务器端,这类应用尤其重视服务器的想用速度,希望系统停顿时间最短,以给用户较好的体验。
  3. G1垃圾回收器:在G1垃圾回收器之前的其他垃圾回收器的垃圾收集范围是整个新生代或老年代,而G1垃圾回收器不再是这样,使用G1垃圾回收器时,Java堆的内存布局和其他垃圾回收器有很大差别,他家整个堆分为多个大小相等的独立区域,虽然还保存着新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是Region的集合,所以G1垃圾回收器对Region采用的是“复制”算法,而对于整个堆内存采用的是“标记-整理”算法。

21. CMS垃圾回收器垃圾收集过程及缺点?

CMS垃圾回收器垃圾收集过程:初始标记——>并发标记——>重新标记——>并发清除,CMS垃圾回收器也被称为并发低停顿垃圾回收器,但是CMS垃圾回收器对CPU资源非常敏感,不能处理浮动垃圾且由于CMS垃圾回收器使用“标记-清除”算法,所以会产生大量的内存空间碎片。

22. G1垃圾回收器垃圾收集过程及优缺点?

  1. G1垃圾回收器垃圾收集过程:初始标记——>并发标记——>最终标记——>筛选回收。
  2. G1垃圾回收器的主要优点:
    2.1 并发与并行:G1垃圾回收器能充分利用多CPU,在多核环境的硬件优势下,使用多CPU来缩短停顿时间,同时G1垃圾回收器可以通过并发的方式让Java程序继续运行。
    2.2 分代收集:虽然G1垃圾回收器可以不需要其他收集器配合就能独立管理整个堆,但它能够采用不同的方式去处理新创建和已存活的旧对象,以获得更好的收集效果。
    2.3 空间整合:G1垃圾回收器从整体上是基于“标记-整理”算法实现的垃圾回收器,从局部上来看是基于“复制”算法实现的垃圾回收器,这两种算法的使用意味着G1垃圾回收器运行期间不会产生内存空间碎片。
    2.4 可预测停顿:降低停顿是G1垃圾回收器和CMS垃圾回收器共同关注的点,G1垃圾回收器降低停顿外,还能建立可预测的停顿时间模型,这是因为它可以有计划的避免在这个Java堆进行全局的垃圾收集,G1垃圾回收器跟踪各个Region里面的垃圾堆积的大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,这种使用Region划分内存空间以及优先级的区域回收方式,保证G1垃圾回收器在有限的时间内可以获得尽可能高的收集效率。

23. JVM有哪些核心参数,如何作调整?

  1. 堆设置:
    1.1 -Xms: 初始堆大小;
    1.2 -Xmx: 最大堆大小;
    1.3 -XX:NewSize = n:新生代和老年代占比;
    1.4 -XX:SurvivorRatio = n:Eden空间和Survivor空间占比;
    1.5 -XX:MaxPermSize = n:设置永久代大小;
    1.6 -XX:PretenureSizeThreshold:大对象直接进入老年代(Serial和ParNew)。
  2. 垃圾回收器设置:
    2.1 -XX:+UseSerialGC:设置串行垃圾回收器;
    2.2 -XX:+UseParNewGC:设置多线程垃圾回收器;
    2.3 -XX:+UseParallelGC:设置并行垃圾回收器;
    2.4 -XX:+UseParallelOldGC:设置老年代并行垃圾回收器;
    2.5 -XX:+UseConMarkSweepGC:设置并发垃圾回收器。

21. JVM常用的调优工具有哪些?

  1. JDK自带命令行工具:
    1.1 jps:查看系统内所用HotSpot进程;
    1.2 jinfo:显示虚拟机配置信息;
    1.3 jstat:收集虚拟机运行数据;
    1.4 jmap -dump:生成指定进程堆转储快照;
    1.5 jhat:分析headdump文件;
    1.6 jstack:显示虚拟机线程快照。
  2. 可视化工具:jconsole和jvisualvm。

24. 内存泄露与内存溢出?

  1. 内存泄露:是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露似乎不会有大的影响,但内存泄露堆积后的后果就是内存溢出。
  2. 内存溢出:是指程序申请内存时,没有足够的内存供申请者使用,或者说,申请的内存空间存储超出该内存空间的类型数据,那么结果就是内存不够用,此时就会出现OOM,即所谓的内存溢出。

25. 线上发生JVM内存泄露,谈谈你的性能调优思路和步骤?

  1. 堆内存泄露:java.lang.OutOfMemoryError:Java heap space,这种场景最为常见,它表明无法在Java堆中分配对象,这个错误可能出现的原因:1. 配置问题,程序指定的堆大小不合适;2. 代码中存在大对象分配;3.可能存在内存泄露,导致在多次GC之后,合适无法找到一块足够大的内存容纳当前对象。解决方案:
    1.1 检查是否存在大对象分配,最有可能的是大数组或者大的字符串;
    1.2 通过jmap命令把堆内存dump下来,使用分析工具进行分析,检查是否存在内存泄露的问题;
    1.3 如果没有找到明显的内存泄露,使用-Xmx加大堆内存大小。
  2. 方法区内存泄露:java.lang.OutOfMemoryError:PerGem space,原因:JDK 1.8之前的JDK通过永久代保存类信息、静态变量、常量、编译器即时编译代码、运行时常量池(字面值常量和符号引用),系统默认设置不能满足系统加载的要求,系统运行一段时间后,永久代就会出现内存溢出。解决方案:
    2.1 检查是否是永久代空间或元空间设置过小;
    2.2 检查代码中是否存在大量的反射操作;
    2.3 dump之后通过分析工具检查是否存在由于反射生成的代理类;
    2.4 重启JVM。
  3. java.lang.OutOfMemoryError:Meta space,原因:加载到内存中的Class数量太多或体积太大;解决方案:增大Metaspace的大小:-XX:MaxMetaspaceSize=512M。
  4. java.lang.OutOfMemoryError:Java heap space:GC Overhead limit exceeded:在一次垃圾回收之后,Java花费了超过98%的时间进行垃圾收集,而它只回收2%的堆,并且编译5次连续垃圾收集中均如此,那么会抛出内存溢出异常,是由于Java堆中无法容纳存活的数据量,没有多少可用空间用于新的分配。解决方案:
    4.1 检查代码中是否有大量的死循环或使用大内存的代码;
    4.2 使用-Xmx增大堆大小;
    4.3 如果没有上述情况,可以加大物理内存。