主要讨论 HotSpot 虚拟机
Java平台体系
图片来源: Java Platform Standard Edition 8 Documentation
JVM五大区
JVM运行时数据区(Runtime Data Area) | 可能抛出异常 | 线程私有 |
---|---|---|
程序计数器(Program Counter Register) | 无 | 是 |
虚拟机栈(VM Stack) | StackOverflowError/OutOfMemoryError | 是 |
本地方法栈(Native Method Stack) | StackOverflowError/OutOfMemoryError | 是 |
堆(Heap) | OutOfMemoryError | 否 |
方法区(Method Area) | OutOfMemoryError | 否 |
程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,可以看作当前线程所执行的字节码的行号指示器
- 字节码解释器工作时通过改变程序计数器的值来选取下一条需要执行的字节码指令
- 分支/循环/跳转/异常处理/线程恢复等基础功能都需要依赖程序计数器来完成
- 每条线程的程序计数器互补影响,独立存储
- 执行Java方法时,程序计数器记录的是正在执行的虚拟机字节码指令的地址
- 执行Native方法时,程序计数器的值为空(Undefined)
- 程序计数器是唯一一个在Java虚拟机规范中没有规定任何
OutOfMemoryError
情况的区域
Java虚拟机栈
Java虚拟机栈(Java Virtual Machine Stacks) 也是线程私有的, 它的生命周期与线程相同
- 虚拟机栈描述的是Java方法执行的内存模型
- 每个Java方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表/操作数栈/动态链接/方法出口等信息
- 每个Java方法从调用到执行完成的过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程
- 64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个
- 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法所需在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小
- 若线程请求的栈深度大于虚拟机锁允许的深度, 将抛出
StackOverflowError
异常 - 若虚拟机可以动态扩展(大部分Java虚拟机都支持)且扩展时无法申请到足够的内存, 将抛出
OutOfMemoryError
异常
局部变量表存放的各种基本数据类型:
局部变量表基本数据类型 | 默认值 | 占用空间bit |
---|---|---|
boolean | false | 1 |
byte | 0x00 | 16 |
char | ux0000 | 16 |
short | 0 | 8 |
int | 0 | 32 |
float | 0.0 | 32 |
long | 0 | 64 |
double | 0.0 | 64 |
reference | null | 32/64 |
对象引用(reference类型), 不等同于对象本身,可能是指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置
returnAddress类型指向了一条字节码指令的地址
本地方法栈
本地方法栈(Native Method Stack)与虚拟机栈的作用非常相识, 只不过是虚拟机栈为Java方法服务, 而本地方法栈为虚拟机使用到的Native方法服务
- 若线程请求的栈深度大于虚拟机锁允许的深度, 将抛出
StackOverflowError
异常 - 若虚拟机可以动态扩展(大部分Java虚拟机都支持)且扩展时无法申请到足够的内存, 将抛出
OutOfMemoryError
异常
Java堆
一般情况下Java堆(Java Heap) 是Java虚拟机所管理的内存中最大的一块
- Java 堆是被所有线程共享的一块内存区域, 在虚拟机启动时创建
- 唯一目的是存放对象实例, 几乎所有对象实现都在这里分配内存
- Java 堆是垃圾收集器管理的主要区域,所以也成为
GC堆(Garbage Collected Heap)
- 根据垃圾收集器的分代收集算法, Java堆还可以细分为: 新生代和老年代等
- Java 堆可以处于物理上不连续的内存空间中, 只要逻辑上是连续的即可
- 可以通过
-Xmx
和-Xms
控制Java 堆的大小 - 当Java 堆中没有内存完成实例分配, 并且堆也无法再扩展时, 将会抛出
OutOfMemoryError
异常
方法区
方法区(Method Area) 与Java 堆一样, 是各个线程共享的内存区域, 用于存储已被虚拟机加载的类信息/常量/静态变量/即使编译器(JITC)编译后的代码等数据
- 别名为Non-Heap(非堆), 目的在于和Java 堆区分开来
- 常被称为永久代(Permanent Generation), 本质上两者并不等价
- 当方法区无法满足内存分配需求时, 将抛出
OutOfMemoryError
异常
运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分
- 用于存放编译器生成的各种字面量和符合引用, 将在类加载后进入方法区的运行时常量池中存放
- 具备动态性, 比如String的intern()方法
- 当常量池无法再申请到内存时会抛出
OutOfMemoryError
异常
直接内存
直接内存(Direct Memory) 不是虚拟机运行时数据区的一部分,但是也能导致OutOfMemoryError异常出现
- JDK 1.4 中新加入
NIO
类,引入了基于通道(Channel
)和缓冲区(Buffer
)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过操作存储在Java堆中的DirectByteBuffer
对象作为这块内存的引用进行操作 - 避免了在Java堆和Native堆来回复制数据,显著提高性能
- 不受Java堆大小限制,受本机总内存(RAM和SWAP或者分页文件)大小和处理器寻址空间的限制
- 通过
-Xmx
配置虚拟机参数时要注意直接内存并不受这个参数限制,需要考虑内存区域总和是否大于物理内存限制 - 可能抛出
OutOfMemoryError
异常
垃圾收集器
垃圾收集 (Garbage Collection, GC) 需要解决3件事情: 那些内存需要回收(What)/什么时候回收(When)/如何回收(How)
对象存活判断
常见判断方法有引用计数算法和可达性分析算法
引用计数算法
- 给对象添加引用计数器, 当有一个地方引用它时就加1, 当引用失效时就减1, 引用计数器为0的对象就是不可能再被使用的
- 实现简单, 判定效率高
- 应用案例有: 微软的COM技术/FlashPlayer/Python/Squirrel等
- 主流Java虚拟机并没有选用引用计数算法来管理内存, 因为其难以解决对象之间相互循环引用问题
可达性分析算法
- 主流的商用程序语言(Java/C#/Lisp)就是使用可达性分析算法 (Reachability Analysis)来判定对象是否存活
- 通过一系列称之为
GC Roots
的对象作为起始点, 从这些节点开始向下搜索, 搜索锁走过的路径称为引用链 (Reference Chain), 当一个对象到GC Roots
没有任何引用链相连(即GC Roots
到这个对象不可达),则此对象是不可用的
在Java中, 可作为GC Roots
的对象有:
- 虚拟机栈 (栈帧中的本地变量表) 中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI (Native方法) 引用的对象
引用类型
Java 对引用的概念进行了扩充, 分为4种引用类型
- 强引用 (Strong Reference): 即代码中普遍存在的引用, 只要引用还存在, 垃圾收集器永远不会回收掉被引用的对象, 如
String str = "I am OK";
- 软引用 (Soft Reference): 用来描述一些还有用但并非必需的对象, 在系统将要发生内存溢出异常之前, 将会对这些对象列入回收范围之中进行第二次回收,若回收后内存还是不足, 才会抛出内存溢出溢出. 在
JDK 1.2
之后, 提供了SoftReference
类来实现软引用 - 弱引用 (weak Reference): 弱引用也是用来描述非必需对象的, 但是它比软引用更弱一些, 被弱引用关联的对象只能生存到下一次垃圾收集发生之前, 无论当前内存是否足够. 在
JDK 1.2
之后, 提供了WeakReference
类来实现弱引用 - 虚引用 (Phantom Reference): 也称幽灵引用或者幻影引用, 它是最弱的一种引用关系. 一个对象是否有虚引用存在, 完全不会对其生存时间构成影响, 也无法通过虚引用来取得一个对象实例. 为一个对象设置虚引用的唯一目的是能在这个对象被收集器回收时收到一个系统通知. 在
JDK 1.2
之后, 提供了PhantomReference
类来实现虚引用
对象回收
- 在可达性分析算法中不可达的对象, 也并非一定会被回收, 至少要经历两次标记过程
- 如果对象进行可达性分析后发现没有与
GC Roots
相连接的引用链, 那他将会被第一次标记并且进行一次筛选, 筛选条件是该对象是否有必要执行finalize()
方法 - 当对象没有覆盖
finalize()
方法或者finalize()
方法已经被虚拟机调用过, 虚拟机将这两种情况都视为 “没必要执行” - 若对象被判定为有必要执行
finalize()
方法, 那么该对象将会被放置在一个叫做F-Queue
的队列中, 并在稍后由一个虚拟机自动建立的、低优先级的Finalizer
线程去执行它, 但是并不承诺会等待它运行结束 (避免该方法长久占用执行资源, 导致其他队列中的对象永久等待, 甚至导致整个内存回收系统崩溃) finalize()
方法是对象逃脱死亡命运的最后一次机会, 稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记, 检查其中对象是否有重新与GC Roots
建立引用链finalize()
方法运行代价高昂, 不建议使用它
回收方法区
- 永久代的垃圾收集主要回收 废弃常量 和 无用的类
- 回收废弃常量与回收Java堆中的对象非常类似
- 类需要同时满足3个条件才能算作无用的类,才可以被回收: 该类所有实例都已经被回收(即 Java 堆中不存在该类的任何实例) / 加载该类的
ClassLoader
已经被回收 / 该类对应的java.lang.Class
对象没有在任何地方被引用, 无法在任何地方通过反射访问该类的方法 - Hotspot 虚拟机可以通过
-Xnoclassgc
控制是否对类进行回收 - Hotspot 虚拟机可以通过
-verbose:class
以及-XX:+TraceClassLoading
和-XX:TraceClassUnLoading
查看类加载和卸载信息 - 在大量使用反射/动态代理/CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁定义ClassLoader的场景都需要虚拟机具备类卸载功能, 以保证永久代不会溢出
垃圾收集算法
常见的垃圾收集(GC)算法有: 标记-清除算法/复制算法/标记-整理算法/分代收集算法
标记-清除算法
- 标记-清除 (Mark-Sweep) 算法是最基础的收集算法, 后续的算法都是基于这种思路并对其不足进行改进而得到的
- 算法分为标记和清除两个阶段,首先标记出要回收的对象, 在标记完成后统一回收所有被标记的对象
- 不足一, 效率问题, 标记和清除这两个过程效率都不高.
- 不足二, 空间问题, 标记清除之后会产生大量不连续的内存碎片, 导致下次分配大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
复制算法
- 复制(Copying) 算法, 将可用内存按容量划分为大小相等的两块, 每次只用其中一块
- 当使用的那一快内存用完了, 就将存活着的对象复制到另一块上面, 然后将已使用过的内存空间一次清理掉
- 内存分配时不用考虑内存空间碎片等复杂情况, 实现简单, 运行高效
- 不足一, 将内存缩小为原来的一半
- 不足二, 在对象存活率较高时就要进行较多的复制操作, 效率将会变低, 所以老年代一般不直接使用这种算法
- 现在的商业虚拟机都采用这种收集算法来回收新生代 (可能采用8:1:1的方式)
标记-整理算法
- 标记-整理(Mark-Compact) 算法, 标记过程和标记-清除算法一样, 但是后续步骤不是直接对可回收对象进行清理, 而是让所有存活对象都向一端移动, 然后直接清理掉端边界以外的内存
- 不足, 效率问题, 标记和整理这两个过程效率都不高
分代收集算法
- 分代收集(Generational Collection) 算法, 根据对象存活周期的不同将内存划分为几块
- 一般讲Java堆划分为新生代和老年代, 根据各个年代的特定采用最适当的收集算法. 新生代每次垃圾收集都有大批对象死去, 只有少量存活, 那就选用复制算法. 老年代对象存活率高, 没有额外空间对它进行分配担保, 使用标记-清除算法或者标记-整理算法
HotSpot 的算法实现
- 枚举根节点, 可达性分析会导致GC停顿(Stop the World), 使用称为OopMap的数据结构实现
- 安全点(Safep`oint), 使用主动式中断(Voluntary Suspension) , 不使用抢先式中断(Preemptive Suspension)
- 安全区域(Safe Region), 线程处于Sleep状态或者Blocked 状态时, 无法响应JVM的中断请求, 所以需要安全区域来解决这个问题, 所以JVM发起GC时就不用管标识自己为Safe Region 状态的线程了
常见的垃圾收集器
如果说垃圾收集算法是内存回收的方法论, 那么垃圾收集器就是内存回收的具体实现
对垃圾收集器来说的并行和并发
- 并行 (Parallel) : 多条垃圾收集器线程并行工作, 此时用户线程仍然处于等待状态
- 并发 (Concurrent): 用户线程和垃圾收集线程同时执行(不一定并行, 可能交替执行)
Serial 收集器
- 新生代收集器, 是最基本/发展历史最悠久的收集器, 在
JDK 1.3.1
之前是新生代收集器的唯一选择 - 单线程, 一条收集线程, 而且收集时必须暂停其他所有的工作线程直到收集结束
- 是虚拟机运行在
Client
模式下的默认新生代收集器 - 优点: 简单/高效
- 缺点: 单线程收集/必须暂停其他所有工作线程
ParNew 收集器
- 新生代收集器, 是Serial 收集器的多线程版本
- 多线程, 多条收集线程, 而且收集时必须暂停其他所有的工作线程直到收集结束
- 是虚拟机运行在
Server
模式下的默认新生代收集器 - 只有它能与
CMS
收集器配合工作 - 在单CPU环境下绝不会有比Serial 收集器更好的效果
- 优点: 简单/高效/多线程
- 缺点: 必须暂停其他所有工作线程
Parallel Scavenge 收集器
- 新生代收集器, 使用复制算法的收集器, 并行的多线程收集器
- 目标是达到一个可控制的吞吐量 (Throughput) , 吞吐量= 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
- 适合用在后台运算而不需要太多交互的任务, 比如服务器
- 优点: 吞吐量可控/多线程
- 缺点: 不适合对响应速度要求高的地方/必须暂停其他所有工作线程
Serial Old 收集器
- 老年代收集器, 单线程收集器, 使用标记-整理算法
- 主要给
Client
模式下的虚拟机使用 Server
模式下, 用途: 一在JDK 1.5
及以前版本中与Parallel Scavenge 收集器搭配使用, 二作为CMS收集器的后备方案, 在并发收集发生Concurrent Mode Failure
时使用- 优点: 简单/高效
- 缺点: 单线程收集/必须暂停其他所有工作线程
Parallel Old 收集器
- 老年代收集器, 使用多线程和标记-整理算法, 是Parallel Scavenge 收集器的老年代版本
JDK 1.6
及之后版本才提供- 在注重吞吐量以及CPU资源敏感的场合(如服务器), 优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器的组合
- 优点: 吞吐量优先/多线程
- 缺点: 必须暂停其他所有工作线程
CMS 收集器
- 老年代收集器, CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器, 基于标记-清除算法实现的
- B/S 架构的服务器尤其重视服务的响应速度, 希望系统停顿时间最短, 以给用户带来较好的体验
- 过程: 初始标记 (CMS initial mark, 会Stop the World), 并发标记(CMS concurrent mark), 重新标记(CMS remark, 会Stop the World), 并发清除(CMS concurrent sweep)
- 优点: 并发收集/低停顿 (Concurrent Low Pause Collector)
- 缺点: 对CPU资源非常敏感 /无法处理浮动垃圾(Floating Garbage) /清理后会有大量空间碎片产生
G1 收集器
- 新生代/老年代收集器, G1(Garbage First) 是当今收集器技术发展的最前沿成果之一, 在
JDK 1.7
后提供 - G1 是一款面向服务端应用的垃圾收集器, 可以独立管理整个GC堆
- 特点: 并行与并发/分代收集/空间整合/
- 优点: 停顿时间短且可预测/收集效果好/不会产生空间碎片
- 缺点: 不够成熟
MinorGC
- 指发生在新生代中的垃圾收集动作, 采用复制算法, 也称新生代GC
- 一般回收速度比较快, 次数也非常频繁
- 大对象直接进入老年代, 避免短命大对象
- 长期存活对象进入老年代, 动态对象年龄判断, 空间分配担保
- 触发条件: 只要
Eden
空间不足就开始进行 - 新生代按
8:1:1
的比例分为三个区:Eden
/from
/to
-XX:+PrintGCDetails
可以打印内存回收日志
FullGC / Major GC
- 针对整个新生代/老生代/元空间的全局范围的GC, 老年代采用标记-清除算法, 也称老年代GC
- 经常会伴随至少一次的 Minor GC (并非绝对)
- 一般比Minor GC 慢10倍以上
- 触发条件: 老年代空间不足/ PermSpace (元空间) 不足 / 统计得到的Minor GC 晋升到老年代的平均大小大于老年代的剩余空间
JVM监控和优化
监控数据: 运行日志/异常堆栈/GC日志/线程快照(threaddump / javacore 文件)/堆转储快照( heapdump / hprof 文件) 等
命令行工具
名称 | 主要作用 |
---|---|
jps | JVM Process Status Tool, 显示指定系统内所有的 HotSpot 虚拟机进程 |
jstat | JVM Statistics Monitoring Toll, 用于收集 HotSpot 虚拟机各方面的运行数据 |
jinfo | Configuration Info for Java, 显示虚拟机配置信息 |
jmap | Memory Map for Java, 生成虚拟机的内存转储快照 (heapdump 文件) |
jhat | JVM Heap Dump Browser, 用于分析heapdump 文件, 会建立 HTTP 服务器, 可在浏览器中查看分析结果 |
jstack | Stack Trace for Java, 显示虚拟机的线程快照 |
可视化工具
名称 | 作用 |
---|---|
jconsole | JConsole (Java Monitoring and Management Console) Java 监视与管理控制台, 是一种基于 JMX 的可视化监视/管理工具 |
jvisualvm | JVisual (All-in-One Java Trobleshooting Tool)多合一运行监控和故障处理工具 |
日志查询
-XX:+PrintGCDetails
可以打印内存回收日志
参数设置
参数 | 说明 |
---|---|
-Xms20M | 限制Java堆的初始大小/最小大小为20M |
-Xmx30M | 限制Java堆的最大大小为30M |
-Xmn10M | 限制Java堆的新生代大小为10M |
-XX:SurvivorRatio=8 | 新生代中Eden区与一个Survivor区的空间比例是8:1 |
JVM 类加载
类从被加载到虚拟机内存中开始, 到卸载出内存位置, 它的整个生命周期包括: 加载(Loading) 、验证 (Verification) 、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading). 其中验证、准备、解析3个部分统称为连接(Linking)
类加载过程
加载
- 通过一个类的全限定名来获取定义此类的二进制流, 来源可以是: class文件/zip包/网络/运行时计算生成(动态代理)/由其他文件生成(JSP)/从数据库中读取
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
- 加载阶段和连接阶段的部分内容是交叉进行的, 但是两个阶段的开始时间仍然保持固定的先后顺序
验证
- 确保Class文件的字节流中包含的信息符合当前虚拟机的要求, 并且不会危害虚拟机自身的安全
- 大致分为4个阶段的检验动作: 文件格式验证(文件格式规范)/ 元数据验证(语义分析,数据类型分析)/ 字节码验证(数据流/控制流分析,方法体校验分析)/ 符号引用验证(访问性分析)
准备
- 正式为类变量(static 变量)分配内存, 并设置类变量初始值, 在方法区中分配
- 通常情况下初始值是零值, 除非是
final
类型常量
解析
- 将常量池内的符号引用替换为直接引用
初始化
- 真正开始执行类中定义的Java 程序代码(字节码)
- 父类初始化方法先执行完毕, 再开始执行子类的初始化方法
- 同一个类加载器下, 一个类型只会初始化一次
双亲委派模型
从Java 虚拟机角度, 只有两种不同的类加载器: 启动类加载器(Bootstrap ClassLoader)和其他类加载器(Other ClassLoader)
从Java开发人员角度, 类加载器有三类: 启动类加载器(Bootstrap ClassLoader) / 扩展类加载器(Extension ClassLoader) / 应用程序类加载器(Application ClassLoader)
- 类加载器的关系模型一般为双亲委派模型 (Parents Delegation Model)
- 除了顶层的启动类加载器外, 其余的加载器都应有自己的父类加载器
- 类加载器之间的关系一般是组合(Composition)而不是继承(Inheritance)
- 工作过程: 如果一个类加载器收到了类加载请求, 它首先不会自己去尝试加载这个类, 而是把这个请求委派给父类加载器去完成, 每一层加载器都是如此, 只有当父加载器反馈自己无法完成(它的搜索范围内未找到所需的类)这个加载请求时, 子加载器才会尝试自己去加载
- 保证Java程序的稳定运行, 避免重复加载
参考
- 《深入理解Java虚拟机》