JVM面试必备
2025-09-08 09:45:51 0 举报
AI智能生成
在准备JVM(Java虚拟机)面试时,重要的是要熟悉其核心概念、架构、内存管理以及性能调优的相关知识点。必须掌握以下几个要点: 1. **JVM架构**:理解JVM的主要组成部分,包括类加载器(ClassLoader)、运行时数据区(如堆、栈、方法区等)、执行引擎以及本地接口。 2. **类加载机制**:类加载的过程包括加载、验证、准备、解析和初始化五个阶段,以及双亲委派模型(Parent Delegation Model)的应用。 3. **内存管理**:深入理解Java堆内存的结构,特别是年轻代、老年代以及持久代(在Java 8以后为元数据区,MetaSpace)的划分和作用;垃圾收集算法(如标记-清除、复制、标记-整理、分代收集)以及常见的垃圾收集器(如Serial、Parallel、CMS、G1等)。 4. **性能调优**:了解如何通过JVM参数和工具(如jvisualvm、jstat、jmap等)监控和调优Java应用程序的性能。 5. **线程同步**:掌握Java内存模型和线程安全相关的知识,理解synchronized和volatile关键字的使用及底层实现。 6. **故障排查**:了解常见的内存泄漏和线程死锁问题的定位和解决方法。 掌握这些内容对于通过JVM相关的面试至关重要,同时也可以加深对Java应用程序性能和稳定性的理解。建议准备示例问题的答案,如解释JVM的工作原理、解释垃圾回收机制、如何诊断和解决内存泄漏等问题。
作者其他创作
大纲/内容
类加载过程
java会启动一个java虚拟机
创建一个引导类加载器
loadclass的类加载过程
加载:通过IO读入字节码文件
验证:验证文件的格式
准备:给类的静态变量分配内存,并赋予默认值
解析:将符号引用替换为直接引用,这个阶段会把静态方法指向数据所存内存的指针或句柄等(直接引用),也就是静态链接的过程
初始化:对类的静态变量初始化为指定的值,执行静态代码块
符号引用:字节码中描述引用目标的符号,不是直接的内存地址,就是常量池中#代表的字符串
直接引用:字节码加载到内存后替换为内存地址
静态链接:静态方法在运行以后不会变化,那么可以直接替换为内容地址使用,也就是方法区的地址
动态链接:某个方法有多态,那么执行的时候就不一定是执行具体的哪个,所以需要再运行的时候在决定地址
类加载器初始化过程
创建JVM启动器实例sun.misc.Launcher
sun.misc.Launcher初始化使用了单例模式设计,保证一个JVM虚拟机内只有一个sun.misc.Launcher实例。
在Launcher构造方法内部,其创建了两个类加载器,
sun.misc.Launcher.ExtClassLoader(扩展类加载器)
sun.misc.Launcher.AppClassLoader(应用类加载器)。创建过程中会把扩展类加载器作为参数传入,最终付费父类加载器属性
引导类加载器BoostClassLoader是C++编写的,所以扩展类加载器的父类是null
JVM默认使用Launcher的getClassLoader()方法返回的类加载器AppClassLoader的实例加载我们的应用程序
垃圾收集
垃圾收集算法
标记复制
内存空间分为两个逻辑空间,非垃圾对象,复制到一片空白区域,然后把原区域全部删除
适合范围
年轻代存活时间一般比较少,所以适合标记复制
具体实现
年轻代 Serial收集器
年轻代Parallel收集器
G1收集器
优点:碎片比较少
缺点,浪费空间
标记清除
标记的是垃圾对象,标记完成后清理掉
适合范围
老年代
具体实现
CMS收集器
缺点
如果垃圾对象很多,效率会降低
清理完后,有很多碎片空间
标记整理
标记存活对象和垃圾对象,把存活对象,按顺序覆盖垃圾对象,没有被覆盖的垃圾对象需要手动清理
适合范围
老年代
具体实现
老年代Serial收集器
老年代Parallel收集器
垃圾收集器
Serial垃圾收集器
分为年轻代和老年代
年轻代 Minor GC:-XX: UseSerialGC
老年代 Full GC:-XX:UseSerialOldGC
串行收集器,单线程执行
Parallel收集器
分为年轻代和老年代
年轻代 Minor GC:-XX: UseParallelGC
老年代 Full GC:-XX:UseParallelOldGC
并行收集器,多线程执行,多核服务器效率比Serial高
JDK8默认使用Parallel收集器
内存较少的时候,比如1-2个G,用Parallel没问题,内存大了会有问题
STW时间比较长,用户体验差
ParNew收集器
和Parallel类似,也是多线程并发,区别在于他可以和CMS配合使用
ParNew收集器负责年轻代收集垃圾,CSM负责老年代
很长一段时间都是使用的这个组合
CMS
只处理老年代
CMS收集器工作原理及步骤
以获取最短回收停顿时间为目标的收集器,非常符合注重用户体验上的应用使用
步骤:
1.初始标记:STW,单线程记录下gc root引用的对象,速度很快
2.并发标记:从gc root关联的对象开始遍历整个对象图的过程,期间用户线程并发运行
3.重新标记:STW,为了修正并发标记期间有变化的对象重新标记,这个阶段比并发标记的时间,采用三色标记里的增量更新算法
4.并发清理:用户线程gc清理线程并行运行,期间产生的新的对象会被标记为黑色,也就是不做处理,会在下次gc中处理
5.并发重置:重置本次GC过程中的标记数据,这个过程会整理空间
三色标记
黑色
表示这个对象已经被垃圾收集器访问过,并且他的所有引用都扫描过了
灰色
表示这个对象已经被垃圾收集器访问过了,但是至少有一个引用没有扫描
白色
表示尚未被垃圾收集器访问过
可能产生的问题:因为用户线程并发运行,所以可能导致已经标记的对象状态发送变化或产生新的对象
缺点:
需要占用cpu资源
会产生浮动垃圾
并发清理期间可能会产生新的对象,并且变为垃圾对象,因为标记过程已经结束,所以这个对象不会在这次清理,会在下次gc清理,这个对象就叫做浮动垃圾
标记清除算法会导致大量碎片空间
通过设置参数实现标记整理
并发失败:在并发标记或并发清理的过程中,如果又有一个大对象,或空间不足了,那么会再次触发Full GC,这时候会进入stw,用serialold收集器收集垃圾
可以通过一些参数避免
三色标记
三色标记产生的问题以及解决方案
浮动垃圾;已经标记过的对象因为方法执行完了,变成垃圾对象,变成了浮动垃圾
解决方案:下次GC会处理
漏标:被引用的对象没有被标记,一般是引用发生变化产生的
解决方案
增量更新
就是在产生新的引用的时候记录下来,在下次扫描
增量更新通过写屏障实现:
可以简单理解为AOP,增量更新是写后操作,SATB是写前操作
记忆集和卡表
用于解决跨代引用导致的扫描效率问题
记忆集:一个集合,标记是否有跨代引用
具体由卡表实现
年轻代使用卡表,每个卡表在老年代都有一个对应的卡页,当有跨代引用的时候,卡表就标记为1,表示需要加入GCroot扫描
老年代使用卡页,每个卡页的大小固定,卡页存放多个对象,当有跨代引用的时候,这个卡页就是dirte card
卡表通过写屏障实现
G1
原始快照SATB
当引用关系被删除的时候,用一个队列存下这个引用,在并发扫描之后重新扫描
不再是物理分代,而是逻辑分代,默认会将内存分为2048个region,每个region都可能是Eden、survivor、old、Humongous,也就是逻辑分代,
当空白区域被分配了年轻代的对象,那么这个区域就是年轻代了,
老年代对象同理
新增了一个Humongous区域,当对象的大小大于region的百分之五十,那么就会将这个区域标记为Humongous,超过region的大小,会使用连续的多个region存储对象
G1收集器工作原理及步骤
工作原理和cms类似,新增可预测停顿时间算法
步骤:
1.初始标记:STW,只标记gc root的应用
2.并发标记:通cms类似
3.最终标记:STW,通cms的重新标记类似
4.筛选回收:首先会对每个region进行评估,对region的回收价值和成本进行排序,根据用户期望的停顿时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)制定回收计划,比如设置最大停顿时间是200ms,筛选回收时间更短,回收空间更大的region优先做回收,累计回收时间接近200ms后,其他region会在下次回收
如果停顿时间设置的很小,那么每次回收的垃圾会很少,最终会累积触发full GC
G1垃圾收集分类
Young GC
当Eden装满的时候,会计算可回收对象所需要的时间,如果接近最大停顿时间,那么久触发Young GC,如果远远小于最大停顿时间,不会触发GC,而是将对象放到新的空白区域,直到新的Eden区域装满之后,再次计算可回收对象所需时间,
MixedGC
老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发
比如设置的45%,当老年代的占有率达到45%,就会触发MixedGC,也会判断最大停顿时间
Full GC
单线程处理垃圾回收,
触发场景
在MixedGC的时候,没有足够的空间复制老年代的存活对象的时候
最大停顿时间设置的太小,导致大量垃圾积压,最终内存不够,就会触发fullGC
大内存减少使用G1,比如大于8G小于几百G
ZGC
适用于大内存的垃圾收集器,目前很少用于产品环境,
暂时不分代
ZGC逻辑过于复杂,目前还没有实现分代,以后可能会实现
颜色指针:ZGC核心设计之一
在64位系统中,不需要全部64位用于寻址,所以ZGC选择保留42位寻址,在后面4位用于标记颜色
STW(Stop The Word)
大内存推荐使用CMS,但是CMS也需要消耗一些cpu资源,所以需要综合考量
暂停用户线程,专心做每件事,对用户来说可能是突然卡了一下,
垃圾收集器主要负责堆内存的清理,但是不仅是堆内存,其他内存区域也会做清理,比如方法区的类的信息,如果这个类没有引用了,也会清理方法区的类信息
如何选择垃圾收集器
没有特别的要求,可以让jvm自己选择
如果内存小于100M,使用串行收集器
Serial收集器
4G以下可以用parallel,
4-8G可以用ParNew+CMS,
8G以上可以用G1,
几百G以上用ZGC
1.8默认Parallel
1.9默认G1
JVM优化
命令
jmap
jmap -heap <pid>
查看内存使用情况
jmap -dump:format=b,file=<filename>.hprof <pid>
查询堆转存文件
可以导入工具中查看
jmap -histo <pid>
查看类统计信息:
jmap -dump:live,format=txt,file=<filename>.txt <pid>
查看线程堆栈信息
jstack
jstack <pid>
打印所有线程信息
jstack <pid> | grep -A 10 <nid>
查询线程堆栈中的10行
nid可以快速定位
top
top -p <pid>
查询进程的内存情况,找到cpu最高的进程
按H,显示线程的详情
pid是10进制的,需要转化为16进制,用于stack中的nid
jinfo
jinfo -flags <pid>
查看jvm的参数
jinfo -sysprods <pid>
查看java系统参数
jstat
jstat -gc <pid>
最常用的命令,可以评估程序内存的使用以及GC情况
jstat -gc <pid> 1000 10
每秒执行一次,共执行10次
通过观察Eden区的增长情况,预估
远程连接jvisualvm
启动普通的jar 程序的JMX端口配置
java ‐Dcom.sun.management.jmxremote.port=8888 ‐Djava.rmi.server.hostname=192.168.50.60 ‐Dcom.sun.management.jmxremot
e.ssl=false ‐Dcom.sun.management.jmxremote.authenticate=false ‐jar microservice‐eureka‐server.jar
e.ssl=false ‐Dcom.sun.management.jmxremote.authenticate=false ‐jar microservice‐eureka‐server.jar
-Dcom.sun.management.jmxremote.port 为远程机器的JMX端口
-Djava.rmi.server.hostname 为远程机器IP
tomcat的JMX配置
在catalina.sh文件里的最后一个JAVA_OPTS的赋值语句下一行增加如下配置行
JAVA_OPTS="$JAVA_OPTS ‐Dcom.sun.management.jmxremote.port=8888 ‐Djava.rmi.server.hostname=192.168.50.60 ‐Dcom.sun.ma
nagement.jmxremote.ssl=false ‐Dcom.sun.management.jmxremote.authenticate=false"
nagement.jmxremote.ssl=false ‐Dcom.sun.management.jmxremote.authenticate=false"
内存泄露
比如:电商项目中经常使用redis加jvm缓存的方式,如果使用一个hashmap存储数据,但是不考虑数据map的容量问题,就会导致大量老旧数据占用大量内存,导致频繁的full gc
成熟的JVM级缓存框架来解决,比如ehcache等自带一些LRU数据淘汰算法的框架来作为JVM级的缓存。
工具
full gc和young gc的原因可能有:
1、元空间不够导致的多余full gc
2、显示调用System.gc()造成多余的full gc,这种一般线上尽量通过XX:+DisableExplicitGC参数禁用,如果加上了这个JVM启动参数,那
么代码中调用System.gc()没有任何效果
么代码中调用System.gc()没有任何效果
3、老年代空间分配担保机制
jvm内存模型
私有内存空间
栈(又叫线程栈)
每个线程在运行的时候,会在栈内存空间中分配一块内容给线程使用
栈帧:用于描述每个方法在运行时的结构,每个方法在调用的时候会分配一个栈帧空间
局部变量表
局部变量可以看成是一个数组,由下标决定变量的位置
操作数栈
代码运行时,操作数会先压到操作数栈中,下一步才是将这个操作数存入局部变量中
帧数据
动态链接
就是把常量池中的符号引用解析为方法区的代码地址
返回地址
会记录返回到调用方法的那个位置以及其他信息
程序计数器
(JVM)中的一种小型内存区域,它用于指示当前线程正在执行的字节码指令的地址
当优先级更高的线程抢占了cpu资源,程序计数器就可以记录运行到的位置以防重新执行
本地方法栈
调用运行环境的方法,比如c++,会分配这么一块内存栈
共享内存空间
堆:存储对象实例的内存区域
堆的逻辑结构
年轻代:一般分配1/3内存
Eden区,通常分配8/10内存
两个Survivor区:通常分配1/10内存
老年代:一般分配2/3内存
大对象直接进去老年代
大对象只有年轻代的算法使用ParNew和serial收集器可以设置
老年代空间分配担保机制:用历次Minor GC的数据预估是否需要先做一次Full GC,可以减少Full GC 的次数
在Minor之前,会判断之前所有Minor GC的从年轻代到老年代的大小的平均值
如果平均大小比老年代剩余空间大,那么会做一次Full GC 在做Minor GC
如果平均大小比老年代剩余空间小,那么会做一次做Minor GC
堆的GC逻辑
新建对象通常会进入Eden区,除非是大对象会直接进老年代
当Eden存不下的时候,会发生一次minor GC
把非垃圾对象转到S0中,清理整个Eden区域
下次Minor Gc的时候,Eden和S0中的非垃圾对象转到S1中,然后清理Eden和S0
对象回收机制
引用计数算法
对象引用一次计数加1,取消引用的时候计数减1,当计数为0的时候就可以回收了
存在的问题,当两个对象互相引用的时候,这两个对象本身的引用取消的时候,因为互相引用没有取消,计数不为0,所以无法回收
可达性分析算法
将GC Root对象作为起点,向下搜索标记对象,没有被标记的对象,就是垃圾对象
引用类型
强引用
就是普通的变量引用,public static User user = new User();
软引用
软引用就是用SoftReference封装的对象,public static SoftReference<User> user = new SoftReference<User>(new User());
一般用于缓存某些对象,方便快速使用,也就是可以释放的对象,
比如:Spring Cache 模块中的 SimpleCacheManager 类
弱引用
不常用,会直接被GC 回收
如何判断一个类是无用的类
类需要同时满足下面3个条件才能算是 “无用的类”
该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
加载该类的 ClassLoader 已经被回收。
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
方法区
常量池、静态变量、类信息
jvm其他组件
字节码执行器:两个作用
执行字节码指令
修改程序计数器的值
类装载子系统
对象加载过程
1.类加载检查:检查类是否被加载过
2.没有就加载类,有就跳过这个步骤
3.分配内存
4.初始化:设置初始值
5.设置对象头
6.执行init方法:这里就是初始化程序员赋的值
包含信息
Mark Word:运行时数据
hash值
GC分代年龄
锁状态标志
Kclass Pointer类型指针,指向方法区类元素的信息
数组长度:如果是数组还有数组长度
两个优化
对齐填充:不足8位自动补齐
计算机内部按照8位检索会很快,所以不足8的倍数位的时候,会自动补齐,加快检索速度
指针压缩:压缩指针减少存储需求的空间
JDK1.6开始有指针压缩,减少每个对象占用的内存,就是为了节约空间
划分内存的方法
指针碰撞:有序的内存空间中,有一个标记,标记这个位置后面是可分配的内存空间
空闲列表:jvm维护了一个列表,记录所有的空闲内存位置,方便分配
如何解决并发问题
CSA:失败重试机制,当多个线程争抢相同位置的内存,失败的会重新尝试
TLAB:每个线程分配一个单独的内存空间,
jvm会先对对象做逃逸分析,如果发现对象不会逃逸,那么久可以把对象分配到栈空间,这样可以减少GC,提高性能
逃逸分析:分析对象的作用域,如果不会逃出当前方法或线程范围,那么就是非逃逸对象
标量替换:栈空间很小,如果栈帧剩余空间是碎片化的,jvm会把对象拆分成散的数据,分出到这些碎片空间中,比如一个成员变量放一个碎片中
标量:就是不可被进一步分解的量,比如int、long等基本类型以及reference类型
聚合量:就是对象,可以被进一步分解成多个标量
0 条评论
下一页