JVM 面试题
什么是JVM。说一说JVM的主要组成部分
Java虚拟机是一个可以执行Java字节码的虚拟机进程。Java通过Java虚拟机实现平台无关性。
Java源文件被编译成能被Java虚拟机执行的字节码文件。 Java被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。
在JVM中,首先通过类加载器把字节码的二进制字节流加载为运行时的数据区域中的数据结构,然后执行引擎将字节码转换为底层指令交由CPU执行,在这个过程中可能需要调用其他语言的本地库接口来实现程序功能。
Java对象创建过程
- 类加载检测。类是否已经加载过,如果没有需要先加载类。
- 分配内存。使用指针碰撞法和空闲列表法分配内存。对于内存分配的并发问题,采用TLAB和CAS、重试来解决。
- 初始化0值。
- 设置对象头。
- 执行init函数。init函数为初始化代码块和构造函数共同合成的函数。
ThreadLocalAllocationBuffer为每一个线程预先在 Eden 区分配一块内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
你知道哪些JVM性能调优参数?
- 设置堆内存大小: -xms初始, -xmx最大
- 栈大小:-xss
- 设置新生代大小和比例。
- -XX:NewSize:新生代大小
- -XX:NewRatio 新生代和老生代占比
- -XX:SurvivorRatio:伊甸园空间和幸存者空间的占比
什么时候触发 Minor/Full GC
Minor: 新生代内存不够用时
Full:
- 调用System.gc();
- 老年代空间不足。
- 空间分配担保失败。
- JDK 1.7 及以前的永久代内存不足
- CMS 由于浮动垃圾导致并发失败。
发生OOM问题,你会进行如何排查呢?
- uptime
- dmesg | grep -E 'kill|oom|out of memory'
- 重启项目,加上 HeapDumpOnOutOfMemoryError 选项,导出堆内存用 jprofiler 分析。
- top 查看 CPU 内存 占用
- free 查看内存情况
- jstat -gcutil 查看java运行时内存信息。
Java 中的类加载器是什么,讲一下类加载器的分类
加载器作用是通过类名来获取二进制字节流。主要分为四种加载器,启动类->扩展类->应用类->自定义类。
为什么要设置三个类加载器,设置一个的话有什么问题?
- 单一职责原则
- 路径隔离,同名的自定义类
Java 动态加载过程,动态加载的方法?
加载、验证、准备、解析、初始化、使用、卸载
- Class.forName("SomeClass");
- ClassLoader.getSystemClassLoader().loadClass("SomeClass");
区别是,Class.forName 可以初始化类,classloader.loadclass 只是将类加载进JVM里。
Tomcat 怎么实现不同应用的Jar 包不冲突?
在tomcat的类加载器层级中:
- Tomcat 将 Common Classloader 作为 应用类加载器的子类
- Common ClassLoad 子类有 Catalina Classloader 和 shared classloader.
- Shared classloader 的子类为各种 webapp classloader
Tomcat 通过破坏双亲委派机制,让类加载器优先在自己的路径中查找加载类,避免 jar 包冲突。
JVM 加载的一个类过程(类的生命周期)
类加载过程:
- 加载:通过类的全限定名加载类的字节码,然后将字节码中的类静态存储数据转换为运行时的内存数据结构,并且在堆中生成该类的class对象,作为对该类在方法区中数据的访问的接口。
- 验证:验证class文件是否符合规范。
- 准备:在堆中为类变量分配内存空间,并且初始化零值。如果类变量用static final 修饰,会用类字段属性表中的constant_value属性赋值。
- 解析:将类中的部分符号引用转换为直接引用。(解析调用,对象:非虚方法)。
- 初始化:开始执行类中编写的Java代码块中的类初始化部分。
- 使用
- 卸载
触发条件:
- 创建类的实例时
- 访问类的静态方法或静态变量的时候
- 使用Class.forName反射类的时候
- 某个子类初始化的时候
卸载:
Java自带的加载器加载的类,在虚拟机的生命周期中是不会被卸载的,只有用户自定义的加载器加载的类才可以被卸载。
Java 运行时数据区域
JVM运行时数据区域规定了Java运行时内存申请、分配、管理的策略,保证了JVM的高效稳定运行。
- 方法区和对是所有线程共享的内存区域;而java栈、本地方法栈和程序员计数器是运行是线程私有的内存区域。
线程私有的:
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享的:
- 堆
- 方法区
- 直接内存 (非运行时数据区的一部分)
程序计数器
- 对PC寄存器的一种模拟
- 是一小块高速内存空间,可以看作是当前线程所执行的字节码的行号指示器
- 为CPU任务切换恢复服务
错误
- 唯一一个没有定义OOM的区域
虚拟机栈
- Java方法执行的线程内存模型
- 生命周期同线程
- 不存在垃圾回收问题
- 堆栈中的栈就是指的虚拟机栈的局部变量表部分
基本单位为栈帧
- 局部变量表
- 操作栈
- 动态连接
- 返回地址
错误
StackOverFlowError
- 不允许动态扩展栈大小而超过时
OutOfMemoryError
- 允许动态扩展,内存不够时
本地方法栈
- 类似于虚拟机栈,服务于C语言写的本地函数
- Hotspot中,本地方法栈和虚拟机栈合二为一
错误
- StackOverFlowError
- OutOfMemoryError
堆
- 存放对象实例
几乎所有的Java对象在堆中分配
例外
- 基于逃逸分析的优化,如栈上分配、标量替换
补充知识:基于逃逸分析的优化
同步省略(锁消除)
- 同步块仅被单线程访问,忽略同步代码
标量替换
- 对象不会被外部访问,并且可以进一步分解时,就不会创建该对象,而是用成员变量替换,这些成员变量会在栈帧或者寄存器上分配空间
栈上分配
- 一个对象没有逃逸,可能被优化成栈上分配。另外这样的一个好处是不用垃圾回收了
垃圾回收的主要区域
基于分代的垃圾回收
新生代
- Eden
- Survivor
- 老年代
- 永久代(1.8后变成元空间)
TLAB
- Thread Local Allocation Buffer
- 从内存模型而不是垃圾回收的角度,在Eden区为每一个线程分配了一个私有缓存区域。
- 多线程频繁创建实例会导致并发分配内存,在缓内的分配可以避免竞争加锁,提高内存分配的吞吐率
- 堆是逻辑连续的
错误
OOM
- GC Overhead Limit Exceed
Java heap space
- -Xmx
方法区
- 包含虚拟机加载的类信息、常量、静态变量、JIT代码缓存、运行时常量池
包含
Class接口可以访问的信息
- 类信息
- 域信息
- 方法信息
- 类变量
- 常量
运行时的常量池
- 从常量池表加载
- 运行时新增(String.itern())
- JIT代码缓存
异常
- OOM
直接内存
- 一些Java类如NIO会用直接内存作为Buffer提高性能
- NIO:基于通道与缓冲区的IO方式,使用Naive函数直接分配堆外内存,然后通过Java堆里的DirectByteBuffer对象作为这块内存的引用直接进行操作。避免了Java堆和Naive堆之间的复制。
比较
堆与栈
- 堆为存储服务
- 栈为运行服务
元空间与永久代
都是方法区的实现
1.7
永久代
- 占用JVM内存
1.8
元空间
- 直接使用物理内存
1.7 vs 1.8
1.7
- 有永久代
- 类变量和字符串常量池拿到堆中
1.8
用元空间替换永久代
原因
- 永久代占用JVM内存,难以设置大小和调优
位置调整
- 字符串常量池、类变量(静态变量)调整到堆中
- 类信息、常量保存在本地内存的元空间中
对象一定分配在堆中吗?有没有了解逃逸分析技术?
不一定的,JVM通过「逃逸分析」,那些逃不出方法的对象会在栈上分配。
逃逸分析:判断一个对象的引用范围是否局限于本地。
通过逃逸分析发现一个对象不会被外部引用,则可以实现一些优化:
- 栈上分配。
- 同步消除。如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同步
- 标量替换。把对象分解成一个个基本类型,并且内存分配不再是分配在堆上,而是分配在栈上。这样的好处有,一、减少内存使用,因为不用生成对象头。二、程序内存回收效率高,并且GC频率也会减少。
说说堆和栈的区别
- 堆为存储服务,栈为运行服务。
- 堆多线程共享,栈线程私有。
- 栈大小一般远小于堆。(Linux 8M或者10M,xss 默认1M)
- 异常错误不同。如果栈内存或者堆内存不足都会抛出异常。 栈空间不足:java.lang.StackOverFlowError。 堆空间不足:java.lang.OutOfMemoryError。
关注过最新的垃圾回收器吗,它是哪个版本以后出来的
ZGC JDK11
GCRoot 有哪些
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁synchronized持有的对象
- ...
说一说分代回收?
JVM 根据内存对象的存活周期不同,将内存划分成几块,如新生代和老年代。对不同带可以⽤不同的回收算法,如新生代使用标记-复制算法,老年代使用标记-清除或者标记-整理。
在分代回收模型里(对象分配规则):
- 新生代分为一个eden区和2个survivor区。对象创建时分配在eden区,若eden区空间不足,触发minorGC。
- 大对象会直接放入老年代,避免频繁拷贝。
- 在一次minorGC后存活的对象其对象头中存储的分代年龄+1,当 age > 15 时放入老年代。另外JVM还会维护一个动态年龄,超过动态年龄的对象直接进入老年代。
- 空间分配担保机制:在进行 minor GC 前,JVM 需要检查老年代可用内存空间是否大于新生代对象总大小,若不满足,则可能造成新生代对象晋升至老年代失败;此时如果允许担保失败,会计算老年代剩余空间是否大于历次晋升的大小,若小于或不允许担保失败,就会触发 full GC。
如何判断一个对象可以回收?
- 判断对象是否存活一般有两种方式:
- 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
- 可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象。
-
说一说你了解的垃圾回收器有哪些?
JVM历史上出现过很多垃圾回收器,比较常用的有CMS和G1以及比较新的ZGC。
CMS
CMS是一种停顿优先的多线程的老年代垃圾回收器,使用标记-清除算法。
回收过程:
- 初始标记:标记与GC Root直接相连的对象,速度很快,需要停顿。
- 并发标记:从GC Root开始,并发的开始对整个对象图进行标记,不需要停顿用户线程。
- 重新标记:为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,需要停顿。
- 并发清除:清除可回收对象,不需要停顿。
缺点:
- 浮动垃圾问题。在并发进行清除时,工作线程还会不断产生垃圾,也就是浮动垃圾,当 CMS 垃圾回收速度无法跟上浮动垃圾产生速度时,会导致并发失败,采用 Serial old 的方式回收,大大降低效率。
- 内存碎片问题。内存碎片可能导致老年代明明有空间却提前触发FullGC。
G1
- 将内存划分为许多相同大小的单元,称为Region。
- 新生代和老年代编程逻辑划分,表示一系列region的集合。
- 对于大对象有专门的Region也就是H Region存放,用连续的多个H区存放大对象。
- G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间(停顿时间),优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)
回收过程:
- 初始标记。
- 并发标记。
- 最终标记。
- 筛选回收:根据停顿时间指定回收计划,自由选择任意多个region构成回收集,然后把决定回收的那一部分region的存活对象复制到空的region中,再清理掉整个旧region的所有空间。
除了并发标记都需要STW。
ZGC
新型垃圾回收期,相比于G1,区别在于Region是动态大小的且可以动态创建销毁。
选择
- ***DK版本选择G1或者CMS,其中,内存小就用CMS,内存大就用G1。
- 新JDK版本可以尝试ZGC,一些新特性可能依赖Linux内核。
你知道哪些垃圾收集算法
- GC最基础的算法有三种: 标记 -清除算法、复制算法、标记-整理算法,我们常用的垃圾回收器一般都采用分代收集算法。
- 标记 -清除算法,“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
- 复制算法,“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
- 标记-整理算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
- 分代收集算法,“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。