JVM
Java 虚拟机
一、名词解释
-
JVM - Java Virtual Machine - Java 虚拟机 - 虚构出来的计算机,在真实计算机上仿真出各种计算机功能
-
JRE - Java Runtime Environment - Java 运行环境 - 提供运行 Java 程序需要的基本类库,比如 操作文档、连接网络
-
JDK - Java Development Kit - Java 开发工具包 - 提供 Java 开发时需要的小工具,比如 javac、jConsole
JVM、JRE、JDK 为包含关系
Java 进程(.java) → 使用 JDK 编译(javac) → Java 字节码(.class、.jar) → JVM 将 字节码 翻译成对应 操作系统(Windows、Linux、Mac) 的 机器代码
二、JVM 架构
1. 类装载子系统
-
功能
- 获取 .class 文档,将 类信息、常量池、静态变量 放入方法区,作为实例化对象时的模板
-
类加载过程
-
加载
-
JVM 只有在第一次主动使用类的时候才会加载该类,不是一次把所有类都加载到内存中
-
加载器分类
- 启动类加载器 Bootstrap Class Loader
- 加载 Java 核心类库,用于提供 JVM 自身需要的类 - rt.jar、resource.jar
- 属于 引导类加载器,由 C/C++ 实现,其他加载器皆为 自定义加载器,由 Java 实现
- 扩展类加载器 Extension Class Loader
- 加载 jre/lib/ext 目录下的扩展包
- 系统类加载器 System Class Loader
- 进程中默认的类加载器
- 启动类加载器 Bootstrap Class Loader
-
类加载采用 双亲委派模型
- 当类加载器收到类加载请求,不会自己先尝试加载这个类,而会把这个请求委派给父类加载器完成,当父类在自己搜索范围内找不到指定类时,子类加载器才会尝试去加载
- 【目的】为了避免黑客串改关键类的代码
-
-
验证
- 确保 .class 文档符合 JVM 要求,不会危害 JVM 安全
-
准备
- 为【静态变量】设置初始值 0
-
解析
- 将常量池内的 符号引用(对类、变量、方法的描述) 替换为 直接引用(直接指向目标的指针)
-
初始化
-
运行 静态代码块
-
为 静态变量 赋值
静态代码块 与 静态变量赋值 的运行顺序,是依照语句的撰写顺序决定的
-
-
2. 运行时数据区
1. 进程计数器
- 纪录当前线程需要运行的下一条指令的行号,负责控制 分支、循环、跳转、异常处理
- 当运行的是 Native 方法时,存储的值是 undefined
- 唯一一个没有规定任何 OutOfMemoryError 的区域
2. 虚拟机栈
-
用于保存栈侦
-
每个方法运行时都会创造一个栈侦
-
栈侦内部保存 局部变量(表)、操作数栈、动态链接、方法返回地址
-
局部变量(表)
- 包含 8 种基本数据类型、对象引用、returnAddress
- long、double 占用 2 个 局部变量空间(Slot),其余占用 1 个
-
操作数栈
-
提供 数值计算的中间空间
数值计算流程 : 从 局部变量表 中,将常量或变量到入栈到 操作数栈,计算完成后,再将栈中元素出栈到 局部变量表 或 方法调用者
-
-
动态链接
- 指向 运行时常量池 该栈所属方法的 符号引用,方便调用实际方法
-
方法返回地址
- 存放调用该方法的 进程计数器 的值,用于返回给 进程计数器,继续运行后续程序
-
-
使用 连续的内存空间
-
配置参数
- 每个线程的虚拟机栈大小 - -Xss - 【默认】1M
3. 本地方法栈
- 与 虚拟机栈 类似,但保存的是 Native 方法的栈侦
以上皆为线程私有,线程间互不影响,与线程生命周期相同
4. 堆
-
保存 对象实例 与 数组
-
线程间共享
-
JVM 管理的内存中最大的一块
-
JVM 会尽可能将 堆空间 保持在尽量低的程度
-
可以不使用连续的内存空间
-
垃圾收集器主要管理的区域
-
配置参数
- 堆空间最小值 - -Xms
- 堆空间最大值 - -Xmx
-
分类
-
年轻代
- 回收频繁
- 分为 伊甸园、幸存者0区、幸存者1区 - 默认比例 8 : 1 : 1
- 当【伊甸园】区满时,将触发 Minor GC,并造成 Stop The World(STW),暂停所有用户线程
- 每次只使用其中 1 个幸存者区,所以年轻代实际可用空间为 90%
- 参数配置
- 伊甸园 较 幸存者区 倍数 - -XX:SurvivorRatio=8
- 放入老年代的累计回收存活次数 - -XX:MaxTenuringThreshold=15
-
老年代
- 回收频率较低
- 年轻代中的对象,经过几次回收后仍存在,将放入老年代 -【默认】15 次
- 老年代 与 青年代 默认比例 2 : 1
- 内存不足将触发 Major GC(速度慢 Minor GC 10倍以上),如垃圾清理后空间仍不足,将报 OOM 异常
- 参数配置
-
老年代 较 青年代 倍数 - -XX:NewRatio=2
-
【对象分配过程】
○ 新对象首先放在 伊甸园,当 伊甸园 满时进行垃圾回收(Minor GC),将 伊甸园 剩余对象放入 幸存者0区
○ 当再次触发回收,幸存者0区没被回收的对象,将被放入 幸存者1区
○ 再次触发回收,幸存者1区 没被回收的对象将放入 幸存者0区,以此类推
○ 直到累计次数 15 次,将对象放入 老年代
-
5. 方法区(元空间)
-
保存 类信息、运行时常量池、静态变量
-
类信息
-
Class 信息
-
Field 信息
-
Method 信息
都会放在运行时常量池中
-
-
运行时常量池
-
常量池在运行时的表现形式
字节码文档(.class)内部包含常量池,用于存放编译期间生成的 字面量 与 符号引用
-
-
存在于本地内存(改善垃圾回收效率、避免拷贝损耗),空间可以不连续
-
参数配置
- 元空间初始大小 - -XX:MetaspaceSize=n - 【默认】 21M
- 最大元空间大小 - -XX:MaxMetaspaceSize=n - 【默认】没有限制
三、异常
1. OutOfMemoryError
- 原因
- 加载数据过多 - 一次从数据库取出太多数据
- 集合中对象引用过多,且使用完后没有清空
- 发生 死循环 或 循环中产生过多对象
- 堆内存分配不合理
2. StackOverFlowError
- 原因
- 超出线程允许请求的栈深度
- 调用方法次数过多
- 通常栈深度 1000 ~ 2000 都没问题
- 超出线程允许请求的栈深度
四、垃圾回收
- 优点
- 开发人员不用考虑内存管理
- 有效防止内存泄漏
- 缺点
- 过度依赖垃圾回收,将降低开发人员解决 内存溢出 问题的能力
1. 回收算法
1. 判断对象以死
-
引用计数算法
- 当对象被引用时,计数值+1,引用失效时,计数值-1,数值为0表示对象已死
- Java 中并没有使用此算法
- 优点
- 实现简单
- 运行效率高
- 缺点
- 无法检测出循环引用
-
可达性分析算法
-
将 GC Roots 作为起点,搜索所有关联到的对象;搜索经过的路径被称为引用链,当一个对象不含有从 GC Roots 出发的引用链,将被视为不可达,认为该对象已死
-
GC Roots 类型
- 局部变量表中的对象
- static 对象
- final 对象
- Native 对象
- 被 同步锁(synchronized) 持有的对象
-
JVM 判断流程
-
发现对象没有与 GC Roots 连接的引用链,会被第一次标记
-
若被标记对象是否覆写了 finalize() 方法,且没有被调用过,则将该对象放入 F-Queue 队列中,否则则直接回收
finalize() 方法为 Object 中的一个垃圾回收方法,相当于一个免死金牌,但只能免死一次
-
对 F-Queue 中的对象进行第二次小规模标记,对象可以在 finalize() 方法中与引用链上的对象创建关联,避免被回收
-
-
2. 垃圾收集算法
-
理论
- 分代收集 - 根据对象的生命周期作划分,并进行分区管理
- 弱分代假说 - 绝大多数对象都是朝生夕灭的
- 强分代假说 - 熬过越多次垃圾收集的对象,越难以消灭
-
标记-清除算法
- 标记出所有需要回收的对象,然后统一进行回收
- 缺点 - 产生大量的内存碎片
-
标记-复制算法
- 将存活对象复制到另一块内存,并将原使用的内存空间清理掉
- 年轻代大多采用此算法,透过 幸存者0区 与 幸存者1区 的设立,可将空间浪费降为总年轻代的10%
- 缺点
- 需要预留一半的内存,浪费空间,导致GC频繁
- 当存活对象较多时,复制成本增加,效率降低
- 当大部分对象都是存活的(老年代),将无法使用这种算法
-
标记-整理算法
- 让存活对象向内存的一端移动,然后清理掉边界外的内存
2. 垃圾收集器
垃圾回收算法的具体实现
- 性能指针
- 吞吐量 : 用户代码时间 / (用户代码时间 + 垃圾收集时间)
- 暂停时间 : 运行垃圾回收时,工作线程被暂停的时间
- 内存占用 : Java 堆所占的内存大小
- 收集频率 : 垃圾收集的频次
1. Serial / Serial Old 收集器
- 单线程收集器,垃圾收集时必须暂停其他线程
- 使用方式 : -XX:+UseSerialGC
2. ParNew 收集器
-
多线程并行 的 年轻代 收集器
-
使用方式
-
-XX:+UseParNewGC
-
-XX:ParallelGCThreads=线程数 - 【默认】CPU 核心数
-
3. Parallel Old 收集器
- 多线程并行 的 老年代 收集器
- 基于 标记-整理算法 实现
- 适合 注重吞吐量、CPU资源敏感 的场景
- 使用方式
- -XX:+UseParallelOldGC
4. Parallel Scavenge 收集器
-
吞吐量优先 的 年轻代 收集器
-
并行多线程回收
-
Java 1.8 默认使用
-
可自动调节年轻代中的 伊甸园、幸存者 比例
-
适合 后台运算、交互不多的场景 - 批次任务、科学计算
-
使用方式
-
-XX:+UseParallelGC
-
-XX:MaxGCPauseMillis=毫秒数 - 设置最大垃圾收集停顿时间
-
-XX:GCTimeRatio=n
- 设置吞吐量,默认为 99
- n 值范围为 1~99
-
-XX:ParllGCThreads=n
- 设置年轻代线程数
- 【默认】CPU核心数小于等于 8,与核心数相同;CPU 核心数大于 8,n值为 3 + (5 * CPU_COUNT ) / 8
-
-XX:+UseAdaptiveSizePolicy - 开启年轻代比例自适应调节
-
5. CMS 收集器
-
以 最短垃圾收集停顿时间 为目标的 老年代 收集器
-
基于 标记-清除算法 实现
-
适合 互联网服务 场景
-
GC 流程
- 初始标记
- 标记 GC Roots 直接关联到的对象,速度快
- 将造成 Stop the World
- 并发标记
- 从 GC Roots 直接关联到的对象遍历所有关联对象
- 过程耗时,但不需暂停用户线程
- 重新标记
- 修正并发标记期间,因用户线程继续运行,导致标记错误的纪录
- 将造成 Stop the World
- 运行时间稍长,但小于并发标记时间
- 并发清除
- 删除判断已经死亡的对象
-
缺点
- 在 并发标记 阶段,不会因占用用户线程导致停顿,但会导致进程变慢,导致总吞吐量降低 - 在 CPU 核心数不足 4 个时,影响大
- 无法处理 浮动垃圾,可能导致 Full GC 发生
- 基于 标记-清除算法,将产生 内存碎片
-
使用方式
- -XX:+UseConcMarkSweepGC
- -XX:CMSFullGCsBeforeCompaction=n
-
运行 Full GC 若干次就进行碎片整理
-
【默认】0次
-
6. G1 收集器
-
年轻代、老年代 皆可使用的垃圾收集器
-
满足低 GC 停顿时间 与 高吞吐量 特征
-
针对多核 CPU 与 大容量内存 机器
-
将 内存 划分成约 2000 个独立的 Region,依旧保留分代思想,但不再是物理隔离
-
Region 大小为 2 的 N 次幂,1MB、2MB、4MB、8MB、16MB、32MB
-
增加 Humongous 内存区域,用于保存超过 1.5 个 Region 的大对象,视为老年代
-
整体采 标记-整理算法;局部采 标记-复制算法 - 不会产生内存碎片
-
跟踪 Region 中垃圾的价值大小,维护一个优先列表,优先回收价值高的区域
-
GC 流程
- 初始标记
- 只标记与 GC Roots 直接关联的对象
- 造成 Stop the World
- 并发标记 : 从 GC Roots 直接关联到的对象遍历所有关联对象
- 最终标记
- 修正并发标记期间,因用户继续运行,导致标记错误的纪录
- 造成 Stop the World
- 筛选回收
- 根据时间进行价值最大化收集
- 造成 Stop the World
- 初始标记
-
使用方式
- -XX:+UseG1GC
- -XX:MaxGCPauseMillis=200 - 期望最大 GC 停顿时间
- -XX:InitiatingHeapOccupancyPercent=n
-
当老年代占整堆大小百分比达到域值n,则进行 Mixed GC
-
【默认】45
-