关于JVM知识点的面试题
- 请谈谈你对JVM的理解?java8的虚拟机有什么更新?
- 什么是OOM?什么是StackOverflowError?有哪些方法分析?
- JVM的常用参数调优你知道哪些?
- 谈谈JVM中,对类加载器你的认识?
JVM体系总体分四大块:
- jvm内存结构
- 类的加载机制
- GC算法 垃圾回收
- GC分析 命令调优
1. JVM的位置
JVM是运行在操作系统之上的,它与硬件没有直接的交互
2. jvm内存结构
主要关注点:
- jvm内存结构都是什么
- 对象分配规则
方法区和对是所有线程共享的内存区域;而java栈、本地方法栈和程序员计数器是运行是线程私有的内存区域。
类装载器ClassLoader
:负责加载class文件,class文件在文件开头有特定的文件标示,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定Java堆(Heap),是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
方法区(Method Area),方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它存储了每一个类的结构信息,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。上面讲的是规范,在不同虚拟机里头实现是不一样的,最典型的就是(JDK1.7及以前)永久代(PermGen space)和(JDK1.8)元空间(Metaspace)。
程序计数器(Program Counter Register),程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。
JVM栈(JVM Stacks),与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表(8种基本数据类型)、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。栈的大小和具体JVM的实现有关,通常在256K~756K之间,与等于1Mb左右。
本地方法栈(Native Method Stacks),它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。如Thread类中的
private native void start0();
, 当程序中写了new Thread().start();
代码,不会立即执行,而是等待CPU的调度执行的Execution Engine执行引擎负责解释命令,提交操作系统执行。
本地接口(Native Interface):本地接口的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++程序,Java 诞生的时候是 C/C++横行的时候,要想立足,必须有调用 C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是
Native Method Stack
中登记native
方法,在Execution Engine
执行时加载native libraies
。与本地库不属于jvm,属于操作系统
栈运行原理
栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧 F1,并被压入到栈中,
A方法又调用了 B方法,于是产生栈帧 F2 也被压入栈,
B方法又调用了 C方法,于是产生栈帧 F3 也被压入栈,
……执行完毕后,先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧……
遵循“先进后出”/“后进先出”原则。
栈+堆+方法区的交互关系
对象分配规则
- 对象优先分配在Eden(伊甸)区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
- 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)
- 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值(15)对象进入老年区。
- 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
- 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。
3. 类的加载机制
主要关注点:
- 什么是类的加载
- 类的生命周期
- 类加载器
- 双亲委派模型
什么是类的加载
类的加载指的是将类的.class
文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class
对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class
对象,Class
对象封装了类在方法区内的数据结构,并且向Java
程序员提供了访问方法区内的数据结构的接口且ClassLoader
只负责class
文件的加载,至于它是否可以运行,则由Execution Engine
决定
类的生命周期
类的生命周期包括这几个部分,加载、连接、初始化、使用和卸载,其中前三部是类的加载的过程,如下图;
- 加载,查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象
- 连接,连接又包含三块内容:验证、准备、初始化。
- 验证,文件格式、元数据、字节码、符号引用验证;
- 准备,为类的静态变量分配内存,并将其初始化为默认值;
- 解析,把类中的符号引用转换为直接引用
- 初始化,为类的静态变量赋予正确的初始值
- 使用,new出对象程序中使用
- 卸载,执行垃圾回收
类加载器
- 启动类加载器(
Bootstrap ClassLoader
)C++:负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库 - 扩展类加载器(
Extension ClassLoader
)Java:该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。 - 应用程序类加载器(
Application ClassLoader
)Java: 该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器
Code测试
1 | public class ClassLoaderTest { |
RUN
1 | null |
sun.misc.Launcher
它是一个java虚拟机的入口应用
类加载机制
- 全盘负责,当一个类加载器负责加载某个
Class
时,该Class
所依赖的和引用的其他Class
也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入 - 双亲委派,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
- 缓存机制,缓存机制将会保证所有加载过的
Class
都会被缓存,当程序中需要使用某个Class
时,类加载器先从缓存区寻找该Class
,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class
对象,存入缓存区。这就是为什么修改了Class
后,必须重启JVM
,程序的修改才会生效
4. GC算法 垃圾回收
主要关注点:
- 什么事GC
- 对象存活判断
- GC算法
- 垃圾回收器
什么事GC
GC是什么(分代收集算法)
- 次数上频繁收集Young区
- 次数上较少收集Old区
- 基本不动元空间
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。因此GC按照回收的区域又分了两种类型,一种是普通GC(minor GC)
,一种是全局GC(major GC or Full GC)
Minor GC和Full GC的区别
- 普通GC(minor GC):只针对新生代区域的GC,指发生在新生代的垃圾收集动作,因为大多数Java对象存活率都不高,所以Minor GC非常频繁,一般回收速度也比较快。
- 全局GC(major GC or Full GC):指发生在老年代的垃圾收集动作,出现了Major GC,经常会伴随至少一次的Minor GC(但并不是绝对的)。Major GC的速度一般要比Minor GC慢上10倍以上
对象存活判断
判断对象是否存活一般有两种方式:
- 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
- 可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象。
GC算法
GC
最基础的算法有三种:标记 -清除算法、复制算法、标记-压缩算法,我们常用的垃圾回收器一般都采用分代收集算法。
- 标记-清除算法,“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。老年代一般是由标记清除或者是标记清除与标记整理的混合实现
- 复制算法,“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。年轻代中使用的是Minor GC,这种GC算法采用的是复制算法(Copying)
- 标记-压缩算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
- 分代收集算法,“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
垃圾回收器
- Serial收集器,串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。
- ParNew收集器,ParNew收集器其实就是Serial收集器的多线程版本。
- Parallel收集器,Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。
- Parallel Old 收集器,Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法
- CMS收集器,CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
- G1收集器,G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征