小伙伴们不管是通过FreemenAPP还是其他招聘软件获得面试时,面试官总会安排一些常见面试题去提问。Freemen小编再网上搜集了Java的一部分常见面试题,希望对Java小伙伴的面试有一定帮助,也希望各位多多支持Freemen,推荐更多小伙伴一起加入FreemenAPP
JVM是面试中必问的部分,本文通过思维导图以面向面试的角度整理JVM中不可不知的知识。
先上图:
1、JVM基本概念
1.1、JVM是什么
JVM的全称是「JavaVirtualMachine」,也就是我们耳熟能详的Java虚拟机。
JVM具备着计算机的基本运算方式,它主要负责把Java程序生成的字节码文件,解释成具体系统平台上的机器指令,让其在各个平台运行。
JVM是运行在操作系统上的,它与硬件没有直接的交互。
当然,严格来说JVM也是虚拟机规范,有很多不同的实现,Sun/OracleJDK和OpenJDK中的默认Java虚拟机是HotSpot虚拟机,是目前使用范围最广的Java虚拟机,一般讲到的JVM默认指的就是HotSpot虚拟机。
1.2、Java程序运行过程
我们都知道Java源文件,通过编译器,能够生产相应的.Class文件,也就是字节码文件,而字节码文件又通过Java虚拟机中的解释器,编译成特定机器上的机器码。
也就是如下:
每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是Java为什么能够跨平台的原因了,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。
1.3、JDK、JRE、JVM
JDK(JavaDevelopmentKitJava开发工具包),JDK是提供给Java开发人员使用的,其中包含了Java的开发工具,也包括了JRE。其中的开发工具包括编译工具(javac.exe)打包工具(jar.exe)等。
JRE(JavaRuntimeEnvironmentJava运行环境)是JDK的子集,也就是包括JRE所有内容,以及开发应用程序所需的编译器和调试器等工具。JRE提供了库、Java虚拟机(JVM)和其他组件,用于运行Java编程语言、小程序、应用程序。
JVM(JavaVirtualMachineJava虚拟机),JVM可以理解为是一个虚拟出来的计算机,具备着计算机的基本运算方式,它主要负责把Java程序生成的字节码文件,解释成具体系统平台上的机器指令,让其在各个平台运行。
JDK中包含JRE,也包括JDK,而JRE也包括JDK。
范围关系:JDKJREJVM。
2、JVM内存区域
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域:
当然,实际上,为了更好的适应CPU性能提升,最大限度提升JVM运行效率,JDK中各个版本对JVM进行了一些迭代,示意图如下:
JDK1.6、JDK1.7、JDK1.8JVM内存模型主要有以下差异:
JDK1.6:有永久代,静态变量存放在永久代上。
JDK1.7:有永久代,但已经把字符串常量池、静态变量,存放在堆上。逐渐的减少永久代的使用。
JDK1.8:无永久代,运行时常量池、类常量池,都保存在元数据区,也就是常说的元空间。但字符串常量池仍然存放在堆上。
2.1、程序计数器
一块较小的内存空间,是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。
正在执行java方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果还是Native方法,则为空。
这个内存区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError情况的区域。
2.2、Java虚拟机栈
与程序计数器一样,Java虚拟机栈(JavaVirtualMachineStack)也是线程私有的,它的生命周期与线程相同。
虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(StackFrame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表存放了编译期可知的各种Java虚拟机基本数据类型、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
2.3、本地方法栈
本地方法栈(NativeMethodStacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
Hot-Spot虚拟机直接把本地方法栈和虚拟机栈合二为一。
与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。
2.4、Java堆
对于Java应用程序来说,Java堆(JavaHeap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存。
Java堆是垃圾收集器管理的内存区域,因此一些资料中它也被称作“GC堆”。
从回收内存的角度看,
Java堆,由年轻代和年老代组成,分别占据1/3和2/3。
而年轻代又分为三部分,Eden、FromSurvivor、ToSurvivor,占据比例为8:1:1,可调。
需要注意的是这些区域划分仅仅是一部分垃圾收集器的共同特性或者说设计风格而已,而非某个Java虚拟机具体实现的固有内存布局,HotSpot里面已经出现了不采用分代设计的新垃圾收集器。
Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。
2.5、方法区(JDK1.8移除)
方法区(MethodArea)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
在JDK1.8以前,HotSpot使用永久代来实现方法区,所以某些场合也认为方法区和永久代是一个概念。
在JDK6的时候HotSpot开发团队就有放弃永久代,逐步改为采用本地内存(NativeMemory)来实现方法区的计划了,到了JDK7的HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出,而到了JDK8,终于完全废弃了永久代的概念,改用在本地内存中实现的元空间(Meta-space)来代替,把JDK7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。
如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。
2.6、运行时常量池
运行时常量池(RuntimeConstantPool)是方法区的一部分——在JDK1.8已经被移到了元空间。
运行时常量池相对于Class文件常量池的一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。
2.7、直接内存
直接内存(DirectMemory)并不是虚拟机运行时数据区的一部分。
显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制。
元空间从虚拟机Java堆中转移到本地内存,默认情况下,元空间的大小仅受本地内存的限制。jdk1.8以前版本的class和JAR包数据存储在PermGen下面,PermGen大小是固定的,而且项目之间无法共用,公有的class,所以比较容易出现OOM异常。
升级JDK1.8后,元空间配置参数,-XX:MetaspaceSize=MXX:MaxMetaspaceSize=M。
3、JVM中的对象
上面已经了解Java虚拟机的运行时数据区域,我们接下来更进一步了解这些虚拟机内存中数据的其他细节,譬如它们是如何创建、如何布局以及如何访问的。以最常用的虚拟机HotSpot和最常用的内存区域Java堆为例,了解一下HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程。
3.1、对象的创建
Java对象创建的大概过程如下:
①类加载检查:虚拟机遇到?条new指令时,?先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引?,并且检查这个符号引?代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执?相应的类加载过程。
②分配内存:在类加载检查通过后,接下来虚拟机将为新?对象分配内存。对象所需的内存??在类加载完成后便可确定,为对象分配空间的任务等同于把?块确定??的内存从Java堆中划分出来。分配?式有“指针碰撞”和“空闲列表”两种,选择那种分配?式由Java堆是否规整决定,?Java堆是否规整?由所采?的垃圾收集器是否带有压缩整理功能决定。
内存分配的两种?式:选择以上两种?式中的哪?种,取决于Java堆内存是否规整。?Java堆内存是否规整,取决于GC收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的。
③初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这?步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使?,程序能访问到这些字段的数据类型所对应的零值。
④设置对象头:初始化零值完成之后,虚拟机要对对象进?必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的GC分代年龄等信息。这些信息存放在对象头中。另外,根据虚拟机当前运?状态的不同,如是否启?偏向锁等,对象头会有不同的设置?式。
⑤执?init?法:在上??作都完成之后,从虚拟机的视?来看,?个新的对象已经产?了,但从Java程序的视?来看,对象创建才刚开始,?法还没有执?,所有的字段都还为零。所以?般来说,执?new指令之后会接着执??法,把对象按照程序员的意愿进?初始化,这样?个真正可?的对象才算完全产?出来。
3.2、对象的内存布局
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(InstanceData)和对齐填充(Padding)。
HotSpot虚拟机对象的对象头部分包括两类信息。第一类是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特,官方称它为“MarkWord”。
3.3、对象的访问定位
建?对象就是为了使?对象,我们的Java程序通过栈上的reference数据来操作堆上的具体对象。对象的访问?式有虚拟机实现?定,?前主流的访问?式有①使?句柄和②直接指针两种:
句柄:如果使?句柄的话,那么Java堆中将会划分出?块内存来作为句柄池,reference中存储的就是对象的句柄地址,?句柄中包含了对象实例数据与类型数据各?的具体地址信息。
直接指针:如果使?直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,?reference中存储的直接就是对象的地址。
4、GC垃圾回收
对于垃圾回收,主要考虑的就是完成三件事:
哪些内存需要回收?
什么时候回收?
如何回收?
4.1、如何判断对象需要回收?
4.1.1、引用计数法
引用计数法的算法:
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;
当引用失效时,计数器值就减一;
任何时刻计数器为零的对象就是不可能再被使用的。
客观地说,引用计数算法(ReferenceCounting)虽然占用了一些额外的内存空间来进行计数,但它的原理简单,判定效率也很高,在大多数情况下它都是一个不错的算法。也有一些比较著名的应用案例,例如微软COM(ComponentObjectModel)技术、使用ActionScript3的FlashPlayer、Python语言以及在游戏脚本领域得到许多应用的Squirrel中都使用了引用计数算法进行内存管理。
但是,在Java领域,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是,这个看似简单的算法有很多例外情况要考虑,例如在处理处理一些相互依赖、循环引用时非常复杂。
4.1.2、可达性分析算法
当前主流的商用程序语言(Java、C#,上溯至前面提到的古老的Lisp)的内存管理子系统,都是通过可达性分析(ReachabilityAnalysis)算法来判定对象是否存活的。这个算法的基本思路就是通过一系列称为“GCRoots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(ReferenceChain),如果某个对象到GCRoots间没有任何引用链相连,或者用图论的话来说就是从GCRoots到这个对象不可达时,则证明此对象是不可能再被使用的。
GCRoots包括;
全局性引用,对方法区的静态对象、常量对象的引用
执行上下文,对Java方法栈帧中的局部对象引用、对JNIhandles对象引用
已启动且未停止的Java线程
4.1.3、引用
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否引用链可达,判定对象是否存活都和“引用”离不开关系。
Java的引用分为四种:强引用(StronglyRe-ference)、软引用(SoftReference)、弱引用(WeakReference)和虚引用(PhantomReference)。
强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Objectobj=newObject()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。Java提供提供了SoftReference类来实现软引用。
弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。Java提供了WeakReference类来实现弱引用。
虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。Java提供了PhantomReference类来实现虚引用。
4.2、垃圾收集算法
4.2.1、标记-清除算法
最早出现也是最基础的垃圾收集算法是“标记-清除”(Mark-Sweep)算法,
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回
收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程。
后续的收集算法大多都是以标记-清除算法为基础,对其缺点进行改进而得到的。
它的主要缺点有两个:
第一个是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过
程的执行效率都随对象数量增长而降低;
第二个是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记-清除算法的执行过程如图:
4.2.2、标记-复制算法
标记-复制算法常被简称为复制算法。为了解决标记-清除算法面对大量可回收对象时执行效率低的问题。
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
这样实现简单,运行高效,不过其缺陷也显而易见,这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费较多。
标记-复制算法的执行过程如图所示。
4.2.3、标记-整理算法
标记-整理算法的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
“标记-整理”算法的示意图如图:
4.3、分代收集理论
当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(GenerationalCollection)的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:
1)弱分代假说(WeakGenerationalHypothesis):绝大多数对象都是朝生夕灭的。
2)强分代假说(StrongGenerationalHypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
基于这两个假说,收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。
设计者一般至少会把Java堆划分为新生代(YoungGeneration)和老年代(OldGeneration)两个区域。顾名思义,在新生代中,每次垃圾收集
时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。
基于这种分代,老年代和新生代具备不同的特点,可以采用不同的垃圾收集算法。
?如在新?代中,每次收集都会有?量对象死去,所以可以选择标记-复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。
??年代的对象存活?率是?较?的,?且没有额外的空间对它进?分配担保,所以必须选择标记-清除或标记-整理算法进?垃圾收集。
因为有了分代收集理论,所以就有了了“MinorGC(新?代GC)”、“MajorGC(?年代GC)”、“FullGC(全局GC)”这样的回收类型的划分
4.4、垃圾收集器
4.4.1、Serial收集器
Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK1.3.1之前)是HotSpot虚拟机新生代收集器的唯一选择。这个收集器是一个单线程工作的收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
Serial/SerialOld收集器的运行过程如下:
4.4.2、ParNew收集器
ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、StopTheWorld、对象分配规则、回收策略等都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码。
ParNew收集器的工作过程如图所示:
4.4.3、ParallelScavenge收集器
ParallelScavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器
ParallelScavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。由于与吞吐量关系密切,ParallelScavenge收集器也经常被称作“吞吐量优先收集器”。
ParallelScavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
4.4.4、SerialOld收集器
SerialOld是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。如果在服务端模式下,它也可能有两种用途:一种是在JDK5以及之前的版本中与ParallelScavenge收集器搭配使用,另外一种就是作为CMS收集器发生失败时的后备预案,在并发收集发生ConcurrentModeFailure时使用。这两点都将在后面的内容中继续讲解。
SerialOld收集器的工作过程如图所示。
4.4.5、ParallelOld收集器
ParallelOld是ParallelScavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。这个收集器是直到JDK6时才开始提供的。ParallelOld收集器的工作过程如图所示。
4.4.6、CMS收集器
CMS(ConcurrentMarkSweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为