《Java 虚拟机》
一、内存区域
1. 运行时数据区域
JVM 内存可分为:
| 区域 | 线程共享/独立 | 说明 |
|---|---|---|
| 程序计数器 | 线程独立 | 当前线程所执行的字节码的行号指示器 |
| 虚拟机栈 | 线程独立 | Java 方法执行的内存模型 |
| 本地方法栈 | 线程独立 | 为 Native 方法服务 |
| 堆 | 线程共享 | 存放对象实例 |
| 方法区 | 线程共享 | 存储类信息、常量、静态变量 |
| 直接内存 | - | 不属于 JVM 运行时数据区 |
2. 程序计数器
定义:是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
作用:
- 字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令
- 分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成
线程私有性:
- 在多线程的环境下,程序计数器还能保证恢复到原来线程的位置
- 这也是它私有的原因
💡 特殊性:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
3. Java 虚拟机栈
定义:描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。
栈帧组成
栈帧包括以下部分:
- 局部变量表
- 各种基本数据类型
- 引用类型
- returnAddress 类型(指向了一个字节码指令的地址)
操作数栈
动态链接
- 方法出口信息
工作原理
Java 栈可类比数据结构中的栈:
- Java 栈中保存的主要内容是栈帧
- 每一次函数调用都会有一个对应的栈帧被压入 Java 栈
- 每一个函数调用结束后,都会有一个栈帧被弹出
方法退出方式:
- 抛出异常
- return 语句
两种方式都会将栈帧弹出。
4. Java 堆内存
定义:几乎所有的对象实例以及数组都在这里分配内存。这段区域也是垃圾回收器经常光顾的地方。
堆内存划分
Java 堆还可以细分为:
1
2
3
4
5
6
堆内存
├── 新生代(Young Generation)
│ ├── Eden 空间
│ ├── From Survivor(S0)
│ └── To Survivor(S1)
└── 老年代(Old Generation/Tenured)
📌 注意:永久代(Permanent Generation)在 JDK 1.8 之后已被移除,替换为元空间(Metaspace)。
5. 方法区
定义:方法区与 Java 堆一样,是各个线程共享的内存区域。
存储内容:
- ✅ 已被虚拟机加载的类信息
- ✅ 常量
- ✅ 静态变量
- ✅ 即时编译器编译后的代码
别名:虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的是与 Java 堆区分开来。
JDK 1.8 的变化:
- 在 JDK 1.8 之后移除了永久代
- 把方法区中的元数据移入直接内存(元空间)
- 原因:随着动态类加载的情况越来越多,这块内存变得不太可控
- 如果设置小了,系统运行过程中就容易出现内存溢出
- 设置大了又浪费内存
- 默认情况下,元空间的大小仅受本地内存限制
6. JDK 1.7 & 1.8 改变
| 版本 | 方法区位置 | 字符串常量池 | 类信息 | 运行时常量池 |
|---|---|---|---|---|
| JDK 1.7 | 永久代 | 移入堆中 | 在方法区 | 在方法区 |
| JDK 1.8 | 元空间(本地内存) | 在堆中 | 在元空间 | 在堆中 |
主要变化:
- ✅ 把永久代(方法区)移除,取而代之的是在本地直接内存上的元空间
- ✅ JDK 1.7:字符串常量池进入到堆中,存储类信息、常量池、运行时常量池等还在方法区
- ✅ JDK 1.8:类信息随着方法区进入元空间,常量池、运行时常量池、字符串常量池等进入到 Java 堆中
7. 内存溢出
StackOverFlowError
触发条件:
- 若 Java 虚拟机栈的内存大小不允许动态扩展
- 当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度时
典型场景:递归调用层次过深
1
2
3
public void stackOverflow() {
stackOverflow(); // 无限递归
}
OutOfMemoryError
触发条件:
- 若 Java 虚拟机栈的内存大小允许动态扩展
- 当线程请求栈时内存用完了,无法再动态扩展
8. 内存泄漏
定义:是指本应该被 GC 回收的无用对象没有被回收,导致的内存空间的浪费。当内存泄漏严重时会导致 OOM。
产生的根本原因:
- 长生命周期的对象持有短生命周期对象的引用
- 尽管短生命周期对象已经不再需要,但因为长生命周期对象持有它的引用而导致不能被 GC 回收
示例代码:
1
2
3
4
5
6
7
public class Simple {
Object object;
void method() {
object = new Object(); // 如果 Simple 实例一直存活,这个 Object 就无法被回收
}
}
二、对象创建过程
1. 对象创建过程
完整流程
- 检查类是否加载
- JVM 遇到一个 new 指令时,去常量池查询是否有这个类的符号引用(全限定类名)
- 如果没有,表示未加载解析过这个类,需要加载解析
- 同时在常量池中添加符号引用,在方法区添加类的信息
- 分配内存
- 分配方式有两种:指针碰撞和空闲列表
- 初始化为零值
- 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)
- 这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用
- 程序能访问到这些字段的数据类型所对应的零值
- 设置对象头
- 对象头存储的信息:
- ✅ 这个对象是哪个类的实例
- ✅ 如何才能找到类的元数据信息
- ✅ 对象的哈希码
- ✅ 对象的 GC 分代年龄
- ✅ 是否启用偏向锁等
- 对象头存储的信息:
- 执行 init 方法
- 把对象按照程序员的意愿进行初始化(也就是构造函数之类的开始初始化)
2. 对象的内存布局
对象在内存中的布局分为三个区域:
(1)对象头(Header)
包括两部分:
标记字段(Mark Word):
- hashCode
- GC 信息
- 锁信息
类型指针(Class Pointer):
- 指向该对象的类
(2)实例数据(Instance Data)
就是对象的数据域。
(3)对齐填充(Padding)
非必要,因为每个对象必须是 8 字节的整数倍。
3. 对象访问定位
方式 1:句柄方式
如果使用句柄的话:
- Java 堆中将会划分出一块内存来作为句柄池
- reference 中存储的就是对象的句柄地址
- 句柄中包含了对象实例数据与类型数据各自的具体地址信息
方式 2:直接指针
如果使用直接指针访问:
- Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息
- reference 中存储的直接就是对象的地址
对比
| 特性 | 句柄方式 | 直接指针方式 |
|---|---|---|
| 访问速度 | 较慢(两次指针定位) | 较快(一次指针定位) |
| 优点 | 对象移动时只需改变句柄中的指针 | 访问速度更快 |
| 缺点 | 需要额外的句柄池空间 | 对象移动时需要更新所有 reference |
| 使用 | - | HotSpot 虚拟机使用 |
💡 HotSpot 的选择:直接指针方式 reference 存储的就是对象的地址,少了一次指针定位,更加高效。
三、垃圾回收
1. JVM 内存分配与回收
垃圾回收主要是在虚拟机内存的堆上。
堆示意图
1
2
3
4
5
6
堆内存
├── 新生代(Young Generation)
│ ├── Eden 区
│ ├── Survivor 区(From/S0)
│ └── Survivor 区(To/S1)
└── 老年代(Old Generation/Tenured)
两种垃圾回收
| 类型 | 名称 | 发生区域 | 特点 |
|---|---|---|---|
| Minor GC | 新生代 GC | 新生代 | 非常频繁,回收速度快 |
| Major GC/Full GC | 老年代 GC | 老年代 | 出现 Major GC 经常会伴随至少一次 Minor GC 速度一般会比 Minor GC 慢 10 倍以上 |
2. 堆内存的常见分配策略
(1)对象优先在 Eden 区分配
大多数情况下,对象在新生代 Eden 区中分配。
(2)大对象直接进入老年代
大对象:需要大量连续内存空间的 Java 对象
- 比如很长的字符串
- 庞大的数组
⚠️ 为什么?:避免在 Eden 区及两个 Survivor 区之间发生大量的内存复制。
(3)长期存活的对象将进入老年代
年龄判断机制:
- 在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1
- Eden 区 → Survivor 区后对象的初始年龄变为 1
- 每熬过一次 Minor GC,年龄 +1
- 当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中
(4)动态年龄分配
为了更好地适应不同程序的内存情况,虚拟机不是永远要求对象年龄必须达到某个值才能进入老年代。
规则:
- 如果 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半
- 年龄大于或等于该年龄的对象就可以直接进入老年代
- 无需达到要求的年龄
(5)空间分配担保
在发生 Minor GC 之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。
3. 对象已经死亡?
方法 1:引用计数法
原理:
- 在对象中添加一个引用计数器
- 每当有一个地方引用它时,计数器值就加一
- 当引用失效时,计数器值就减一
- 任何时刻计数器为零的对象就是不可能再被使用的
缺点:无法解决循环引用问题
方法 2:可达性分析(主流)
基本思想:
- 通过一系列的称为 “GC Roots” 的对象作为起点
- 从这些节点开始向下搜索,节点所走过的路径称为引用链
- 当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的
可作为 GC Roots 的对象:
- ✅ 虚拟机栈(栈帧中的本地变量表)中引用的对象
- ✅ 方法区中类静态属性引用的对象
- ✅ 方法区中常量引用的对象
- ✅ 本地方法栈中 JNI(即本地方法)引用的对象
对象是否真的死亡?
一个对象的死亡至少要经历两次标记过程:
第一次标记:
- 没有发现与 GC Root 相连的引用链,第一次标记
- 随后筛选:此对象是否有必要执行
finalize方法 - 当对象没有覆盖
finalize方法,或finalize方法已经被虚拟机调用过时 - 虚拟机将这两种情况视为”没有必要执行”
第二次标记:
- 将被筛选出的对象放置在一个 F-Queue 队列中
- 之后 GC 将对 F-Queue 进行第二次小规模标记
- 这个队列会执行
finalize方法
对象自救:
- 对象可以在
finalize方法中拯救自己 - 就是在
finalize方法中将自己与引用链上任何一个对象关联即可 - 就可移出队列
⚠️ 注意:只能拯救自己一次,因为任何一个对象的
finalize方法只会被系统调用一次。
4. 四类引用
| 引用类型 | 回收时机 | 说明 |
|---|---|---|
| 强引用 | 永远不会回收 | Object obj = new Object();垃圾收集器永远不会回收掉被引用的对象 |
| 软引用 | 内存溢出前 | 还有用但非必须的对象 在系统将要发生内存溢出异常之前 将会把这些对象列入回收范围进行第二次回收 |
| 弱引用 | 下一次 GC | 只能生存到下一次垃圾收集发生之前 当垃圾回收器工作时,无论内存是否够用,都会被回收 |
| 虚引用 | 任何时候 | 最弱,主要用来跟踪对象被垃圾回收的活动 |
5. 垃圾回收算法
(1)标记-清除算法
过程:
- 标记:标记出所有需要回收的对象
- 清除:在标记完成后统一回收所有被标记的对象
特点:
- ✅ 最基础的收集算法
- ❌ 效率不稳定(大量对象需要回收时很耗时间)
- ❌ 产生内存碎片
(2)复制算法
原理:
- 将内存分为大小相同的两块,每次使用其中的一块
- 当这一块的内存使用完后,就将还存活的对象复制到另一块去
- 然后再把使用的空间一次清理掉
优点:
- ✅ 解决了效率问题
- ✅ 不会产生内存碎片
缺点:
- ❌ 可用内存减半
应用场景:
- 目前用于分代算法中的新生代
- 在新生代中并不是按照 1:1 进行划分的,而是分为 (Eden + S0) 和 S1 两部分
(3)标记-整理算法
原理:
- 标记过程仍然与”标记-清除”算法一样
- 但后续步骤不是直接对可回收对象回收
- 而是让所有存活的对象向一端移动
- 然后直接清理掉端边界以外的内存
特点:
- ✅ 不会产生内存碎片
- ✅ 适合老年代
(4)分代收集算法
思想:当前虚拟机的垃圾收集都采用分代收集算法。
原理:
- 根据对象存活周期的不同将内存分为几块
- 一般将 Java 堆分为新生代和老年代
- 根据各个年代的特点选择合适的垃圾收集算法
新生代:
- 每次收集都会有大量对象死去
- 选择复制算法
- 只需要付出少量对象的复制成本就可以完成每次垃圾收集
老年代:
- 对象存活几率比较高
- 而且没有额外的空间对它进行分配担保
- 必须选择”标记-清除“或”标记-整理“算法进行垃圾收集
6. 垃圾收集器
| 收集器 | 串行/并行/并发 | 作用区域 | 垃圾算法 | 目标 | 适用场景 |
|---|---|---|---|---|---|
| Serial | 串行 | 新生代 | 复制算法 | 高响应 | 单 CPU 模式下的客户端 |
| Serial Old | 串行 | 老年代 | 标记整理 | 高响应 | 单 CPU 模式下的客户端 |
| ParNew | 并行 | 新生代 | 复制算法 | 高响应 | 多核 CPU 下,与 CMS 配合 |
| Parallel Scavenge | 并行 | 新生代 | 复制算法 | 高吞吐量 | 后台不需要太多交互情况下 |
| Parallel Old | 并行 | 老年代 | 标记整理 | 高吞吐量 | 后台不需要太多交互情况下 |
| CMS | 并发 | 老年代 | 标记清除 | 高响应 | 网站或者 B/S 架构 |
| G1 | 并发 | 新生代和老年代 | 标记整理 | 高响应 | 替换 CMS |
Serial 收集器
特点:
- 针对新生代
- 采用复制算法
- 单一线程收集
- 进行垃圾收集时,必须暂停所有工作线程,直到完成(Stop The World)
适用场景:客户端模式下简单高效
ParNew 收集器
特点:
- ParNew 垃圾收集器是 Serial 收集器的多线程版本
- 采用多个 GC 线程
- 对 CPU 的依赖较高
Parallel Scavenge 收集器(吞吐量收集器)
特点:
- Parallel Scavenge 垃圾收集器因为与吞吐量关系密切,也称为吞吐量收集器
- 有一些特点与 ParNew 收集器相似
- 新生代收集器
- 采用复制算法
- 多线程收集
- 采用自适应调节吞吐量设计
Serial Old 收集器
特点:
- 针对老年代
- 采用”标记-整理”算法(还有压缩,Mark-Sweep-Compact)
- 单线程收集
- 一般作为 CMS 收集器发生失败的后备收集器
Parallel Old 收集器
特点:
- Parallel Scavenge 的老年代版本
- 关注吞吐量
- 针对老年代
- 采用”标记-整理”算法
- 多线程收集
CMS 收集器
定义:一种以获取最短回收停顿时间为目标的收集器。
特点:
- 针对老年代
- 基于”标记-清除”算法(不进行压缩操作,产生内存碎片)
- 以获取最短回收停顿时间为目标
- 非常符合那些集中在互联网站或者 B/S 系统的服务端上的 Java 应用
- 并发收集、低停顿
- 但是无法处理浮动垃圾
- 是 HotSpot 在 JDK 1.5 推出的第一款真正意义上的并发(Concurrent)收集器
- 第一次实现了让垃圾收集线程与用户线程(基本上)同时工作
四个步骤:
- 初始标记 Stop The World
- 主要是标记一下 GC Root 能直接关联的对象
- 并发标记
- 主要是沿着 GC Root 关联的对象去遍历整个对象图的过程
- 重新标记 Stop The World
- 主要是修正由于并发标记阶段产生的新的可回收对象
- 并发清除
- 清除那些已经被标记为死亡的对象
G1 收集器
定义:主要面向服务端的垃圾收集器。
特点:
- 可处理新生代和老年代
- 基于”标记-整理”算法
- 可控制停顿时间
- 可以管理全堆内存
Region 划分:
- G1 在使用时,它将整个 Java 堆划分为多个大小相等的独立区域(Region)
- 每一个 Region 都可以根据需要,扮演新生代的 Eden 空间、Survivor 空间,或者老年代空间
四个步骤:
- 初始标记 Stop The World
- 标记一下 GC Root 能直接关联的对象
- 此阶段需要停顿线程,但耗时很短
- 并发标记
- 从 GC Root 开始对堆中对象进行可达性分析,找到存活对象
- 此阶段耗时较长,但可与用户程序并发执行
- 最终标记
- 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录
- 虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面
- 最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中
- 这阶段需要停顿线程,但是可并行执行
- 筛选回收
- 首先对各个 Region 中的回收价值和成本进行排序
- 根据用户所期望的 GC 停顿时间来制定回收计划
- 此阶段其实也可以做到与用户程序一起并发执行
- 但是因为只回收一部分 Region,时间是用户可控制的
- 而且停顿用户线程将大幅度提高收集效率
四、类加载器和类加载过程
1. 类的生命周期
1
2
3
4
5
6
7
8
9
类的生命周期
├── 加载(Loading)
├── 连接(Linking)
│ ├── 验证(Verification)
│ ├── 准备(Preparation)
│ └── 解析(Resolution)
├── 初始化(Initialization)
├── 使用(Using)
└── 卸载(Unloading)
📌 注意:其中大框内的统称为连接阶段,解析过程可以在初始化之后。
2. 类加载过程
(1)加载
类加载过程的第一步,主要完成下面 3 件事情:
- 获取二进制字节流
- 通过全类名获取定义此类的二进制字节流
- 按所需格式存储在方法区中
- 转换数据结构
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 生成 Class 对象
- 在内存中生成一个代表该类的 Class 对象
- 作为方法区这些数据的访问入口
(2)验证
目的:这个阶段直接决定了 JVM 是否能够承受恶意代码攻击。
性能影响:从执行性能上来讲,验证阶段的工作量在 JVM 的类加载子系统中又占了一部分。
(3)准备阶段(类变量初始化)
定义:正式为类变量分配内存并设置类变量初始值阶段。
初始值:
- 通常情况下是类类型默认零值
- 不是程序员指定的初始值
示例:
1
public static int value = 123;
在准备阶段,value 的值是 0,而不是 123。
(4)解析(把符号引用变为直接引用)
定义:是 JVM 将常量池中符号引用替换成直接引用(类方法、字段在内存中的指针、偏移量)过程。
主要针对:
- 类或接口
- 字段
- 类方法
- 接口方法
- 方法类型
- 方法句柄
- 调用限定符
这 7 类符号引用。
(5)初始化
定义:这是类加载的最后一步,这才是真正执行类中定义的 Java 程序代码(字节码)。
执行过程:
- 执行类构造器
<clinit>()方法过程 - 是由编译器自动收集类中的所有变量的赋值动作和静态语句块中的语句合并产生的
3. 类加载器
定义:把类加载阶段中通过一个类的全限定类名来获取描述此类的二进制字节流这个动作放到 JVM 外部实现,实现这个动作的代码模块称”类加载器”。
💡 唯一性:对于任意一个类,都需要加载它的类加载器和这个类本身一同确立在 JVM 中的唯一性。也就是说,如果加载一个类的类加载器不同,那么这两个类在 JVM 中也是不同的。
三种类加载器
JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自 java.lang.ClassLoader:
| 类加载器 | 实现 | 加载路径 |
|---|---|---|
| BootstrapClassLoader (启动类加载器) | C++ 实现 | 负责加载 %JAVA_HOME%/lib 目录下的 jar 包和类或被 -Xbootclasspath 参数指定的路径中的所有类 |
| ExtensionClassLoader (扩展类加载器) | Java 实现 | 负责加载目录 %JRE_HOME%/lib/ext 目录下的 jar 包和类或被 java.ext.dirs 系统变量所指定的路径下的 jar 包 |
| AppClassLoader (应用程序类加载器) | Java 实现 | 面向用户的加载器 负责加载当前应用 classpath 下的所有 jar 包和类 |
从 JVM 角度的分类
从 JVM 角度只有两种类加载器:
- Bootstrap ClassLoader
- C++ 实现的
- 是 JVM 的一部分
- 其他类型的类加载器
- 由 Java 实现的
- 全部继承
java.lang.ClassLoader抽象类
4. 双亲委派模型
工作原理
每一个类都有一个对应它的类加载器。系统中的 ClassLoader 在协同工作的时候会默认使用双亲委派模型:
- 在类加载的时候,系统会首先判断当前类是否被加载过
- 已经被加载的类会直接返回,否则才会尝试加载
- 加载的时候,首先会把该请求委派给父类加载器的
loadClass()处理 - 当父类加载器无法处理时,才由自己来处理
- 当父类加载器为 null 时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器
📌 注意:类加载器之间的父子关系不是以继承方式实现的,而是以组合方式(优先级)实现的。
优点
- 避免类的重复加载
- 当父类已经加载了该类时,就没有必要子类加载器再加载一次
- 保护程序安全
- 防止核心 API 被随意篡改
- 例如自己写的
java.lang.String类不会被加载,保证了 Java 核心类的安全性
五、虚拟机字节码执行引擎
1. 运行时栈帧结构
定义:栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。
栈帧包括
- 局部变量表
- 操作数栈
- 动态连接
- 方法的返回地址
- 一些额外的附加信息
注意事项:
- 执行引擎运行的所有字节码指令只针对当前栈帧进行操作
- 一个栈中能容纳的栈帧是受限的
- 过深的方法调用可能会导致 StackOverFlowError
- 我们可以认为设置栈的大小
(1)局部变量表
定义:是变量值的存储空间,由方法参数和方法内部定义的局部变量组成。
特点:
- 局部变量表的大小在编译时就可以确定,存储在 Code 中的
max_local - 局部变量表不存在系统初始化的过程
- 这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用
访问方式:
- JVM 通过索引定位方式使用局部变量表
this 关键字:
- 在方法执行时,如果是实例方法
- 局部变量表的第 0 位索引的 slot 默认是用于传递方法所属类实例的引用(也就是 this)
(2)操作数栈
作用:
- 在做算术运算的时候通过操作数栈来进行
- 或者调用其他方法的时候通过操作数栈来进行参数传递
特点:
- 操作数栈的最大深度也在编译的时候写入到 Code 属性的
max_stacks数据项中 - Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈
- 操作数栈的每一个元素可以是任意 Java 类型
优化:
- 两个栈帧(局部变量表和操作数栈)是完全相互独立的
- JVM 会做一些优化处理,令两个栈帧出现一部分重叠
(3)动态连接
定义:每一个栈帧中都包含一个指向运行时常量池中该栈所属方法的符号引用。
目的:持有该引用是为了支持方法调用过程中的动态连接(符号引用转换成直接引用)。
(4)方法返回地址
方法执行后有两种方式可以退出这个方法:
方式 1:正常完成出口
执行引擎遇到任意一个方法返回的字节码指令。
方式 2:异常完成出口
- 在方法执行的过程中遇到了异常
- 并且这个异常没有在方法内进行处理
- 也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出
返回位置:
- 无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置
- 这个就是存放返回位置的地址
2. 方法调用
定义:方法调用不等同于方法执行,方法调用只是确定调用方法版本(哪个方法)。
解析
定义:在类加载阶段,将其中一部分符号引用转换成直接引用。
前提:
- 方法在程序运行之前就可以确定调用的版本
- 并且运行期不可变
符合这个条件的方法:
- ✅ 静态方法
- ✅ 私有方法
- ✅ 实例构造器
- ✅ 父类方法
- ✅ final 方法
这些方法都可以称非虚方法(不能多态)。
分派
宗量:方法的接受者与方法的参数称为方法的宗量。
静态分派
- 所有依赖静态类型来定位方法执行版本的分派称为静态分派
- 发生在编译阶段
- 典型应用是方法重载
动态分派
- 在运行期间根据实际类型来确定方法执行版本的分派称为动态分派
- 发生在程序运行期间
- 典型的应用是方法的重写
单分派
根据一个宗量对目标方法进行选择。动态分派就是其中一种。
多分派
根据多于一个宗量对目标方法进行选择。静态分派时其中一种。
3. 基于栈的字节码解释执行引擎(概念模型)
Javac 编译器输出的字节码指令流,基本上是一种基于栈的指令集架构。
优点:可移植性好
六、JVM 性能调优
1. 命令
| 命令 | 功能 |
|---|---|
| jps | 查看进程状况工具 |
| jstat | 统计监视工具(内存、垃圾回收、类加载) |
| jinfo | 运行时参数 |
| jmap | 生成 heap dump(堆内信息和对象信息) |
| jhat | 分析 heap dump,可由浏览器观看 |
| jstack | 堆、栈跟踪工具(线程快照) |
2. JVM 参数
调整最大堆和最小堆内存
1
-Xmx -Xms
规则:
- 当剩余空间大于 70% 时,减少到最小堆限制
- 当剩余空间小于 40% 时,增加到最大堆限制
调整新生代和老年代的比值
1
-XX:NewRatio=4
说明:新生代:老年代 = 1:4
调整 Survivor 和 Eden 的比值
1
-XX:SurvivorRatio=8
说明:From:To:Eden = 1:1:8
设置新生代、老年代大小
1
2
3
-XX:NewSize # 新生代大小
-XX:MaxNewSize # 最大新生代大小
-XX:MaxTenuringThreshold # 晋升老年代的年龄阈值
📚 参考资料:本文内容基于个人学习笔记整理