banner
NEWS LETTER

JVM面试题

Scroll down

1.双亲委派模型的介绍,破坏的情景

如果一个类加载器接到加载类的请求时,它首先不会自己去加载这个类,而是把它委托给它的父加载器加载,不断递归,如果父加载器能加载这个类,则成功返回。否则就让子加载器去加载。

它的机制在ClassLoader.loadClass()

1)先查看类加载器中缓存中有没有目标类,有的话直接放回。
2)判断当前类加载器中的父加载器是否为空,如果不为空则调用父加载器的loadClass()方法
3)如果它的父加载器为空说明它的父加载器为引导类加载器,则调用findBootstrapClassorNull(name),让引导类加载器加载
4)如果以上三条都没有成功加载,则调用findClass(name)方法

1.1Class.forName(),ClassLoader.loadClass()区别

调用Class.forName()方法把Class文件加载到内存的同时,进行类的初始化。ClassLoader.loadClass()把Class文件加载到内存后不会立即初始化,等到第一次使用时才初始化

破坏方法:

1)双亲委派模型主要通过loadClass()实现的,重写loadClass()实现
2)通过线程上下文加载器破坏 父加载器需要加载子加载器才能加载的类时可以通过Thread获取子类加载器 Thread.currentThread().getContextClassLoader()
https://blog.csdn.net/weixin_43729854/article/details/106266262
3)热替换
类的热替换指的是程序运行的时候,对内存中的类信息进行替换,程序无需重启,替换的类会立即生效。实现原理:自定义一个类的加载器,类加载器的父加载器为空,之所以为空是为了防止双亲委派去父加载器找。没回调用这个类时先重新创建一个类加载器,然后通过这个类加载器加载这个类。

2.java对象产生的过程

1)检查类是否加载,没有就加载类就加载
2)为对象分配内存(内存地址是连续指针碰撞,所谓的指针碰撞就是用一个指针记录当前空闲地址的位置,内存地址使用不连续空闲列表,空闲列表记录着当前未被使用的空间)
3)处理并发问题(分配内存还要处理并发问题,有两种方法一个是通过CAS,另一个是通过虚拟机给每个线程分配一块TALB(私有的空间))
3)属性的默认初始化(分配零值)
4)设置对象头信息
5)执行初始 方法

3.java对象的组成部分(java对象的内存布局,java对象模型)

1)对象头

  • 哈希值
  • GC分代年龄(surivor区用的)
  • 锁状态标志
  • 线程持有的锁
  • 偏向线程id
  • 偏向时间戳

2)实例数据区
实例对象的一些字段属性内容。
3)对齐填充字节
JVM要求java对象所占的内存大小应该是8bit的倍数,这部分的作用将对象大小补充为8bit的倍数

4.CMS是什么

CMS是一个低延时的老年代垃圾收集器,它的垃圾收集算法用的是标记清除算法。它的过程主要有四个阶段:初始标记,这个阶段会短暂的stop-world,它主要是直接标记出GCRoot能直接关联的对象;并发标记,从GC ROOT直接关联的对象开始遍历整个对象图,耗时虽然耗时长但不需要停顿用户线程;重新标记,修正并发标记阶段中程序运行导致标记变动的部分,这个阶段停顿时间较长;并发清除,清除已经死亡的对象,这个阶段也不需要停顿用户线程。
它的缺点:由于并发标记和并发清除垃圾回收线程和用户线程同时运行会产生新的垃圾(浮动垃圾),这些垃圾要等到下一次GC才能回收。

5.G1是什么

G1是一个延时可控的情况下尽可能提高吞吐量的垃圾收集器,它既收集年轻代也收集老年代,用的算法是标记压缩。它将 java堆分成2048个大小相同的Region块,新生代和老年代不再是物理隔离的了,每个region块可能是伊甸园区,survivor区或是老年代。由于G1将堆分成了许多块,这样存在一个块中的对象被其他块中的对象引用的问题,G1 通过记忆集来解决这一问题,每一个块都有自己的记忆集,记忆集上记着其他块中引用该对象的对象的相关信息。然后G1还有可停顿预测模型,所谓的可停顿预测模型跟踪就是G1跟踪各个块的回收价值开销,在后台维护一个优先列表,每次你根据允许的时间内优先回收最大价值的块。

优点:停顿时间略超CMS,但解决了浮动垃圾问题

6.内存泄露的原因

内存泄露指的是那些未使用的对象长期占着内存没有及时释放。具体的原因有:资源未关闭造成的,如io流没有及时关闭;使用HashSet时候没有给泛形类型重写hashCode方法和equals方法;对象被静态成员使用;

7.为什么要区分老年代和新生代

根据对象的存活时间,有的对象存活时间长,有的对象存活时间短。将寿命长的对象放在一个区,将寿命短的对象放在一个区,不同区采用不同的垃圾收集算法,寿命长的回收频率低,寿命短的回收频率高,这样也可以提高效率,分区也可以避免全局遍历。

7.1为什么要有survivor

如果Eden区每次满了清垃圾,存活的对象被迁移到老年区,老年区满了就会触发Full GC,Full GC是非常耗时的。有了survivo区,Eden满了后存活的对象被survivor,反复几次清理后没清理掉这样可以减小老年区的压力。survivor相当于一个筛子,筛掉生命周期短的,将生命周期长的放入老年区,减小老年代清理的频率。

7.2只有一个survivor行不行

两个分区可以解决内存碎片化问题,survivor假若只有一个,有对象被回收,会产生内存碎片,两个survivor的话,minor GC会移动对象,解决内存碎片问题。

8.简述下java内存结构(运行时数据区)

  • 程序计数器:线程私有,一小块内存空间,没有垃圾回收。程序计数器用于保存虚拟机下一条指令。
  • 虚拟机栈:线程私有,生命周期与虚拟机相同,虚拟机栈是java方法的内存模型,在每个方法执行前都会创建一个栈帧用于存放局部变量、操作数栈、动态链接、方法出口等。一个方法从调用到执行完的过程对应着栈帧在虚拟机栈中入栈与出栈的过程。每一个线程只能有一个活动栈帧,方法执行时入栈,方法执行完后出栈。方法体中的基本变量和引用变量都存储在栈上,其他都存储在堆上。
  • 本地方法栈:线程私有,本地方法栈与与虚拟栈类似,不过本地方法栈是为本地方法服务的。所谓的本地方法就是native修饰的方法,一般是c或c++写的,由于java无法直接与操作系统交互,所以需要本地方法来完成。
  • 堆:线程共享,堆是java虚拟机管理的内存的最大一块,他主要是用来存放所有对象实例,还存放字符串常量池和静态变量(jdk1.7),它也是垃圾收集主要管理的区域。(-Xms :指定初始内存大小,-Xmx:指定最大内存大小)
  • 方法区:方法区也是线程共享的,主要存储被JVM虚拟机加载的类信息,常量,编译后的字节码等。

8.1 方法区的演进

1)JDK1.6由永久代管理,使用堆的一部分内存,字符串常量池和静态变量都在其中
2)JDK1.7版本任然有永久代,字符串常量池和静态变量从永久代移动到堆中
3)JDK1.8将方法区由元空间实现,元空间的内存交给本地内存管理,不占用分配给虚拟机的内存。

为什么要用元空间取代永久代

JVM加载的类信息个数不确定,太小容易溢出,太大浪费内存空间,难以确定合适的内存大小。

8.2 虚拟机栈是否需要垃圾回收

不需要,虚拟机栈是由一个个栈帧组成的,当方法执行完后栈帧自动出栈,故不需要垃圾回收。

8.3从jvm的角度分析方法的局部变量是否是线程安全的。

如果局部变量没逃离方法的作用域,那么就是线程安全的。如果逃离了方法的作用域,比如作为方法的返回值,那么其他的线程可能会拿到这个局部变量,这时要考虑线程安全问题

9.类的加载过程

类的加载过程包括加载、验证、准备、解析、初始化

  • 加载:通过一个类的全限定名获取该类的全限定名获取该类的二进制流,并在内存中生成该类的java.lang.Class对象。
  • 验证:保证字节流中的信息符合虚拟机的要求。
  • 准备:为静态变量分配内存并且设置静态变量的初始值,这里的初始值就是各个类型的零值
  • 解析:将常量池的符号引用转为直接引用
  • 初始化:执行静态变量的赋值操作和静态语句块。

10.堆是分配对象的唯一选择吗

如果经过逃逸分析,发现这个对象没有逃出方法的话,那么就可能优化成栈上分配。所谓的逃逸分析就是看这个对象在方法内定义后是否被外部方法引用,如果只在方法内部,那么就没有发生逃逸,被外部方法引用则发生逃逸。

如果没有逃逸分析编译器有三种优化:

  • 栈上分配:如果一个对象没有逃逸则,将堆上分配改为栈分配,方法执行完毕栈帧自动出栈,这样也无需进行垃圾回收。
  • 同步省略:如果这个对象只被一个线程访问到,那么就不需要同步操作,编译器会把同步相关的代码省略掉,所以也叫锁消除。
  • 标量替换:如果一个对象不会被外界访问,编译器把它拆成诺干个成员变量。

11.垃圾回收相关算法

1)引用计数算法
每个对象有个引用计数器,记录着自己被其他对象引用的情况。对于一个对象a,假如有其他对象引用了它,对象A的计数器就加1;如果引用失效了计数器就减1;当A的计数器为0,就表示该对象不可用,可以被回收
优点:实现简单,效率高
缺点:由于每个对象都有引用计数器,增加了空间开销,更新引用计数器又增加了时间开销。并且无法处理循环引用的情况。

2)可达性分析算法
从根节点开始遍历看目标,看目标对象是否可达,可达是指对象之间是否有引用链,若可达这说明对象存活的,若不可达则标记对象已经死亡。

GC Root:

  • 虚拟机栈所引用的对象
  • 本地方法栈所引用的对象
  • 静态对象引用的对象
  • 字符串对象
  • 同步锁持有的对象

补充:对象的finalize机制

一个对象要经过两次标记才能判断以及死亡,每个对象都有finalize(),这个方法可以使被一次标记的对象重新存活,具体过程:
1.一个对象被可达性算法标记为死亡后判断是否有必要执行finalize()方法
2.如果这个对象没有重写finalize()方法或是finalize()方法已经执行过一遍则直接判定对象死亡,否则将这个对象加入一个低优先级的队列。
3.稍后JVM垃圾回收器会对这个队列进行二次标记,二次标记前会调用finalize()方法,如果该对象与任何一个存活的对象(引用链)的对象建立了关系,那么该对象会被移出即将被回收对象集合。下一次相同的情况,该对象不会调用finalize()方法而是直接标记为死亡。

12.三种垃圾回收算法

1)标记-清除算法
标记清除算法主要分为两个步骤:

  • 标记:从根节点开始遍历标记所有被引用的对象
  • 清除:遍历堆中所有对象,若该对象没有被标记则将其回收

缺点:效率低,需要STW(停止所有的用户程序),这种方式会造成空间碎片,需要一个空闲列表来维护。

2)复制算法
将内存分为两块,每次只使用其中的一块,进行垃圾回收时将其存活的对象复制到未使用的内存对象中,然后交换两个内存的角色。

优点:没有标记和清除的过程,没有空间碎片的问题
缺点:会浪费一半的内存空间

3)标记-压缩(标记-整理)

  • 跟标记-清除一样,标记所有存活的对象
  • 将所有存活的对象按顺序移动到内存的另一端
  • 清除边界以外的所有空间

9.JVM判断一个类是否相同

首先判断两个类的类加载器是否相同,然后再看全类名是否相同

10.新生代晋升老年代

1)新创建的对象会放入伊甸园区,当伊甸园区空间不足时时就触发young GC
2)伊甸园区存活的对象和from区的对象会移动到to区,他们的寿命加1,再交换form和to区
3)当survivor区的对象的寿命超过某个阀值时(默认是15)就会放入老年代
4)如果新生代和老年代的内存都满了会触发Full GC

11.为什么有了及时编译器(JIT)还需要解释器

当java虚拟机启动时,解释器可以先发挥作用,而不必等待及时编译器编译完后再执行,这样可以省去不编译的运行时间。随着时间的推移,越来越多的代码被编译为本地代码。

补充:
JIT是如何进行及时编译的?
JIT通过热点探测功能,将有价值的代码编译成本地机器指令。

热点代码探测技术:
1)方法调用计数器
这个计数器用于统计方法被调用的次数,当调用超过一个阈值就会触发JIT编译(Client默认是1500,Server默认是10000)
当一个方法被调用时,先会检查该方法是否被JIT编译过,有就直接使用本地代码,没有就将此方法的计数器加1,方法调用计数器和回边计数器之和是否超过方法调用计数器的阈值,如果有则用JIT编译。

2)回边计数器
统计方法循环体代码的执行次数。

3)热点衰减
方法计数器统计的并不是一个绝对的次数,是一个相对的频率,即一段时间内方法调用的次数。当超过一定的时间,方法调用次数不足以触发JIT编译,那么这个方法调用技术器会减半。

11.评估GC的性能指标

  • 吞吐量:运行用户代码的时间占总运行时间的比例(运行用户代码时间 / (运行用户代码时间 + 垃圾回收时间))
  • 暂停时间:执行行垃圾收集的时间。
  • 内存占用:java堆区所占内存大小。

吞吐量、暂停时间和内存占用共同构成一个不可能三角,最多满足其中的两项。
如果以吞吐量为优先,会降低内存回收的执行频率,但会GC的暂停时间更长。反之如果以低延时为优先条件,那么GC的暂停时间变短,只能频繁地执行GC,单位时间内GC的总时间会变长,吞吐量会下降。

12.java四种引用

  • 强引用:只要沿着GCRoot引用链能够找到的对象,都不会被垃圾回收。一般new出来的对象都是强引用。
  • 软引用:垃圾回收后,内存仍不足就会回收软引用指向的对象
  • 弱引用:垃圾回收时,不管内存充不充足都会回收弱引用指向的对象。
  • 虚引用:它不是单独使用,也不能通过虚引用引用对象。设置虚引用的唯一目的是跟踪其他对象的回收过程。虚引用必须跟引用队列一起使用,例如一个对象被回收时,发现它关联这一个虚引用就把它加入到一个引用队列中

13.三大类加载器

1)引导类加载器(BootstrapClassloader)

  • 用来加载java的核心库,JAVAHOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容
  • 他是C/C++写的,没有父加载器

2)扩展类加载器(ExtClassloader)

  • 继承Classloader,父加载器为引导类加载器
  • 加载jre/lib/ext目录下的类

3)系统类加载器(应用类加载器)AppClassloader

  • 继承Classloader类,父加载器为扩展类加载器
  • 加载java.class.path下的类

14.常量怎么被回收?类怎么被回收?

1)只要这个常量池的常量没有被任何地方引用,就可以被回收。
2)回收类需要满足一下的三个条件,比较苛刻

  • 该类所有的实例都已经被回收
  • 该类的类加载器以被回收
  • 该类对应的java.lang.Class对象没有在任何地方被引用

15.内存分配策略

  • 优先分配到Eden
  • 大对象直接分配到老年代
  • 长期存活的对象分配到老年代
  • 动态对象年龄判断:如果survivor区中相同年龄所有对象大小大于survivor空间的一半,年龄大于等于该年龄的对象可以直接进入老年代
  • 空间分配担保:

只要老年代的连续空间大于新生代总对象或者历次晋升的平均大小就会进行Minor GC,否则进行 Full GC

16.AOT(ahead of time运行前编译)与JIT(just in time边运行边编译)的区别

1)JIT可以根据当前硬件的实时情况和进程运行情况编译成合适的字节码指令
2)JIT支持运行时的多态性
3)JIT支持跨平台

其他文章
cover
Spring面试题
  • 22/11/16
  • 13:02
  • java资料
cover
java基础面试题
  • 22/11/16
  • 13:02
  • java资料