JVM理论
[toc]
JVM理论
1. GC垃圾收集
1.1 GC目的与什么时候垃圾收集
目的: 识别并丢弃未不再使用的对象来释放和重用资源
在内存中存在没有引用的对象或者超出作用域的对象时进行的.
当对象未null, 下个垃圾回调周期中, 此对象可能会被回收(非立即回收, 需下一次垃圾回收才释放占用内存)
JVM有一个低优先级的垃圾回收线程, 正常下不会执行,只有当虚拟机空闲或者当前堆空间不足时,才会触发执行扫描没有被任何引用的对象, 添加到要回收的集合中,进行回收
1.2 finalize()与析构函数(finalization)
finalize(): Object类中的方法, 在垃圾回收时, 会调用被回收对象的finalize()方法, 可以覆盖此对象来实现垃圾回收(一旦决定垃圾回收, 则先调用对象的finalized()方法, 下个垃圾回收动作时,才会真正回收对象占用内存空间)
finalization(): 调用JNI或者JNA写的native方法时, 要在finalization中调用native方法释放内存
1.3 判断finalize()是否有必要执行
如果对象没有覆盖finalize()或者finalize()已被虚拟机调用过, 那么就没必要执行, 对象已被回收
如果对象被判断必要执行finalize(), 会被方法F-Queue()队列, 虚拟机会用低优先级执行finalize(), 但不会确保所有finalize()被执行, 如果finalize()出现耗时操作,直接停止该方法,将对象清除.
2. JVM内存区域
2.1 内存结构

@startmindmap
* JVM
** 子系统
*** 类装载\nClass Loader
*** 执行引擎\nExecution Engine
** 子组件
*** 运行时数据区\nRuntime Data Area
**** 线程私有
***** 程序计数器
***** 虚拟机栈
***** 本地方法栈
**** 线程共享
***** 堆
***** 方法区
***** 直接内存
*** 本地接口\nNative Interface
@endmindmap
- 类装载(Class Loader): 根据类名装载class文件到运行时数据区(Runtime Data Area)中的方法区(Method Area)
- 执行引擎(Execution Engine): 执行class指令
- 本地接口(Native Interface): 与本地方法库(Native Method Libraries)交互, 与其他语言的交互接口
- 运行时数据区(Runtime Data Area): JVM内存
执行流程:
- IDE编写Java源码
- 编译器(javac)将源码编译为字节码文件(.class文件)
- 类加载器(ClassLoader)将字节码加载到内存中,将其放入运行时数据区中的方法区内; 然后再堆区创建java.lang.Class对象,封装类再方法区的数据结构
- 字节码文件交由命令解析器执行引擎, 将字节码翻译为底层系统指令, 再交由CPU执行, 这个过程需要调用其他语言的本地接口来实现整个程序的功能.
2.2 程序计数器
定义: 较小的内存区域, 当前线程所指向字节码行号指示器, 如果执行本地方法, 则程序计数器为undefined.
作用:
- 字节码解释器工作时通过改变这个计数器值来选取下一条字节码指令. 分支, 循环,跳转,异常处理, 线程恢复等都依赖计数器完成, 从而实现代码的流程控制
- 多线程情况下, 程序计数器记录当前线程执行位置. 线程切换时, 恢复现场.
特点:
- 较小占用的内存空间
- 线程私有, 每个线程有自己的程序计数器
- 生命周期: 随线程创建而创建, 随线程结束而销毁
- 唯一不会出现
OutofMemoryError
的内存区域
2.3 Java虚拟机栈
定义: 线程私有, 生命周期与线程相同, 描述Java方法执行的内存模型, 每次方法调用的数据都是通过栈传递, 每个虚拟机栈由一个个栈帧组成, 而每个栈帧都由: 局部变量表, 操作数栈, 动态链接, 方法出口信息等.
局部变量表中存放了编译器可知的数据类型(基础数据类型), 对象引用(reference类型, 可能时执行对象起始地址的引用指针, 也可能指向一个代表对象的句柄或与次对象相关的位置)
压栈,出栈过程:
- 栈顶栈帧是当前正在执行的活动栈(当前正在执行的方法), PC寄存器也会指向这个地址, 只有这个活动的栈帧的本地变量可以被操作数栈使用
- 当栈顶栈帧在运行过程中, 需要创建局部变量时, 会将局部变量值存入栈帧局部变量表中.
- 当栈帧调用另外一个方法, 对应方法会创建栈帧, 并将新的栈帧压入栈顶, 变为活动栈帧
- 方法指向结束(return或者抛出异常), 当前栈帧被移出, 栈帧返回值变为活动栈帧的操作数, 如果没有返回值, 那么活动栈帧操作数没有变化
- 虚拟机栈与线程对象, 数据不是线程共享的, 所以不用关心数据一致性问题, 不存在同步锁问题
特点:
- 虚拟机栈与线程一一对应, 跟随线程生命周期
- 局部变量表在编译时就确定了, 随栈帧的创建而创建, 创建时只需分配事先规定的大小即可, 在方法运行时,局部变量表大小不变
- 会出现2种异常
StatckOverFlowError
,OutofMemoryError
- StatckOverFlowError: 如果虚拟机栈内存大小不允许动态扩展, 当当前线程请求栈深度超过java虚拟机栈最大深度时, 抛出异常
- OutofMemoryError: 如果虚拟机栈内存大小可动态扩展, 当动态扩展栈无法申请到足够内存空间时, 抛出异常
2.3 Java虚拟机栈
定义: JVM为native方法准备的空间, 描述本地方法运行过程的内存模型
过程:
- 本地方法执行时, 会在本地方法栈创建一块栈帧, 存放该方法的局部变量表, 操作数栈, 动态链接, 方法出口信息等.
- 方法执行完成后, 对应栈帧出栈, 释放内存空间, 也会抛出StackOverFlowError 和 OutOfMemoryError 异常
- 如果虚拟机不支持native方法或者本地不依赖传统栈, 那么可以不提供本地方法栈. 如果支持本地方法栈, 那么这个栈一遍会在线程创建时, 按照线程分配(也就是跟随线程生命周期)
2.4 方法区
定义: 线程共享内存区域, 存储已被虚拟机记载的类信息, 常量, 静态变量, 即时编译器编译后的代码等数据, 索然java虚拟机规范把方法区描述为堆的一个逻辑部分, 但是有个别名非堆(Non-Heap). 目的是与堆区分开. 也被称为永久代
方法区与永久代的区别: 方法区是Java虚拟机规范中的定义, 永久代是HotSpot虚拟机的概念, 对应的是方法区的一种实现. 其他虚拟机没有永久代的说法. JDK1.8后, 方法区被移出, 取而代之的是元空间, 元空间使用的是直接内存
指定元空间大小: (如果不指定大小, 随着类的创建, 虚拟机会耗尽所有可用系统内存)
-XX:MetaspaceSize=N // 元空间初始大小
-XX:MaxMetaspaceSize=N // 最大大小
为何永久代(PermGen)替换为了元空间(Metaspace)
- 永久代由JVM设置固定大小上限, 无法调整; 而元空间使用直接内存, 受本机内存限制, 内存溢出几率更小
- 元空间存放类的元数据, 由系统实际可用空间来控制, 可加载类更多
- 合并HotSpot与JRockit代码
特点:
- 线程共享, 整个虚拟机只有一个方法区
- 永久代
- 内存回收效率低. 方法区信息一般长期存在, 一般只有少量信息被回收,主要回收目标为: 常量池回收, 类型卸载
- 要求宽松, 运行固定大小,也允许动态扩展,甚至不实现垃圾回收
运行时常量池
存放编译期生成的各种字面量和符号引用. 作为方法区一部分, 受方法区内存限制
JDK1.8后, 字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区变为了元空间
2.5 直接内存
不是虚拟机运行时数据区的一部分, 也不是虚拟机规范定义的内存区域,但是也会频繁使用
NIO中, 使用Native函数直接分配堆外内存, 然后通过java堆的DirectByteBuffer对象作为这块内存的引用进行操作, 在一些场景中显著提高性能, 避免了Java堆与Native堆之间来回复制数据
不受java堆限制, 但是还是受到本机内存大小与处理器寻址空间的限制
直接内存与堆内存比较:
- 直接内存申请空间耗费更高性能
- 直接内存读取IO性能优于普通堆内存
- 直接内存作用链: 本地IO -> 直接内存 -> 本地IO
- 堆内存作用链: 本地IO -> 直接内存 -> 堆内存 -> 直接内存 -> 本地IO
2.6 堆
定义: 管理内存最大的一块, 所有线程共享内存区域, 虚拟机启动时创建. 唯一目的就是存放对象实例, 几乎所有对象实例以及数组都在这里分配内存
特点:
- 线程共享,所有线程访问一个堆
- 虚拟机启动时创建
- 垃圾回收主要场所
- 进一步可分为新生代(Eden区, From Survivor, to Survivor), 老年代
- 不同区域存放不同生命周期对象, 根据不同区域使用不同的垃圾回收算法, 更具有针对性
- 会出现2个错误
java.lang.OutOfMemoryError: GC Overhead Limit Exceeded
: JVM花太多时间执行垃圾回收只能回收很少堆空间时, 会报错java.lang.OutOfMemoryError: Java heap space
: 堆内存空间不足已存放新创建对象, 引发错误(与配置最大堆内存, 物理内存大小等有关)
3 HotSpot虚拟机对象
3.1 内存对象创建
- 虚拟机当遇到一个new指令, 先检查常量池是否已加载对应的类
- 没有, 则先执行对应类的加载
- 加载完成, 开始分配内存, 分配内存时还需要考虑并发
- 必要的对象设置(成员变量初始值设置, 元信息, 哈希码等), 最后调用构造函数方法进行初始化
3.1.1 内存对象布局
类型 | 名称 | 说明 |
---|---|---|
对象头 | Mark Word | 4字节, 记录hash码,GC分带年龄,锁状态,线程持有锁,偏向线程id,偏向时间戳等 |
^ | Class对象指针 | 4字节, 执行对象所属类型 |
实例数据 | 对象实际数据 | 实际数据大小,实例成员变量的值(包括父类与子类成员变量) |
对齐填充 | 对其(可选) | 确保对象总长度为8个字节整数倍, 当数据部分没有对齐时, 对齐填充, 无实际意义 |
3.1.2 对象创建方法
header | 解释 |
---|---|
使用new关键字 | 调用构造方法 |
使用Class.newInstance方法 | 调用无参构造方法 |
使用Constructor.newInstance方法 | 已确定构造方法 |
使用clone方法 | 未调用构造方法 |
使用反序列化 | 未调用构造方法 |
3.1.3 对象创建分配内存
分配内存根据JAVA堆是否规整, 有2种方式:
- 指针碰撞: 如果java堆规整(使用的内存放一边, 未用的放另外一边). 分配内存将指针指示器向空闲内存移动一段与对象大小相等的距离, 以便完成分配内存操作
- 空闲列表: 如果java堆不规整, 则由虚拟机维护一个列表记录哪些内存可用, 分配时在列表中查询最够大的内存分配给对象, 分配后更新列表
3.1.4 处理并发安全问题
对象创建在虚拟机非常频繁, 可能会出现线程安全问题:
- 对分配内存空间动作进行同步处理(CAS+失败重试来保障更新操作原子性)
- 把内存分配动作按照线程放到不同空间中进行, 即每个线程预先分配一小块内存, 称为本地线程分派缓冲(Threa Local Allocation Buffer, TLAB).哪个线程需要分配内存,就在哪个线程的TLAB上分配. 只有当线程的TLAB用完并分配新的TLAB时, 才需要进行同步锁, 通过使用
-XX:+/-UserTLAB
参数来确定虚拟机是否使用TLAB
3.2 对象访问方式
所有对象的存储空间是在堆中分配的, 而对象的引用是在虚拟机栈分配. 也就是说创建一个对象会在2个地方分配内存, 在堆中分配内存实际建立对象, 而在虚拟机栈中只是一个执行堆对象的指针(引用), 根据引用存放地址不同, 分为不同访问方式:
- 句柄访问方式: 堆中由名为'句柄池'的内存空间, 包含对象实例数据, 对象类型数据各自的具体地址信息. 引用类型的变量存放的时该对象句柄地址. 访问时, 先根据变量找到句柄地址, 再根据对象句柄中对象地址找到对象
- 直接指针访问方式: 引用类型变量之间存放对象的地址, 通过引用直接访问对象, 但对象的内存空间需要额外的策略存储对象所属类的信息地址
HotSpot采用第二种方式, 直接指针访问. 一次寻址操作,性能上比句柄访问快一倍, 采用MarkWord来额外存储对象所属类信息地址.
3.3 内存溢出
内存溢出: 指不再使用的对象或者变量一直占据内存中. 理论上会被GC回收. 但是如果长周期对象持有短周期对象, 长周期持有则不对被GC扫描到. 导致短周期对象不会被回收而导致内存溢出
4. 垃圾收集策略与算法
程序计数器, 虚拟机栈, 本地方法栈跟随线程生命周期, 栈帧随方法出入栈. 这几个地方内存分配回收是有确定性的. 这几个区域不需要多考虑回收问题. 方法或者线程结束, 内存也会回收.
对于堆和方法区, 只有运行时才能确定创建哪些对象,这部分分配和回收时动态的, 垃圾回收正是关注这部分内存
4.1 java引用内些
类型 | 说明 |
---|---|
强引用 | 发生GC时不会被回收 |
软引用 | 有用但不一定必须的对象, 发生内存溢出前会被回收, 通常用来实现内存敏感缓存 |
弱引用 | 有用但不是必须的对象, 下次GC时必定回收 |
虚引用(幽灵引用/幻影引用) | 无法通过虚引用获得对象, 使用PhantomReference 实现虚引用,在gc时返回一个通知 |
4.2 判断对象是否存活
引用计数法: 在对象头维护一个counter计数器, 当对象被引用时, counter+1; 引用失败, counter-1. 当counter=0时, 就认为该对象无效了
实现简单, 效率高, 但java虚拟机没有采用主要是因为很难解决循环依赖问题: 对象A, B都拥有彼此引用, 所以counter就不等于0, 无法通知GC回收可达性分析: 与GC Roots有直接或者间接关联的就是有效对象; 无关联的就是无效对象
GC Roots:- JAVA虚拟机栈(栈帧中本地变量表)中引用的对象
- 本地方法栈(native方法)中引用的对象
- 方法去常量引用的对象
- 方法去类静态属性引用的对象
- 所有被同步锁持有的对象
并不包括堆中对象引用的对象, 这样就不会有循环依赖问题
4.3 废弃常量与无用类
废弃常量: 运行时常量池主要回收的废弃常量, 如果字符串常量池中没有任何String对象引用该字符串常量的话, 这个常量就是废弃常量. 当发生内存回收且有必要的话, 该常量会被清除出常量池.
无用类: 判断无用类需要3个条件, 当满足该条件后, 虚拟机可以堆该无用类回收(可以, 不代表必须执行):
- 该类的所有实例都被回收, 堆中没有该类的实例
- 加载该类的Classloader被回收
- 该类对应的java.lang.Class没有被任何地方引用, 无法在任何地方通过反射访问该类的方法
4.4 垃圾收集算法
4.4.1 标记-清除算法
过程:
标记过程: 遍历所有GC Roots, 将所有GC Roots可达对象标记为存活对象
清除过程: 遍历堆中所有对象,将没有标记的对象全部清除, 同时清除所有存活对象标记, 以便下次垃圾回收缺点
- 效率问题: 标记,清除过程效率都不高
- 空间问题: 标记清除后会产生大量不连续的内存碎片, 碎片太多可能导致以后分配较大对象时, 无法找到足够连续内存而不得不提前触发另外一次垃圾收集动作
4.4.2 复制算法 (针对新生代)
将内存划分为大小相等的两块,每次只使用其中一块. 当一块内存用完, 需要进行垃圾收集时, 将存活者对象复制到另外一块上, 第一块全部清除
优缺点:
优点: 顺序分配内存, 实现简单,运行高效, 不用考虑内存碎片
缺点: 内存缩小为原来一半, 浪费空间, 对象存活率高时会频繁复制
4.4.3 标记-整理算法 (针对老年代)
- 过程:
标记过程: 遍历所有GC Roots, 将所有GC Roots可达对象标记为存活对象, 与标记-清除算法一模一样
整理过程: 移动所有存活对象, 按照内存地址依次排序,然后将末端地址以后内存全部回收
4.4.4 分代算法
按照存活周期的不同, 将内存划分为几块, 根据各个年代采用最适当的垃圾收集算法
- 新生代: 复制算法
- 老年代: 标记-清除, 标记-整理
4.5 JVM新生代垃圾收集
一般JVM采用复制算法作为新生代垃圾算法. 具体过程为:
- 为了解决空间利用率问题. 将内存分为了3块: Eden, From Survivor, To Survivor. 比例为 8:1:1
- 每次使用Eden和其中一块Survivor(From Surivor).
- 大部分情况下, 对象首先在Eden区域分配; 一次Minior GC后, 如果对象存活, 对象会进入From Survivor. 并且对象年龄加1(Eden -> Survivor后对象初始话年龄为1)
- 当年龄大到一定程度后(默认15岁), 晋升到老年代
- JVM通过
-XX:MaxTenuringThreshold
来设置默认晋升年龄阈值 - 实际运行时, HotSpot会遍历所有对象, 按照年龄从小到大对其所占大小累积, 当累积某个年龄大小超过Survivor区一半时, 取这个年龄与MaxTenuringThreshold值较小的作为晋升年龄
- 经过这次GC后, Eden, From区被清空, From, To交换角色;保证名为To Survivor区域都是空的.
- MinorGC一直执行这个过程
- 如果MinorGC后, From区域不够用,有一些达不到老年代条件的实例放不下, 只好通过分配担保机制, 将放不下部分会提前进入老年代
为何大对象直接进入老年代?
大对象需要连续的内存空间(如字符串,数组), 为了避免为大对象分配内存时由于分配担保机制代理的复制而降低效率
空间分配担保: 为了确保在MiniorGC之前, 老年代本身还有容纳新生代所有对象的剩余空间. 规则: 老年代连续空间大于新生代对象总大小或者历次晋升平均大小, 就会进行Minor GC, 否则进行Full GC
4.6 GC进行的区域
针对HotSpot, GC分为2个大类
- Partial GC: 并不收集整个GC堆模式
- Young GC: 只收集新生代的GC, 新生代Eden区分配满时触发. 部分存活对象会晋升到老年代, Young GC后老年代占用量通常会升高
- Old GC: 只收集老年代的GC, 只有CMS的concurrent collection是这个模式
- Mixed GC: 收集整个新生代和部分老年代的GC, 只有G1有这个模式
- Full GC: 等价于Major GC, 收集整个堆. 如果Young GC晋升大小比老年代大, 除了CMS的concurrent collection外, 其他能收集老年代的GC会同时收集整个堆, 包括YoungGC, 所以不需要事先单独触发.还有如System.gc(),heapdump带GC, 默认也会触发Full GC.
4.7 Hotspot垃圾收集器
4.7.1 Serial垃圾收集器(单线程, 新生代)
- 新生代单线程收集器, 标记,清理都是单线程.
- 只会使用一条垃圾收集线程去完成垃圾收集工作, 更重要的是它在进行垃圾收集工作时必须暂停其他所有的工作线程(Stop the world), 直到它收集结束
- 适合客户端使用: 客户端所需内存小, 创建内存不会太多, 堆内存不大, 因此垃圾收集回收时间短, 即使stop the world, 也不会明显卡顿
- 新生代采用标记-复制算法,老年代采用标记-整理算法.
4.7.2 ParNew收集器(多线程, 新生代)
- Serial收集多线程版本, 多线程进行垃圾收集, 其余行为(控制参数, 收集算法, 回收策略等待)与Serial完全一致, 多核环境下比Serial有更好的表现
- 多线程进行垃圾收集, 清理过程依然需要Stop the world
- 追求降低用户停顿时间, 多CPU下性能比Serial有一定程度提升; 但是线程切换需要额外开销, 单CPU不如Serial.
- 新生代采用标记-复制算法,老年代采用标记-整理算法.
4.7.3 Parallel Scavenge收集器(多线程, 新生代)
- 多线程, 新生代垃圾收集器, 追求CPU吞吐量, 高效利用CPU, 能够较短时间内完成指定任务, 适合没有交互的后台计算
- 吞吐量 = 用户线程时间/(用户线程时间+GC时间)
- 通过设置参数, 使JVM自适应调解测率, 把内存调优交由JVM完成, 只需要设置-Xmx(最大堆大小), -XX:MaxGCPauseMillis(最大停顿时间), -XX:GCTimeRatio(吞吐量大小)设置优化目标后, 具体细节参数调节工作由虚拟机完成
-XX:GCTimeRadio: 设置垃圾回收占总CPU时间百分比
-XX:MaxGCPauseMillis: 垃圾处理过程最久停顿时间
-XX:+UseAdaptiveSizePolicy: 开启自适应策略, 只需设置好MaxGCPauseMillis或GCTimeRadio, 收集器会自动调整新生代大小, Eden, Survivor比率, 对象进入老年代年龄, 以最大程度接近设置
- 参数设置新老代
-XX:+UseParallelGC :使用Parallel Scavenge(年轻代)+ Serial Old(老年代)串行
-XX:+UseParallelOldGC: 使用Parallel Scavenge(年轻代)+ Parallel Old(老年代)并行
4.7.4 Serial Old收集器(单线程, 老年代)
- Serial收集器老年代版本, 单线程收集器
- JDK1.5版本与Parallel Scavenge收集器配合使用
- 作为CMS收集器后备方案
4.7.5 Parallel Old收集器(多线程, 老年代)
- Parallel Scavenge收集器老年代版本, 多线程和标记-整理算法
- 在注重吞吐量与CPU资源场合, 优先考虑Parallel Scavenge和Parallel Old收集器
4.7.6 CMS收集器(多线程, 老年代)
CMS(Concurrent Mark Sweep, 并发标记清除)收集器使已最短回收停顿时间为目标的收集器(追求低停顿时间), 使用户线程和GC线程并发执行, 因此用户不会感觉到明显停顿, 加上-XX:+UseConcMarkSweepGC
来指定CMS垃圾收集. 整个过程分4个步骤:
- 初始阶段: 暂停所有其他线程, 并记录所有直接与GC Root相连的对象, 速度很快
- 并发标记: 同时开启GC和用户线程, 用一个闭包结构去记录可达对象. 但是闭包结构不能保证包含当前所有可达的对象(用户线程可能正在不断更新引用). 所以GC线程无法保证可达性分析实时性. 会跟踪记录这些发生引用更新的地方
- 重写标记: 为了修正并发标记阶段因为用户程序运行而导致的标记变动的那一部分对象的标记记录, 这一阶段停顿时间比初始阶段稍长, 远远比并发标记阶段短
- 并发清除: 开启用户线程, 同时GC线程对未标记区域清理
主要优点: 并发收集, 低停顿
缺点:
- 对CPU资源敏感
- 无法处理浮动垃圾
- 使用"标记-清除"算法导致收集接收时会产生大量空间碎片
- 当剩余内存不能满足程序运行要求是, 会出现 Concurrent Mode Failure, 临时CMS会采用Serial Old回收器进行垃圾收集, 此时性能会降低
对于内存碎片空间问题
-XX:+UseCMSCompactAtFullCollection:每次Full GC完成后,都会进行一次内存压缩整理, 将零撒各处对象整理到一块
-XX:CMSFullGCsBeforeCompaction=N: 设置N次GC后, 再进行一次内存整理
4.7.6 G1收集器(多线程)
G1(Garbage-First)是面向服务器的垃圾收集器,主要针对配备多个处理器以及大内存的机器, 以极高概率满足GC停顿时间要求同时, 还具备高吞吐性能特征,
它没有新生代, 老年代的概念, 而是将堆划分为一块块独立的Region. 当需要垃圾回收时, G1收集器后台维护了一个优先列表, 首先预估每个Region垃圾数量, 根据允许的收集时间, 每次从垃圾回收价值最大的Region开始回收, 因此可以获得最大的回收效率
一个对象与其内部所引用对象可能不在一个Region, 垃圾回收时:
每个Region都有一个Remembered Set, 用来记录本区域内所有对象引用的对象所在区域, 进行可达性分析时, 只需再GC Roots中再加上Remembered Set即可防止对整个堆的遍历.
G1特色:
- 并发与并行: 充分利用CPU, 多核环境下的硬件优势,使用多CPU来缩短Stop the world停顿时间. 部分收集器需要停顿java线程执行GC动作,G1则仍可通过并发的方式让java程序继续执行
- 分代收集: 不需要其他收集器就能独立管理整个GC堆, 还保留了分代的概念
- 空间整合: 整体来看基于"标记-整理"算法实现收集器, 局部来看是居于"标记,复制"算法实现
- 可预测停顿: 能建立可预测的停顿时间模型, 能让使用者明确指定长度为M毫秒的时间段内
运作步骤:
- 初始阶段: Stop the world, 仅使用一条初始标记线程对所有与GC Roots直接关联的对象进行标记
- 并发阶段: 一条标记线程与用户线程并发执行, 进行可达性分析, 速度很慢
- 再标记阶段: Stop the world, 使用多条标记线程并发执行, 重写标记在并发阶段变化的对象
- 清理阶段: Stop the world,清点出有存活对象的分区和没有存活对象的分区. 不会清理垃圾对象,不会执行存活对象复制
- 复制阶段: Stop the world, 分配新的内存和复制对象的成员变量, 内存分配耗时短, 但是成员变量复制有可能比较长, 与存活对象数量,对象复杂度成正比, 对象越复杂, 耗时越长
- 筛选回收: Stop the world, 回收废弃对象,使用多条筛选回收线程并发执行
4.7.7 ZGC收集器(多线程)
ZGC(the Z Garbage Collector): 基于标记-复制算法, 停顿不超过10ms, 停顿不随堆大小或者活跃对象大小而增加, 支持8MB到4TB的堆(未来支持16TB)
执行:
- 初始阶段: Stop the world, 多条初始标记线程对所有与GC Roots直接关联的对象进行标记, 耗时很短
- 并发标记: 对象重定位, 多条标记线程与用户线程并发执行, 进行可达性分析, 速度很慢
- 再标记: Stop the world, 时间很短, 最多1ms, 超过则再次进入并发标记阶段
- 并发转移准备:
- 初始转移: Stop the world,
- 并发转移:
关键技术:
解决转移过程中准确访问对象问题, 实现了并发转移.
并发GC线程在转移过程中, 用户线程在不停访问对象, 应用线程在访问对象时会触发"读屏障", 如果发现对象被移动了, 那么读屏障会把读出的指针更新到对象的新地址上, 这样应用线程始终访问对象的新地址
着色指针:
ZGC只支持64位系统, 将64位虚拟地址空间分为多个子空间:
[0-4TB)对应java堆, [4TB-8TB)对应M0地址空间,[8TB-12TB)对应M1地址空间,[12TB-16TB)预留未使用, [16TB-20TB) Remapped空间
应用程序创建对象时, 首先在堆申请一个虚拟地址(单不会映射到真正的物理地址), 同时会为该对象在M0, M1, Remapped地址空间分别申请一个虚拟地址, 且三个虚拟地址对应同一物理地址. 但这3个空间同一时间只有一个有效
与此同时, ZGC使用64位地址空间 0-41位存对象地址, 42存M0标记, 43存M1标记, 44存Remapped标记, 45存finalizable回收标记, 47-63位存0(未使用)
读屏障:
读屏障是JVM向应用程序插入一段代码的技术, 当应用程序从堆中读取对象引用时, 就会执行这段代码(注意: 仅从堆中读取对象引用), 作用: 在对象标记和转移时, 用于确定对象的引用地址是否满足条件,并作出相应动作
Object o = obj.FieldA // 从堆中读取引用,需要加入屏障
<Load barrier>
Object p = o // 无需加入屏障,因为不是从堆中读取引用
o.dosomething() // 无需加入屏障,因为不是从堆中读取引用
int i = obj.FieldB //无需加入屏障,因为不是对象引用
执行过程:
- 初始化: ZGC初始化后, 真个内存空间地址被设置为Remapped, 运行一段时间进入垃圾收集
- 并发标记阶段: 第一次进入标记阶段视图为M0, 如果被GC线程标记或被应用线程访问, 则将对象地址从Remapped调整为M0, 标记结束后, M0代表活跃对象, Remmaped代表不活跃对象, 第二次进入并发标记阶段, 地址视图会调整为M1.
- 并发转移阶段: 地址视图再次被设置为Remapped, 如果被GC线程或者应用线程访问过, 将地址视图从M0调整为Remapped
整个过程只需要设置地址指针的42-45位,速度更快
参数:
-Xms10G // 最小堆内存
-Xmx10G // 最大堆内存
-XX:ReservedCodeCacheSize=256m // JIT编译代码放到CodeCache中, 最大代码缓存
-XX:InitialCodeCacheSize=256m // 初始化CodeCache大小
-XX:+UnlockExperimentalVMOptions // 开启实验新增vm参数
-XX:+UseZGC // 开启ZGC
-XX:ConcGCThreads=2 // 并发回收垃圾线程,默认占总核数12.5%, 调大后GC变快, 但是占用程序运行时CPU资源, 吞吐会收到影响
-XX:ParallelGCThreads=6 // STW阶段使用线程数, 默认位总核数60%,
-XX:ZCollectionInterval=120 // ZGC发生最小时间间隔, 默认秒
-XX:ZAllocationSpikeTolerance=5 // ZGC触发自适应算法修正系数, 默认2, 数值越大, 越早触发ZGC
-XX:+UnlockDiagnosticVMOptions // 是否启用主动回收
-XX:-ZProactive // 关闭ZGC主动回收
-Xlog:safepoint,classhisto*=trace,age*,gc*=info:file=/opt/logs/logs/gc-%t.log:time,tid,tags:filecount=5,filesize=50m // GC日志及格式,日志大小等
ZGC的多次触发机制:
- 阻塞内存分配请求触发:当垃圾来不及回收,垃圾将堆占满时,会导致部分线程阻塞.我们应当避免出现这种触发方式.日志中关键字是“Allocation Stall”.
- 基于分配速率的自适应算法:最主要的GC触发方式,其算法原理可简单描述为”ZGC根据近期的对象分配速率以及GC时间,计算出当内存占用达到什么阈值时触发下一次GC”.通过ZAllocationSpikeTolerance参数控制阈值大小,该参数默认2,数值越大,越早的触发GC.日志中关键字是“Allocation Rate”.
- 基于固定时间间隔:通过ZCollectionInterval控制,适合应对突增流量场景.流量平稳变化时,自适应算法可能在堆使用率达到95%以上才触发GC.流量突增时,自适应算法触发的时机可能会过晚,导致部分线程阻塞.我们通过调整此参数解决流量突增场景的问题,比如定时活动、秒杀等场景.日志中关键字是“Timer”.
- 主动触发规则:类似于固定间隔规则,但时间间隔不固定,是ZGC自行算出来的时机,我们的服务因为已经加了基于固定时间间隔的触发机制,所以通过-ZProactive参数将该功能关闭,以免GC频繁,影响服务可用性. 日志中关键字是“Proactive”.
- 预热规则:服务刚启动时出现,一般不需要关注.日志中关键字是“Warmup”.
- 外部触发:代码中显式调用System.gc()触发. 日志中关键字是“System.gc()”.
- 元数据分配触发:元数据区不足时导致,一般不需要关注. 日志中关键字是“Metadata GC Threshold”.
3. JDK监控和故障处理工具
3.1 jps: 查看所有 Java 进程
jps(JVM Process Status): 类似ps
命令, 用于查看所有java进程的启动类, 传入参数和java虚拟机参数等信息
jps -q 进程的本地虚拟机唯一ID
jps -l 列车主类全民, 如果进程时jar包, 输出jar路径
jps -v 输出虚拟机进程启动时JVM参数
jps -m 输出传递给java进程的main()函数参数
3.2 jstat: 监视虚拟机各种运行状态信息
jstat(JVM Statistics Monitoring Tool)监视虚拟机各种运行状态信息的命令行工具, 可以显示本地或者远程(需要远程主机提供RMI支持)虚拟机的类信息, 内存, 垃圾收集, JIT信息, 没有GUI, 只提供纯文本控制台环境服务器上, 运行期间定位虚拟机性能问题的首选工具
jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
jstat -class vmid: 显示 ClassLoader 的相关信息;
jstat -compiler vmid:显示 JIT 编译的相关信息;
jstat -gc vmid:显示与 GC 相关的堆信息;
jstat -gccapacity vmid:显示各个代的容量及使用情况;
jstat -gcnew vmid:显示新生代信息;
jstat -gcnewcapcacity vmid:显示新生代大小与使用情况;
jstat -gcold vmid:显示老年代和永久代的行为统计,从jdk1.8开始,该选项仅表示老年代,因为永久代被移除了;
jstat -gcoldcapacity vmid:显示老年代的大小;
jstat -gcpermcapacity vmid:显示永久代大小,从jdk1.8开始,该选项不存在了,因为永久代被移除了;
jstat -gcutil vmid:显示垃圾收集信息;
-t参数可以在输出信息上加一个 Timestamp 列,显示程序的运行时间.
信息字段说明:
- S0C:年轻代中 To Survivor 的容量单位 KB;
- S1C:年轻代中 From Survivor 的容量单位 KB;
- S0U:年轻代中 To Survivor 目前已使用空间单位 KB;
- S1U:年轻代中 From Survivor 目前已使用空间单位 KB;
- EC:年轻代中 Eden 的容量单位 KB;
- EU:年轻代中 Eden 目前已使用空间单位 KB;
- OC:老年代的容量单位 KB;
- OU:老年代目前已使用空间单位 KB;
- MC:元空间的容量单位 KB;
- MU:元空间目前已使用空间单位 KB;
- YGC:从应用程序启动到采样时年轻代中 gc 次数;
- YGCT:从应用程序启动到采样时年轻代中 gc 所用时间 (s);
- FGC:从应用程序启动到采样时 老年代Full Gcgc 次数;
- FGCT:从应用程序启动到采样时 老年代代Full Gcgc 所用时间 (s);
- GCT:从应用程序启动到采样时 gc 用的总时间 (s)。
3.3 jinfo: 实时地查看和调整虚拟机各项参数
jinfo vmid: 输出jvm进程全部参数和系统属性(第一部分系统属性, 第二部分JVM参数)
jinfo -flag name vmid: 输出对应名称的参数具体值
jinfo -flag MaxHeapSize 17340
// -XX:MaxHeapSize=2124414976
jinfo -flag PrintGC 17340
// -XX:-PrintGC
不停机下修改JVM参数
jinfo -flag PrintGC 17340
// -XX:-PrintGC
jinfo -flag +PrintGC 17340
jinfo -flag PrintGC 17340
// -XX:+PrintGC
3.4 jmap: 生成堆存储快照
jmap(Memory map for java)生成堆转储快照. 还可以查询finalizer执行队列, java堆, 永久代详细信息, 如空间使用率,当前使用哪种垃圾收集器等
添加-XX:+HeapDumpOnOutOfMemoryError
参数, 在OOM时自动生成dump文件
linux也可以用 kill -3
发送进程退出信号也能拿到dump文件
jmap -dump:format=b,file=~/heap.hprof 17340 //输出指定应用程序堆快照
3.5 jhat: 分析heapdump文件
jhat用于分析heapdump文件, 并建立一个http/html服务器, 在浏览器查看分析结果
jhat ~/heap.hprof
....
Started HTTP server on port 7000
Server is ready.
3.6 jstack: 生成虚拟机此刻线程快照
jstack(stack trace for java)用于生成虚拟机当前线程快照, 线程快照是当前虚拟机每一条线程正在执行的方法堆栈的集合
生成线程快照注意是定位线程长时间停顿原因, 如线程死锁, 死循环, 外部资源导致的长时间等待等导致的线程长时间停顿.
3.7 JConsole: java监视管理控制台
JConsole: 基于JMX的可视化监控, 管理工具. 方便监视本地以及远程服务器的java进程的内存使用情况
开启JConsole远程连接, 需要在java程序启动时加上
-Djava.rmi.server.hostname=外网访问 ip 地址
-Dcom.sun.management.jmxremote.port=60001 //监控的端口号
-Dcom.sun.management.jmxremote.authenticate=false //关闭认证
-Dcom.sun.management.jmxremote.ssl=false
3.8 Visual VM:多合一故障处理工具
VisualVM基于NetBeans开发, 可以做到:
- 显示虚拟机进程以及进程的配置,环境信息(jps、jinfo).
- 监视应用程序的 CPU,GC,堆,方法区以及线程的信息(jstat、jstack).
- dump 以及分析堆转储快照(jmap,jhat).
- 方法级的程序运行性能分析,找到被调用最多、运行时间最长的方法.
- 离线程序快照:收集程序的运行时配置,线程 dump,内存 dump 等信息建立一个快照,可以将快照发送开发者处进行 Bug 反馈.
- 其他 plugins 的无限的可能性......
3.9 其他工具
MAT(Memory Analyzer Tool)工具是 eclipse 的一个插件(MAT 也可以单独使用),它分析大内存的 dump 文件时,可以非常直观的看到各个对象在堆空间中所占用的内存大小,类实例数量,对象引用关系,利用 OQL 对象查询,以及可以很方便的找出对象 GC Roots 的相关信息.
idea插件 jProfile
3. 常用JVM参数
参数名 | 含义 | 默认值 | 说明 |
---|---|---|---|
-Xms | 初始堆大小 | 物理内存的1/64(<1GB) | 默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制. |
-Xmx | 最大堆大小 | 物理内存的1/4(<1GB) | 默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制 |
-Xmn | 年轻代大小(1.4or lator) | - | 注意:此处的大小是(eden+ 2 survivor space).与jmap -heap中显示的New gen是不同的。整个堆大小=年轻代大小 + 老年代大小 + 持久代(永久代)大小.增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8 |
-XX:NewSize | 设置年轻代大小(for 1.3/1.4) | - | - |
-XX:MaxNewSize | 年轻代最大值(for 1.3/1.4) | - | - |
-Xss | 每个线程的堆栈大小 | - | JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.根据应用的线程所需内存大小进行调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右一般小的应用, 如果栈不是很深,应该是128k够用的 大的应用建议使用256k.这个选项对性能影响比较大,需要严格的测试. 和threadstacksize选项解释很类似, 官方文档似乎没有解释,在论坛中有这样一句话:-Xss is translated in a VM flag named ThreadStackSize”一般设置这个值就可以了 |
-XX:NewRatio | 年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代) | - | -XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。 |
-XX:SurvivorRatio | Eden区与Survivor区的大小比值 | - | 设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10 |
-XX:PretenureSizeThreshold | 对象超过多大是直接在旧生代分配 | - | 单位字节 新生代采用Parallel ScavengeGC时无效另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象. |
-XX:ParallelGCThreads | 并行收集器的线程数 | - | 此值最好配置与处理器数目相等 同样适用于CMS |
-XX:MaxGCPauseMillis | 每次年轻代垃圾回收的最长时间(最大暂停时间) | - | 如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值. |
-XX:MetaspaceSize | 设置 Metaspace 的初始(和最小大小) | - | 元空间大小 |
-XX:MaxMetaspaceSize | 设置 Metaspace 的最大大小 | - | 如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存 |
-XX:+UseSerialGC | 串行垃圾收集器 | - | - |
-XX:+UseParallelGC | 串Parallel Scavenge(年轻代)+ Serial Old(老年代)串行行垃圾收集器 | - | - |
-XX:+USeParNewGC | ParNew收集器 | - | - |
-XX:+UseConcMarkSweepGC | CMS垃圾收集器 | - | - |
-XX:+UseG1GC | G1 垃圾收集器 | - | - |
-XX:+UseGCLogFileRotation | gc日志自动滚动生成 | - | - |
-XX:NumberOfGCLogFiles | 保留gc日志数 | - | - |
-XX:GCLogFileSize | 保留gc日志大小 | - | - |
Xloggc:/path/to/gc.log | gc日志保存路径 | - | - |