jvm面试终极37问
入门部分
为什么要学习jvm?
答:
1.学习JVM,能帮助我们更好地了解java这门语言,理解java语言底层的一些知识,因为很多时候要解决一个问题的话就必须深入到字节码层次去分析,这样才能得到准确的结论,而字节码就是JVM的一部分。
2.学习JVM,同样也是为了项目上线后去排查一些程序l在og日志中无法呈现的问题,比如内存溢出、GC太过频繁导致的高延迟问题,这时候就需要去看GC日志了。
你了解哪些jvm产品?
答:HotSpot,JRockit,J9,TaobaoVM,Dalvik
jvm的构成有哪几部分?
答:
1.Java类加载子系统:负责加载类到内存。
2.运行时数据区:负责存储数据信息和对象。
3.执行引擎:负责解释执行字节码,执行GC操作等。
4.本地库接口:负责融合不同的编程语言为java所用。
类加载部分
你知道哪些类加载器?
答:
1.应用类加载器AppClassLoader
2.扩展类加载器ExtClassLoader
3.根类加载器BootstrapClassLoader
什么是双亲委派加载模型?
答:
1.如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
2.如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
3.如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
4.父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常。
双亲委派方式加载类有哪些优势和劣势?
答:
优势:基于这种双亲委派机制实现了类加载时的优先级层次关系,同时也可以保证同一个类只被一个加载器加载(例如Object类只会被BootstrapClassLoader加载),这样更有利于java程序的稳定运行。
劣势:检查类是否加载的委派过程是单向的,这个方式虽然从结构上说比较清晰,使各个ClassLoader的职责非常明确,但是同时也会带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader所加载的类。
描述一下类加载的基本步骤是什么?
答:
1.通过一个类的全限定名(类全名)来获取其定义的二进制字节流。
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3.在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
什么情况下会触发类的加载?
答:
1.访问类的静态成员(例如类变量,静态方法)
2.构建类的实例对象(例如使用new 关键字构建对象或反射构建对象)
3.构建子类实例对象(构建类的对象时首先会加载父类类型)
类加载时静态代码块一定执行吗?
答:
不一定执行,类加载时是否执行静态代码块取决于加载的方式,类加载的方式有很多种,并不是每一种都能执行静态代码块。
如何理解类的主动加载和被动加载?
答:
Java类在加载时,其初始化时机,可从如下两种方式进行分析:
主动使用:会执行加载、连接、初始化静态域
被动使用:只执行加载、连接,不初始化类静态域
为什么要自己定义类加载器,如何定义?
答:
为什么我们要自己定义类加载器呢?在Java的日常应用程序开发中,类的加载几乎是由JDK默认提供的类加载器相互配合来完成类的加载的,但我们也可以自定义类加载器,来定制类的加载方式。例如:
1.修改类的加载方式(打破类的双亲委派模型)
2.扩展加载源(例如从数据库中加载类)
3.防止源码泄漏(对字节码文件进行加密,用时再通过自定义类加载器对其进行解密)
4.隔离类的加载(不同框架有相同全限定名的类)
如何创建自定义类加载器呢?第一种简单的方式就是继承URLClassLoader,此类可以直接从指定目录、jar包、网络中加载指定的类资源。第二种方式我们可以通过继承java.lang.ClassLoader抽象类的方式,实现自己的类加载器,以满足一些特定的需求,通常建议把自定义的类加载逻辑写在findClass方法中。
字节码增强部分
为何要学习字节码?
答:
对于开发人员,了解字节码可以更准确、直观地理解Java语言中更深层次的东西,比如通过字节码,可以很直观地看到Volatile关键字如何在字节码上生效。另外,字节码增强技术在Spring AOP、各种ORM框架、热部署中的应用屡见不鲜,深入理解其原理对于我们来说大有裨益。除此之外,由于JVM规范的存在,只要最终可以生成符合规范的字节码就可以在JVM上运行,因此这就给了各种运行在JVM上的语言(如Scala、Groovy、Kotlin)一种契机,可以扩展Java所没有的特性或者实现各种语法糖。理解字节码后再学习这些语言,可以“逆流而上”,从字节码视角看它的设计思路,学习起来也“易如反掌”。
如何解读字节码内容?
答:
1.直接解读:java文件的源代码编译后,可以通过notepad++(需要安装一下HEX-Editor插件)打开字节码文件,文件内容默认是一种16进制的格式。JVM对于字节码是有规范要求的,看似杂乱的十六进制是符合一定结构规范的。JVM规范要求每一个字节码文件都要按照固定的顺序组成。
2.jclasslib插件应用:如果每次查看反编译后的字节码都使用javap命令的话,会非常繁琐。这里推荐一个Idea插件,这个插件的名字为jclasslib。代码在编译后,我们可以在菜单栏”View”中选择”Show Bytecode With jclasslib”,可以很直观地看到当前字节码文件的类信息、常量池、方法区等信息。
字节码内容由哪几部分构成?
答:
一个class类文件的结构组成如下:
magic(魔数)
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](属性数组)
什么是字节码增强?,为什么要进行字节码增强?
答:
1.字节码增强技术相当于是一把打开运行时JVM的钥匙,利用它可以对现有字节码进行修改或者动态生成新的字节码,进而对运行中的程序做修改,实现热部署。也可以跟踪JVM运行中程序的状态,进行性能诊断等。
2.此外,我们平时使用的动态代理、AOP也与字节码增强密切相关,它们实质上还是利用各种手段生成符合规范的字节码文件。综上所述,掌握字节码增强后可以高效地定位并快速修复一些棘手的问题(如线上性能问题、方法出现不可控的出入参需要紧急加日志等问题),也可以在开发中减少冗余代码,大大提高开发效率。
你了解哪些字节码增强技术?
答:
ASM技术,javassist技术、Java Agent技术
什么是热替换以及具体如何实现?
答:
1.java的热替换,是指程序在运行的时候,对内存的方法区中的类定义进行替换。因为堆中的 Class 对象是对方法区对象的封装,所以可以理解为对Class对象的替换,当一个Class被替换后,系统无需重启,替换的类会立即生效。
2.实现类的第一步就是动态编译,将.java文件编译成.class文件,第二步就是动态加载,编写一个自定义的类加载器,将编译好的类加载到运行环境中去。
关于热替换的详细介绍可以参考这篇博客:
java的热替换
JVM运行内存部分
JVM的运行内存是如何划分的?
答:
包括堆,栈、方法区和程序计数器
JVM的程序计数器用于做什么?
答:
1.程序计数器(Program Counter Register)也称之为PC寄存器,是一块较小的内存空间,用来存储指向下一条指令的地址,也可以看作是当前线程执行的字节码的行号指示器。
2.在虚拟机的概念模型里,字节码解析器的工作是通过改变这个计数器的值来选取下一条 要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
JVM的虚拟机栈结构是怎样的?
答:
1.Java 虚拟机栈(Java Virtual Machine Stacks)描述的是 Java 方法执行时的内存模型,解决的是程序运行时数据的操作问题,即程序如何执行,或者
说如何处理数据。而堆解决的是数据存储的问题,即数据怎么放,放哪里。
2.由于跨平台性的设计,Java的指令都是根据栈来设计的。优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
3.Java 虚拟机栈是线程私有的,每个方法在被线程调用时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用直至执行完成的过程,都对应着一个栈帧在虚拟机栈中入栈到出栈的过程。栈的生命周期和线程一致,也就是线程结束了,该虚拟机栈也销毁了。
4.如果线程请求的栈深度大于虚拟机所允许的栈深度就会抛出 StackOverflowError 异常。如果虚拟机是可以动态扩展的,如果扩展时无法申请到足够的内存就会抛出 OutOfMemoryError 异常。
5.栈桢构成分析
每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在,在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。每个栈帧中存储着:
1)局部变量表(Local Variables)
2)操作数栈(Operand Stack)(或表达式栈)
3)动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
4)方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
5)一些附加信息
总之,栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
JVM的虚拟机栈的局部变量表的作用是什么?
答:
1.局部变量表也称之为局部变量数组或本地变量表,用于存放方法参数和方法内部定义的局部变量信息。在Java程序被编译为Class文件时,就已经确定了每个方法所需局部变量表的大小。
2.局部变量表以变量槽为最小单位,每个变量槽可以存放一个32位以内的数据类型,故每个变量槽都应该能存放 boolean、byte、char、short、int、float、refrence或returnAddress类型的数据,对于long、double两种,会占用两个变量槽。
JVM的虚拟机栈的操作数栈的作用是什么?
答:
1.每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last - In - First -Out)的 操作数栈(Operand Stack)。
2.操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)。
JVM的堆的构成是怎样的?
答:
1.Java堆(Java Heap)是JVM 中内存最大的一块,被所有线程共享,在虚拟机启动 时创建,主要用于存放对象实例,大部分对象实例也都是在这里分配。但是,随着JIT编译器的发展和逃逸分析技术的逐渐成熟,栈上分配、标量替换优化的技术将会导致一些微妙的变化,所有的对象都分配在堆上渐渐变得不那么绝对了。小对象未逃逸还可以在直接在栈上分配。
2.JAVA堆内存在JVM中可分为年轻代和老年代。年轻代又分为Eden和两个Survivor区。
JVM对象分配内存的过程是怎样的?
答:
基于堆的内存架构,对象内存分配过程如下:
1.编译器通过逃逸分析(JDK8已默认开启),确定对象是在栈上分配还是在堆上分配。
2.如果是在堆上分配,则首先检测是否可在TLAB(Thread Local Allocation Buffer)上直接分配。
3.如果TLAB上无法直接分配则在Eden加锁区进行分配(线程共享区)。
4.如果Eden区无法存储对象,则执行Yong GC(Minor Collection)。
5.如果Yong GC之后Eden区仍然不足以存储对象,则直接分配在老年代。
6.新生代由Eden 区和两个幸存区构成(假定为s1,s2), 任意时刻至少有一个幸存区是空的(empty),用于存放下次GC时未被收集的对象。
7.GC触发时Eden区所有”可达对象”会被复制到一个幸存区,假设为s1,当幸存区s1无法存储这些对象时会直接复制到老年代。
8.GC再次触发时Eden区和s1幸存区中的”可达对象”会被复制到另一个幸存区s2,同时清空eden区和s1幸存区。
9.GC再次触发时Eden区和s2幸存区中的”可达对象”会被复制到另一个幸存区s1,同时清空eden区和s2幸存区.依次类推。
10.当多次GC过程完成后,幸存区中的对象存活时间达到了一定阀值(可以用参数 -XX:+MaxTenuringThreshold 来指定上限,默认15),会被看成是“年老”的对象然后直接移动到老年代。
JVM年轻代幸存区设置的比较小会有什么问题?
答:
Eden区被回收时,对象要拷贝到幸存区,假如幸存区比较小,拷贝的对象比较大,对象就会直接存储到老年代,这样会增加老年代GC的频率。而分代回收的思想就会被弱化。
JVM年轻代Eden区设置的比较小会有什么问题?
答;
Eden区设置的比较小,会增加GC的频率,可能会导致STW的时间边长,影响系统性能。
JVM堆内存为什么要分为年轻代和老年代?
答:
因为老年代和年轻代两个区的存储对象特性不一样。Young区存储的就是那些生命周期短,使用一两次就不再使用的对象,回收一次基本上该区域十之有八的对象全部被回收清理掉,因此Young区采用的垃圾回收算法也就是“标记-复制”算法。Old区存储的是那些生命周期长,经过多次回收后仍然存活的对象,就把它们放到Old区中,Old区一般不去判断这些对象的可达性,直到Old区不够用为止,再进行一次统一的回收,释放出足够的连续的内存空间。所以选择“标记-清除”或“标记-整理”算法进行垃圾收集。所以区分为年轻代和老年代为了更好的实现垃圾回收。
如何理解JVM方法区以及它的构成是怎样的?
答:
方法区(Methed Area)是一种规范,用于存储已被虚拟机加载的类信息、常 量、静态变量、即时编译后的代码等数据。不同jdk,方法区的实现不同,HotSpot 虚拟机在 JDK 8 中使 用 Native Memory 来实现方法区。
方法区构成分析:
方法区(Methed Area)是一种规范,用于存储已被虚拟机加载的类信息、常 量、静态变量、即时编译后的代码等数据。
1)类信息包括对每个加载的类型(类class、接口interface、枚举enum、注解annotation)以及属性和方法信息。
2)常量信息可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。
什么是逃逸分析以及可以解决什么问题?
答:
逃逸分析一种数据分析算法,基于此算法可以有效减少Java对象在堆内存中的分配。Hotspot虚拟机的编译器能够分析出一个新对象的引用范围,然后决定是否要将这个对象分配到堆上。例如:
1)当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
2)当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。
使用逃逸分析,编译器可以对代码做如下优化:
1)栈上分配:将堆分配转化为栈分配。如果一个对象在方法内创建,要使指向该对象的引用不会发生逃逸,对象可能是栈上分配的候选。
2)同步省略:如果一个对象被发现只有一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
3) 分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
何为内存溢出以及导致内存溢出的原因?
答:
内存中剩余的内存不足以分配给新的内存请求就会内存溢出。内存溢出可能直接导致系统崩溃。
原因:
1)创建的对象太大导致堆内存溢出
2)创建的对象太多导致堆内存溢出
3)方法出现了无限递归调用导致栈内存溢出
4)方法区内存空间不足导致内存溢出。
5)内存泄露也会导致最后内存溢出。
何为内存泄露以及导致内存泄露的原因?
答:
动态分配的内存空间,在使用完毕后未得到释放,结果导致一直占据该内存单元,直到程序结束。这个现象称之为内存泄漏。
原因:
1)大量使用静态变量(静态变量与程序生命周期一样)
2)IO/连接资源用完没关闭(记得执行close操作)
3)内部类的使用方式存在问题(实力内部类或默认引用外部类对象)
4)缓存(Cache)应用不当(尽量不要使用强引用)
5)ThreadLocal应用不当(用完记得执行remove操作)
JAVA的四大引用你知道多少?
答:
强引用、软引用、弱引用、虚引用4种,这4种引用强度依次逐渐减弱。强引用就是指在程序代码中普遍存在的,类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
1.强引用:通过关键字new创建的对象所关联的引用就是强引用。
2.软引用:软引用和强引用不同,如果内存空间足够多,一个对象被软引用,则垃圾回收器不会将其回收;如果内存空间不足,这些引用对象就会被回收。所以,软引用就是当回收器没有回收某个对象时,程序就可以对其使用。
3.弱引用:弱引用用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。
4.虚引用:不影响对象的生命周期,如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。
JVM垃圾回收部分
何为GC以及为何要GC?
答:
GC(Garbage Collection)称之为垃圾回收,是对内存中的垃圾对象,采用一定的算法进行内存回收的一个动作。比方说,java中的垃圾回收会对内存中的对象进行遍历,对存活的对象进行标记,其未标记对象可认为是垃圾对象,然后基于特定算法进行回收。
为何要学习GC
深入理解GC的工作机制,可以帮我们写出更好的Java应用,提高开发效率,同时也是进军大规模应用开发的一个前提。
你知道哪些GC算法?
答:
标记清除:标记清除(Mark-Sweep)算法分为“标记”和“清除”阶段,它首先会标记出内存中所有不需要回收的对象,然后从内存中清除所有未标记的对象。此垃圾回收算法适合对象存活率较高的的内存区域(比方说JVM中的老年代)。
标记复制:标记复制(Mark-Copy)算法是将内存分为大小相同的两块,当这一块使用完了,就把当前存活的对象复制到另一块,然后一次性清空当前区块。“标记-复制”算法内存空间利用率低,适用于那些对象生命周期短、回收频率高的内存区域(比方说JVM中的年轻代)。
标记整理:标记整理清除(Mark-Sweep-Compact)算法结合了“标记-清除”和“复制”两个算法的优点。第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把存活对象“压缩”复制到堆的其中一块空间中,按顺序排放。第三阶段清理掉存活边界以外的全部内存空间。标记整理算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题,由于需要向一侧移动等一系列操作,其效率相对低一些,但对内存空间管理上十分优异。适用于那些生命周期长、回收频率低,但注重回收一次内存空间得到足够释放的场景。
分代回收?
答:
内存中的对象通常可以将其分为两大类:
1.存活时间短(这样的对象比较多)
2.存活时间长(这样的对象比较少)
于是提出分代回收的思想,将VM中的内存分为年轻代(Young Generation)和老年代(Old Generation)
Young区存储的就是那些生命周期短,使用一两次就不再使用的对象,回收一次基本上该区域十之有八的对象全部被回收清理掉,因此Young区采用的垃圾回收算法也就是“标记-复制”算法。Old区存储的是那些生命周期长,经过多次回收后仍然存活的对象,就把它们放到Old区中,Old区一般不去判断这些对象的可达性,直到Old区不够用为止,再进行一次统一的回收,释放出足够的连续的内存空间。所以我们选择“标记-清除”或“标记-整理”算法进行垃圾收集。
在分代回收过程中,垃圾收集事件(Garbage Collection events)通常分为:
Minor GC (小型GC):年轻代GC事件,(新对象)分配频率越高, Minor GC 的频率就越高。
Major GC (大型GC): 老年代GC事件。
Full GC (完全GC):整个堆的GC事件。
一般情况下可以将Major GC与Full GC看成是同一种GC。
JVM中有哪些垃圾回收器?
答:
Serial收集器:Serial GC是最古老也是最基本的收集器,但是现在依然广泛使用。
Parallel收集器:Parallel 收集器为并行收集器,它可利用多个或多核CPU优势实现多线程并行GC操作,其目标是减少停顿时间,实现更高的吞吐量(Throughput)。
CMS 收集器:CMS的官方名称为 “Mostly Concurrent Mark and Sweep Garbage Collector”,其设计目标是追求更快的响应时间。
G1收集器:G1(Garbage-First )收集器是一种工作于服务端模式的垃圾回收器,主要面向多核,大内存的服务器。G1 在实现高吞吐的同时,也最大限度满足了GC 停顿时间可控的目标。
服务频繁fullgc,younggc次数较少,可能原因?
答:
原因一:系统承载高并发请求,或者处理数据量过大,导致Young GC很贫乏,而且每次Young GC过后存活对象太多,内存分配不合理,Survivor区过小,导致对象频繁进入老年代,频繁触发Full GC。
原因二:系统一次性加载过多数据进内存,搞出来很多大对象,导致频繁有大对象进入老年带,必然频繁触发Full GC。
原因三:系统发生了内存泄漏,莫名其妙创建大量的对象,始终无法回收,一直占用在老年代里,必然频繁触发Full GC。
原因四:Metaspace(永久代)因为加载类过多触发Full GC。
原因五:误调用System.gc()触发Full GC。