JVM面试基础

JVM

包含JVM面试入门必知.

一. 概述

1. JDK, JRE, JVM关系

  1. JDK: Java Development Kits, Java开发工具包, 包括JRE和Java开发辅助工具;
  2. JRE: Java Runtime Environment, Java运行时环境, 包括JVM和程序所需类库;
  3. JVM: Java Virtual Machine, Java虚拟机;

2. JVM工作机制

Java源代码编译运行过程

源代码 -> 字节码文件 -> 放到JVM上运行

JVM工作机制:

  1. 使用类加载器子系统将class字节码文件加载到JVM内存;
  2. 在JVM内存空间存储相关数据;
  3. 在执行引擎中将字节码文件翻译为CPU能执行的指令;
  4. 将指令发送给CPU执行;

3. JVM落地产品

  1. Sun公司的 HotSpot;
  2. BEA公司的 JRockit;
  3. IBM公司的 J9 VM;

二. 类加载机制

1. 类加载器分类

  1. 启动类加载器: 负责加载 Java 平台中扩展功能的一些 jar 包,包括 $JAVA_HOME/jre/lib/*.jar 或 -Djava.ext.dirs 参数指定目录下的 jar 包、以及 $JAVA_HOME/jre/lib/ext/classes 目录下的 class;
  2. 扩展类加载器: 负责加载 Java 平台中扩展功能的一些 jar 包,包括 $JAVA_HOME/jre/lib/*.jar 或 -Djava.ext.dirs 参数指定目录下的 jar 包、以及 $JAVA_HOME/jre/lib/ext/classes 目录下的 class;
  3. 应用类加载器: 负责加载 classpath中指定的jar包及目录中的class;
  4. 自定义类加载器: 程序员自己开发一个类继承 java.lang.ClassLoader,定制类加载方式;

启动类加载器是扩展类加载器的父加载器, 扩展类加载器是应用类加载器的父加载器.

2. 双亲委派机制

  1. 当我们需要加载任何一个范围内的类是, 首先找到这个范围对应的类加载器, 但是当前这个类加载器不是马上开始查找, 而是将任务交给上一级类加载器, 上一级类加载器继续上交任务, 一直到最顶级的启动类加载器;
  2. 启动类加载器开始在自己负责的范围类查找, 如果能找到, 则直接开始加载, 如果找不到, 则交给下一级的类加载器继续查找;
  3. 一直到应用程序类加载器, 如果应用程序类加载器同样找不到要加载的类, 那么抛出 ClassNotFoundException ;

3. 双亲委派机制的好处

  1. 避免类的重复加载: 父加载器加载了一个类, 就不必让子加载器再去查找, 同时也保证了在整个JVM范围重工全类名是类的唯一标识;
  2. 安全机制: 避免恶意替换JRE定义的核心API;

三. JMM

1. 本地接口(Native Interface)

本地接口的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++程序。因为 Java 诞生的时候是 C/C++ 横行的时候,要想立足,必须有能力调用 C/C++。于是就在内存中专门开辟了一块区域处理标记为 native 的代码,它的具体做法是 Native Method Stack 中登记 native 方法,在Execution Engine 执行时加载 native libraies。

目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过 Java 程序驱动打印机或者 Java 系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用 Socket 通信,也可以使用 Web Service 等等,不多做介绍。

2. 本地方法栈(Native Method Stack)

专门负责在本地方法运行时,提供栈空间,存放本地方法每一次执行时创建的栈帧。它的具体做法是在 Native Method Stack 中登记 native 方法,在 Execution Engine 执行时加载本地方法库

3. 程序计数器

也叫PC寄存器(Program Counter Register)。用于保存程序执行过程中,下一条即将执行的指令的地址。也就是说能够保存程序当前已经执行到的位置。这个位置由执行引擎读取下一条指令,是一个非常小的内存空间,从内存空间使用优化这个角度来看:几乎可以忽略不记。

4. 执行引擎(Execution Engine)

作用:用于执行字节码文件中的指令。

执行指令的具体技术:

解释执行:第一代JVM。
即时编译:JIT,第二代JVM。
自适应优化:目前Sun的Hotspot JVM采用这种技术。吸取了第一代JVM和第二代JVM的经验,在一开始的时候对代码进行解释执行, 同时使用一个后台线程监控代码的执行。如果一段代码经常被调用,那么就对这段代码进行编译,编译为本地代码,并进行执行优化。若方法不再频繁使用,则取消编译过的代码,仍对其进行解释执行。
芯片级直接执行:内嵌在芯片上,用本地方法执行Java字节码。

4. 直接内存

4.1 作用

特定情况下提升性能

4.2 场景

直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中定义的内存区域.

在JDK1.4 中新加入了NIO(New Input/Output)类, 引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式, 它可以使用native 函数库直接分配堆外内存, 然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能, 因为避免了在 Java 堆和 Native 堆中来回复制数据.

本机直接内存的分配不会受到 Java 堆大小的限制, 受到本机总内存大小限制.

配置虚拟机参数时, 不要忽略直接内存防止出现 OutOfMemoryError 异常.

4.3 直接内存和堆内存比较

直接内存申请空间耗费更高的性能, 当频繁申请到一定量时更明显. 直接内存IO读写的性能要优于普通的堆内存, 在多次读写操作的情况下差异明显.

四. 方法区

1. 不同版本的具体实现

标准层面: 方法区.

1.6及之前称为永久代, 1.7提出去永久代, 1.8则是元空间(Meta Space)

永久代概念辨析:

从堆空间角度来说
新生代:从标准和实现层面都确定属于堆
老年代:从标准和实现层面都确定属于堆
永久代
名义上属于堆
实现上不属于堆
从方法区角度来说
方法区的具体实现:JDK 版本 ≤ 1.7 时,使用永久代作为方法区。
方法区的具体实现:JDK 版本 ≥ 1.8 时,使用元空间作为方法区。

2. 元

对比类和对象,类相当于是对象的元信息.

3. 元空间存储数据说明

  • 类信息:类中定义的构造器、接口定义
  • 静态变量(类变量)
  • 常量
  • 运行时常量池
  • 类中方法的代码

五. Java栈

1. 方法栈

方法栈并不是某一个JVM的内存空间, 而是描述方法被调用过程的一个逻辑概念.

1、从数据结构角度来说

栈和堆一样:都是先进后出,后进先出的数据结构

2、从 JVM 内存空间结构角度来说

栈:通常指 Java 方法栈,存放方法每一次执行时生成的栈帧。
堆:JVM 中存放对象的内存空间。包括新生代、老年代、永久代等组成部分。

2. 栈帧

2.1 栈帧存储的数据

栈帧中主要保存3 类数据:

  • 本地变量(Local Variables):输入参数和输出参数以及方法内的变量。
  • 栈操作(Operand Stack):记录出栈、入栈的操作。
  • 栈帧数据(Frame Data):包括类文件、方法等等。

2.2 栈帧的结构

  • 局部变量表:方法执行时的参数、方法体内声明的局部变量
  • 操作数栈:存储中间运算结果,是一个临时存储空间
  • 帧数据区:保存访问常量池指针,异常处理表

2.3 栈帧工作机制

每执行一个方法都会产生一个栈帧,保存到栈的顶部,顶部栈就是当前方法,该方法执行完毕后会自动将此栈帧出栈

当一个方法 A 被调用时就产生了一个栈帧 F1,并被压入到栈中,

A 方法又调用了 B 方法,于是产生栈帧 F2 也被压入栈,

B 方法又调用了 C 方法,于是产生栈帧 F3 也被压入栈,

……

C 方法执行完毕后,弹出 F3 栈帧;

B 方法执行完毕后,弹出 F2 栈帧;

A 方法执行完毕后,弹出 F1栈帧;

……

3. 栈溢出异常

原因总结:方法每一次调用都会在栈空间中申请一个栈帧,来保存本次方法执行时所需要用到的数据。但是一个没有退出机制的递归调用,会不断申请新的空间,而又不释放空间,这样迟早会把当前线程在栈内存中自己的空间耗尽

4. 栈空间的线程私有验证

线程对栈内存空间的使用方式是彼此隔离的。每个线程都是在自己独享的空间内运行,反过来也可以说,这个空间是当前线程私有的

六. 堆

1. 概述

1.1 堆空间组成部分

1.2 堆空间工作机制

新创建的对象会被放在Eden区
当Eden区中已使用的空间达到一定比例,会触发Minor GC
每一次在Minor GC中没有被清理掉的对象就成了幸存者
幸存者对象会被转移到幸存者区
幸存者区分成from区和to区
from区快满的时候,会将仍然在使用的对象转移到to区
然后from和to这两个指针彼此交换位置
口诀:复制必交换,谁空谁为to

如果一个对象,经历15次GC仍然幸存,那么它将会被转移到老年代
如果幸存者区已经满了,即使某个对象尚不到15岁,仍然会被移动到老年代
最终效果:
Eden区主要是生命周期很短的对象来来往往
老年代主要是生命周期很长的对象,例如:IOC容器对象、线程池对象、数据库连接池对象等等
幸存者区作为二者之间的过渡地带
关于永久代:
从理论上来说属于堆
从具体实现上来说不属于堆

1.3 永久代在各个JDK版本之间的演变

永久代 常量池
≤JDK1.6 在方法区
=JDK1.7 有,但开始逐步“去永久代” 在堆
≥JDK1.8 在元空间

1.4 方法区, 元空间, 永久代之间关系

1.5 堆, 栈, 方法区之间关系

2. 堆溢出异常

java.lang.OutOfMemoryError,也往往简称为 OOM

Java heap space:针对新生代、老年代整体进行Full GC后,内存空间还是放不下新产生的对象
PermGen space:方法区中加载的类太多了(典型情况是框架创建的动态类太多,导致方法区溢出)

七. 可视化工具

查看本地及远程的JAVA GUI监控工具

使用方法参考: http://www.tianshouzhi.com/api/tutorials/jvm/352

1. jconsole

路径: %JAVA_HOME%/bin/jconsole.exe

2. jvisualvm

路径: %JAVA_HOME%/bin/jvisualvm.exe

八. 垃圾回收

1. GC概念

GC即 garbage collection , 垃圾回收: 将内存中不再使用的空间释放, 清理掉不再使用的对象.

堆内存是线程共享空间, 服务器运行时, 废弃的对象及时清理, 才能让项目保持健康的运行状态.

不再使用或获取不到的对象是就是垃圾对象.

使用垃圾标记法和垃圾回收算法结合, 完成垃圾回收工作.

2. 垃圾标记算法

2.1 引用计数法

每当有一个指向对象的引用, 则引用计数器+1, 否则-1, 当引用计数器归零的时候判断该对象为垃圾, 可以进行回收.

缺点: 无法解决循环引用.

2.2 GC Roots 可达性分析

GC Root 对象作为根节点, 顺着引用路径一直查找到堆空间内, 找到堆空间中的对象.

若没有堆外内存指向堆内的引用, 则判定对象为垃圾对象.

关于gc root对象:
1. Java栈中的局部变量;
2. 本地方法栈中的局部变量;
3. 方法区中的类变量, 常量;

3. 垃圾回收算法

3.1 引用计数法

当一个对象引用计数器归零的时候, 则将该对象进行回收, 这种方法避免了GC的停顿, 不用STW.

缺点: 导致内存空间碎片化, 无法解决循环引用, 因此没有JVM用这个方法.

3.2 标记清除法

当有效空间耗尽的时候, 暂停整个程序, 然后标记垃圾对象, 进行清除.

缺点:

  1. STW;
  2. 内存碎片化;
  3. 效率低, 标记和清除共要遍历两次;

3.3 标记压缩法

先遍历所有对象, 然后进行标记, 将所有可用对象进行复制放在同一块内存区域, 再回收所有的垃圾对象.

缺点:

  1. STW;
  2. 效率更低;

3.4 复制算法

将内存区域分为两块, 每次使用其中一块.

当这块区域需要进行垃圾回收的时候, 先标记可用对象, 然后复制到另一块区域, 再将这块区域所有对象清除.

缺点:

  1. 浪费了一半的内存区域;
  2. 对于对象存活较多的老年代效率很差;

3.5 分代算法

新生代适合复制算法;

老年代适合标记清除或标记压缩算法.

3.6 分区算法

上面介绍的分代收集算法是将对象的生命周期按长短划分为两个部分,而分区算法则将整个堆空间划分为连续的不同小区间,每个小区间独立使用,独立回收。这样做的好处是可以控制一次回收多少个小区间。在相同条件下,堆空间越大。一次GC耗时就越长,从而产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割为多个小块,根据目标停顿时间每次合理地回收若干个小区间(而不是整个堆),从而减少一次GC所产生的停顿。

4. 垃圾回收器

参考: https://heavy_code_industry.gitee.io/code_heavy_industry/pro016-JVM/lecture/chapter08/verse04.html

4.1 串行垃圾回收器

串行:在一个线程内执行垃圾回收操作。

新生代串行回收器 SerialGC:采用复制算法实现,单线程垃圾回收,独占式垃圾回收器

老年代串行回收器 SerialOldGC:采用标记压缩算法,单线程独占式垃圾回收器

4.2 并行垃圾回收器

并行:在多个线程中执行垃圾回收操作。

新生代 ParNew 回收器:采用复制算法实现,多线程回收器,独占式垃圾回收器。

新生代 ParallelScavengeGC 回收器:采用复制算法多线程独占式回收器

老年代 ParallelOldGC 回收器: 采用标记压缩算法,多线程独占式回收器

4.3 对比

新生代

名称 串行/并行/并发 回收算法 适用场景 可以与CMS配合
SerialGC 串行 复制 单CPU
ParNewGC 并行 复制 多CPU
ParallelScavengeGC 并行 复制 多CPU且关注吞吐量

老年代

名称 串行/并行/并发 回收算法 适用场景
SerialOldGC 串行 标记压缩 单CPU
ParNewOldGC 并行 标记压缩 多CPU
CMS 并发,几乎不会暂停用户线程 标记清除 多CPU且与用户线程共存

5. finalize机制

在对象被回收时, 会调用finalize方法.

九. JVM参数设置-入门

十. JVM相关参数汇总

参考:

https://heavy_code_industry.gitee.io/code_heavy_industry/pro016-JVM/lecture/

posted @ 2022-10-18 02:40  疯一风  阅读(25)  评论(0编辑  收藏  举报