JVM 八股文面试题
Java如何实现跨平台?
不直接跑在操作系统上,而是跑在 JVM 上。
JDK和JRE有什么区别?
- **JRE(Java Runtime Environment)**Java 程序的运行环境:只有JVM+核心类库(如 java.lang.*、java.util.*)
- **JDK(Java Development Kit)**Java 开发工具包:JRE+开发工具(编译器(javac)、打包工具(jar)、诊断工具(jconsole、jmap、jstack 等))
类的加载流程是怎样的?
- 加载:从磁盘本地文件加载到JVM内存
- 链接
- 验证:检查字节码是否符合JVM规范
- 准备:为静态变量赋默认值(数值型为0,布尔为false,引用为null)
- 解析:将符号引用替换为直接引用(内存地址)
- 初始化:执行静态代码块
- 使用
- 卸载
有哪几种类加载器?
- 启动类加载器(Bootstrap ClassLoader):负责加载核心类库(如java.lang.*、java.util.*)
- 扩展类加载器(Extension ClassLoader):负责加载 扩展目录($JAVA_HOME/lib/ext) 下的类库
- 应用类加载器(Application ClassLoader):负责加载 classpath 下的类(我们自己写的代码、第三方依赖)
- 自定义类加载器(Custom ClassLoader):开发者可自己继承 ClassLoader,实现特殊的加载逻辑
什么是双亲委派机制?
类加载后由下向上请求,由上向下加载,这种方式主要是为了保证核心类库的安全,同时避免重复加载同一个类;
当一个类加载器(ClassLoader)收到“加载某个类”的请求时,它不会自己立刻去加载,而是先把请求交给它的“父加载器”去尝试。如果父加载器能找到并加载,就用父加载器的结果。如果父加载器找不到,才由自己来加载。
为什么有时候要打破双亲委派?
比如在 Tomcat 或者 SPI 这种场景下,因为父加载器加载不到我们应用里的类,或者我们需要不同Web 应用要有自己独立的类空间(避免类冲突),这时候就需要自定义类加载器,不走默认的委派规则。
如何打破双亲委派?
比如Tomcat主要通过WebAppClassLoader重写loadClass方法,优先加载当前应用类,仅对核心类委托父类加载的方式,打破了双亲委派的“父类优先”原则,满足了Web容器的特殊需求
如何判断两个class对象是否相同
- 类的完整类名必须一致,包括包名
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
JVM 内存结构(运行时数据区)有哪些?
- 堆:存放几乎所有对象实例和数组
- 虚拟机栈:每个线程执行 Java 方法时都会创建一个栈帧,用来存储局部变量、操作数栈、返回地址等
- 方法区:存储类的结构信息(类元数据)、静态变量、常量池、方法字节码等,JDK8前叫永久代(堆内存),JDK8之后改叫元空间(本地内存)
- 程序计数器:记录当前线程正在执行的字节码指令地址
- 本地方法栈:为 JVM 调用本地(Native)方法服务
栈帧是什么,里面有哪些东西?
虚拟机栈是由一系列 栈帧(Stack Frame) 组成的,每次调用一个方法,就会在栈里压入一个新的栈帧;方法执行完毕,就会弹出对应的栈帧
- 局部变量表(Local Variables):存放方法的参数和方法体内定义的局部变量
- 操作数栈(Operand Stack):用于方法执行中的各种运算(加减乘除)、临时存放数据、方法返回值
- 动态链接(Dynamic Linking):存放指向运行时常量池的引用,用于支持将符号引用 →转为直接引用
- 方法出口(Return Address):记录方法调用后返回的位置
常见的垃圾回收算法以及他们的优劣势?
- 标记-清除:从 GC Roots 出发,标记所有存活的对象,清理掉没有被标记的对象(垃圾)
- 优点:实现简单,不需要额外内存
- 缺点:会产生大量内存碎片
- 标记-整理:先标记存活对象,把所有存活对象往一端移动,然后清理边界以外的垃圾
- 优点:没有内存碎片
- 缺点:移动对象时需要调整引用,成本高
- 复制算法:将内存分为两块,只使用一块,GC时将存活对象复制到另一块
- 优点:没有内存碎片,复制效率高
- 缺点:会浪费一半内存
- 分代收集:新生代使用复制算法,老年代使用标记-清除/标记-整理算法
- 优点:结合各算法优势,提高效率
- 缺点:需要分区管理,增加实现复杂度
怎么判断一个对象是不是垃圾?
- 引用计数法:每个对象维护一个“引用计数器”,有一个地方引用它时 +1,引用失效时 -1;当计数器 = 0,就认为对象可以回收。
- 优点:实现简单,判断效率高
- 缺点:存在循环引用的问题
- 可达性分析算法:从一组 GC Roots(栈、静态变量、常量池、本地方法引用)出发,沿引用链往下找,能找到的对象存活(可达),找不到的就是垃圾(不可达)
- 优点:解决了循环引用的问题
GC Roots有哪些?
- 线程栈里的局部变量(方法里 new 的对象)
- 类的静态变量
- 本地方法(JNI)里引用的对象
有哪些引用类型?
- 强引用:GC 永远不会回收它(对象一定存活)
- 软引用:GC 会根据内存情况决定是否回收软引用对象(内存紧张才回收)
- 弱引用:GC 一发现弱引用对象就会回收,不管内存是否够用(一遇 GC 就回收)
- 虚引用:不能通过虚引用获取对象(get() 永远返回 null)(可以看作已经回收了)
什么情况对象从新生代进入老年代?
- 存活年龄达到阈值(默认15)
- 大对象直接进入老年代(可通过参数控制)
- survivor区空间不足放不下
- 动态年龄判断,同龄对象占空间超过一半,则小于等于该年龄的对象都会提前进入老年代
内存泄漏与内存溢出的区别?
- 内存泄露:无用对象太多,无法被GC回收,导致内存被垃圾占满了,等于是占着茅坑不拉屎(如ThreadLocal没有remove())
- 内存溢出:有用对象太多,内存被用完了,属于是茅坑被占满了,没位置了(如递归没有控制边界)
G1为什么支持大内存回收低延迟的?
因为G1把堆划分成多个独立的小区域(Region),采用按 Region 回收 + 并行 + 并发标记 + 按收益优先回收的方式,实现了可预测的停顿时间控制。
什么情况触发Full GC?
- 老年代空间不足 + Major GC 失败
- 方法区/元空间溢出
- System.gc() 调用
- 大对象晋升失败
- 空间担保机制
空间分配担保机制是什么?
空间担保机制指的是在触发 Minor GC 前,JVM 会先检查老年代的可用空间是否足够存放新生代中可能晋升的对象,如果不够,就提前触发一次 Full GC 来“担保”空间安全,避免内存溢出。
cms的垃圾回收流程?
- 初始标记:标记 GC Roots 能直接关联到的对象,需要STW
- 并发标记:从初始标记的对象出发,并发地遍历整个对象图,找到所有可达对象
- 重新标记:修正并发标记期间因为用户线程运行而导致的标记遗漏,需要STW
- 并发清除:清除掉未被标记的垃圾对象,释放空间,可能产生内存碎片
cms垃圾回收哪几步需要stw?
初始标记和重新标记
字符串实现1.8和1.9的区别?
1.8使用字符数组char[],1.9使用字符数组byte[]
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 Bob's Blog!




