最近在牛客网上看到很多程序员面试都被问到JVM相干的一些问题,为了让大家更好的参考,我把这些JVM相干面试题都整顿了一下,而后本人也做了一些解答,如果有什么不对的欢送大家在评论区通知我,大家一起交流学习!
作为浏览福利,我整顿了一些Java面试题(有脑图、手写pdf、md文档),须要的可【点击此处】获取!
请简略形容一下JVM加载class文件的原理是什么?
考察点:JVM
参考答复:
JVM中类的装载是由ClassLoader和它的子类来实现的,Java ClassLoader 是一个重要的Java运行时零碎组件。它负责在运行时查找和装入类文件的类。
Java中的所有类,都须要由类加载器装载到JVM中能力运行。类加载器自身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,咱们简直不须要关怀类的加载,因为这些都是隐式装载的,除非咱们有非凡的用法,像是反射,就须要显式的加载所须要的类。
类装载形式,有两种
(1)隐式装载,程序在运行过程中当碰到通过new 等形式生成对象时,隐式调用类装载器加载对应的类到jvm中,
(2)显式装载,通过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后调整堆的大小。
(2)非堆内存调配
JVM应用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;由XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。
(3)VM最大内存
首先JVM内存限度于理论的最大物理内存,假如物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简略的说就32位处理器虽 然可控内存空间有4GB,然而具体的操作系统会给一个限度,这个限度个别是2GB-3GB(一般来说Windows零碎下为1.5G-2G,Linux系 统下为2G-3G),而64bit以上的处理器就不会有限度了。
(3)上面是以后比拟风行的几个不同公司不同版本JVM最大堆内存:
● jvm是如何实现线程的?
考察点:JVM
参考答复:
线程是比过程更轻量级的调度执行单位。线程能够把一个过程的资源分配和执行调度离开。一个过程里能够启动多条线程,各个线程可共享该过程的资源(内存地址,文件IO等),又能够独立调度。线程是CPU调度的根本单位。
支流OS都提供线程实现。Java语言提供对线程操作的同一API,每个曾经执行start(),且还未完结的java.lang.Thread类的实例,代表了一个线程。
Thread类的要害办法,都申明为Native。这意味着这个办法无奈或没有应用平台无关的伎俩来实现,也可能是为了执行效率。
实现线程的形式
A.应用内核线程实现内核线程(Kernel-Level Thread, KLT)就是间接由操作系统内核反对的线程。
内核来实现线程切换
内核通过调度器Scheduler调度线程,并将线程的工作映射到各个CPU上程序应用内核线程的高级接口,轻量级过程(Light Weight Process,LWP)用户态和内核态切换耗费内核资源
应用用户线程实现
零碎内核不能感知线程存在的实现
用户线程的建设、同步、销毁和调度齐全在用户态中实现,所有线程操作须要用户程序本人解决,复杂度高
用户线程加轻量级过程混合实现
轻量级过程作为用户线程和内核线程之间的桥梁
● 请问什么是JVM内存模型?
考察点:JVM内存模型
参考答复:
Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入何时对另一个线程可见。从形象的角度来看,JMM定义了线程和主内存之间的形象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个公有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的正本。
本地内存是JMM的一个抽象概念,并不实在存在。它涵盖了缓存,写缓冲区,寄存器以及其余的硬件和编译器优化。其关系模型图如下图所示:
● 请列举一下,在JAVA虚拟机中,哪些对象可作为ROOT对象?
考察点:JAVA虚拟机
参考答复:
- 虚拟机栈中的援用对象
- 办法区中类动态属性援用的对象
- 办法区中常量援用对象
- 本地办法栈中JNI援用对象
● GC中如何判断对象是否须要被回收?
考察点:JAVA虚拟机
参考答复:
即便在可达性剖析算法中不可达的对象,也并非是“非回收不可”的,这时候它们临时处于“期待”阶段,要真正宣告一个对象回收,至多要经验两次标记过程:如果对象在进行可达性剖析后发现没有与GC Roots相连接的援用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行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虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。
假如一个场景,要求stop the world工夫十分短,你会怎么设计垃圾回收机制?
绝大多数新创建的对象调配在Eden区。在Eden区产生一次GC后,存活的对象移到其中一个Survivor区。在Eden区产生一次GC后,对象是寄存到Survivor区,这个Survivor区曾经存在其余存活的对象。
一旦一个Survivor区已满,存活的对象挪动到另外一个Survivor区。而后之前那个空间已满Survivor区将置为空,没有任何数据。通过反复屡次这样的步骤后仍旧存活的对象将被移到老年代。
● 请阐明一下eden区和survial区的含意以及工作原理?
考察点:JVM
参考答复:
目前支流的虚拟机实现都采纳了分代收集的思维,把整个堆区划分为新生代和老年代;新生代又被划分成Eden 空间、 From Survivor 和 To Survivor 三块区域。
咱们把Eden : From Survivor : To Survivor 空间大小设成 8 : 1 : 1 ,对象总是在 Eden 区出世, From Survivor 保留以后的幸存对象, To Survivor 为空。一次 gc 产生后:
1)Eden 区活着的对象 + From Survivor 存储的对象被复制到 To Survivor
2) 清空 Eden 和 From Survivor ; 3) 颠倒 From Survivor 和 To Survivor 的逻辑关系: From 变 To , To 变 From 。能够看出,只有在 Eden 空间快满的时候才会触发 Minor GC 。而 Eden 空间占新生代的绝大部分,所以 Minor GC 的频率得以升高。当然,应用两个 Survivor 这种形式咱们也付出了肯定的代价,如 10% 的空间节约、复制对象的开销等。
● 请简略形容一下JVM分区都有哪些?
考察点:JVM
参考答复:
java内存通常被划分为5个区域:程序计数器(Program Count Register)、本地办法栈(Native Stack)、办法区(Methon Area)、栈(Stack)、堆(Heap)。
● 请简略形容一下类的加载过程
考察点:JVM
参考答复:
如下图所示,JVM类加载机制分为五个局部:加载,验证,筹备,解析,初始化,上面咱们就别离来看一下这五个过程。
加载
加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为办法区这个类的各种数据的入口。留神这里不肯定非得要从一个Class文件获取,这里既能够从ZIP包中读取(比方从jar包和war包中读取),也能够在运行时计算生成(动静代理),也能够由其它文件生成(比方将JSP文件转换成对应的Class类)。
验证
这一阶段的次要目标是为了确保Class文件的字节流中蕴含的信息是否合乎以后虚拟机的要求,并且不会危害虚拟机本身的平安。
筹备
筹备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在办法区中调配这些变量所应用的内存空间。留神这里所说的初始值概念,比方一个类变量定义为:
public static int v = 8080;
实际上变量v在筹备阶段过后的初始值为0而不是8080,将v赋值为8080的putstatic指令是程序被编译后,寄存于类结构器<client>办法之中,这里咱们前面会解释。
然而留神如果申明为:
public static final int v = 8080;
在编译阶段会为v生成ConstantValue属性,在筹备阶段虚构机会依据ConstantValue属性将v赋值为8080。
解析
解析阶段是指虚拟机将常量池中的符号援用替换为间接援用的过程。符号援用就是class文件中的:
- CONSTANT_Class_info
- CONSTANT_Field_info
- CONSTANT_Method_info
等类型的常量。
上面咱们解释一下符号援用和间接援用的概念:
符号援用与虚拟机实现的布局无关,援用的指标并不一定要曾经加载到内存中。各种虚拟机实现的内存布局能够各不相同,然而它们能承受的符号援用必须是统一的,因为符号援用的字面量模式明确定义在Java虚拟机标准的Class文件格式中。
间接援用能够是指向指标的指针,绝对偏移量或是一个能间接定位到指标的句柄。如果有了间接援用,那援用的指标必然曾经在内存中存在。
初始化
初始化阶段是类加载最初一个阶段,后面的类加载阶段之后,除了在加载阶段能够自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。
初始化阶段是执行类结构器<client>办法的过程。<client>办法是由编译器主动收集类中的类变量的赋值操作和动态语句块中的语句合并而成的。虚构机会保障<client>办法执行之前,父类的<client>办法曾经执行结束。p.s: 如果一个类中没有对动态变量赋值也没有动态语句块,那么编译器能够不为这个类生成<client>()办法。
留神以下几种状况不会执行类初始化:
- 通过子类援用父类的动态字段,只会触发父类的初始化,而不会触发子类的初始化。
- 定义对象数组,不会触发该类的初始化。
- 常量在编译期间会存入调用类的常量池中,实质上并没有间接援用定义常量的类,不会触发定义常量所在的类。
- 通过类名获取Class对象,不会触发类的初始化。
- 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是通知虚拟机,是否要对类进行初始化。
- 通过ClassLoader默认的loadClass办法,也不会触发初始化动作。
类加载器
虚拟机设计团队把加载动作放到JVM内部实现,以便让应用程序决定如何获取所需的类,JVM提供了3品种加载器:
启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定门路中的,且被虚拟机认可(按文件名辨认,如rt.jar)的类。
扩大类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs零碎变量指定门路中的类库。
应用程序类加载器(Application ClassLoader):负责加载用户门路(classpath)上的类库。
JVM通过双亲委派模型进行类的加载,当然咱们也能够通过继承java.lang.ClassLoader实现自定义的类加载器。
当一个类加载器收到类加载工作,会先交给其父类加载器去实现,因而最终加载工作都会传递到顶层的启动类加载器,只有当父类加载器无奈实现加载工作时,才会尝试执行加载工作。采纳双亲委派的一个益处是比方加载位于rt.jar包中的类java.lang.Object,不论是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保障了应用不同的类加载器最终失去的都是同样一个Object对象。
● 请简略阐明一下JVM的回收算法以及它的回收器是什么?还有CMS采纳哪种回收算法?应用CMS怎么解决内存碎片的问题呢?
考察点:JVM
参考答复:
垃圾回收算法
标记革除
标记-革除算法将垃圾回收分为两个阶段:标记阶段和革除阶段。在标记阶段首先通过根节点,标记所有从根节点开始的对象,未被标记的对象就是未被援用的垃圾对象。而后,在革除阶段,革除所有未被标记的对象。标记革除算法带来的一个问题是会存在大量的空间碎片,因为回收后的空间是不间断的,这样给大对象分配内存的时候可能会提前触发full gc。
复制算法
将现有的内存空间分为两快,每次只应用其中一块,在垃圾回收时将正在应用的内存中的存活对象复制到未被应用的内存块中,之后,革除正在应用的内存块中的所有对象,替换两个内存的角色,实现垃圾回收。
当初的商业虚拟机都采纳这种收集算法来回收新生代,IBM钻研表明新生代中的对象98%是朝夕生死的,所以并不需要依照1:1的比例划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次应用Eden和其中的一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地拷贝到另外一个Survivor空间上,最初清理掉Eden和方才用过的Survivor的空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1(能够通过-SurvivorRattio来配置),也就是每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存会被“节约”。当然,98%的对象可回收只是个别场景下的数据,咱们没有方法保障回收都只有不多于10%的对象存活,当Survivor空间不够用时,须要依赖其余内存(这里指老年代)进行调配担保。
标记整顿
复制算法的高效性是建设在存活对象少、垃圾对象多的前提下的。这种状况在新生代常常产生,然而在老年代更常见的状况是大部分对象都是存活对象。如果仍然应用复制算法,因为存活的对象较多,复制的老本也将很高。
标记-压缩算法是一种老年代的回收算法,它在标记-革除算法的根底上做了一些优化。首先也须要从根节点开始对所有可达对象做一次标记,但之后,它并不简略地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种办法既防止了碎片的产生,又不须要两块雷同的内存空间,因而,其性价比比拟高。
增量算法
增量算法的根本思维是,如果一次性将所有的垃圾进行解决,须要造成零碎长时间的进展,那么就能够让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。顺次重复,直到垃圾收集实现。应用这种形式,因为在垃圾回收过程中,间断性地还执行了利用程序代码,所以能缩小零碎的进展工夫。然而,因为线程切换和上下文转换的耗费,会使得垃圾回收的总体成本上升,造成零碎吞吐量的降落。
垃圾回收器
Serial收集器
Serial收集器是最古老的收集器,它的毛病是当Serial收集器想进行垃圾回收的时候,必须暂停用户的所有过程,即stop the world。到当初为止,它仍然是虚拟机运行在client模式下的默认新生代收集器,与其余收集器相比,对于限定在单个CPU的运行环境来说,Serial收集器因为没有线程交互的开销,分心做垃圾回收天然能够取得最高的单线程收集效率。
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,应用”标记-整顿“算法。这个收集器的次要意义也是被Client模式下的虚拟机应用。在Server模式下,它次要还有两大用处:一个是在JDK1.5及以前的版本中与Parallel Scanvenge收集器搭配应用,另外一个就是作为CMS收集器的后备预案,在并发收集产生Concurrent Mode Failure的时候应用。
通过指定-UseSerialGC参数,应用Serial + Serial Old的串行收集器组合进行内存回收。
ParNew收集器
ParNew收集器是Serial收集器新生代的多线程实现,留神在进行垃圾回收的时候仍然会stop the world,只是相比拟Serial收集器而言它会运行多条过程进行垃圾回收。
ParNew收集器在单CPU的环境中相对不会有比Serial收集器更好的成果,甚至因为存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百的保障能超过Serial收集器。当然,随着能够应用的CPU的数量减少,它对于GC时系统资源的利用还是很有益处的。它默认开启的收集线程数与CPU的数量雷同,在CPU十分多(譬如32个,当初CPU动辄4核加超线程,服务器超过32个逻辑CPU的状况越来越多了)的环境下,能够应用-XX:ParallelGCThreads参数来限度垃圾收集的线程数。
-UseParNewGC: 关上此开关后,应用ParNew + Serial Old的收集器组合进行内存回收,这样新生代应用并行收集器,老年代应用串行收集器。
Parallel Scavenge收集器
Parallel是采纳复制算法的多线程新生代垃圾回收器,仿佛和ParNew收集器有很多的类似的中央。然而Parallel Scanvenge收集器的一个特点是它所关注的指标是吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的工夫与CPU总耗费工夫的比值,即吞吐量=运行用户代码工夫 / (运行用户代码工夫 + 垃圾收集工夫)。进展工夫越短就越适宜须要与用户交互的程序,良好的响应速度可能晋升用户的体验;而高吞吐量则能够最高效率地利用CPU工夫,尽快地实现程序的运算工作,次要适宜在后盾运算而不须要太多交互的工作。
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,采纳多线程和”标记-整顿”算法。这个收集器是在jdk1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器始终处于比拟难堪的状态。起因是如果新生代Parallel Scavenge收集器,那么老年代除了Serial Old(PS MarkSweep)收集器外别无选择。因为单线程的老年代Serial Old收集器在服务端利用性能上的”连累“,即便应用了Parallel Scavenge收集器也未必能在整体利用上取得吞吐量最大化的成果,又因为老年代收集中无奈充分利用服务器多CPU的解决能力,在老年代很大而且硬件比拟高级的环境中,这种组合的吞吐量甚至还不肯定有ParNew加CMS的组合”给力“。直到Parallel Old收集器呈现后,”吞吐量优先“收集器终于有了比拟货真价实的利用恭喜,在重视吞吐量及CPU资源敏感的场合,都能够优先思考Parallel Scavenge加Parallel Old收集器。
-UseParallelGC: 虚拟机运行在Server模式下的默认值,关上此开关后,应用Parallel Scavenge + Serial Old的收集器组合进行内存回收。-UseParallelOldGC: 关上此开关后,应用Parallel Scavenge + Parallel Old的收集器组合进行垃圾回收
CMS收集器
CMS(Concurrent Mark Swep)收集器是一个比拟重要的回收器,当初利用十分宽泛,咱们重点来看一下,CMS一种获取最短回收进展工夫为指标的收集器,这使得它很适宜用于和用户交互的业务。从名字(Mark Swep)就能够看出,CMS收集器是基于标记革除算法实现的。它的收集过程分为四个步骤:
- 初始标记(initial mark)
- 并发标记(concurrent mark)
- 从新标记(remark)
- 并发革除(concurrent sweep)
留神初始标记和从新标记还是会stop the world,然而在消耗工夫更长的并发标记和并发革除两个阶段都能够和用户过程同时工作。
G1收集器
G1收集器是一款面向服务端利用的垃圾收集器。HotSpot团队赋予它的使命是在将来替换掉JDK1.5中公布的CMS收集器。与其余GC收集器相比,G1具备如下特点:
并行与并发:G1能更充沛的利用CPU,多核环境下的硬件劣势来缩短stop the world的进展工夫。
分代收集:和其余收集器一样,分代的概念在G1中仍然存在,不过G1不须要其余的垃圾回收器的配合就能够单独治理整个GC堆。
空间整合:G1收集器有利于程序长时间运行,调配大对象时不会无奈失去间断的空间而提前触发一次GC。
可预测的非进展:这是G1绝对于CMS的另一大劣势,升高进展工夫是G1和CMS独特的关注点,能让使用者明确指定在一个长度为M毫秒的工夫片段内,耗费在垃圾收集上的工夫不得超过N毫秒。
CMS:采纳标记革除算法
解决这个问题的方法就是能够让CMS在进行肯定次数的Full GC(标记革除)的时候进行一次标记整顿算法,CMS提供了以下参数来管制:
-XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5
也就是CMS在进行5次Full GC(标记革除)之后进行一次标记整顿算法,从而能够管制老年带的碎片在肯定的数量以内,甚至能够配置CMS在每次Full GC的时候都进行内存的整顿。