JVM高频面试题总结

该总结主要参考了《JavaGuide》,《深入理解Java虚拟机》,我之前的博客,如有错误,请在评论区留言,非常感谢!
一共总结了20多个常问问题,有一些问题比较复杂(比如垃圾收集器),只进行了片面的总结,想详细了解,建议读一下《深入理解Java虚拟机》。

1. Jvm运行时内存

在这里插入图片描述

  • 程序计数器:字节码解释器通过改变计数器的值来选取下一条要执行的指令,程序流程控制都依赖于程序计数器;在多线程的时候,程序计数器记录当前线程位置,当线程切换回来的时候能够知道上次执行到哪。

  • 虚拟机栈:存放栈帧,栈帧包括局部变量表、操作数栈、方法出口信息等。局部变量表主要存放了各种数据类型和对象引用。虚拟机栈会报两个错误StackOverFlowErrorOutOfMemoryError

  • 本地方法栈:为虚拟机提供使用到的Native方法服务。

  • 堆:存放对象实例和数组

  • 方法区(也被称为永久代):存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

注意1:⽅法区和永久代的关系很像Java 中接⼝和类的关系,类实现了接⼝,方法区是一种规范,⽽永久代就是 HotSpot 虚拟机的⼀种实现⽅式。

注意2:1.8的时候将方法区移除,其中的常量池和静态变量放在堆空间中,元空间存放类信息等元数据。

2. 方法/函数如何调用?

每⼀次函数调⽤,都会有⼀个对应的栈帧被压⼊ Java 栈,每⼀个函数调⽤结束后,都会有⼀个栈帧被弹出。

栈帧包括:函数的返回地址和参数,临时变量,函数调用的上下文。

3. 为什么要将永久代 (PermGen) 替换为元空间(MetaSpace) 呢

整个永久代有⼀个 JVM 本身设置固定⼤⼩上限,如果设置小了容易出现内存溢出,⽽元空间使⽤的是直接内存,受本机可⽤内存的限制,虽然元空间仍旧可能溢出,但是⽐原来出现的⼏率会更⼩。

可以使用-XX:MetaspaceSize-XX:MaxMetaspaceSize设置元空间初始大小以及最大可分配大小

4. 对象的创建过程

主要分为5个过程:类加载检查,分配内存,初始化零值,设置对象头,执行init方法

  • 类加载检查:虚拟机遇到一个new指令时,首先检查该指令在常量池中是否有符号引用,如果没有必须先执行相应的类加载过程(双亲委派机制);

  • 分配内存:经过类加载检查后,虚拟机就要给类分配内存,分配方式有指针碰撞和空闲列表两种,选择哪种要根据内存是否规整;

  • 初始化零值:内存分配完成后,虚拟机将这部分内存初始化为零值,保证对象的实例字段在Java中可以不赋初值就可以使用;

  • 设置对象头:对象头中存储着对象是哪个类的实例,对象的哈希值,GC分代年龄等;

  • 执行init方法:上面工作完成后,从虚拟机的角度来看一个新的对象以及产生,但是从Java程序的角度来看,对象的创建才刚开始,init方法还没有执行,所有字段都为0,执行完init方法后,这样一真正可用的对象才算完全产生。

指针碰撞:用过的内存都会整合到一边,没用过的整合到另一边,中间有个分界值指针,只需要向没用过的内存方向移动对象内存大小位置即可;(Serial,ParNew)

空闲列表:虚拟机会维护一个列表,记录哪些内存是可用的,分配的时候找一块足够大的内存块给对象实例,最后更新列表记录。(CMS)

5. 对象的访问定位有哪两种方式?

使用句柄和直接指针。

  • 使⽤句柄的话,那么Java堆中将会划分出⼀块内存来作为句柄池, reference 中存储的就是对象的句柄地址,⽽句柄中包含了对象实例数据与类型数据各⾃的具体地址信息 ;

在这里插入图片描述

  • 使⽤直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,⽽reference 中存储的是对象的地址。

在这里插入图片描述

6. 一个新对象new之后会被分配到新生代的哪里,为什么 ?

  • 对象优先在 eden 区分配,当 eden 区没有⾜够空间进⾏分配时,虚拟机将发起⼀次 Minor GC. 一个对象只有经历多次GC后才有资格进入老年代。
  • ⼤对象直接进⼊⽼年代 ,因为⼤对象需要⼤量连续内存空间(⽐如:字符串、数组),为了避免为⼤对象分配内存时由于分配担保机制带来的复制⽽降低效率。

7 对象什么时候进入老年代?新生代老年代比例

  • 默认情况下,在新生代中的对象经历15次GC后会进入老年代,-XX:MaxTenuringThreshold

  • 新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2,Edem : from : to = 8 : 1 : 1

8 如何判断对象是否死亡 ?

引⽤计数法和可达性分析法。

  • 引用计数法:给对象添加一个引用计数器,每被引用一次就加一,引用失效一个就减一,任何时候当引用计数器为0时这个对象就被判断死亡;
  • 可达性分析法:这个算法的基本思想就是通过⼀系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所⾛过的路径称为引⽤链,当⼀个对象到 GC Roots 没有任何引⽤链相连的话,则证明此对象是不可⽤的。
    在这里插入图片描述

9. 哪些对象可以作为GC Root

  • java虚拟机栈中的引用的对象。

  • 方法区中的类静态属性引用的对象。 (一般指被static修饰的对象,加载类的时候就加载到内存中。)

  • 方法区中的常量引用的对象。

  • 本地方法栈中的JNI(native方法)引用的对象

10. 如何判断⼀个常量是废弃常量 ?

没有任何对象引用这个常量的时候,这个常量就被成为废弃常量。

11. 如何判断⼀个类是⽆⽤的类?

类需要同时满⾜下⾯ 3 个条件:

  • 该类所有的实例都已经被回收;
  • 加载该类的 ClassLoader 已经被回收;
  • 该类对应的 java.lang.Class 对象没有在任何地⽅被引⽤,⽆法在任何地⽅通过反射访问该类的⽅法。

12. 垃圾回收机制和垃圾回收算法

垃圾回收机制:首先判断一个对象是否已经死去,然后根据垃圾回收算法进行回收。判断一个对象是否死亡常用的方法有:引用计数法和可达性分析法,常用的垃圾回收算法有标记清除法,复制法,标记整理法。

现在JVM中应用的是分代收集算法:在新生代中由于每次收集都会有⼤量对象死去,所以可以选择复制算法 ;而⽼年代的对象存活⼏率是⽐较⾼的,⽽且没有额外的空间对它进⾏分配担保,所以选择“标记-清除”或“标记-整理”算法进⾏垃圾收集 ;

13. 垃圾回收算法与垃圾收集器介绍(为什么分为老年代和新生代

(详细的垃圾回收算法和收集器介绍请参考《深入理解Java虚拟机》,或者其他博客,这里只做简单总结)

13.1 常见的垃圾回收算法?

标记清除法,复制算法,标记整理等,分代收集算法。

13.2 为什么分为老年代和新生代?(分代收集算法介绍一下)

在新⽣代中,每次收集都会有⼤量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。⽽⽼年代的对象存活⼏率是⽐较⾼的,⽽且没有额外的空间对它进⾏分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进⾏垃圾收集。

利用分代收集算法可以综合各种垃圾回收算法的优势,提高回收效率与内存利用率。

13.1 常见的垃圾回收器?
  • Serial 收集器(单线程的,收集垃圾时其他线程的工作被暂停,新生代采用复制算法,老年代采用标记整理算法)
  • ParNew 收集器(新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法 ,Serial 收集器的多线程版本 )
  • Parallel Scavenge 收集器(1.8默认,收集器关注点是吞吐量 ,新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法 )
  • CMS:⼀种以获取最短回收停顿时间为⽬标的收集器:初始标记(暂停其他线程)-并发标记-重新标记-并发清除 。

注意:Parallel Scavenge 收集器关注点是吞吐量(⾼效率的利⽤ CPU)。 CMS 等垃圾收集器的关注点更多的是⽤户线程的停顿时间(提⾼⽤户体验)。所谓吞吐量就是 CPU 中⽤于运⾏⽤户代码的时间与 CPU 总消耗时间的⽐值。 新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。

14. 平时怎么看gc?

在Edit Configuration中的VM options中输入命令-XX:+PrintGCDetails,运行程序后控制台会打印GC垃圾回收的详细信息

15. 什么场景下会OOM,如何发现OOM?

一般有四种情况:堆内存溢出,元空间溢出,直接内存溢出,栈内存溢出(如果虚拟机栈可以动态扩展,并且扩展时无法申请到足够的内存,抛出oom异常);

一般通过在Edit Configuration中的VM options中,设置-XX:+HeapDumpOnOutOfMemoryError在发生异常时dump出当前的内存,然后打开Jprofiler生成的内存快照,就可以看出是哪个类、哪一行代码执行的结果占用过多内存;

16. OOM如何排查并解决?

  • 先尝试将堆内存调大,比如设置初始分配内存-Xms1024m,最大分配内存-Xmx2048m,看看是不是还会报OOM错误;
  • 如果还是不行,考虑代码出现死循环,可以用Jprofile工具,首先在Edit Configuration中的VM options中输入命令XX:+HeapDumpOnOutOfMemoryError,表示oom Dump,然后打开Jprofiler生成的内存快照,就可以看出是哪个类、哪一行代码执行的结果占用过多内存

17. Metaspace(元空间)占用空间异常排查、堆占用异常排查

首先想到的是增加元空间或者堆内存大小,通过设置-XX:MaxMetaspaceSize=500m或者-Xms1024m -Xmx1024m调整,而且要设置-XX:+PrintGCDetails打印GC的信息,如果调整大小后还是异常,那么一般是程序出了问题,需要用到内存快照工具去分析一下是哪个实例占据超大内存或者在哪行出现了问题,然后去修改对应代码。

18. 说一下JVM的分代晋升过程,什么时候会从新生代晋升到老年代?

堆内存分为新生代和老年代,新生代又分为Eden区,幸存者from区和to区,每次垃圾回收都会将Eden区幸存的对象放到幸存者to区,from区中对象也移到to区,然后from和to区互换名称,默认情况下当一个对象实例经过15次YoungGC还存活就将其放入老年代。

如果创建的是大对象,比如数组和字符串,就直接将其放入老年代,因为大对象需要大量连续的内存空间,如果将其放入新生代,则会因为分配担保机制带来大量的复制,这会降低JVM的效率。

19. 如何解决Minor GC、Full GC频繁的问题?

扩大堆内存,检查是否有非法的超大对象

20. 什么情况下会触发Full GC?

GC分为Partial GC和Full GC

我知道的一般有3种情况触发Full GC:

(1)发生Young GC之前进行检查,如果检查出本次Young GC后可能升入老年代的对象大小,可能超过了老年代当前可用内存空间,此时必须先触发一次Full GC给老年代腾出更多的空间,然后再执行Young GC。

(2)执行Young GC之后有一批对象需要放入老年代,此时老年代如果没有足够的内存空间存放这些对象,此时必须立即触发一次Full GC。

(3)老年代内存使用率超过了92%,也要直接触发Full GC,当然这个比例是可以通过参数调整的。

概括成一句话,就是老年代空间也不够了,没法放入更多对象了,这个时候务必执行Full GC对老年代进行垃圾回收。

21. 类加载器有哪些?

Java自带的类加载器有:ApplicationClassLoader,ExtensionClassLoader,BootStrapClassLoader

22. 双亲委派模型 ,为什么需要双亲委派机制 ?(自定义的String类会不会执行)

目的:为保障安全

检查顺序从下至上,加载顺序从上到下。如果一个类加载器需要加载类,那么首先它会把这个类请求委派给父类加载器去完成,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。

比如说我们自己写了个java.lang包,自己定义了个String类,然后在实例化String类时,ApplicationClassLoader会把这个请求委托给ExtensionClassLoader,ExtensionClassLoader又会委托给BootStrapClassLoader,根加载器加载java自带的String类,所以我们自己定义的String类就不会被执行。

23. 自己设计双亲委派模型应该怎么设计

先分析一下双亲委派机制的执行流程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。每一个层次的类加载器都是如此。因此,所有的加载请求最终都应该传送到顶层的启动类加载器中。只有当父加载器反馈自己无法完成这个加载请求时(搜索范围中没有找到所需的类),子加载器才会尝试自己去加载。

所以应该至少设置3层类加载器,自底向上依次是:应用程序类加载器(Application ClassLoader)扩展类加载器(Extension ClassLoader)启动类加载器(Bootstrap ClassLoader)。如果应用程序中没有自定义过自己的类加载器,默认使用应用程序类加载器。可以在扩展类加载器中拦截一下。

24. 破坏类加载机制?

在Java中,采用双亲委派机制来实现类的加载

Tomcat中的web 容器类加载器破坏了双亲委托模式的(当某个请求想从 Web 应用的 WebappX 类加载器中加载类时,该类加载器会先查看自己的仓库,而不是预先进行委托处理)

25. java的类加载器作用

java的类加载器ClassLoader是一个抽象类,主要是用于将.class文件加载到JVM内存中,并转换成JVM可以识别的Class对象。

.class->机器码 这⼀步。 JVM 类加载器⾸先加载字节码⽂件,然后通过解释器逐⾏解释执⾏,这种⽅式的执⾏速度会相对⽐较慢。⽽且,有些⽅法和代码块是经常需要被调⽤的(热点代码),所以后⾯引进了 JIT 编译器,⽽ JIT 属于运⾏时编译。当 JIT 编译器完成第⼀次编译后,其会将字节码对应的机器码保存下来,下次可以直接使⽤。

26. 强引用弱引用虚引用

在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值