文章目录
- JVM面试题
- 基础
- 内存管理
- 虚拟机执行
JVM面试题
基础
1.什么是JVM?
- JVM,也就是 Java 虚拟机,它是 Java 实现跨平台的基石。
- Java 程序运行的时候,编译器会将 Java 源代码(.java)编译成平台无关的 Java 字节码文件(.class),接下来对应平台的 JVM 会对字节码文件进行解释,翻译成对应平台的机器指令并运行。
同时,任何可以通过 Java 编译的语言,比如说 Groovy、Kotlin、Scala 等,都可以在 JVM 上运行。
2.JVM的组织架构?
- JVM 大致可以划分为三个部门:类加载器、运行时数据区和执行引擎。
类加载器:负责从文件系统、网络或其他来源加载 Class 文件,将 Class 文件中的二进制数据读入到内存当中。
运行时数据区: JVM 在执行 Java 程序时,需要在内存中分配空间来处理各种数据,这些内存区域主要包括方法区、堆、栈、程序计数器和本地方法栈。
执行引擎: 执行引擎是 JVM 的心脏,负责执行字节码。它包括一个虚拟处理器,还包括即时编译器(JIT Compiler)和垃圾回收器(Garbage Collector)。
内存管理
3.JVM的内存区域是什么?
- JVM 的内存区域可以粗暴地划分为
堆
和栈
- 当然了,按照 Java 的虚拟机规范,可以再细分为
堆
、方法区
、程序计数器
、虚拟机栈
、本地方法栈
、等。 - 其中
方法区
和堆
是线程共享区,虚拟机栈
、本地方法栈
和程序计数器
是线程私有的。
3.1堆
- Java 堆(Java Heap)是虚拟机所管理的内存中最大的一块,被所有线程共享,在虚拟机启动时创建。
- Java 堆中经常会出现
新生代
、老年代
、Eden空间
、From Survivor空间
、To Survivor空间
等名词
3.2方法区
- 方法区是一块比较特别的区域,和堆类似,也是各个线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
- Java 虚拟机规范对它的约束非常宽松,所以方法区的具体实现历经了许多变迁,例如 JDK 1.7 时,方法区被永久代(Permanent Generation)所代替,而在 JDK 1.8 时,永久代被彻底移除,取而代之的是元空间(Metaspace)。
3.3程序计数器
- 程序计数器(Program Counter Register)也被称为 PC 寄存器,是一块较小的内存空间。它可以看作是当前线程所执行的字节码行号指示器。
3.4Java虚拟机栈
- Java 虚拟机栈(Java Virtual Machine Stack),通常指的就是“栈”,它的生命周期与线程相同。
- Java 虚拟机栈(JVM 栈)中是一个个栈帧,每个栈帧对应一个被调用的方法。当线程执行一个方法时,会创建一个对应的栈帧,并将栈帧压入栈中。当方法执行完毕后,将栈帧从栈中移除。
3.5本地方法栈
- 本地方法栈(Native Method Stacks)与虚拟机栈相似
- 区别在于虚拟机栈是为虚拟机执行 Java 方法服务的,而本地方法栈是为虚拟机使用到的本地(Native)方法服务的。
4.堆和栈的区别是什么?
- 堆属于线程共享的内存区域,几乎所有的对象都在对上分配,生命周期不由单个方法调用所决定,可以在方法调用结束后继续存在,直到不在被任何变量引用,然后被垃圾收集器回收。
- 栈就是前面提到的 JVM 栈(主要存储局部变量、方法参数、对象引用等),属于线程私有,通常随着方法调用的结束而消失,也就无需进行垃圾收集。
5.JDK1.6、1.7、1.8内存区域的变化?
- JDK1.6、1.7、1.8 内存区域发生了变化,主要体现在方法区的实现:
- JDK1.6 使用永久代实现方法区;
- JDK1.7 时发生了一些变化,将字符串常量池、静态变量,存放在堆上
- 在 JDK1.8 时彻底干掉了永久代,而在直接内存中划出一块区域作为 元空间,运行时常量池、类常量池都移动到元空间。
6.内存泄露和内存溢出是什么意思?
- 内存泄露:申请的内存空间没有被正确释放,导致内存被白白占用。
- 内存溢出:申请的内存超过了可用内存,内存不够了。
- 两者关系:内存泄露可能会导致内存溢出。
- 用一个有味道的比喻,内存溢出就是排队去蹲坑,发现没坑位了,内存泄漏,就是有人占着茅坑不拉屎,占着茅坑不拉屎的多了可能会导致坑位不够用。
7.内存泄漏的原因?
- 造成内存泄漏的原因有以下几种:
- 静态集合类:静态集合的生命周期和 JVM 一致,所以静态集合引用的对象不能被释放。
- 单例模式:和上面的例子原理类似,单例对象在初始化后会以静态变量的方式在 JVM 的整个生命周期中存在。如果单例对象持有外部的引用,那么这个外部对象将不能被 GC 回收,导致内存泄漏。
- 连接(IO/数据)未释放:创建的连接不再使用时,需要调用 close 方法关闭连接,只有连接被关闭后,GC 才会回收对应的对象(Connection,Statement,ResultSet,Session)。忘记关闭这些资源会导致持续占有内存,无法被 GC 回收。
- 变量作用域过大:一个变量的定义作用域大于其使用范围,很可能存在内存泄漏;或不再使用对象没有及时将对象设置为 null,很可能导致内存泄漏的发生。
- hash值发生改变:对象 Hash 值改变,使用 HashMap、HashSet 等容器中时候,由于对象修改之后的 Hah 值和存储进容器时的 Hash 值不同,所以无法找到存入的对象,自然也无法单独删除了,这也会造成内存泄漏。说句题外话,这也是为什么 String 类型被设置成了不可变类型。
- ThreadLocal使用不当:使用完 ThreadLocal 一定要记得使用 remove 方法来进行清除。
8.如何判断对象仍然存活?
两种方式:引用计数算法、可达性分析算法
引用计数算法:
- 引用计数器:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;
- 当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
可达性分析算法:
- 将一系列 GC Roots 作为初始的存活对象合集(Gc Root Set),然后从该合集出发,探索所有能够被该集合引用到的对象,并将其加入到该集合中,
- 这个过程我们也称之为标记(mark)。最终,未被探索到的对象便是死亡的,是可以回收的
9.Java堆的内存分区了解吗?
- Java堆划分为新生代和老年代两个区域,新生代存放存活时间短的对象,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放;
- 新生代分为三个区域:eden、from、to(8:1:1);from和to区域统称为survivor
- 发生垃圾收集时,将 Eden 和 Survivor 中仍然存活的对象一次性复制到另外一块 Survivor 空间上,然后直接清理掉 Eden 和已用过的那块 Survivor 空间。默认 Eden 和 Survivor 的大小比例是 8∶1。
10.垃圾收集算法了解吗?
- 标记-清除算法
- 标记-复制算法
- 标记-清除-整理算法
11.Minor GC/Young GC、Major GC/Old GC、Mix
部分收集(Partial GC):指目标不是完整收集整个 Java 堆的垃圾收集,其中又分为:
新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS 收集器会有单独收集老年代的行为。
混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有 G1 收集器会有这种行为。
整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾收集。
12.Minor GC/Young GC什么时候触发?
- 新创建的对象优先在新生代 Eden 区进行分配,如果 Eden 区没有足够的空间时,就会触发 Young GC 来清理新生代。
13.什么时候触发Full GC?
一共有6种触发条件:
- System.gc()等命令触发:System.gc()、jmap -dump 等命令会触发 full gc
- Young GC 之前检查老年代:在要进行 Young GC 的时候,发现
老年代可用的连续内存空间
<新生代历次Young GC后升入老年代的对象总和的平均大小
,说明本次 Young GC 后可能升入老年代的对象大小,可能超过了老年代当前可用内存空间,那就会触发 Full GC。 - Young GC 之后老年代空间不足:执行 Young GC 之后有一批对象需要放入老年代,此时老年代就是没有足够的内存空间存放这些对象了,此时必须立即触发一次 Full GC
- 老年代空间不足,老年代内存使用率过高,达到一定比例,也会触发 Full GC。
- 空间分配担保失败( Promotion Failure),新生代的 To 区放不下从 Eden 和 From 拷贝过来对象,或者新生代对象 GC 年龄到达阈值需要晋升这两种情况,老年代如果放不下的话都会触发 Full GC。
- 方法区内存空间不足:如果方法区由永久代实现,永久代空间不足 Full GC。
14.对象什么时候会进入老年代?
一共有四种情况:
长期存活的对象将进入老年代:在对象的对象头信息中存储着对象的迭代年龄,迭代年龄会在每次 YoungGC 之后对象的移区操作中增加,每一次移区年龄加一.当这个年龄达到 15(默认)之后,这个对象将会被移入老年代。
#可以通过这个参数设置这个年龄值。 - XX:MaxTenuringThreshold
大对象直接进入老年代:有一些占用大量连续内存空间的对象在被加载就会直接进入老年代.这样的大对象一般是一些数组,长字符串之类的对。
#HotSpot 虚拟机提供了这个参数来设置。 -XX:PretenureSizeThreshold
动态对象年龄判定:HotSpot 虚拟机并不是永远要求对象的年龄必须达到- XX:MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代
空间分配担保:假如在 Young GC 之后,新生代仍然有大量对象存活,就需要老年代进行分配担保,把 Survivor 无法容纳的对象直接送入老年代。
15.有哪些垃圾收集器?
- 一共有7种常见的垃圾收集器:如下
- 就目前来说,JVM 的垃圾收集器主要分为两大类:分代收集器和分区收集器,分代收集器的代表是 CMS,分区收集器的代表是 G1 和 ZGC。
16.垃圾收集器的作用?
- 垃圾回收器的核心作用是自动管理 Java 应用程序的运行时内存。
- 它负责识别哪些内存是不再被应用程序使用的(即“垃圾”),并释放这些内存以便重新使用。
17.CMS垃圾收集器的垃圾收集过程?
CMS 收集齐的垃圾收集分为四步:
- 初始标记(CMS initial mark):单线程运行,需要 Stop The World,标记 GC Roots 能直达的对象。
- 并发标记((CMS concurrent mark):无停顿,和用户线程同时运行,从 GC Roots 直达对象开始遍历整个对象图。
- 重新标记(CMS remark):多线程运行,需要 Stop The World,标记并发标记阶段产生对象。
- 并发清除(CMS concurrent sweep):无停顿,和用户线程同时运行,清理掉标记阶段标记的死亡的对象。
18.G1垃圾收集器了解吗?
- Garbage First(简称 G1)收集器是垃圾收集器的一个颠覆性的产物,它开创了局部收集的设计思路和基于 Region 的内存布局形式。
- 以前的收集器分代是划分新生代、老年代、持久代等。
- G1 把连续的 Java 堆划分为多个大小相等的独立区域(Region),每一个 Region 都可以根据需要,扮演新生代的 Eden 空间、Survivor 空间,或者老年代空间。收集器能够对扮演不同角色的 Region 采用不同的策略去处理。
- 这样就避免了收集整个堆,而是按照若干个 Region 集进行收集,同时维护一个优先级列表,跟踪各个 Region 回收的“价值,优先收集价值高的 Region。
- G1垃圾收集器的运行过程如下:
- 初始标记(initial mark),标记了从 GC Root 开始直接关联可达的对象。STW(Stop the World)执行。
- 并发标记(concurrent marking),和用户线程并发执行,从 GC Root 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象、
- 最终标记(Remark),STW,标记再并发标记过程中产生的垃圾。
- 筛选回收(Live Data Counting And Evacuation),制定回收计划,选择多个 Region 构成回收集,把回收集中 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。需要 STW。
19.有了CMS,为什么还要引入G1?
优点:CMS 最主要的优点在名字上已经体现出来——并发收集、低停顿。
缺点:CMS 同样有三个明显的缺点。
Mark Sweep(标记-清除) 算法会导致内存碎片比较多
CMS 的并发能力比较依赖于 CPU 资源,并发回收时垃圾收集线程可能会抢占用户线程的资源,导致用户程序性能下降。
并发清除阶段,用户线程依然在运行,会产生所谓的理“浮动垃圾”(Floating Garbage),本次垃圾收集无法处理浮动垃圾,必须到下一次垃圾收集才能处理。如果浮动垃圾太多,会触发新的垃圾回收,导致性能降低。
G1 主要解决了内存碎片过多的问题
虚拟机执行
20.说说解释执行和编译执行的区别?
解释和编译的区别?
- 解释:将源代码逐行转换为机器码。
- 编译:将源代码一次性转换为机器码。
解释执行和编译执行的区别?
- 解释执行:程序运行时,将源代码逐行转换为机器码,然后执行。
- 编译执行:程序运行前,将源代码一次性转换为机器码,然后执行。
21.类的声明周期?
一个类从被加载到虚拟机内存中开始,到从内存中卸载,整个生命周期需要经过七个阶段:
加载 (Loading)
连接:
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
初始化 (Initialization)
使用(Using)
卸载(Unloading)
22.类加载的过程知道吗?
除去使用和卸载,就是 Java 的类加载过程。这 5 个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。
22.1加载
载入过程中,JVM 需要做三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口。
22.2验证
JVM 会在该阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行。
22.3准备
VM 会在该阶段对类变量(也称为静态变量,static 关键字修饰的)分配内存并初始化,对应数据类型的默认初始值,如 0、0L、null、false 等。
22.4解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。
22.5初始化
该阶段是类加载过程的最后一步。在准备阶段,类变量已经被赋过默认初始值,而在初始化阶段,类变量将被赋值为代码期望赋的值。换句话说,初始化阶段是执行类构造器方法(javap 中看到的 <clinit>
() 方法)的过程。
23.类加载器有哪些?
- 启动类加载器(Bootstrap ClassLoader)用来加载 java 核心类库,无法被 java 程序直接引用。
- 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
- 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
- 用户自定义类加载器 (user class loader),用户通过继承 java.lang.ClassLoader 类的方式自行实现的类加载器
24.什么是双亲委派机制?
双亲委派模型的工作过程:
- 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类
- 而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此
- 因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时 ,子加载器才会尝试自己去完成加载。
25.为什么要用双亲委派机制?
答案是为了保证应用程序的稳定有序。
例如类 java.lang.Object,它存放在 rt.jar 之中,通过双亲委派机制,保证最终都是委派给处于模型最顶端的启动类加载器进行加载,保证 Object 的一致。反之,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为 java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中就会出现多个不同的 Object 类
26.如何破坏双亲委派机制?
如果不想打破双亲委派模型,就重写 ClassLoader 类中的 fifindClass()方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。而如果想打破双亲委派模型则需要重写 loadClass()方法。