前言:该篇主要对Java虚拟机相关的题目进行介绍。


JVM篇

基本上在面试的时候,都会或多或少的涉及JVM,主要看面试官的侧重点,笔者在面试过程中,是通过volatile问题,引导了JVM相关问题上的。

1)JVM的内存区域,各区域存储什么,及其作用。

程序计数器

#1.当前线程正在执行字节码行号指示器。

#2.为了线程切换后能够恢复到正确的执行位置,每个线程都需要一个独立的程序计数器。(线程私有

#3.当线程执行的是一个Java方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址。

#4.当线程执行的是一个Native方法,则计数器的值为空(Undefined)(原因:native方法体并不是由Java字节码构成,所以无法应用“Java字节码地址”的概念,所以JVM规范规定,当执行Native方法时,计数器的值未定义(任何值都可以))。

#5.该区域是唯一一个在JVM规范中没有规定任何OOM情况的区域。

Java虚拟机栈

#1.描述Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

#2.生命周期与线程相同,线程运行完毕后,相应的内存自动回收。(线程私有

#3.这个区域可能有两种异常:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常(如:将一个函数反复递归自己,最终会出现这种异常)。如果JVM栈可以动态扩展(大部分JVM是可以的),当扩展时无法申请到足够内存则抛出OutOfMemoryError(OOM)异常

本地方法栈

#1.本地方法栈与Java虚拟机栈所发挥的作用很相似,区别在于Java虚拟机栈为执行Java代码方法服务,而本地方法栈是为Native方法服务。

#2.和JVM栈一样,这个区域也会抛出StackOverflowError和OutOfMemoryError异常。

#3.线程私有。

方法区

#1.方法区域是全局共享的,比如每个线程都可以访问同一个类的静态变量。它存储了已被JVM加载的类信息、静态变量、编译器编译后的代码等。如,当程序中通过getName、isInterface等方法来获取信息时,这些数据来源于方法区。

#2.由于使用反射机制的原因,虚拟机很难推测哪个类信息不再使用,因此这块区域的回收很难!另外,对这块区域主要是针对常量池回收,值得注意的是JDK1.7已经把常量池转移到堆里面了

#3.当方法区无法满足内存分配需求时,会抛出OutOfMemoryError。

#1.堆是Java虚拟机所管理内存最大的一块,被线程全局共享,在虚拟机启动时创建。

#2.几乎所有的对象实例都在堆中分配内存。但随着JIT编译器的发展,栈上分配等技术的优化,所以是“几乎所有的对象”。

#3.堆是垃圾收集器的主要区域。

#4.根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。

#5.如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

2)JVM栈中具体存储的内容,如何存储。

在JVM栈中,方法在执行的时候会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、返回地址(方法出口)等信息。

#1.局部变量表存放编译期可知的各种基本数据类型(byte、short、char、int、long、float、double、boolean)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

#2.对于基本数据类型,long和double类型的数据会占2个局部变量空间(slot),其余数据类型只占1个。局部变量表所需的内存空间在编译期间完成分配。所以方法在运行期间所需的局部变量空间大小是一定的。

#3.静态方法和实例方法的局部变量表基本类似,但是实例方法表中,第一个位置存放的是当前对象的引用,因为实例方法依赖于实例对象,方法与对象之间关联。

#4.由于Java没有寄存器,因此参数传递都是通过操作数栈,关于操作数栈具体的栈操作过程,参考:

#5.动态链接就是Class文件常量池中的符号引用,在运行期间转换为直接引用。对象较小,直接分配在栈上,可自动回收,减轻GC压力。

3)GC(垃圾回收),常见的垃圾回收算法以及其使用场景。

JVM垃圾回收,通俗来讲就是清除内存中(主要堆内存)“无用的”对象(不能再被任何途径使用的对象)。

常见的垃圾回收算法:

标记-清除算法

a.最基础的垃圾回收算法,分“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

b.缺点:

#1.效率问题,标记和清除两个过程效率都不高。

#2.空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序需要分配较大对象时,无法找到足够的连续内存而不得不提前触发一次垃圾回收操作,影响性能。

c.使用场景:在老年代的垃圾回收中,因为老年代的存活时间较长,不会像新生代那样,频繁的“出生”与“死去”。

复制算法

复制算法是为了解决效率问题,它将内存按容量划分为大小相等的两块,每次只使用其中一块,当这一块的内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次性清理掉。这样使得每次都是对整个半区进行内存回收,并且使用的是一块连续的内存空间,不存在内存碎片,简单高效,该算法是以空间换时间的一种方式。

使用场景:使用在MinorGC中,MinorGC发生在新生代的垃圾回收过程中。新生代的对象具有“朝生夕死”的特征,通过高效的复制算法可以,快速的进行垃圾回收操作。

标记-整理算法

复制算法在对象存活率较高时就要进行较多的复制操作,效率将变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端状态,所以老年代一般不使用复制算法。根据老年代的特点(存活时间较长),就提出了“标记-整理”算法,标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让存活的对象都向一端移动,然后清理掉端边界以外的内存。

使用场景:对老年代进行垃圾回收操作。

④分代收集算法

当前的商业虚拟机都采用分代收集算法进行垃圾回收。思想:根据对象的存活周期分为新生代和老年代,根据各年代对象的特点采用合适的收集算法。新生代采用复制算法,老年代采用“标记-清除”或“标记-整理”算法。

4)OOM问题的解决思路及其解决办法。

OOM问题在实际生产过程中经常发生,随着时间的推移,用户人数的增加,JVM的内存不足,容易造成OOM问题。在JVM内存模型中,除了程序计数器外,Java虚拟机栈、本地方法栈、方法区、堆这几块区域中都可能发生OOM问题。

首先需要了解JVM的常见配置参数,见下图。

java jvm常见面试题 jvm面试题总结_java jvm常见面试题

常见OOM问题:

①Java Heap=>Java.lang.OutOfMemoryError: Java heap space

出现该问题,首先要确定是内存泄漏(申请的空间无法释放)还是内存溢出,通过对dump文件的分析,一般可以确定,如果是内存泄漏,则需要找到泄漏的对象,并想办法解决该问题;如果是内存溢出,则需要检查JVM的-Xms和-Xmx的参数是否合适。

②OOM for Perm=>java.lang.OutOfMemoryError: Java perm space

出现该问题查看JVM的-XX:MaxPermSize(永代区的最大值)是否满足需要,根据实际情况进行调整。另外,注意一点,Perm一般是在JVM启动时加载类进来,如果是JVM运行较长一段时间而不是刚启动后溢出的话,很有可能是由于运行时有类被动态加载进来,此时建议用CMS策略中的类卸载配置。

如:-XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled。

③OOM for GC=>java.lang.OutOfMemoryError: GC overhead limit exceeded

此OOM是由于JVM在GC时,对象过多,导致内存溢出,建议调整GC的策略,在一定比例下开始GC而不要使用默认的策略,或者将新代和老代设置合适的大小,需要进行微调存活率。
解决方法:改变GC策略,在老年代80%时开始GC,设置-XX:SurvivorRatio(-XX:SurvivorRatio=8)和-XX:NewRatio(-XX:NewRatio=4)的值。

④OOM for native thread created=>java.lang.OutOfMemoryError: unable to create new native thread

出现该问题,需要考虑栈的大小是否合适,可适当调小栈的大小,利用-Xss。

上述4种是比较常见的OOM问题,其他OOM问题,请参考:

http://zhaohe162.blog.163.com/blog/static/38216797201110232341953/


5)什么情况下会发生 MinorGC、FullGC。

MinorGC发生在新生代的垃圾回收中,在Eden区没有足够的空间进行分配时,会触发一次MinorGC。

FullGC发生在整个堆垃圾回收中,Full GC触发条件:

#1.调用System.gc时,系统建议执行Full GC,但是不必然执行。

#2.老年代空间不足。

#3.方法区空间不足。

#4.通过Minor GC后进入老年代的平均大小(内存)大于老年代的可用内存。

参考:

https://www.zhihu.com/question/41922036

6)CMS 具体过程、每个过程是单线程还是多线程运行、每个过程是否和用户线程并行运行,CMS 适用于什么场景,哪些是 GC Roots 对象。

JVM垃圾回收算法为内存回收的方法论,JVM垃圾收集器为内存回收的具体实现。

常见7中垃圾收集器,可根据收集对象的不同状态分类:

新生代垃圾收集器(新生代采用复制算法):Serial、ParNew、Parallel Scavenge

老年代垃圾收集器(老年代采用标记-整理算法):CMS、Serial Old(MSC)、Parallel Old

G1垃圾收集器对新生代和老年代都可以收集,处于当中。

Serial:最基本,发展历史最悠久的收集器。是单线程的,并且会"Stop The World",直到它收集结束。用于client模式。

ParNew:为Serial收集器的多线程(并行多线程)版本。也会"Stop The World"。能与CMS配合的新生代收集器。

Parallel Scavenge:并行的多线程收集器。主要目的是达到一个可控的吞吐量,也会"Stop The World"。

Serial Old:是Serial收集器的老年代版本,单线程,会"Stop The World"。

Parallel Old:是Parallel Scavenge收集器的老年代版本,并行多线程。

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器主要应用于以Java为基础的互联网站或B/S系统的服务器端,因为这类系统重视服务器的响应速度,希望停顿时间最短,以带给用户较好的体验。CMS收集器是基于“标记-清除”算法的实现。

CMS运行分四个过程:

①初始标记

②并发标记

③重新标记

④并发清除

解释:

初始标记和重新标记这两个步骤需要“Stop The World”(暂停正在执行的任务),所以这两个过程是单线程的。并发标记和并发清除可以与用户线程一起工作(多线程

初始标记仅仅只是标记一下GC Roots能直接关联到的对象,虽然要暂停虚拟机,但是速度很快。

并发标记紧随初始标记,在初始标记的基础上向下追溯标记,并发标记线程是与用户线程并发执行,用户不会感觉到停顿。

重新标记是为了修正并发标记阶段因用户线程运行而导致标记产生变动的那一部分对象(因为并发标记是并行执行的),该阶段也会暂停虚拟机(Stop The World),停顿时间会比初始标记要长一些,但远比并发标记的时间短。

并发清除与用户线程并行执行,清除垃圾对象。

由于整个过程中耗时最长并发标记并发清除都可以与用户线程一起执行,因此,从总体上来说CMS收集器的内存回收过程是与用户线程并行执行的。

CMS适用的场景对响应时间重要性需求大于吞吐量的要求,也就是要响应时间快,如实时交易平台、实时通信平台,要求响应时间顿,基本无停顿感。

GC Roots对象包括:

①虚拟机栈帧中的本地变量表的引用对象。(个人理解为实例对象)

②方法区中类静态属性引用的对象。

③方法区中常量引用的对象。

④本地方法栈中JNI(也就是Native方法)引用的对象。

因为以上四种对象的存活时间较长,不会被轻易清除。


by Shawn Chen,于2018.6月,开始找工作途中......