最近在牛客网看到很多程序员面试都被问到JVM相关的问题。为了给大家更好的参考,我把这些JVM相关的面试题整理了一下,自己也做了一些解答。有什么不对的地方请在评论区告诉我,大家一起交流学习!作为阅读福利,整理了一些Java面试题(附脑图、手写pdf、md文档),需要的可以【点此】获取!请简述JVM加载类文件的原理?检查点:JVM参考答案:JVM中的类加载是通过ClassLoader及其子类实现的。JavaClassLoader是一个重要的Java运行时系统组件。它负责在运行时从类文件中查找和加载类。Java中的所有类都需要通过类加载器加载到JVM中才能运行。类加载器本身也是一个类,它的工作就是将类文件从硬盘读入内存。在写程序的时候,我们几乎不需要关心类加载,因为这些都是隐式加载的,除非我们有特殊用途,比如反射,否则需要显式加载需要的类。类加载有两种方式:(1)隐式加载,当程序在运行过程中遇到new等产生的对象时,隐式调用类加载器将相应的类加载到jvm中,(2)显式loading,通过class.forname()等方法,显式加载需要的类,隐式加载和显式加载的区别:两者的本质是一样的。Java类的加载是动态的。它不会一次加载所有类然后运行它们。而是确保程序运行的基础类(如基类)完全加载到jvm中。至于其他的类,只需要在什么时候加载。这当然是为了节省内存开销。●什么是Java虚拟机?为什么Java被称为“平台无关的编程语言”?检查点:JVM参考答案:Java虚拟机是一个可以执行Java字节码的虚拟机进程。Java源文件被编译成Java虚拟机可以执行的字节码文件。Java旨在允许应用程序在任何平台上运行,而无需程序员分别为每个平台重写或重新编译。Java虚拟机使这成为可能,因为它知道底层硬件平台的指令长度和其他特征。●jvm的最大内存限制是多少?检查点:JVM参考答案:(1)堆内存分配JVM分配的初始内存由-Xms指定,默认为物理内存的1/64;JVM分配的最大内存由-Xmx指定,默认为物理内存的1/4。当默认的空闲堆内存小于40%时,JVM会增加堆直到-Xmx的最大限制;当空闲堆内存大于70%时,JVM将减少堆,直到达到-Xms的最小限制。所以服务器端一般会设置-Xms和-Xmx相等,以避免每次GC后调整heapsize。(2)非堆内存分配JVM使用-XX:PermSize设置非堆内存的初始值,默认为物理内存的1/64;最大非堆内存大小由XX:MaxPermSize设置,默认为物理内存的1/4。(3)VM最大内存首先,JVM内存被限制为实际最大物理内存。假设物理内存无限大,那么JVM内存的最大值与操作系统有很大关系。简单来说,虽然32位处理器的可控内存空间为4GB,但具体操作系统会有限制,一般为2GB-3GB(一般来说Windows系统下为1.5G-2G,而Linux系统下1.5G-2G)。2G-3G),64位以上的处理器就没有限制了。(3)以下是目前比较流行的几家不同公司不同版本的JVM最大堆内存:●jvm是如何实现线程的?检查点:JVM参考答案:线程是比进程更轻量级的调度执行单元。线程可以分离进程的资源分配和执行调度。一个进程中可以启动多个线程,每个线程可以共享进程的资源(内存地址、文件IO等),并且可以独立调度。线程是CPU调度的基本单位。所有主流操作系统都提供线程实现。Java语言为线程操作提供了相同的API。java.lang.Thread类的每一个执行过start()但还没有结束的实例代表一个线程。Thread类的关键方法都声明为Native。这意味着此方法不能或尚未使用与平台无关的方式实现,可能是为了提高执行效率。线程的实现方式A、使用内核线程实现内核线程(Kernel-LevelThread,KLT)是操作系统内核直接支持的线程。内核完成线程切换。内核通过调度器对线程进行调度,并将线程的任务映射到各个CPU。资源使用用户线程来实现系统内核无法感知线程存在的实现。用户线程的建立、同步、销毁、调度完全在用户态完成。所有的线程操作都需要用户程序自己处理,复杂度高。用户线程与轻量级进程混合在一起。实现轻量级进程作为用户线程和内核线程之间的桥梁●什么是JVM内存模型?考察点:JVM内存模型参考答案:Java内存模型(简称JMM),JMM决定了一个线程对共享变量的写入何时对另一个线程可见。JMM从抽象的角度定义了线程与主存的抽象关系:线程间的共享变量存储在主存(mainmemory)中,每个线程都有一个私有的本地内存(localmemory),线程的一份副本读/写共享变量存储在本地内存中。本地内存是JMM的一个抽象概念,并不真正存在。它涵盖了高速缓存、写入缓冲区、寄存器以及其他硬件和编译器优化。关系模型图如下图所示:●请列举,JAVA虚拟机中哪些对象可以作为ROOT对象?检查点:JAVA虚拟机引用答:虚拟机栈方法区引用对象类静态属性引用对象方法区常量本地方法栈引用对象JNI引用对象●如何判断一个对象是否需要回收在气相色谱?调查点:JAVA虚拟机参考答案:即使是可达性分析算法中不可达的对象,也不是“必须回收”的。此时,他们暂时处于“等待”阶段。它需要经过两次标记过程:如果对象经过可达性分析后发现没有引用链连接到GCRoots,则第一次标记并筛选一次。筛选条件是对象是否需要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过时,虚拟机将这两种情况都视为“不需要执行”。(也就是说直接回收)如果确定这个对象是必须要执行finalize()方法的,那么这个对象就会被放入一个叫做F-Queue的队列中,之后由虚拟机自动创建,低优先级的Finalizer线程来执行它。这里所谓的“执行”是指虚拟机触发这个方法,但并不承诺等待它结束。这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者出现死循环(更极端的情况),很可能导致F-Queue中的其他对象永久等待,甚至导致整个内存恢复系统崩溃。finalize()方法是对象逃脱回收的最后机会。稍后,GC会对F-Queue中的对象进行第二次小范围的标记。如果对象想在finalize()中跳出回收——只要重新连接引用链就可以和上面的任意一个对象建立关联即可,比如将自己(this关键字)赋值给对象的某个类变量或成员变量,然后在第二次标记时从“待回收”集合中移除;如果此时对象还没有逃脱,那么基本上就真的是回收了。●请说明JAVA虚拟机的作用是什么?研究点:Java虚拟机参考答案:说明运行字节码程序消除了平台依赖。jvm将java字节码解释为特定平台的特定指令。一种通用的高级语言如果要运行在不同的平台上,至少需要编译成不同的目标代码。引入JVM后,Java语言在不同平台上运行时不需要重新编译。Java语言的使用方式,即Java虚拟机,屏蔽了特定平台相关的信息,使得Java语言编译器只需要生成运行在Java虚拟机上的目标代码(字节码),就可以在各种平台上使用未经修改。跑步。Java虚拟机在执行字节码时,将字节码解释成特定平台上的机器指令执行。假设一个需要非常短的停止世界时间的场景,你将如何设计垃圾收集机制?大多数新创建的对象都分配在Eden区。在Eden区发生GC后,存活的对象被移动到其中一个Survivor区。Eden区发生GC后,对象被存放在Survivor区,而这个Survivor区已经有其他存活的对象。一旦Survivor区域已满,幸存对象将移动到另一个Survivor区域。那么之前的空间就满了,Survivor区就会空空如也,没有任何数据。重复此步骤多次后仍然存活的对象将被移至老年代。●请解释一下edenarea和survivalarea的含义和工作原理?检查点:JVM参考答案:目前主流的虚拟机实现采用分代收集的思想,将整个堆区划分为新生代和老年代;新生代分为Eden空间、FromSurvivor和ToSurvivor区。我们将Eden:FromSurvivor:ToSurvivor空间大小设置为8:1:1,Eden区总是诞生对象,FromSurvivor保存当前存活的对象,ToSurvivor为空。发生gc后:1)Eden区的存活对象+FromSurvivor中存放的对象复制到ToSurvivor2)Eden和FromSurvivor被清除;3)FromSurvivor和ToSurvivor的逻辑关系是颠倒的:From变成To,To变成From。可以看出,只有在Eden空间快满的时候才会触发MinorGC。Eden空间占据了新生代的大部分,因此可以降低MinorGC的频率。当然,我们使用两个Survivor的方式也付出了一定的代价,比如10%的空间浪费,复制对象的开销等等。●请简要描述JVM分区是什么?检查点:JVM参考答案:Java内存通常分为五个区域:程序计数器(ProgramCountRegister)、本地方法栈(NativeStack)、方法区(MethonArea)、栈(Stack)、堆(Heap)。●请简述类加载过程的检查点:JVM参考答案:如下图所示,JVM类加载机制分为加载、验证、准备、解析和初始化五个部分。下面我们分别来看一下这五个部分。过程。加载加载是类加载过程中的一个阶段。这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为这个类在方法区的各种数据的入口。请注意,它不一定必须从Class文件中获取。可以从ZIP包中读取(比如从jar包和war包中读取),也可以在运行时计算生成(动态代理),也可以通过其他文件生成(比如转换JSP文件)到相应的Class类)。该阶段验证的主要目的是确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,不会危及虚拟机本身的安全。Preparation准备阶段是正式为类变量分配内存并设置类变量初值的阶段,即在方法区分配这些变量使用的内存空间。注意这里提到的初值概念。例如,类变量定义为:publicstaticintv=8080;实际上,准备阶段后变量v的初始值为0,而不是8080。将v赋值给8080的putstatic指令是一个程序,编译后存放在类构造函数的方法中,即我们稍后会解释。但是注意如果声明是:publicstaticfinalintv=8080;编译阶段会为v生成ConstantValue属性,准备阶段虚拟机根据ConstantValue属性将v赋给8080。解析解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用在类文件中:CONSTANT_Class_infoCONSTANT_Field_infoCONSTANT_Method_info和其他类型的常量。接下来解释一下符号引用和直接引用的概念:符号引用与虚拟机实现的布局无关,被引用的目标也不一定非要加载到内存中。各种虚拟机实现的内存布局可以不同,但??是它们所能接受的符号引用必须是一致的,因为符号引用的字面形式在Java虚拟机规范的Class文件格式中有明确的定义。直接引用可以是指向目标的指针、相对偏移量或可以间接定位目标的句柄。如果存在直接引用,则引用的目标必须已经存在于内存中。初始化初始化阶段是类加载的最后阶段。在前面的类加载阶段之后,除了加载阶段自定义类加载器外,其他操作均由JVM主导。在初始阶段,实际执行类中定义的Java程序代码。初始化阶段是执行类构造函数的方法的过程。方法是由编译器自动收集的类中类变量的赋值操作和静态语句块中的语句组合而成的。虚拟机保证在执行方法之前已经执行了父类的方法。p.s:如果一个类中没有静态变量赋值或者静态语句块,那么编译器就不需要为这个类生成()方法。注意以下几种情况不会进行类初始化:通过子类引用父类的静态字段只会触发父类的初始化,不会触发子类的初始化。定义对象数组不会触发类的初始化。常量在编译期间会被存放在调用类的常量池中。本质上,并没有直接引用定义常量的类,也不会触发定义常量所在的类。通过类名获取Class对象不会触发类的初始化。通过Class.forName加载指定类时,如果指定参数initialize为false,则不会触发类初始化。其实这个参数就是告诉虚拟机是否初始化类。通过ClassLoader默认的loadClass方法,不会触发初始化动作。类加载器虚拟机设计团队将加载动作放在JVM之外,让应用程序自行决定如何获取需要的类。JVM提供了3种类型的加载器:BootstrapClassLoader:负责加载JAVA_HOME\lib目录下的类,或者-Xbootclasspath参数指定的路径下,被虚拟机识别(通过文件名识别,比如rt.jar).ExtensionClassLoader:负责加载JAVA_HOME\lib\ext目录下的类库,或者java.ext.dirs系统变量指定的路径下的类库。应用类加载器(ApplicationClassLoader):负责加载用户路径(classpath)上的类库。JVM通过双亲委派模型加载类。当然,我们也可以通过继承java.lang.ClassLoader来实现自定义的类加载器。当一个类加载器接收到一个类加载任务时,它会先交给它的父类加载器来完成,所以最后的加载任务会交给顶层的启动类加载器,只有当父类加载器无法完成加载任务,它将尝试执行加载任务。使用双亲委托的好处之一是,比如加载位于rt.jar包中的类java.lang.Object,无论哪个加载器加载这个类,最终都会委托给顶层启动类loader进行加载,这确保了使用不同的类加载器最终得到相同的Object对象。●请简单说明JVM的回收算法及其收集器是什么?CMS使用哪种回收算法?如何使用CMS解决内存碎片问题?检查点:JVM参考答案:垃圾收集算法mark-sweepmark-sweep算法将垃圾收集分为两个阶段:标记阶段和清理阶段。在标记阶段,首先通过根节点,标记从根节点开始的所有对象。未标记的对象是未引用的垃圾对象。然后,在清理阶段,清理所有未标记的对象。mark-and-sweep算法带来的一个问题就是会产生大量的空间碎片,因为回收的空间是不连续的,所以在为大对象分配内存时,可能会提前触发fullgc。复制算法将现有的内存空间分成两块,每次只使用其中一块。垃圾回收时,将正在使用的内存中存活的对象复制到未使用的内存块中,然后清除正在使用的内存。块中的所有对象,交换两个内存的角色,完成垃圾回收。今天的商业虚拟机使用这种收集算法来回收新一代。IBM研究表明,新生代中98%的对象在一夜之间生死,所以没有必要按1:1的比例划分内存空间,而是将内存划分一个较大的Eden空间和两个较小的Survivor空间,每个都使用伊甸园和一个幸存者空间。回收时,一次性将Eden和Survivor中的存活对象复制到另一个Survivor空间,最后清理掉刚刚使用过的Eden和Survivor空间。HotSpot虚拟机默认的Eden和Survivor的比例是8:1(可以通过-SurvivorRattio配置),即每个新生代中的可用内存空间是整个新生代容量的90%,只有10%内存将被“浪费”。当然,98%的可回收对象只是一般场景下的数据。我们无法保证不超过10%的对象会存活下来。当Survivor空间不够时,我们需要依靠其他内存(这里指的是老年代)进行分配保证。标记排序复制算法的效率是以存活对象少、垃圾对象多为前提的。这种情况经常发生在年轻代,但在老年代更常见,大部分对象都是存活对象。如果仍然使用复制算法,由于存活对象数量多,复制成本会很高。mark-compression算法是一种老年代恢复算法,它在mark-clear算法的基础上做了一些优化。首先,它还需要从根节点标记所有可达的对象,但之后,它不会简单地清理未标记的对象,而是将所有存活的对象压缩到内存的一端。之后,边界外的所有空间都被清除。这种方式既避免了碎片的产生,又不需要两块相同的内存空间,所以性价比比较高。增量算法增量算法的基本思想是,如果一次性处理完所有垃圾,系统需要暂停很长时间,那么可以让垃圾回收线程和应用线程交替执行。每次,垃圾收集线程只收集一小块内存空间,然后切换到应用程序线程。依次重复,直到垃圾收集完成。这样,由于应用程序代码在垃圾收集过程中是间歇性执行的,因此可以减少系统停顿时间。但是由于线程切换和上下文切换的消耗,垃圾回收的整体成本会增加,导致系统吞吐量下降。Garbagecollector串行收集器串行收集器是最古老的收集器。它的缺点是当Serial收集器要进行垃圾收集时,必须挂起所有用户进程,即stoptheworld。到现在为止,它仍然是运行在客户端模式下的虚拟机默认的新生代收集器。与其他收集器相比,对于仅限于单CPU的运行环境,Serial收集器没有线程交互开销,集中精力进行垃圾收集,自然可以获得最高的单线程收集效率。SerialOld是老版本的Serial收集器,也是单线程收集器,使用“标记-排序”算法。这个收集器的主要意思也是虚拟机在Client模式下使用的。在Server模式下,它主要有两个用途:一是在JDK1.5及更早版本中与ParallelScanvenge收集器配合使用,二是在并发收集发生ConcurrentModeFailure时作为CMS收集器的备份计划。使用时。通过指定-UseSerialGC参数,Serial+SerialOld的串行收集器组合用于内存回收。ParNew收集器ParNew收集器是新一代Serial收集器的多线程实现。请注意,它在垃圾收集期间仍然会停止世界,但与Serial收集器相比,它会运行多个进程进行垃圾收集。ParNew收集器在单CPU环境下永远不会比Serial收集器有更好的效果。即使由于线程交互的开销,收集器在超线程技术实现的双CPU环境中也不能100%有效。保证覆盖串行收集器。当然,随着可以使用的CPU数量的增加,对于GC时的系统资源利用率还是很有好处的。默认启用的收集线程数与CPU数相同。在CPU数量较多的环境下(比如32个,现在CPU往往有4核和超线程,越来越多的服务器有超过32个逻辑CPU),可以使用-XX:ParallelGCThreads参数来限制数量垃圾收集线程。-UseParNewGC:打开这个开关后,使用ParNew+SerialOld的收集器组合进行内存回收,使得新生代使用并行收集器,老年代使用串行收集器。ParallelScavenge收集器Parallel是一种多线程的新一代垃圾收集器,使用复制算法,它看起来与ParNew收集器有很多相似之处。但是ParallelScanvenge收集器的一个特点是它关注的是吞吐量(Throughput)。所谓吞吐量就是CPU花在运行用户代码上的时间与CPU消耗的总时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾回收时间)。暂停时间越短,越适合需要与用户交互的程序。良好的响应速度可以提升用户体验;而高吞吐量可以最有效地利用CPU时间,尽快完成程序的计算任务。主要适用于后台计算。不需要太多交互的任务。ParallelOld收集器是ParallelScavenge收集器的旧版本,使用多线程和“标记排序”算法。这个收集器是jdk1.6才提供的。在此之前,新一代的ParallelScavenge收集器一直处于尴尬的状态。原因是,如果新一代是ParallelScavenge收集器,那么老年代就只能选择SerialOld(PSMarkSweep)收集器了。由于单线程老年代SerialOld收集器对服务器端应用性能的“拖累”,即使使用ParallelScavenge收集器也未必能够最大化整个应用的吞吐量,并且由于老年代收集器无法充分利用服务器多个CPU的处理能力。在老年代比较多、硬件比较先进的环境下,这种组合的吞吐量可能甚至不如ParNew和CMS的组合那么“厉害”。直到ParallelOld收集器的出现,“吞吐量优先”的收集器终于有了更名副其实的应用恭喜。在注重吞吐量和对CPU资源敏感的场合,可以优先使用ParallelScavenge加ParallelOld收集器。-UseParallelGC:虚拟机以Server模式运行的默认值。打开这个开关后,ParallelScavenge+SerialOld的收集器组合用于内存回收。-UseParallelOldGC:开启此开关后,使用ParallelScavenge+ParallelOld的收集器组合进行垃圾收集。CMS收集器。CMS(ConcurrentMarkSwep)收集器是一个比较重要的收集器。现在被广泛使用。划重点,CMS是一个收集器,其目标是获取最短的恢复停顿时间,这使得它非常适合与用户交互的业务。从名字(MarkSwep)可以看出,CMS收集器是基于mark-and-sweep算法实现的。它的收集过程分为四个步骤:initialmark,concurrentmark,remark,concurrentsweeplongconcurrentmark和concurrentsweep阶段都可以和用户进程并发工作。G1收集器G1收集器是服务器端应用程序的垃圾收集器。HotSpot团队赋予它的使命是在未来替代JDK1.5发布的CMS收集器。与其他GC收集器相比,G1有以下特点:并行和并发:G1可以充分利用CPU,多核环境下的硬件优势可以缩短stoptheworld的停顿时间。分代收集:与其他收集器一样,分代的概念在G1中仍然存在,但是G1可以在没有其他垃圾收集器配合的情况下管理整个GC堆。空间整合:G1收集器有利于程序的长时间运行。在分配大对象时,不会因为无法获取连续空间而提前触发GC。可预测的非暂停:这是G1相对于CMS的另一大优势。减少暂停时间是G1和CMS共同关心的问题。它允许用户明确指定在M毫秒的时间段内,垃圾被消耗收集的时间不得超过N毫秒。CMS:使用标记清除算法解决这个问题的方法是让CMS在执行一定次数的FullGC(标记清除)时执行一次标记清除算法。CMS提供了以下参数来控制:-XX:UseCMSCompactAtFullCollection-XX:CMSFullGCBeforeCompaction=5表示CMS在5次FullGC(标记清除)后进行标记排序算法,这样旧带的碎片可以控制在以内一定数量,CMS甚至可以配置为每次执行FullGC时进行内存分配。整齐的。