JVM 初级面试题

本贴最后更新于 1198 天前,其中的信息可能已经水流花落

JVM 初级面试题

一. JVM 的构成

  • 类加载器

  • 运行时数据区(内存)

    • 程序计数器

      记录下一条指令的地址, 简单来说就是线程执行到了哪, CPU 时间片切换回来程序需要知道从哪开始执行, 在物理上是通过寄存器实现

    • 方法区

      方法区是一种规范, 规定了存储类相关的信息, 永久代和元空间是他的实现

      JDK8 之前是在堆内存中分一块区域(逻辑)叫做永久代, 用来存放运行时常量池和类信息

      JDK8 的时候把永久代修改成了元空间, 在本地内存(操作系统内存)中开辟一块区域, 不再使用堆内存的一部分, 运行时常量池仍然放在堆中

      什么是常量池, 什么是运行时常量池?

      常量池就是一张常量表, 存在 class 文件中, 通过常量符号可以找到对应的方法名, 参数类型, 字面量等信息

      运行时常量池就是在运行中, class 中的常量池信息会加载到内存中

    • 线程共享的内存区域, 存放对象

    • 虚拟机栈(线程栈)

      每个线程默认 1M 的栈内存用来存储栈帧

    • 本地方法栈

  • 执行引擎

    • 解释器

      解释 class 文件, 翻译成机器码

    • 即时编译器

      会对热点代码优化

    • GC

      垃圾收集器

  • 本地方法接口

二. 类的加载

  1. 加载

    加载进内存, 生成对应的 C++ 结构 instanceKlass, 生成代表这个类的 Class 对象, 作为对外暴露类的入口

  2. 连接

    1. 验证

      验证格式, 安全性检查

    2. 准备

      给静态变量分配内存, 设置默认值, 如果是 final 的基本类型, 或者字符串, 赋值在准备阶段完成

    3. 解析

      将常量池中的符号引用解析成直接引用, 就是把原本 class 中存储的常量符号, 比如方法名, 字面量等信息, 解析成真正的内存地址

  3. 初始化

    执行的构造方法, 就是静态代码块

    初始化的情况

    main 方法的类总是被首先初始化

    首次访问静态变量或静态方法

    子类初始化时, 父类还没初始化, 会跟着初始化

    Class.forName()

    new 对象

    不会初始化的情况

    访问 final 的静态常量, 基本类型或者字符串

    类.class

    创建类的数组的时候

    手动使用加载器加载类的时候, 比如: classLoad.loadClass("xxx.xxx.xxx"), 只会加载类, 不会初始化

    Class.forName()第二个参数为 false 的时候

三. 双亲委派

类加载器分四种

  1. BootstrapClassLoader 启动类加载器, C++ 实现, 无法访问, 加载 jre/lib 下的类
  2. ExtensionClassLoader 扩展类加载器, 上级为 BootstrapClassLoader, 加载 jre/lib/ext 下的类
  3. ApplicationClassLoader 应用程序加载器, 上级为 ExtensionClassLoader, 加载 classpath 下的类
  4. 自定义加载器, 继承抽象类 ClassLoader, 重写 findClass 方法, 上级为 ApplicationClassLoader

双亲委派指的是类加载器加载 1 个类的时候, 会优先从上级获取, 如果没有才自己加载

四. 引用的类型

一般来说有四种, 但还听说过第五种

  1. 强引用

    普通对象的引用, 平时我们一般都使用这个

  2. 软引用

    当 GC 发生的时候, 并且内存不够, 此时会把软引用的对象释放, 可以配合引用队列使用

  3. 弱引用

    当 GC 发生的时候, 不管内存够不够, 都会释放该引用的对象, 可以配合引用队列使用

  4. 虚引用

    虚引用主要是为了跟踪垃圾回收过程, 必须配合引用队列使用, 在对象被垃圾回收的时候, 会将这个引用加入队列, 在虚引用出队列前, 不会彻底销毁该对象, 一般用来释放堆外内存(直接内存)

  5. 终结器引用

    虚拟机会给我们的对象创建终结器引用, 当对象被回收时, 会把终结器引用放入引用队列, 等待 finalizeHander 线程调用 finalize 方法

五. 垃圾回收算法

先了解 1 下如果怎么确定一个对象是否是垃圾: 引用计数, 可达性分析(根可达), java 使用后者, 听说最早版本的 python 是使用引用计数

  1. 标记清除, 优点: 速度快, 缺点: 内存碎片
  2. 标记整理, 优点: 没有内存碎片, 缺点: 速度慢
  3. 复制, 优点: 没有内存碎片, 缺点: 浪费一部分空间

六. 垃圾收集器

  1. 新生代

    1. Serial, 单线程, 使用复制算法
    2. ParNew, 多线程并行, 使用复制算法
    3. Parallel, 多线程并行, 吞吐量优先
  2. 老年代

    1. SerialOld, 单线程, 使用标记清除整理算法

    2. ParallelOld, 多线程并行, 吞吐量优先, 使用标记清除整理算法

    3. CMS, 多线程并行 + 并发, 响应时间优先, 使用标记清除清除算法

      CMS 垃圾回收的 4 个步骤

      1. 初始标记(不能并发)
      2. 并发标记(可以并发)
      3. 重新标记(不能并发)
      4. 并发清除(可以并发)

      由于使用标记清除算法, 所以无法整理内存碎片, 当内存无法满足程序需求的时候, 会启动 SerialOld 垃圾收集器, 导致一次非常慢的 FullGC, 可以修改配置使 FullGC 的时候采用 MSC 算法压缩堆内存, 但是使用 MSC 算法合并整理内存的时候, 不能并发

  3. 整堆

    G1, 多线程并行 + 并发, 同时注重吞吐量和响应时间, 将整个堆分成多个 Region 区域(512K), 整体上是标记整理算法, 两个区域之间是复制算法

    G1 垃圾收集器的三种情况(网上资料计较混乱, 无法验证真伪)

    1. 新生代垃圾收集
    2. 混合垃圾收集(老年代内存占用达到 45%, 可以通过参数设置)
    3. FullGC

    整个过程: 初始标记(不能并发), 并发标记(三色标记), 最终标记(不能并发), 垃圾清除

七. MinorGC/YoungGC 和 FullGC

MinorGC/YoungGC 是清理新生代的垃圾收集

FullGC 是清理老年代的垃圾回收, JDK8 之前永久代内存不够也会触发 FullGC

八. 新生代老年代

  1. 新生代

    默认占用堆内存的三分之一

    • 伊甸区, 默认新生代的十分之八, 新建的对象放在伊甸区, 每次发生 YongGC 时, 会把不需要清除的对象放入幸存区 from, 并且年龄加 1
    • 幸存区 from, 默认新生代的十分之一, 每次发生 YongGC 时, 会把幸存区 from 的对象复制到幸存区 to, 然后幸存区 to 和幸存区 from 身份交换, 幸存的对象年龄加 1, 当达到 16 岁(默认, 可以配置)时, 扔到老年代
    • 幸存区 to, 默认新生代的十分之一

    当创建的对象特别大, 伊甸区内存不够时, 或者幸存的对象超过幸存区 50% 大小的时候, 直接扔到老年代

  2. 老年代

    老年代里的对象一般来说是存活比较久的, 当老年代满了的时候会发生 FullGC

九. happen-before

  1. 程序次序规则: 在一个线程内哪怕指令重排序, 但代码产生的结果要保证和不重排序一致
  2. 管程锁定规则: 解锁之前的改动要对获取锁的线程可见
  3. volatile 变量规则: 对 volatile 变量写的操作一定对读的操作可见
  4. 线程启动规则: 在线程启动之前的操作, 一定要对被启动的线程可见
  5. 线程终止规则: 线程结束的时候, 确保结束之前的操作对其他线程可见
  6. 线程中断规则: 线程中断的时候, 确保中断之前的操作对该线程可见
  7. 传递规则: A 线程可见 B 线程的操作, B 线程可见 C 线程的操作, 那么要确保 A 线程可见 C 线程的操作
  8. 对象终结规则: 确保对象的构造方法的操作对 finalize 方法可见

十. GC 调优

其实这个没啥好说的, 根据不同的情况再调整参数, 此处针对 Web 场景补充 1 点点

一般我们 Web 项目的对象都是垃圾, 都是数据库查出来, 返回给前端, 然后就可以回收了, 所以对应的应该把新生代调大一些, 最好伊甸区大于: 预估并发量 * 请求平均消耗的内存

  • 面试

    面试造航母,上班拧螺丝。多面试,少加班。

    325 引用 • 1395 回帖 • 1 关注
  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    180 引用 • 120 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...