JVM面试题

文章目录

  • 文章地址链接
  • 类加载机制
    • 双亲委派模式
      • 启动类加载器
      • 扩展类加载器
      • 应用类加载器
  • JVM 内存区域
    • 程序计数器
    • 虚拟机栈
    • 本地方法栈
    • 方法区
      • 新年代
        • Eden 区
        • From Survivor 区
        • To Survivor 区
        • Minor GC 过程
      • 老年代
  • JVM 垃圾回收算法
    • 标记清除算法 Mark Sweep
    • 复制算法 Coping
    • 标记整理算法 Mark Compact
    • 分代收集算法 Generational Collection
      • 新生代回收算法
      • 老年代回收算法
  • JVM 垃圾收集器
    • Serial 垃圾收集器(单线程、复制算法)
    • ParNew 垃圾收集器(Serial + 多线程、复制算法)
    • Parallel Scavenge 垃圾收集器(多线程复制算法)
    • Serial Old 垃圾收集器(单线程标记整理算法)
    • Parallel Old 垃圾收集器(多线程标记整理算法)
    • CMS 垃圾收集器(单线程标记清除算法)
    • G1 垃圾收集器(单线程标记整理算法)
    • 如何选择合适的垃圾收集器


文章地址链接

分类 博客链接
Java 面试题总结之基础篇 待完成…
Java 面试题总结之并发编程篇 https://blog.csdn.net/weixin_38251871/article/details/104667961
Java 面试题总结之常用设计模式篇 https://blog.csdn.net/weixin_38251871/article/details/104658445
Java 面试题总结之数据库篇 待完成…
Java 面试题总结之 Spring 篇 待完成…
Java 面试题总结之 Spring Boot 篇 待完成…
Java 面试题总结 之 Spring Cloud 篇 待完成…
Java 面试题总结之 Dubbo 篇 待完成…
Java 面试题总结之 Redis 篇 待完成…
Java 面试题总结之 Zookeeper 篇 https://blog.csdn.net/weixin_38251871/article/details/104741902
Java 面试题总结之消息中间件篇 待完成…
Java 面试题总结之 Nginx 篇 待完成…
Java 面试题总结之分布式事务篇 待完成…
Java 面试题总结之分布式锁篇 待完成…

什么是 JVM?

  • JVM 是一台虚拟计算机, JVM 是运行在操作系统之上的, 没有和硬件直接交互, 不同的操作系统有不同的虚拟机, 它类似一个小而高效的 CPU, 它包括字节码指令、寄存器、栈、堆、垃圾回收器和一个存储方法域的地方

为什么 Java 能够跨平台?

  • Java 源文件通过编译器产生相应的字节码 【.class】 文件, 字节码文件通过 JVM 中的解释器, 编译成特定机器上的机器码, 每一种平台上的解释器都是不同的, 这就是为什么 Java 能够跨平台的原因

在这里插入图片描述

类加载机制

JVM 的类加载机制分为了五个部分 加载、验证、准备、解析、初始化、使用和卸载

JVM面试题_第1张图片

  • 加载
    • 在内存中生成一个代表这个类的 Class 对象, 作为方法区这个类的各种数据的入口
  • 验证
    • 确保 Class 文件中的字节流中包含的信息是否符合当前虚拟机的要求, 并且不会对 JVM 产生危害
  • 准备
    • 正式为类变量分配内存并设置类变量初始值阶段, 在方法区中分配这些变量的内存空间, 比如 public static String MSG = "Hello World", 在这个阶段的初始赋值为 null, 是在解析阶段才把值赋给 MSG 变量, 但是如果你声明为 final 类型的变量, 那么在这个阶段 JVM 就会根据 ConstantValue 属性将值赋给 MSG 变量
  • 解析
    • JVM 将常量池中的 符号引用 替换为 直接引用
      • 符号引用 : 其引用的目标并不一定要已经加装到内存中, 而是各种虚拟机实现的内存布局可以各不相同, 但是他们所接受的符号引用必须是相同的, 因为符号引用的字面量形式明确定义在 JVM 规范的 Class 文件格式中
      • 直接引用 : 是指向目标的指针、相对偏移量、一个能间接定位到目标的句柄, 如果有直接引用, 那引用的目标已经确定在内存中存在
  • 初始化
    • 开始执行类中定义的 Java 程序代码, 是执行类的构造方法的过程, 构造方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的, 虚拟机会保证子类的构造方法执行之前, 父类的构造方法已经执行完毕, 当一个没有对静态变量赋值也没有静态代码块, 那么编译器可以不为这个类生成构造方法
    • 不会执行类初始化的情况 :
      • 通过子类引用父类的静态字段, 只会触发父类的初始化
      • 定义对象数组, 不会触发该类的初始化
      • 常量在编译期间会存入调用类的常量池中, 本质上并没有直接引用定义常量的类, 不会触发定义常量所在的类
      • 通过类名获取 Class 对象
      • 通过反射 Class.forName(String class) 加载指定类的时候, 当设置指定的参数 initialize = false 的情况下
      • 通过类加载器的 loadClass() 加载

双亲委派模式

  • 当一个类收到类加载请求的时候, 首先不会自己尝试去加载这个类, 而是把这个请求委派给父类去加载, 每个请求都是一样的, 所以所有的加载请求都委派到启动类加载器中, 只有当父类加载反馈自己无法加在这个请求的时候才会让自己的子类尝试去加载
  • 使用双亲委派模式的优势是: 不管加载什么类, 都是委托给 BootStrap Loader 进行加载, 这样就保证了使用不同的类加载器最终得到的都是同一个 Object 对象

JVM面试题_第2张图片

启动类加载器

  • 负责加载 %JAVA_HOME%\lib 下的 jar 包, 也可以通过 -Xbootclasspath 参数指定加载路径并且被虚拟机认可的类

扩展类加载器

负责加载 %JAVA_HOME%\jre\lib\ext 下的 jar 包, 也可以通过 java.ext.dirs 系统变量指定路径中的类库

应用类加载器

加载 classpath下指定的路径

JVM 内存区域

  • JVM 的内存区域主要划分为 线程私有【程序计数器、虚拟机栈、本地方法区】、线程共享【JAVA 堆、方法区】和直接内存
  • 线程私有数据区域生命周期和线程一样, 依赖用户线程的 启动 - 结束 来进行 创建 - 销毁
  • 线程共享数据区域生命周期和是跟随 JVM启动 - 结束 来进行 创建 - 销毁
  • 直接内存不属于 JVM 运行时数据区, 在 NIO 中提供了基于 BufferChannelIO 方式, 其可以使用 native 方法直接分配堆外内存, 然后使用 DirectByteBuffer 对象作为这块内存的引用进行操作, 这样就会避免了 Java 堆与 native 方法中来回复制数据

JVM面试题_第3张图片

程序计数器

  • 线程私有, 是一块内存较小的内存空间, 是当前程序所执行的字节码的行号指示器, 每个线程都有一个独立的程序计数器, 当正在执行 java 方法的时候, 计数器记录的是虚拟机字节码指令的地址, 如果是 native 方法是空

虚拟机栈

线程私有, 描述的是 java 方法执行的内存模型, 每个方法执行的时候都会创建一个栈帧来存储局部变量表、操作数栈、动态链接、方法出口等等, 每个方法从调用开始 (创建) 到结束 (销毁) 就对应着一个栈帧在虚拟机栈中的入栈到出栈的过程, 栈帧 是用来存储数据和过程的数据结构, 同时也被用来处理动态链接、方法返回值和异常分派.

本地方法栈

线程私有 类似虚拟机栈, 虚拟机栈是执行 Java 方法服务, 而本地方法栈是为 native 方法服务

方法区

线程共享, 也称为永久代 用于存储被 JVM 加载的类信息、常量、静态变量和即时编译器编译后的字节码文件等, 它和其它存放对象的区域不同, 在程序运行期间 GC 不会对其进行垃圾回收, 如果加载的 Class 逐渐增长, 就可能出现 OutOfMemoryException 异常

  • Runtime Constant Pool 是方法区的一部分, 用于存储编译时生成的字面量和符号引用, 在类加载后存放到运行时常量池中
  • JDK 1.8 中, 永久代已经被 metaspace 所替代, 元空间类似永久代, 区别在于 metaspace 并不在 JVM 中, 而是在本地内存. 默认的情况下 metaspace 的大小只是受到本地内存的限制, 类的元数据放在本地内存中, 而字符串和静态变量放置在 Java 堆中

Java 堆划分

  • 新生代
    • 伊甸(eden)
    • 幸存 (From Survivor)
    • 幸存 (To Survivor)
  • 老年代
  • 永久代

新年代

用来存放新生的对象, 一般占据堆空间的三分之一, 由于经常地创建对象, 所以也会频繁触发 Minor GC 进行 GC 操作

Eden 区

新创建的 Java 对象就存放地方, 当 Eden 区内存不够的时候就会触发 Minor GC, 对新生代进行 GC, 但是如果创建的对象占用很多的内存空间, 就会直接分配到老年代中

From Survivor 区

上一次 GC 的幸存者, 作为这一次 GC 的被扫描者

To Survivor 区

保留 MinorGC 后的幸存者

Minor GC 过程

Java 中的新生代采用的是复制算法进行 GC

  • 首先把 Eden 区和 From Survivor 区中存活的对象复制到 To Survivor 区中, 把这些对象的年龄 +1, 如果有对象的年龄 (默认 15) 达到老年代的标准, 就复制到老年代中, 当然 To Survivor 空间不够了也放在老年代中
  • 然后清空 Eden 与 From Survivor 区中剩余的死亡对象,
  • 最后, From SurvivorTo Survivor 的角色互换, 原来的 To Survivor 区作为下次 GC 时候的 From Survivor

老年代

主要存放的是程序中生命周期长的内存对象, 老年代的对象比较的稳定, 所以不会频繁地触发 Major GC 操作, 一般情况下在进行 Major GC 之前都会触发一次 Minor GC, 让新生代的对象晋升到老年代, 当无法找到足够大的连续的内存空间分配给新创建的对象也会触发 Major GC 进行垃圾回收

JVM 垃圾回收算法

确定垃圾的方式 :

  • 引用计数法
    • Java 中, 引用和对象是有关联的, 如果要操作对象需要通过引用进行. 一个对象如果没有任何关联的引用, 不太可能再被使用, 说明该对象是可回收对象. 但是引用计数法会有循环引用问题
  • 可达性分析
    • 为了解决引用计数法的循环引用问题, Java 使用了可达性分析方法, 通过 GC Roots 根节点开始向下搜索,
      GC Roots 与一个对象没有可达路径, 则认为该对象是不可达对象, 不可达对象变成可回收对象的条件是要经过 2 次标记以上后都是不可达对象, 才会面临着回收

标记清除算法 Mark Sweep

  • 分为两个阶段 标记 - 清除, 标记阶段通过根节点进行搜索标记所有可达对象, 清除阶段清除回收不可达的对象所占用的空间
  • 该算法的缺点是内存的碎片化严重, 以后可能不能分配连续空间给新创建的大对象存储

JVM面试题_第4张图片

复制算法 Coping

为了解决标记清楚算法内存碎片化的问题而提出的算法, 首先按照内存的容量将内存分为两块大小一样的空间, 每次只是使用其中的一块, 当这块内存满了之后将可达对象复制到另一块去, 最后清空已使用的内存空间
它的缺点是: 造成空间浪费, 一旦存活的对象增多, 其效率也会逐渐降低

JVM面试题_第5张图片

标记整理算法 Mark Compact

结合标记清楚算法和复制算法后, 为了避免缺陷提出来的标记整理算法, 标记阶段和标记清楚算法相同, 但是标记后不再是回收不可达对象, 而是将存活的可达对象移到内存的一端, 然后清理边界外的空间
JVM面试题_第6张图片

分代收集算法 Generational Collection

  • 目前大部分 JVM 所采用的方式, 主要是依据对象的存活周期进行分类, 短命对象归为新生代, 长命对象归为老年代, 可以根据不同的区域选择相应的算法
  • 新生代的特点 : 每次进行 GC 时都会有大量的对象需要被回收
  • 老年代的特点 : 每次进行 GC 时只有少量的对象需要被回收

新生代回收算法

现在大部分的 JVM 都采用复制算法对新生代进行回收, 因为新生代每次 GC 的时候都有大量的对象需要被回收, 而只是复制少量的对象到另一块存储空间. 一般将新生代中划分为一块较大的 Eden 空间和两块较小的 Survivor 空间, 每次使用 Eden 和其中的一块 Survivor 空间, 当进行回收的时候, 将两块空间中还存活的对象复制到另外一块 Survivor 空间中

JVM面试题_第7张图片

老年代回收算法

老年代采用标记整理算法, 因为老年代每次 GC 的时候都只有少量的对象需要被回收,

  • JVM 中的永久代 (Permanent Generation) 用来存储 class 类、常量和方法描述信息等, 对于永久代主要回收废弃的常量和无用的类
  • 对象的内存分配主要在新生代中的, 少数情况下 (创建大对象) 会直接分配到老年代
  • 当新生代的 Eden SpaceFrom Survivor Space 空间不足的时候就会触发一次 Minor GC, GC 之后会把存活的对象复制到 To Survivor Space, 然后将 Eden SpaceFrom Survivor Space 进行清理
  • 如果 To Survivor Space 无法存储足够的对象的时候, 就会把该区域里的对象存储到老年代
  • GC 完成后, From Survivor SpaceTo Survivor Space 角色互换
  • Survivor Space 中的对象躲过 Minor GC 之后, 就会年龄 + 1, 在达到默认年龄 15 的时候就会移到老年代中

JVM 垃圾收集器

垃圾收集算法是方法论, 而垃圾回收器是内存回收的落地实现, Java 堆内存主要分为 新生代老年代, 新生代主要使用复制算法和标记清除垃圾回收算法, 老年代主要使用标记整理垃圾回收算法, 在 JVM 中对新生代和老年代提供了多种不同的垃圾收集器

JVM面试题_第8张图片

Serial 垃圾收集器(单线程、复制算法)

只会使用一个 CPU 或一条线程去完成垃圾收集工作, 并且在进行垃圾收集的同时, 必须暂停其他所有的工作线程, 直到垃圾收集结束, Serial 垃圾收集器虽然在收集垃圾的过程中需要所有其他的工作线程, 但是简单高效, 对于单个 CPU 的情况下, 没有线程交互的开销, 可以获得最高的单线程垃圾收集的效率

  • 优点 : 简单高效, 拥有很好的单线程收集效率
  • 缺点 : 收集过程需要暂停所有工作线程
  • 算法 : 复制算法
  • 适用范围 : 新生代
  • 应用 : Client 模式下的默认新生代收集器

JVM面试题_第9张图片

ParNew 垃圾收集器(Serial + 多线程、复制算法)

ParNew 类似 Serial 垃圾收集器, 不同点是多线程进行操作的, ParNew 收集器默认开启 CPU 条目相同的线程数, 可以通过 -XX:ParallelGCThreads 参数来限制垃圾收集器的线程数

  • 优点 : 在多个 CPU 的情况下, 效率比 Serial 收集器高
  • 缺点 : 收集过程需要暂停所有工作线程, 单线程情况下比 Serial 收集器差
  • 算法 : 复制算法
  • 适用范围 : 新生代
  • 应用 : Server 模式下虚拟机中首选的新生代收集器

JVM面试题_第10张图片

Parallel Scavenge 垃圾收集器(多线程复制算法)

Parallel Scavenge 同样使用复制算法, 也是一个多线程的垃圾收集器, 主要关注的是程序达到一个可控制的吞吐量. 高吞吐量可以最高效率地利用 CPU 时间, 尽快地完成程序的运算任务, 主要适用于在后台运算而不需要太多交互的任务

Serial Old 垃圾收集器(单线程标记整理算法)

Serial OldSerial 垃圾收集器老年代版本, 是一个单线程的收集器, 使用标记整理算法

  • 新生代 Serial 和 老年代 Serial Old 搭配的垃圾收集过程图

JVM面试题_第11张图片

新生代 Parallel ScavengeParNew 收集器类似, 都是多线程收集器, 并且都是使用的是复制算法, 在进行 GC 的时候都需要暂停所有线程
Parallel Scavenge/ParNew 与老年代 Serial Old 搭配垃圾收集图

JVM面试题_第12张图片

Parallel Old 垃圾收集器(多线程标记整理算法)

Parallel OldParallel Scavenge 老年代版本, 使用多线程标记整理算法, 在 JDK 1.6 之前的新生代都是使用 Parallel Scavenge 收集器搭配老年代 Serial Old 收集器, 只可以保证新生代的吞吐量, 无法保证整体的吞吐量. Parallel Old 正式为了保证老年代同样的吞吐量优先的垃圾收集器. 当系统对吞吐量要求比较高的时候, 可以考虑 Parallel Scavenge 搭配 Parallel Old 垃圾收集器

JVM面试题_第13张图片

CMS 垃圾收集器(单线程标记清除算法)

Concurrent Mark Sweep 收集器是一种老年代垃圾收集器, 主要目标是获取最短垃圾回收停顿时间, 和其他的老年代使用标记整理算法不一致, 而是采用的是多线程标记清楚算法, 这样可以对交互比较高的应用程序提高用户的体验

  • CMS 分为四个阶段
    • 初始标记
      • 标记 GCRoots 能够关联的对象, 但是需要短暂地停止所有的工作线程
    • 并发标记
      • 进行 GCRoots 跟踪过程, 和用户线程一起工作, 不需要暂停工作线程
    • 重新标记
      • 修正正在并发标记期间, 因为程序继续运行时导致标记产生变动的那一部分对象的标记记录, 需要暂停所有的工作线程
    • 并发清除
      • 和用户线程一起工作清楚那些 GCRoots 不可达的对象, 不需要暂停工作线程, 由于耗时最长的并发标记和并发清除过程中, 垃圾收集器可以和用户一起并发地工作, 所以 CMS 收集器的内存回收是和用户线程一起并发执行的
  • 优点 : 并发收集, 低停顿
  • 缺点 : 会产生大量的空间碎片, 并发阶段会降低吞吐量

JVM面试题_第14张图片

G1 垃圾收集器(单线程标记整理算法)

对比 CMS 做出的改进

  • 基于标记整理算法, 不会产生内存碎片
  • 可以精确地控制停顿时间, 在不牺牲吞吐量的情况下, 实现低停顿垃圾回收
  • G1 收集器避免全局区域垃圾收集, 把堆内存划分为大小固定的几个区域, 并且跟踪这些区域的垃圾收集进度, 并在后台维护一个优先级列表, 每次根据所允许的收集时间, 优先回收垃圾最多的区域. 区域划分和优先级区域回收机制, 确保 G1 收集器可以在有限的时间内获取最高的垃圾收集效率
  • 特点
    • 并行与并发、分代收集、空间整合【Mark Compact, 不会出现内存碎片】、可预测停顿【能让使用者名气指定一个 M 毫秒单位的时间段, 消耗在垃圾收集器的时间不能超过这个值】
  • 应用 :
    • 50% 以上的堆被存活对象占用
    • 对象的分配和晋升的速度变化大
    • 垃圾回收时间比较长

JVM面试题_第15张图片

如何选择合适的垃圾收集器

  • 优先调整堆的大小让服务器自己来选择
  • 如果内存小于100M,使用串行收集器
  • 如果是单核,并且没有停顿时间要求,使用串行或JVM自己选
  • 如果允许停顿时间超过1秒,选择并行或JVM自己选
  • 如果响应时间最重要,并且不能超过1秒,使用并发收集器
  • 对于G1收集

你可能感兴趣的:(面试)