一文掌握 JVM 面试要点
共 9456字,需浏览 19分钟
·
2022-05-28 17:18
之前发表的「吃透MySQL系列」专栏与「吃透Redis系列」专栏收到很多小伙伴的来信,回馈效果都很好。但是反应关于JVM的文章很少。
因此,我打算开一个「吃透JVM系列」的专栏。
之前发过一篇关于JVM面试知识点总结的文章。但是缺乏系统每个知识点的讲解,于是我打算以那篇文章为目录根据每个知识点后面为大家详细讲解,不过需要等吃透MySQL系列讲解完。
今天,先公布之前JVM面试总结的修订版,并给大家预先宣传一波,「关注公众号,持续阅读后续精彩好文」。
本文将作为本专栏「吃透Redis系列」目录,也是大厂面试标准回答,具体每个点的详细解析会收录于本专栏,关注【小龙coding】,持续阅
读后续精品文章!!
❝本文收录于【面试笔记】,更多付费文章,可以后台回复【面试笔记】获取,【点击此处试读】。
❞
1、运行时数据区域
「堆」
对象实例、数组
-Xms表示堆初始大小
-Xmx表示堆最大大小
逻辑上连续,线程共享,虚拟机启动时创建,最大
没有内存完成实例分配,且无法扩展,OOM
「方法区」
存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存
被线程共享,不会频繁GC
「实现」:jdk7把静态变量和字符串常量池移到堆中,jdk8移除永久代,把方法区移致元空间,它位于本地内存。
❝注意:JDk6、JDk7方法区即PermGen(永久代),JDK8方法区就是MetaSpace(元空间)
❞
运行时常量池:
「Class文件存放什么:」
类的版本、字段、方法、接口
常量池表(Constant pool)(存放编译期生成的各种「字面量」与「符号引用」)
「字面量」:字面量就是指由字母、数字等构成的字符串或者数值常量 「符号引用」:类和接口的全限定类名+字段的名称与描述符+方法的名称与描述符
int a = 1;//1、2、“abcdefg”就是字面量
int b = 2;//a b c d字段名就是符号引用,还有方法名、全限定类名等都属于=符号引用。
String c = "abcdefg";
String d = "abcdefg";
Class文件存放的常量池表的内容将在类加载后存放到方法区的运行时常量池(「符号引用转为直接引用」)
「与Class文件常量池区别」:动态性(可以在运行期间将常量放入池(String:intern()))
「OOM」:常量池无法申请到内存JVM常量池解析见下文
「Java虚拟机栈(栈帧组成)」
局部变量表(存放基本数据类型+对象引用+返回地址)
操作数栈
动态链接
方法出口
异常:
StackOverFlowError:线程请求的栈深度大于虚拟机允许的
OOM:栈容量动态扩展无足够内存
命令参数:java -Xss2M stackjava
「本地方法栈」:
为虚拟机使用到的Native方法服务
「程序计数器」:
当前执行的字节码指令,「唯一没有OOM的地方」 | 执行本地方法时本地方法计数器为NULL | 「线程私有」
「直接内存」
2、对象的创建五种方式
2.1、new-构造函数
2.2、Class类的newInstance方法-构造函数 「(相当于用无参构造」)
2.3、Constructor类的newInstance方法-构造函数
(「bClass.getConstructors() 可以按顺序获取所有构造函数」)
2.4、反序列化
2.5、clone
「代码:」
Constructor相关
Class bClass = B.class;
B b = bClass.newInstance();
System.out.println(b.getName());
Constructor constructor[]= (Constructor[]) bClass.getConstructors();
B b1 = constructor[0].newInstance("11",22);
System.out.println(b1.getName()+b1.getAge());
反序列化
//序列化过程 B需要实现序列化接口
ObjectOutputStream objectOutputStream=new ObjectOutputStream(new FileOutputStream("b.txt"));
objectOutputStream.writeObject(new B("11",2));
objectOutputStream.close();
//反序列化
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream("b.txt"));
B b = (B)objectInputStream.readObject();
System.out.println(b.getName()+b.getAge());
3、对象创建过程
1、当虚拟机遇到一条字节码new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,检查这个符号引用代表的类是否被加载过,若没有执行相关类加载过程。「//类加载检查」
2、类加载通过后分配内存,若堆内存规整,执行指针碰撞分配内存,否则使用空闲列表分配;「//分配内存」
3、划分内存还需要考虑并发问题,可以CAS同步处理,或则本地线程分配缓冲。「//并发问题处理」
4、然后内存空间初始化操作(默认值,一)些必要的对象设置(元信息、哈希码),再行init()方法(按照程序员意愿初始化)。
4、对象的访问定位
「句柄:「指针的指针。堆划分一块内存作为句柄池(句柄池+实例池),引用存储句柄的地址,句柄中包含了」对象实例数据指针」(指向堆对象实例数据)+「对象实例类型指针」(指向方法区对象类型数据)
优点:稳定,对象移动时只改变句柄中对象实例数据指针,而引用本身不变指向句柄
「直接指针」:直接指向对象,保存对象内存起始地址的指针
优点:访问速度快,一次定位
5、对象内存分配
「指针碰撞」
堆内存规整,将堆分为空闲和使用过两部分,空闲的放一边,用过的放一边,中间放一个指针指向分界处,分配对象内存时就将指针向空闲部分移动相应大小
「空闲列表」
堆内存不规整,需借助列表存放可用空间,分配对象内存时查看列表找到足够的空间分配给对象,并更新列表
6、对象并发安全问题
「分配内存需考虑并发问题」:可能正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况
「CAS+失败重试保证更新原子性」:对分配内存空间动作进行同步处理
「本地线程分配缓冲」:每个线程在Java堆中预先分配一小块内存(本地线程分配缓冲)哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,本地缓冲区用完了分配新的缓冲区才需要同步锁定
7、对象内存布局
java对象=对象头+实例数据+对齐填充
对象头=Mark Word+ 对象所属类 的指针组成(如果是数组对象,还会包含长度)
Mark Word=存储对象自身的运行时数据,例如hashCode,GC分代年龄,锁状态标志,线程持有的锁等等
8、OOM异常
JVM堆,无法给实例分配内存,且无法扩展时,OOM。
方法区(以及运行时常量池)无法满足内存分配需求时,OOM.
Java虚拟机栈+本地方法栈,扩展时无法申请到足够内存,OOM(线程请求深度超过JVM允许栈深度,StackOverflowException).
9、内存泄漏与内存溢出
无用内存得不到释放。程序申请了内存,使用完后又不能归还JVM,造成内存泄漏,内存泄漏多了就造成内存溢出。内存溢出——》
OOM(内存满了,没有内存给实例分配空间,且无法扩展)
Student stu=new Student();
List stus=new ArrayList<>();
stus.add(stu);
stu=null;
stu占用的内存得不到释放,stus占用着student. 发生内存泄漏
10、判断对象是否是垃圾
「引用计数器法:」
对象添加一个引用计数器,每当有个引用就加一,引用失效减一,当为减为0对象不可用
缺点:相互引用,A<-->B,然后A、B已经没有被其他有用对象引用,本视为垃圾,但是由于互相引用不能被检查回收。
「可达性分析法:」
从GC Roots到该对象可达
11、GC Roots
Java虚拟机栈(栈帧中的本变量表)中引用的对象 方法区「常量、静态变量」引用的对象 本地方法栈JNI引用对象 所有被同步锁持有的对象
总结记忆口诀:两栈一方法
局部变量表:存放方法参数和方法内部定义的局部变量
12、四种引用状态(强软弱虚)
「强引用」
Object obj=new Object(); StronglyReference ——不会被回收
「软引用」
new SoftReference(obj); 描述有用但非必须的对象——内存不够回收
「弱引用」
WeakReference 弱引用一定会被回收,下一次GC回收
「虚引用」
PhantomReference 为了能在这个对象被收集器回收时收到一个系统通知
13、方法区的回收
「废弃的常量」
没有被引用
如:字面量回收
❝一个字符串“abc”放入常量池,现在没有一个值为"abc"的字符串对象,也就是 没有任何字符串对象引用常量池中“abc”的常量,且虚拟机其他地方没有引用这个字面量,如果发生垃圾回收且有必要时,常量会被系统清理出常量池 String s1="abc"; Strig s2=new String("abc"); 其他接口,方法,字段,符号引用类似
❞
「无用类的卸载」
该类的所有实例被回收,堆中不存在该类及其派生子类的实例 加载该类的CassLoder被回收 该类对应的java.lang.Class对象没有在任何地方被引用
14、垃圾收集算法
「标记-清除」
先标记后清除,先标记垃圾对象,然后统一回收垃圾对象 缺点:要标记和清除,效率不高,还容易出现内存碎片化
「复制算法」(适用新生代—存活率低)
「对象存活率高时会有大量复制,效率低」 ,老年代存活率高,不适用 |(分配担保) 将内存容量分为大小相等两部分,先使用一块内存,用完了将还存活的对象复制到另一块内存上。由于内存被分为两部分,使得「只能用一半内存」
「标记-整理」
前面和标记清除一样,先标记,但是不会立刻清除,先把「存活的对象都移到一端」,然后直接清除掉边界以外的对象。不会出现碎片化。
「分代收集」
堆分为年轻代老年代,新生代存活率低使用复制算法,老年代存活率高使用标记清除/标记整理。
15、垃圾收集器
「注重低延迟」
「CMS」:基于标记清除的并发垃圾收集器
初始标记:标记GC Roots直达的对象(「stw」) 并发标记:跟踪标记GC Roots所有可达对象 重新标记:重新标记那些由于并发标记中用户程序跟到执行导致标记发生变化的对象(「stw」) 并发清除:清除标记垃圾
「优点」:支持并发,停顿时间短
「缺点」:使用标记清除算法,空间碎片。并发标记产生浮动垃圾。
「G1」:并发+并行(重新标记+筛选回收)
「弱化分代(老年代与年轻代一起回收),引入分区。将堆分为多个大小相等区域分而治之。」
初始标记:标记GC Roots直达的对象,并且修改TAMS(Next Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以用的Region中创建新对象,「需要停顿线程,但耗时很短」 并发标记:跟踪标记GC Roots所有可达对象 最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,对象变化记录在线程Remenbered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段「需要停顿线程」,但可并行执行 筛选回收:对各个region区域进行回收价值与成本的排序,根据用户期望的GC停顿时间来执行计划(最少时间回收最多垃圾区域,停顿用户线程)
「特点:」
「空间整合:「整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不」会产生内存空间碎片」。
「可预测的停顿」:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
16、G1与CMS区别
使用范围:CMS使用在老年代,G1收集范围新生代与老年代 STW的时间:CMS注重低延迟,G1可预测的停顿 垃圾碎片:CMS使用标记清除算法,造成内存空间碎片;G1进行空间整合使用标记-整理,不会有内存空间碎片 垃圾回收过程 使用场景
「stw」:垃圾回收,暂停所用用户线程执行,避免垃圾回收时产生新垃圾。
17、类加载过程
「加载」:根据类的全限定类名获取二进制字节流,将字节流代表的静态存储结构->运行时存储结构,在内存生成「Class对象」,作为方法区这个类数据访问入口 「验证」:检验加载的class文件正确性(修饰符、权限、) 「准备」:为类的「静态变量」分配内存,并赋默认值 「解析」:将常量池中符号引用转为直接引用(class文件常量池转至方法区运行时常量池) 「初始化」:(针对类变量初始化)对静态变量和静态代码块执行初始化工作
18、类加载器
启动类加载器:加载核心类库(JAVA_HOME/lib 如rt.jar) 扩展类加载器:加载扩展类(JAVA_HOME/jre/lib/ext) 系统类加载器:加载用户类路径ClassPath下的类 用户自定义加载器:继承java.lang.ClassLoader
19、类加载方式
隐式加载:当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中 显示加载:通过class.forname()等方法,显式加载需要的类
20、双亲委派模型
含义:类加载请求来了,类加载器自己先不加载,先让父类加载器加载,父类不行自己再来
「怎样打破双亲委派机制:」
自定义类加载器,重写loadClass方法 线程上下文类加载器
Java涉及SPI机制的都用线程上下文类加载器。父类加载器请求子类加载器完成加载动作
SPI机制:为接口找寻服务(jdbc)
❝SPI约定:服务提供者为接口提供接口实现后,会在jar包的META-INF/service/目录下创建一个以服务接口命名的文件。
❞
JDBC4.0使用SPI机制,DriverManager需要去jar包下的META-INF/services/java.sql.Driver目录下去寻找对应的Driver加载,但是
DriverManager在rt.jar中,使用启动类加载器(BootStrapClassLoader),它需要调用服务提供者放在classpath下的类,启动类加载器无
法加载,就只得使用线程上下文加载器,让父类加载器调用子类加载器完成,打破了双亲委派机制。
「好处」:避免类重复加载+防止核心类篡改+安全性
21、GC
Minor GC:回收年轻代
Major GC :回收老年代
Full GC:回收年轻代与老年代,方法区域
22、内存分配与回收策略
「对象优先在Eden区分配」
大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。
「大对象直接进入老年代」
大对象指「需要连续分配空间」的对象,长字符串,数组。
对象过大,由于需要连续的内存空间,会导致提前进行垃圾回收以获取足够的连续空间 -XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。
「长期存活的对象」
对象在Eden区出生,经过一次Minor GC存活下来,分代年龄就会加一,增加到一定年龄就会移向老年代。默认是15. -XX:MaxTenuringThreshold用来定义该年龄的阈值。
「动态对象年龄判定」
虚拟机并不是永远要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,当「Survivor区相同年龄的所有对象大小总和大于Survivork空间一半」,则年龄大于或等于该年龄的对象直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
「空间分配担保(*)」
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小。
如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。
23、分代垃圾收集器是怎样工作的
「分代回收器有两个分区」:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。
新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1。
「它的执行流程如下:」
把 Eden + From Survivor 存活的对象放入 To Survivor 区;
清空 Eden 和 From Survivor 分区;
From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。
每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。「大对象也会直接进入老生代。」
老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。
24、Full GC触发条件
对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
「调用System.gc()」
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
「老年代空间不足」
「场景:」
大对象直接进入老年代(老年代空间足,但是没有足够的连续空间) 长期存活的对象直接进入老年代
「解决:」
1、尽量「不要创建过大的对象」以及数组 2、可以通过-Xmn「调大新生代大小」,让「对象尽量在新生代被回收」,不进老年代 3、可以通过-XX:MaxTenuringThreshold「调大分代年龄阈值」,让对象得新生代多存活一段时间
「空间分配担保失败」
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。
「解释一」
老年代最大可用的连续空间<新生代所有对象总空间 && HandlerPromotionFailure设置不允许担保失败 full gc
老年代最大可用的连续空间>新生代所有对象总空间 && HandlerPromotionFailure设置允许担保失败 && 通过Minor GCJ进入老年代的象平均大小>老年代最大连续空间大小 full gc
「解释二」
Minor GC前,先判断老年代最大连续空间是否大于新生代所有对象总空间。
若大于,安全;若小于,查看HandlerPromotionFailure设置是否允许担保失败,允许,再看通过Minor GCJ进入老年代的对象平均大小>老年代最大连续
空间太小,则还是失败,进行Full GC。
「jdk1.7以前的永久代空间不足」
永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
25、为什么有垃圾收集还会有内存泄漏问题?
「对象定义在错误的范围」如果长生命周期的对象持有短生命周期的引用,就很可能会出现内存泄露。「异常处理不当」各种资源的关闭一定要放在finally里面
26、堆与栈的区别?
「申请方式」:栈系统自动申请,堆需手动申请c语言malloc(),java new Object();
栈系统分配速度快,堆慢,容易内部碎片;栈地址空间连续,堆是不连续的(链表存储空闲内存地址);
「内容不一样」(堆存对象实例与数组,关注存储;栈存储局部变量表,操作数栈,关注运行);
「大下限制」(栈预先设定好的,编译器即可确定,堆取决有效虚拟内存,运行期间确定)
27、逃逸分析
**概念:**当一个对象在方法中被定义后,它可能被方法外部其他对象所引用,则称逃出方法(内存逃逸现象)
使用逃逸分析,编译器优化
同步省略:对象没有方法逃逸,只能被一个线程访问到,可以不用同步 「将堆分配转为栈分配」
如果JIT经过逃逸分析,发现有些对象没有逃逸出方法,那么有可能堆内存分配会被优化成栈内存分配
28、JVM参数
-Xmx3550:设置堆最大值 -Xms3660m:设置初始堆大小 -Xss128k:设置线程栈大小 -Xmn2g:设置年轻代大小 -XX:NewSize=1024m:设置年轻代初始值 -XX:MaxNewSize=1024m:设置年轻代最大值 -XX:SurvivorRatio=4:设置Survivor区与Eden区比值 -XX:MaxTenuringThreshold=15:设置分代年龄阈值,满15就进入老年代 -XX:PretenureSizeThreshold:大对象直接进入老年代
29、内存持续上升,我该如何处理
1、启动程序之前通过 HeapDumpOnOutOfMemoryError 和 HeapDumpPath 这两个参数「开启堆内存异常日志」
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=
2、从日志从发现异常
3、通过top命令查看进程cpu使用率
4、再通过 top -Hp pid 查看进程下所有「具体线程占用系统资源情况」。
5、再通过 jstack pid 查看具体线程的「堆栈信息」(线程ID、状态(wait,sleep),是否持有锁)
6、再通过 jmap 查看「堆内存的使用情况」 jmap -heap pid
7、通过以上命令分析基本可以看出什么问题导致内存上升,现在分析问题产生的原因
8、我们在启动时,已经设置了 dump 文件,通过 MAT 打开 dump 的内存日志文件,分析即可。
须知
30、JVM性能调优与故障处理
31、基本故障处理工具
32、可视化故障处理工具
最后三节,属于进阶内容,前面基础一定要掌握好。由于篇幅有限,关注公众号,后期会专门针对大厂面试常问性能调优与工具进行讲解。
后记
关注我公众号“小龙coding”,我们一起探讨,帮助修改简历,回答疑问,项目分析,只为帮助迷茫的你高效斩获心仪offer!
后续会陆续更新大厂面经面试题与解析,大厂内推「直达部门主管」,也有交流群大家一起探讨共同进步。加油噢!