JVM面试37问
1入门部分
1.1为什么要学习JVM?
深入理解JVM可以帮助我们从平台角度提高解决问题的能力
1、有效防止内存泄漏(Memory leak)。
2、优化线程锁的使用(Thread Lock)
3、科学进行垃圾回收(Garbage collection)
4、提高系统吞吐量(throughput)
5、降低延迟(Delay) 提高其性能(perfoxmance)
1.2你了解哪些JVM产品?
1、HotSpot VM (目前应用最官方,最主流的JVM虚拟机)(Sun公司研发,后在2010年由Oracle公司收购)。
2、JRockit VM (BEA公司研发,后在2008年由Oracle公司收购)。
3、J9 VM (IBM 内部使用)。
4、TaobaoJVM(AliJVM团队开发,基于OpenJDK开发了AlibabaJDK)
1.3JVM的构成有哪几部分?
1.类加载系统(ClassLoader System):负责加载类到内存。
2.运行时数据区(Runtime Date Area):负责存储数据信息(对象、方法)。
3.执行引擎(Execution Engine) 负责解释执行字节码、执行GC操作等。
4.本地库接口(Native Interface)负责融合不同的编程语言为Java所用。
2类加载部分
2.1你知道哪些类加载器?
启动类加载器(BootstrapClassLoader)
负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如 rt.jar、charsets.jar等
扩展类加载器(ExClassLoader)
负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR 类包
应用程序加载器(AppClassLoader)
负责加载ClassPath路径下的类包,主要就是加载你自己写的那 些类 自定义加载器:负责加载用户自定义路径下的类包
还可以自一些定义类加载器(继承URLClassLoader/ClassLoader)
2.2什么是双亲委派类加载模型?
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
2.3双亲委派方式加载类有什么优势、劣势?
优势:
1、 避免类的重复加载,确保一个类的全局唯一性。Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种关系可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
2、 保护程序安全,防止核心API被随意篡改。
劣势:
检查类是否加载的委托过程是单向的,这个方式虽然从结构上说比较清晰,使各个ClassLoader的职责非常明确,但是同时会带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader所加载的类。
通常情况下,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中,为应用类。
按照这种模式,应用类访问系统类自然是没有问题,但是系统类访问应用类就会出问题。比如在系统类中提供了一个接口,该接口需要在应用中得以实现,该接口还绑定一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中。这时,就会出现该工厂方法无法创建由应用类加载器加载的应用实例的问题。
2.4描述一些类加载时候的基本步骤是怎样的?
加载 —> 验证 —> 准备 —> 解析 —> 初始化
加载
加载,是指Java虚拟机查找字节流(查找.class文件),并且根据字节流创建java.lang.Class对象的过程。这个过程,将类的.class文件中的二进制数据读入内存,放在运行时区域的方法区内。然后在堆中创建java.lang.Class对象,用来封装类在方法区的数据结构。
类加载阶段:
(1)Java虚拟机将.class文件读入内存,并为之创建一个Class对象。
(2)任何类被使用时系统都会为其创建一个且仅有一个Class对象。
(3)这个Class对象描述了这个类创建出来的对象的所有信息,比如有哪些构造方法,都有哪些成员方法,都有哪些成员变量等。
验证
验证阶段作用是保证Class文件的字节流包含的信息符合JVM规范,不会给JVM造成危害。如果验证失败,就会抛出一个java.lang.VerifyError异常或其子类异常。验证过程分为四个阶段:
文件格式验证:验证字节流文件是否符合Class文件格式的规范,并且能被当前虚拟机正确的处理。
元数据验证:是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言的规范要求
字节码验证:主要是进行数据流和控制流的分析,保证被校验类的方法在运行时不会危害虚拟机。
符号引用验证:符号引用验证发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在解析阶段中发生。
准备
准备阶段为变量分配内存并设置类变量的初始化。在这个阶段分配的仅为类的变量(static修饰的变量),而不包括类的实例变量。对已非final的变量,JVM会将其设置成“零值”,而不是其赋值语句的值:pirvate static int size = 12;。那么在这个阶段,size的值为0,而不是12。但final修饰的类变量将会赋值成真实的值。
解析
解析过程是将常量池内的符号引用替换成直接引用。主要包括四种类型引用的解析。类或接口的解析、字段解析、方法解析、接口方法解析。
初始化
初始化,则是为标记为常量值的字段赋值的过程。换句话说,只对static修饰的变量或语句块进行初始化。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
2.5什么情况下会触发类的加载?
1、创建类的实例,也就是new一个对象
2、访问类的静态变量
3、访问类的静态方法
4、反射,Class.ForName
5、初始化一个类的子类(会首先初始化子类的父类)
6、虚拟机启东时,定义了main()方法的那个类
2.6类加载时静态代码块一定会执行吗?
不一定。类初始化时才会执行静态代码。
2.7如何理解类的主动加载和被动加载?
主动加载:对类进行引用时如果类没有进行过初始化,则先触发其初始化叫做主动引用。有且只有下面情况下才会进行主动引用。
1、遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景用:
使用new关键字实例化对象的时候。
调用类的静态成员(除final修饰)和静态方法。
2、使用java.lang.reflect包的方法对类进行反射调用的时候。
3、当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4、当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
被动加载:除此之外,所有引用类的方式都不会触发初始化,称为被动引用。
1、通过子类引用父类的的静态字段,不会导致子类初始化。
2、通过数组定义来引用类,不会触发此类的初始化。
3、常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
2.8为什么要自己定义类加载器,如何定义?
1、修改类的加载方式(打破类的双亲委派模型)
2、扩展加载源(例如从数据库中加载类)
3、防止源码泄漏(对字节码文件进行加密,用时再通过自定义类加载器对其进行解密)
4、隔离类的加载(不同框架有相同全限定名的类)
定义类加载器:
1、继承URLClassLoader,此类可以直接从指定目录、jar包、网络中加载指定的类资源。URLClassLoader继承ClassLoader,可以从指定目录、jar包、网络中加载指定的类资源,我们自己定义类加载器,最简单的方式就是继承URLClassLoader进行类加载实
2、继承java.lang.ClassLoader抽象类的方式,实现自己的类加载器,以满足一些特殊的需求。建议把自定义的类加载逻辑写在findclass()方法中。
3字节码增强部分
3.1为何要学习字节码?
对于开发人员,了解字节码可以更准确、直观地理解Java语言中更深层次的东西。
比如通过字节码,可以很直观地看到Volatile关键字如何在字节码上生效。
另外,字节码增强技术在Spring AOP、各种O RM框架、热部署中的应用屡见不鲜,深入理解其原理对于我们来说大有裨益。
除此之外,由于JVM规范的存在,只要最终可以生成符合规范的字节码就可以在JVM上运行,因此这就给了各种运行在JVM上的语言(如Scala、Groovy、Kotlin)一种契机,可以扩展Java所没有的特性或者实现各种语法糖。理解字节码后再学习这些语言,可以“逆流而上”,从字节码视角看它的设计思路,学习起来也“易如反掌”。
3.2如何解读字节码内容?
1、直接解读:利用软件工具进行解析(notepad++(需要安装一下HEX-Editor插件))
2、在控制台利用Javap指令进行反编译
3、jclasslib插件应用,可以直接解读字节码的类信息、常量池、方法区等信息
3.3字节码内容由哪几部分构成?
magic(魔术)(ca fa ba be)
minor_version(次版本号)
major_version(主版本号)
constant_pool_count(常量池计数器)
constant_pool[constant_pool_count-1](常量池)
access_flags(类的访问标志)
this_class(当前类名索引值)
super_class(父类名索引值)
interfaces_count(接口计数)
interfaces[interfaces_count](接口数组)
fields_count(成员变量计数)
fields[fields_count](成员变量数组)
methods_count(方法计数)
methods[methods_count](方法数组)
attributes_count(属性计数)
attributes[attributes_count](属性数组)
3.4什么是字节码增强?
字节码增强技术相当于是一把打开运行时JVM的钥匙,利用它可以对现有字节码进行修改或者动态生成新的字节码,进而对运行中的程序做修改,实现热部署。也可以跟踪JVM运行中程序的状态,进行性能诊断等。
此外,我们平时使用的动态代理、AOP也与字节码增强密切相关,它们实质上还是利用各种手段生成符合规范的字节码文件。,
3.5为什么要进行字节码增强?
掌握字节码增强后可以高效地定位并快速修复一些棘手的问题(如线上性能问题、方法出现不可控的出入参需要紧急加日志等问题),也可以在开发中减少冗余代码,大大提高开发效率。
3.6你了解哪些字节码增强技术?
ASM技术 ASM Core API不需要把这个类的整个结构读取进来,就可以用流式的方法来处理字节码文件。好处是非常节约内存,但是编程难度较大。
Javassist是一个用于分析、编辑和创建Java字节码的类库,相比ASM在指令层次上操作字节码会更加简单直观。可以无须关注字节码刻板的结构,直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构或者动态生成类。
Java Agent是Java Instrumentation API的一部分,它提供了向现有已编译的Java类添加字节码的功能,相当于字节码插桩的入口。可以侵入运行在JVM上的应用程序,进而修改应用程序中各种类的字节码。
3.7什么是热替换以及如何实现?
热替换是在不停止正在运行的系统的情况下进行类(对象)的升级替换;
利用asm技术或基于asm技术的Javassist、Java Agent等,动态创建或修改一个.Class文件,然后利用加载器将其重新加载到已经运行的服务器上,将原来的.class文件进行替换
4JVM运行内存部分
4.1JVM运行内存是如何划分的?
方法区(Method Area) 堆区(Heap) 虚拟机栈(VM Stack)
本地方法栈(Native Method Stack) 程序计数器(Program Counter Register)
4.2JVM中的程序计数器用于做什么?
程序计数器(Program Counter Register)也称之为PC寄存器,是一块较小的内存空间,用来存储指向下一条指令的地址,也可以看作是当前线程执行的字节码的行号指示器。
4.3JVM虚拟机栈的结构是怎样的?
每个栈帧中存储着:
1)局部变量表(Local Variables)
2)操作数栈(Operand Stack)(或表达式栈)
3)动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
4)方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
5)一些附加信息
4.4JVM虚拟机栈中局部变量表的作用是什么?
局部变量表也称之为局部变量数组或本地变量表,用于存放方法参数和方法内部定义的局部变量信息。在Java程序被编译为Class文件时,就已经确定了每个方法所需局部变量表的大小。
4.5JVM虚拟机栈中操作数栈的做用时什么?
每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last - In - First -Out)的 操作数栈(Operand Stack)。
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)
4.6JVM堆的构成是怎样的?
堆内存划分新生代和老年代;
新生代又划分为
1、Eden区
2、辛存区0
3、辛存区1
老年代只有老年区
4.7Java对象分配内存的过程是怎样的?
1、编译器通过逃逸分析(JDK8已默认开启),确定对象是在栈上分配还是在堆上分配。
2、如果是在堆上分配,则首先检测是否可在TLAB(Thread Local Allocation Buffer)上直接分配。
3、如果TLAB上无法直接分配则在Eden加锁区进行分配(线程共享区)。
4、如果Eden区无法存储对象,则执行Yong GC(Minor Collection)。
5、如果Yong GC之后Eden区仍然不足以存储对象,则直接分配在老年代。
4.8JVM年轻代幸存区设置的比较小会有什么问题?
伊甸园区被回收时,对象要拷贝到幸存区,假如幸存区比较小,拷贝的对象比较大,对象就会直接存储到老年代,这样会增加老年代GC的频率。而分代回收的思想就会被弱化。
4.9JVM年轻代伊甸园区设置的比例比较小会有什么问题?
伊甸园设置的比较小,会增加GC的频率,可能会导致 的时间边长,影响系统性能。
4.10JVM堆内存为什么要分成年轻代和老年代?
为了更好的实现垃圾回收。
4.11如何理解JVM方法区以及它的构成是怎样的?
方法区(Methed Area)是一种规范,用于存储已被虚拟机加载的类信息、常 量、静态变量、即时编译后的代码等数据。
1)类信息包括对每个加载的类型(类class、接口interface、枚举enum、注解annotation)以及属性和方法信息。
2)常量信息可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。
方法区主要存储类型信息、运行时常量池、静态变量、JIT代码缓存、属性信息、方法信息等
4.12什么是逃逸分析以及可以解决什么问题?
逃逸分析一种数据分析算法,基于此算法可以有效减少Java对象在堆内存中的分配。Hotspot虚拟机的编译器能够分析出一个新对象的引用范围,然后决定是否要将这个对象分配到堆上。例如:
1)当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
2)当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。
使用逃逸分析,编译器可以对代码做如下优化:
1)栈上分配:将堆分配转化为栈分配。如果一个对象在方法内创建,要使指向该对象的引用不会发生逃逸,对象可能是栈上分配的候选。
2)同步省略:如果一个对象被发现只有一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
3) 分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
4.13何为内存溢出以及导致内存溢出的原因?
内存溢出:内存中剩余的内存不足以分配给新的内存请求就会内存溢出。内存溢出可能直接导致系统崩溃。
导致内存溢出的原因:
内存泄漏是导致内存溢出的一种原因,但内存溢出不全是由内存泄漏引起的,还可能是:
1)创建的对象太大导致堆内存溢出
2)创建的对象太多导致堆内存溢出
3)方法出现了无限递归调用导致栈内存溢出
4)方法区内存空间不足导致内存溢出。
4.14何为内存泄漏以及内存泄漏的原因是什么?
动态分配的内存空间,在使用完毕后未得到释放,结果导致一直占据该内存单元,直到程序结束。这个现象称之为内存泄漏
4.15JAVA中的四大引用你知道多少?
强引用: 通过对象自身类型或父类类型直接引用当前对象称之为强引用,使用"强引用"引用的对象,即便JVM内存空间不足,触发了GC操作,JVM宁愿抛出OutOfMemoryError运行时错误,也不会回收有强引用引用的对象。
软引用:通过SoftReference类型对象实现。"软引用"引用的对象,在JVM认为内存不足时,会去试图回收其引用的对象。即JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果“软引用”所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。后续,我们可以调用ReferenceQueue的poll()方法来检查是否有它所关心的对象被回收。
弱引用:通过WeakReference类实现。当GC时,;‘’一旦发现了具有“弱引用”引用的对象,不管当前内存空间足够与否,都可能会回收它的内存。由于垃圾回收器是一个优先级很低的线程,因此不一定会很快回收弱引用的对象。弱引用也可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
虚引用:也叫幻象引用,通过PhantomReference类对象来实现。无法通过虚引用访问对象的任何属性或函数。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
5JVM垃圾回收部分 5.1何为GC以及为和要GC? 何为GC: GC(Garbage Collection)称之为垃圾回收,是对内存中的垃圾对象,采用一定的算法进行内存回收的一个动作。比方说,java中的垃圾回收会对内存中的对象进行遍历,对存活的对象进行标记,其未标记对象可认为是垃圾对象,然后基于特定算法进行回收。 GC的作用 深入理解GC的工作机制,可以帮你写出更好的Java应用,提高开发效率,同时也是进军大规模应用开发的一个前提。 5.2你知道哪些GC算法? 标记清除(Mark-Sweep)算法分为“标记”和“清除”阶段,它首先会标记出内存中所有不需要回收的对象,然后从内存中清除所有未标记的对象。 标记复制(Mark-Copy)算法是将内存分为大小相同的两块,当这一块使用完了,就把当前存活的对象复制到另一块,然后一次性清空当前区块。 标记整理清除(Mark-Sweep-Compact)算法结合了“标记-清除”和“复制”两个算法的优点。第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把存活对象“压缩”复制到堆的其中一块空间中,按顺序排放。第三阶段清理掉存活边界以外的全部内存空间。 5.3JVM中有哪些垃圾回收器? Serial GC(串行收集器) Parallel GC(并行收集器) CMS (并发收集器) G1(Garbage-First )收集器 5.4服务频繁fullgc,younggc次数较少,可能原因? 原因一:系统承载高并发请求,或者处理数据量过大,导致Young GC很贫乏,而且每次Young GC过后存活对象太多,内存分配不合理,Survivor区过小,导致对象频繁进入老年代,频繁触发Full GC。 原因二:系统一次性加载过多数据进内存,搞出来很多大对象,导致频繁有大对象进入老年带,必然频繁触发Full GC。 原因三:系统发生了内存泄漏,创建大量的对象,始终无法回收,一直占用在老年代里,必然频繁触发Full GC。 原因四:Metaspace(元空间代)因为加载类过多触发Full GC。



