深入理解java虚拟机-第二部分 自动内存管理
第二章 Java内存区域与内存溢出异常
2.1 概述
手动内存管理:即拥有每一个对象的所有权,又担负每个对象从开始到终结的维护责任。
自动内存管理:把内存控制权交给java虚拟机,排查错误,修正问题艰难
2.2 运行时数据区域
java虚拟机在执行java程序时会把它所管理的内存划分为若干区域

2.1.1 程序计数器 Program Counter Register
当前线程所执行字节码的行号指示器,虚拟机的概念模型中字节码解释器通过改变计数器的值来选取下一条执行的字节码指令
线程私有,唯一没有规定Out Of Memory Error 的区域
2.1.2 Java虚拟机栈 Java Virtual Machine Stack
线程私有,生命周期与线程相同
java方法执行的线程内存模型:方法执行时VM同步创建一个栈帧,用于存储 局部变量表、操作数栈、动态链接、方法出口等
局部变量表:存放编译期可知的各种javaVM基础数据类型(boolean,byte,short,char,int,long,float,double),对象引用(reference,对象起始地址引用指针,对象句柄或与此对象相关的位置)和returnAddress(指向一条字节码指令地址)。
这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示。64位长度的long和double数据类型会占据两个slot,其余占用一个slot。
局部变量表内存编译期完成分配。
区域规定异常:
- StackOverFlowError:线程请求的栈深度大于虚拟机允许的深度。
- OutOfMemoryError:如果虚拟机栈帧可以扩展,当扩展时无法申请到足够的内存。
2.1.3 本地方法栈 Native Method Stacks
与VM stacks类似,只是为本地方法服务。
虚拟机规范未做强制规定,HotSpot直接本地方法栈和虚拟机栈合二为一
区域规定异常:栈深度溢出或栈扩展失败时抛出StackOverFlowError和OutOfMemoryError
2.1.4 Java堆 java Heap
虚拟机管理内存中最大的一块,线程共享
规范描述:所有对象实例以及数组都应当在堆上分配,存在栈上分配,标量替换等优化手段。
Java堆是垃圾收集器管理的内存区域,从回收内存的角度,由于垃圾收集器大部分是基于分代收集理论设计的,所以经常出现:新生代、老年代、永久代、Eden、From Survivor,To Survivor等名词。这里的区域划分是一部分垃圾收集器的共同特征或风格而已,不是虚拟机实现的固有布局更不是《java虚拟机规范》对java 堆的细致划分。
从内存分配的角度:所有线程共享的Java堆可以划分出多分线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB) 提升对象的分配速率。
java堆存储内存容的共性:只能是对象的实例,细分的目的只是为了更好的回收或更快的分配
区域规定异常:OutOfMemoryError:Java堆中没有内存分配实例且无法再扩展
2.1.5 方法区 Method Area
线程共享区域,用于存储已经被虚拟机加载的:类型信息、常量、静态变量、即时编译后的代码缓存数据等。
虽然规范把方法区描述为java堆的一个逻辑部分,但方法区有一个别名:非堆(Non-Heap)
HotSpot将收集器的分代设计扩展至方法区,或者说 是用永久代实现方法区。原则上方法区的实现属于虚拟机细节,不受规范约束。
区域规定异常:OutOfMemoryError:方法区无法满足新的内存分配需求时
2.2.6 运行时常量池 Runtime Constant Pool
是方法区的一部分,class文件中的常量池表(Constant Pool Table),存放编译期生成的字面量与符号引用,在类加载后将放置运行时常量池。
具备动态性,运行期间也可以将新的常量放置常量池。
区域规定异常:OutOfMemoryError :当常量池无法再申请到内存时。
2.2.7 直接内存 Direct Memory
并不是虚拟机运行时数据区的一部分,也不是规范中定义的区域,但是频繁被使用。
NIO的Channel与Buffer可直接分配堆外内存
大于物理内存限制:OutOfMemoryError
2.3 HotSpot 虚拟机内存对象
探讨HotSpot虚拟机再java堆中对象分配、布局、访问的过程
2.3.1 对象的创建
对象创建过程:
- new 关键字,检查类是否已经被加载、解析、初始化。
- 虚拟机为对象分配内存:如果java 堆中内存绝对规整使用指针碰撞;如果并不规整,使用空闲列表。java堆是否规整收集器是否带有压缩整理(Compact)。
- 对象分配过程中的并发:A.堆内存分配动作进行同步,实际是CAS+失败重试保证更新原子性。B.分配动作按照线程划分在不同空间 线程分配缓冲(Thread Local Allocation Buffer TLAB)
- 内存空间初始化TLAB也可提前至TLAB分配时顺便进行。
- 对对象进行必要设置。对象类型实例、元数据信息,哈希码、分代年龄等。
- 虚拟机角度对象已经创建完成,java程序角度,构造函数
()方法还未执行。
2.3.2 对象的内存布局
HotSpot对象在内存中的布局可以划分为三个部分:对象头header,实例数据 Instance Data 和对齐填充 Padding
header:包含2部分信息:
A: 运行时数据:哈希码、GC分代年龄、锁状态标识、线程持有的锁、偏向线程Id、偏向时戳等。数据长度在32 和 64 位系统分别是 32 和 64 bit ,官方称为MarkWorld
MarketWorld 32bit : 25bit hashCode + 4bit 分代年龄 + 2bit 锁标志位 + 1bit 0
MarketWorld不同状态存储信息
| 存储内容 | 标志位 | 状态 |
|---|---|---|
| hashCode,分代年龄 | 01 | 未锁定 |
| 指向锁记录的指针 | 00 | 轻量级锁定 |
| 指向重量级锁的指针 | 10 | 膨胀(重量级锁定) |
| 空,不需要记录信息 | 11 | GC标记 |
| 偏向线程ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
B:类型指针指向类型元数据,并不是所有虚拟机实现都必须在对象数据上保留类型指针,如果对象是数组,必须有记录数组长度的数据
实例数据
代码定义的各种类型字段,包括继承和自己定义
OOPs Ordinary Object Pointers
对齐数据
无意义,仅占位符作用 。HotSpot 要求对象的起始地址必须是 8 byte的倍数
2.3.2 对象的访问定位
java程序通过栈上的reference数据来操作堆上的具体对象。
规范只规定了reference是指向对象的引用。具体实现由虚拟机决定。
主流有两种指针实现方式:
A. 句柄:java堆中划分出来一块作为句柄池,reference中存储对象的句柄地址,句柄中包含了对象的实例数据和数据类型的具体地址信息
优点:reference中存储稳定的句柄地址,在对象移动时,只改变句柄中实例数据指针
B. 直接指针:需要考虑如何访问对象类型数据信息
优点:速度快,节省了一次指针定位的开销,java中对象访问频繁,成本开销积累可观
第三章 垃圾收集器与内存分配策略
3.1 概述
垃圾收集器需要完成三件事:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
java内存运行时区域中:
- 程序计数器、虚拟机栈、本地方法栈 生命周期与线程相同,不需考虑回收问题
- Java 堆和方法区只有运行时才知道内存使用,分配与回收都是指的这两个区域
3.2 对象已死?
垃圾回收器回收前的第一件事:确定哪些对象还活着,哪些对象已经死去。
3.2.1 引用计数法
在对象中添加一个引用计数器,有地方引用时计数加一,引用失效计数减一,计数为零不再被使用。
特点:原理简单、判定高效,必须配合大量额外处理才能保证正确工作,纯粹的引用计数很难解决:循环引用计数问题
3.2.2 可达性分析
主流上用语言选择 可达性分析判断对象是否存活。
算法基本思路:通过 GC Root 根对象作为起始节点集开始,根据引用关系向下搜索,搜索过程的路径称为引用链 Reference Chain,如果一个对象到GC Roots间没有任何引用链相连,即 GC Roots 到这个对象不可达时,则这个对象不再被使用。
Java GC Roots 包含:
- Java虚拟机栈(栈帧中本地变量表)中的引用对象。如 参数、局部变量、临时变量
- 方法区中静态属性引用的对象,如java类的引用类型静态变量
- 方法区常量引用的对象,如 常量池中的引用
- java 本地方法栈中 JNI native 方法 引用的对象
- Java虚拟机内部引用,基本数据类型对应的class,常驻异常对象NPE,OOME,系统类加载器
- 被同步锁( synchronized 关键字)持有的对象
- 反应java虚拟机内部情况的JMXBean、JVNTI中注册的回调,本地代码缓存等。
3.2.3 再谈引用 引用类型
- Strongly Reference 强引用:引用赋值,垃圾回收器永远不会回收被引用对象
- Soft Reference 软引用:描述有用但非必需对象在发生OOME之前,将此类对象列入回收范围之中进行第二次回收。
- Weak Reference 弱引用:非必须对象,比软引用更弱一些,此类对象只能存活到下一次垃圾收集之前。
- phantom Reference 虚引用:最弱的引用关系,不影响别引用对象的生存,无法通过虚引用获取对象实例,目的是为了在对象被回收时收到一个系统通知
3.2.4 生存还是死亡 对象回收流程
- 如果对象在进行可达性分析时没有与GC Roots 相连的引用链时,会被第一次标记,随后进行一次筛选,是否要执行 finalize()方法,如果没有覆盖或已经被虚拟机调用则无需执行。
- 如果需要执行finalize()方法,则将对象放置于F-Queue的队列,稍后由虚拟机创建的一条低优先级的线程执行他们的finalize()方法,虚拟机触发开始,不保证等待结束
- 之后垃圾收集器将对F-Queue进行第二次小范围标记。若对象重新被引用,则不被回收,否则将被回收。
3.2.5 回收方法区
方法区回收的内容:废弃的常量和不再使用的类型
- 废弃的常量:没有引用指向常量,没有该值的字面量。
- 不再使用的类型
- 该类的实例度已经被回收
- 该类的加载器已经被回收
- 该类的class对象没有地方引用,没有任何地方通过反射访问该类的方法
3.3 垃圾收集算法
从判定对象死亡角度:
- 引用计数式垃圾收集器,也被称为 直接垃圾收集器 主流java虚拟机未涉及
- 追踪式垃圾收集器, 也被称为 间接垃圾收集器 主要实现类型
3.3.1 分代收集理论
大多数实现遵循 分代收集理论,建立在两个假说之上:
- 弱分代假说:绝大多数对象都是朝生夕灭的
- 强分代假说:熬过越多次垃圾收集过程的对象越难以消亡。
- 跨代引用假说:跨代引用相对于同代引用占比极少:
分代收集理论具体到java 虚拟机里:一般会吧java 堆 划分为:
- 新生代 Young Generation :朝生夕死,存活对象逐步晋升至老年代
- 老年代 Old Generation
跨代引用问题:第三条经验法则 即 跨代应用假说,避免全局扫描,建立全局数据结构(被称为 记忆集 Remembered Set)。将老年代划分为若干小块,标识出哪些存在跨代引用,发生Minor G C时将包含跨代引用的小块加入GC Roots 集合。需要在引用改变时增加数据维护,垃圾收集时减少扫描。
名词定义:
- 部分收集 Partial GC:指目标不是整个java 堆的垃圾收集。
- 新生代收集 Minor GC/Young GC :目标只是新生代的垃圾收集
- 老年代收集 Major GC/Old GC :目标只是老年代的垃圾收集。目前只有CMS会有单独的老年代收集。注意:Major GC 说法有些混淆,需要根据上下文推定指:老年代收集 or 整堆收集
- 混合收集 Mixed GC :目标是整个新生代和部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
- 整堆收集 Full GC:收集整个java 堆和方法区的垃圾收集
3.3.2 标记-清除算法
标记-清除算法:Mark-Sweep
- 标记:标记需要回收的对象
- 清除:清除被标记的对象
缺点: - 执行效率不稳定:大量对象且大多数需要回收时必须进行大量标记和清除动作,对着对象的增加效率降低
- 内存空间碎片化:产生大量不连续的内存空间,可能导致在进行大对象分配时无法找到足够连续空间导致提前出发GC动作
3.3.3 标记-复制算法
标记-复制算法:Mark-Copy。
- 半区复制算法解决 标记-清除 面对大量回收对象时的低效问题:将内存按照容量等分两块,每次使用一块,用完时将其中还存活的对象复制到另一块,之前已使用的块清空。
- 后续提出了优化半区复制的Appel式回收:把新生代分为 Eden 空间和 两块较小的 Survivor 空间,每次分配只使用 Eden 和其中一块 Survivor ,GC时将存活的对象复制到未使用的Survivor。HotSpot中 Eden:Survior 默认为 8:1.没办法保证每次GC时存活的对象数量,需要其他区域进行分配担保。
3.3.4 标记-整理算法
标记-整理算法:Market-Compact
首先标记,然后将存活对象向内存空间一端移动,直接清理掉边界以外的内存。
移动存活对象,并更新所有引用负担极重的操作,需要暂停用户程序,称为 Stop The Word
3.4 HotSpot 的算法细节实现
介绍各款垃圾收集器
3.4.1 根节点枚举
所有收集器再:根节点枚举 和 整理内存碎片 都必须暂停用户线程 Stop The Word。根节点枚举必须保证在一致性快照中进行,枚举期间跟节点集合对象引用关系不能变化。
主流虚拟机都是采用准确式内存,虚拟机应当知道什么位置存储着引用。HopSpot使用OopMap(Ordinary Object Pointer,OOP 普通对象指针)达到这一目的。
OopMap: 记录了 栈上和寄存器 什么位置存储着引用,避免整栈扫描,只需查询OopMap
类在加载完成时记录对象内偏移量以及对应的数据类型。
3.4.2 安全点
OopMap可以帮助Vm快速完成GC Roots 枚举。导致引用关系变化(OopMap内容变化)的指令非常多,为每条指令生成OopMap代价高昂。
HotSpot只在特殊位置 记录这些信息,被称为 安全点 SafePoint。
安全点 SafePoint :“A point in program where the state of execution is known by the VM”即可以被VM知道在这个点是一个执行状态。
从线程的角度:码执行过程中的一些特殊位置,当线程执行到这些位置的时候,说明虚拟机当前的状态是安全的,如果有需要,可以在这个位置暂停,比如GC
安全点的选定:基本上是以"是否具有让程序长时间执行的特征"为标准,特征是:指令列的复用比如 方法调用、循环跳转、异常跳转等。
安全点的到达:
- 抢先式中断:系统首先把所有用户线程全部中断,如果发现线程中断地点不在安全点,恢复线程执行到安全点。
- 主动式中断:不直接操作线程,设置标志位,线程执行时主动轮询标志位 ,中断标志为真时,在最近的安全点主动挂起。
- 轮询标志位的位置和安全点事重合的,另外加上创建对象和其他需要在堆上分配内存的地方,放置内存不够。
3.4.3 安全区域
用户线程sleep/blocked 时无法响应虚拟机中断请求,不能走到安全点挂起,引入安全区域。
安全区域 Safe Region:能够确保在某一段代码片段中,引用关系不会发生改变。安全区域内任意点开始GC都是安全的。
流程:
- 线程执行安全区域代码时,标识自己已经进入安区域,这段时间发起GC时就不用去关心安全区域内的线程。
- 当线程要离开安全区域时,检查中断标志(是否完成根节点枚举、或其他需要暂停用户线程的阶段),如果完成,则继续执行,否则继续等待直到收到可以信号。
3.4.4 记忆集与卡表
为了解决对象跨代引用问题,垃圾收集器在新生代中建立的数据结构,用于避免把整个老年代加入GC Roots 扫描范围。
记忆集 Remember Set:用于记录从非收集区域指向收集区域的指针集合的抽象数据结构
所有涉及部分区域收集 Partial GC行为的垃圾收集器,都会面临区域引用的问题。
收集器通过记忆集了解某一非收集区域是否有指向收集区域的指针。并不需要了解跨区域指针的细节,因此设计者在实现时有多种方式:
- 字长精度:每个记录精确到一个机器字长(处理器寻址位数,如32,64),该字包含跨区域指针。
- 对象精度:每个记录精确到一个对象,该对象里有字段包含跨区域指针
- 卡精度:每个记录精确到 一块内存区域,该区域内有对象包含跨区域指针
卡精度指的用一种称为卡表 Card Table 方式去实现记忆集,是记忆集目前最常用实现形式。定义了记忆集的记录精度,与堆内存的映射关系等
抽象的意思是:只定义了行为意图,并没有规定行为的具体实现。卡表就是记忆集的一种具体实现
HotSpot 使用字节数组形式来定义卡表,字节数组中的每个元素都标志着内存区域中的 一块特定大小的内存块,被称为 卡页 Card Page。
卡页中通常包含不止一个对象,只要有一个卡页存在着跨区域指针就讲对应的数组元素标记为1 ,称这个元素变脏 Dirty,在GC时只需要筛选出卡表中变脏的元素,找出跨区域指针把他们加入GC Roots 一起扫描
3.4.5 写屏障
卡表元素维护:有其他分区元素引用本区域元素时,其对应的卡表元素就应该变脏。
HotSpot通过写屏障 Write Barrier(注意:与并发乱序执行时的内存屏障区分开),可以看做虚拟机层面对 "引用字段赋值" 的Around AOP 切面,赋值前的写屏障叫写前屏障"Pre-WriteBarrier",赋之后的写屏障叫写后屏障"Post-Write Barrier"。
G1之前其他收集器只用到了 写后屏障,更新卡表引用。
3.4.6 可达性分析
可达性分析算法理论上要求全程在保证一致性的快照中进行,即暂停所有用户线程。
根节点枚举中GC Roots 比整个堆极小,且有各种优化技巧(OopMap,Remeber Set的),停顿短暂且固定(不随堆容量增长)。
遍历对象与堆容量成正比。
三色标记:按照是否访问过标记成三种颜色
- 白色:表示尚未被垃圾收集器访问过。
- 黑色:表示对象已经被垃圾收集器访问过,且这个对象所有引用都已经扫描过。代表已经扫描过安全存活的,有引用指向黑色,不用重新扫描黑色,不能直接指向白色
- 灰色:表示对象已经被垃圾收集器访问过,且至少存在一个引用未被扫描。
黑色对象误标记为白色,导致对象消失的 充要条件: - 赋值器插入一条或多条从黑色到白的的新引用。
- 赋值器删除全部从灰色到白的的直接或间接引用。
解决并发扫描问题只需要破坏两个条件中的一条即可,解决方案可分为: - 增量更新 Incremental Update:破坏第一个条件,当黑色插入新的指向白的引用时,将插入的引用记录下来,并发扫描结束再将这些新插入引用的黑色对象为根重新扫描,可以理解为黑色对象插入白色引用后变灰灰色。垃圾收集器:CMS
- 原始快照 Snapshot At The Beginning SATB:当灰色对象要删除指向白色对象的引用时,将这个删除的引用记录下来,并发扫描结束后,再将这些删除引用中的灰色对象为根重新扫描,可以理解为无论关系是否删除都按照扫描开始的快照图进行搜索。垃圾收集器:G1、shenandoah
3.5 经典垃圾收集器
收集算法时内存回收的方法论,垃圾收集器是内存回收的实践者。
HotSpot 垃圾收集器

3.5.1 Serial 收集器
新生代、单线程垃圾收集器,执行垃圾收集时必须暂停其他所有工作线程
特点:简单高效(与其他收集器的单线程相比),对于内存资源受限环境,收集器里的额为内存消耗最小,单核处理器或核心较少的处理器环境Serial没有线程交互开销,收集效率高。
对于运行在客户端模式下的虚拟机是一个不错的选择
Serial/Serial Old 收集器运行示意图

3.5.2 ParNew收集器
ParNew实际时Serial的多线程版本,同时使用多条线程进行垃圾收集,其余行为:控制参数、收集算法Stop The world、对象分配规则、回收策略等都与Serial收集器完全一致。
主要与CMS配合使用
ParNew/Serial Old 收集器运行示意图

3.5.3 Parallel Scavenge 收集器
新生代,多线程、标记-复制 回收算法
目标:控制吞吐量,吞吐量优先收集器,吞吐量=运行用户代码时间/(运行用户代码时间+运行垃圾收集时间)
参数
- -XX: MaxGCPauseMillis 收集器尽力保证回收花费时间不超过用户设定值。
- -XX: GCTimeRatio 0-100,垃圾收集占总时间比率 。
- -XX: +UseAdaptiveSizePolicy,参数激活后参数激活后无需人工指定新生代大小(-Xmn)、Eden和Survivor比例(-XX: SurvivorRatio)、晋升老年代对象大小等细节
3.5.4 Serial Old 收集器
是Serial的老年代版本,单线程、标记整理算法
Serial/Serial Old 收集器运行示意图

3.5.5 Parallel Old 收集器
Parallel Scavenge 收集的老年年代版本,多线程并发收集,标记-整理算法
吞吐量优先
Parallel Scavenge/Parallel Old 收集器运行示意图

3.5.6 CMS 收集器
CMS Concurrent Mark Sweep 以获取最短回收停顿时间为目标,老年代,标记-清除算法实现
收集步骤:
- 初始标记(CMS init mark)Stop The World ,标记GC Roots 直接关联到的对象,速度快
- 并发标记(CMS concurrent mark)从GC Roots 直接关联对象遍历整个对象图,耗时长,不停顿用户线程
- 重新标记(CMS remark)Stop The World,修正并发标记期间用户程序运行导致标记变动那一部分对象的标记记录(增量更新 see 3.4.6),比初始标记稍长
- 并发清除(CMS concurrent sweep)清理死亡对象,不需要移动对象,与用户线程并发进行。
CMS收集器运行示意图
![]()
CMS 优点:并发收集、低停顿
缺点: - CMS收集器对处理器资源敏感:与用户线程并发运行,处理器资源不足时,用户程序执行速度可能大幅下降。
- CMS无法处理浮动垃圾:有可能出现"Concurrent Mode Fail" 并发失败 而进行完全 Stop The World 的Full GC。在CMS 并发标记和清除阶段,用户线程继续运行会产生新的垃圾,但是无法在本次标记回收,称为浮动垃圾。同样,由于用户线程持续运行,需要预留足够的空间提供给用户线程,在并发收集时程序运作使用,可以使用参数设置CMS触发回收动作阈值。
- CMS 是基于标记-清除算法实现的收集器,收集结束时会有大量的碎片空间,在大对象分配时,没有足够的连续空间将导致提前触发Full GC。
3.5.7 G1 收集器
Garbage First 简称G1,开启了收集器面向局部收集的设计思路和基于 Region 的内存布局形式。能够建立起"停顿时间模型"的收集器。
G1 Mixed GC 模式:可以面向堆内任何部分组成回收集 Collction Set,简称 CSet,回收时的衡量标准:回收收益最大。
Region时实现目标的关键:虽然仍遵循分代收集理论设计,内存布局不是分代划分,而是把内存连续的java堆,划分为多个大小相等的独立区域 Region,每个Region都可以根据需要扮演 Eden,Survivor,老年代,收集器对不同角色的Region采取不同的策略
Humongous Region:Region中的特殊分类,专门用于存储大对象,超过Region一半大小的即可认为是大对象。收集器将大对象区域看作老年代。可通过-XX:G1HeapRegionSize 设定Region大小
Region回收:将Region作为最小的回收单位避免整堆收集,G1维护了每个Region的回收价值和时间的优先级列表,根据用户的收集停顿时间 -XX:MaxGCPauseMills,优先回收价值最大的Region
Region回收难点解决:
- 跨Region引用:解决思路(3.3.1、3.4.4)使用记忆集避免整堆作为GC Roots扫描,每个Region维护自己的记忆集,记录别的Region指向自己的指针。G1记忆集存储结构为哈希表,key是别的Region的起始值,value是一个集合存储的元素是卡表的索引号。
- 并发标记阶段收集线程与用户线程互不干扰:G1通过原始快照(SATB)算法实现。回收过程中创建对象:每个Region设计了两个TAMS(Top at Mark Start)的指针把Region中的一部分空间划分出来,并发回收时新分配的对象都在两个指针范围之上,默认这个地址上的对象都是已经被标记的,可能导致并发失败进一步Full GC。
- 可靠的停顿预测模型:G1的停顿预测模型是以衰减均值为理论基础实现的。
G1收集器的运作过程: - 初始标记 init marking:仅标记GC Roots直接关联的对象,并修改TAMS值,让用户线程能在并发运行时正确分配新对象。需要Stop The World。
- 并发标记 concurrent marking:从GC Roots 开始对堆中对象进行可达性分析,耗时较长,与用户线程并发。对象图扫描结束后需重新处理SATB记录下的并发时引用变动的对象。
- 最终标记 final marking:处理并发阶段结束遗留的少量SATB记录,需要Stop The World。
- 筛选回收 live data counting and evacuation:更新Region统计数据,对每个Region的回收价值和成本进行排序,根据用户的期望停顿时间制定回收计划,可以任意选择多个Region构成回收集,然后把决定回收的Region的存活对象复制到空的Region中,再清理掉整旧的Region全部空间。存在对象移动,Stop The World,多条收集线程并行完成。
G1 整体上看是 标记-整理算法 实现的收集器,但从局部(两个Region之间)看是基于 标记-复制算法
G1收集器运行示意图
![]()
3.6 低延迟垃圾收集器
todo 待做
3.7 选择合适的垃圾收集器
必须因地制宜,按需选用
3.7.1 Epsilon 收集器
无操作收集器,"垃圾收集器" 并不能形容它的全部职责,更贴切的应该是"自动内存管理系统",因为除了垃圾收集之外 还需要负责:管理与布局、对象的分配、与解释器,编译器,监控子系统等协作。为了隔离收集器与java虚拟机解释、编译、监控等子系统的关系,提出了收集器应该统一接口。
3.7.2 收集器的权衡
选择合适收集器影响因素:
- 应用关注点是什么:
- 数据分析、科学计算 目标是尽快算出结果,吞吐量是关注点
- SLA应用,停顿时间影响服务质量甚至导致事务超时,延迟是关注点
- 如果是客户端应用或嵌入式,那么垃圾收集器的内存占用则不可忽视
- 运行引用的基础设施:硬件规格、系统架构,处理器、内存,操作系统等
- JDK 的发行商、版本。
3.7.3 虚拟机以及垃圾收集器3日志
JDK9 之前 HotSpot没有提供统一的日志处理框架,虚拟机各个功能模块的日志开关分布在不同参数上,日志级别、日志大小、输出格式、重定向等都要单独解决。
JDK9 HotSpot所有功能的日志都收归到 -Xlog 参数上
-Xlog[:[selector][:output][:decorators][:output-options]]
- 选择器 selector:由标签Tag和日志级别 Level 共同组成
- Tag:虚拟机中某个功能模块的名称,告诉虚拟机希望输出哪些 功能模块的日志,垃圾收集器的标签为 gc
- Level:日志级别从低到高:Trace,Debug,Warning,Error,Off
- 修饰器 decorators:每行日志附加额外的信息 time,uptime,timemillis,uptimemillis, pid,tid,level,tags 等
3.7.4 垃圾收集器参数总结
todo 待做
3.8 实战:内存分配与回收策略
Java技术体系的自动内存管理根本目标自动化的解决两个问题:
- 自动给对象分配内存
- 回收给对象的内存
3.8.1 对象优先在Eden分配
一般 对象在 新生代 Eden区 中分配,Eden没有足够空间时将触发 Minor GC
3.8.2 大对象直接进入老年代
大对象:需要大量连续空间的java对象,典型的大对象包括 长字符串、元素数量庞大的数组等
通过参数-XX:PertenureSizeThreshold 参数指定大于该参数的值直接在老年代分配,避免复制操作,参数只对Serial、ParNew收集器有效
3.8.3 长期存活的对象将进入老年代
HotSpot虚拟机的大多数收集器都采用分代收集来管理堆,虚拟机给每个对象定义了年龄Age计数器,每经历一次 Minor GC ,年龄+1,当年龄达到一定程度晋升到老年代,默认为15,可通过参数-XX:MaxTenuringThreshold 设置
3.8.4 动态对象年龄判断
为了更好适应程序内存情况,并不是永远都要达到-XX:MaxTenuringThresHold才能晋升
如果Survivor空间相同年龄的对象大小总和大于Survivor空间的一半,则年龄大于或等于该年龄的对象直接晋升老年代
3.8.5 空间分配担保
Minor GC 前应该检查老年代最大可用连续空间是否大于新生代对象总空间,如果不是,则表明如果Minor会冒风险
JDK6 update24 后,老年代最大可用连续空间是否大于平均晋升大小,或大于新生代所有对象空间,是则进行Minor GC,否则将进行Full GC。
第四章 虚拟机性能监控、故障处理工具
4.1 概述
给系统定位问题的时候,知识、经验是关键,数据是依据,工具是运用知识处理数据的手段。
4.2 基础故障处理工具
JDK bin 目录下工具分类
- 商业授权工具:主要是JMC Java Mission Control,以及他要使用到的 JFR Java Fight Record,商业环境需付费
- 正版支持工具:长期支持,不同平台不同版本可能略有差异
- 试验性工具:没有技术支持 并且是实验性质的
4.2.1 JPS 虚拟机进程状况工具
jps (JVM Process Status Tool)可以列出 正在运行的JVM进程并显示虚拟机执行的主类(main class ,main()函数 所在的类)名称以及这些进程本地虚拟机唯一ID(LVMID,Local Virtual Machine Identifier).
命令 格式 jps [options] [hostid]
JPS工具主要参数

4.2.1 jstat 虚拟机统计信息监视工具
jstat(JVM Statistics Monitoring Tool)用于监视虚拟机各种运行状态信息的命令行工具。可以显示本地或远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据。
命令格式:jstat [optiona vmid [interval [s|ms] [count]]]
命令行中VMID 与 LVMID 说明:本地虚拟机进程 VMID 与 LVMID 是一致的,如果是远程虚拟机进程那么VMID的格式应当是: [protocol:][//]llvmid[@hostname[:port]/servicename]
option 代表用户想要查询的虚拟机信息,主要分为三类:类加载、垃圾收集、运行时编译
jstat 工具主要选项

4.2.3 jinfo java配置信息工具
jinfo(Config Info for Java):实时查看和调整虚拟机的各项参数
jps -v 查看虚拟机启动时显示指定的参数
jinfo - flag 查询未被指定的系统默认值
jinfo -flag[+|-]name 或 -flag name=value运行期间修改参数
命令格式: jinfo [option] pid
4.2.4 jmap java内存映射工具
jamp(Memory Map for Java) :用于生成堆转储快照(称为heapdump或dump文件),还可以查询finalize队列、java堆和方法区信息:如空间使用率、垃圾收集器等
其他获取dump文件的方法, -XX:+HeapDumpOnOutOfMemoryError 、kill -3
命令格式:jmap [option] vmid
jamp主要选项

4.2.5 jhat java堆转储快照分析工具
jhat(Java Heap Analysis Tool)分析转储快照,但是功能简陋,且无可视化,一般尽快将转储快照复制到其他机器,使用专业工具分析如:IBM Heap Analyzer
4.2.6 jstatck java堆栈跟踪工具
jstatck(Stack Trace for Java ):用于生成虚拟机当前时刻的线程快照(称threaddump或javacore文件)。线程快照就是 当前虚拟机内每一条线程正在执行的方法堆栈集合,生成快照的原因一般目的是定位线程出现长时间卡顿:死锁、死循环、请求外部资源长时间挂起等。
命令格式 :jstack [option] vmid
jstatck主要选项

4.2.7 基础工具总结
基础工具命令永远不会过时
4.3 可视化故障处理工具
JDK除了附带大量命令行工具外,还提供了几个集成度更高的可视化工具,更加便捷的进行故障诊断和调试。主要包括JConsole、JHSDB、VisualVM和JMC
4.3.1 JHSDB 基于服务性代理的调试工具
JDK提供了JCMD和JHSDB两个集成式多功能工具箱,不仅集成了所有基础工具所提供的专项能力,而且具有"后发优势",做的更好更强大。
JHSDB是一块基于服务代理(Serviceablity Agent ,SA)实现的进程外调试工具。服务性代理是HotSpot虚拟机中一组用于映射Java虚拟机运行信息的、主要基于Java语言(含少量JNI代码)实现的API集合。
服务性代理以HotSpot内部数据结构为参考物进行设计,把C++的数据抽象出Java模型对象。
JCMD、JHSDB和基础工具的对比

4.3.2 JConsole java监视与管理控制台
JConsole java monitoring and management console :是一款基于JMX Java Management Extensions 的可视化监视、管理工具。
它的主要功能是通过JMX的MBean(Management Bean)对系统进行信息收集和参数动态调整。
4.3.3 VisualVm 多合一故障处理工具
VisualVM(All-inOne java Troubleshooting Tool)是功能强大的运行监视与故障处理程序之一
4.3.4 Java Mission Control 可持续在线监控工具
4.3.4 HotSpot 虚拟机插件及工具
第五章 调优案例与实战分析
5.1 概述
在处理实际问题中,除了知识与工具,经验同样是一个很重要的因素。
5.2 案例分析
5.2.1 大内存硬件上的程序部署策略
单体较大内存部署策略:
- 通过单独的Java虚拟机实例管理大量的Java 堆内存
- 同时使用若干个 Java虚拟机,建立逻辑集群利用物理资源。
5.2.2 集群间同步导致的内存溢出
网络问题,导致组件内存累积
5.2.3 堆外内存溢出的错误
除了java堆栈外还有些区域占用内存比较多:
- 直接内存:可通过 -XX:MaxDierctMemorySize调整大小 内存不足时抛出OOME 或OOME:Direct buffer memory
- 线程堆栈:可通过 -Xss 调整大小,内存不足抛出StackOverFlow(线程请求的栈深度大于虚拟机允许的深度)或OOME(如果虚拟机栈容量可动态扩展,当栈扩展无法申请到内存时)
- Socket缓存区:每个socket链接都有Receive和send 两个缓存区,分别占有大约37KB和25KB内存,如果无法分配,可能抛出 IOException:Too many open files。
- JNI代码:如果代码使用了JNI调用本地库,本地库使用的内存也不再堆中,占用java虚拟机的本地方法栈和本地内存。
- 虚拟机和垃圾收集器:虚拟机和垃圾收集器 工作也要占用一定内存。
5.1.4 外部命令导致系统缓慢
java程序通过Runtime.exec() 执行shell脚本,导致处理器和内存资源消耗过高
5.2.5 服务器虚拟机进程崩溃
服务处理能力不匹配,直接调用导致任务积累,连接、线程占用越来越多,最终服务崩溃,改用异步消息队列。
5.2.6 不恰当的数据结构导致内存占用过大
内存使用效率低
5.2.7 由于windows虚拟机内存导致的长时间停顿
程序最小化时,工作内存被自动交换到磁盘的页面文件中。使用参数最小化时保持内存
5.2.7 由于安全点导致长时间停顿
概念: user \ sys \ real
* user:进程执行用户态代码所消耗的处理器时间
* sys :进程执行核心态代码所消耗的处理器时间
* real:执行时间从开始到结束所消耗的时钟时间
处理器时间:线程占用处理器一个核心的耗时计数。
时钟时间 : 现实世界中的时钟计数
HotSpot安全点特征:是否让程序具有长时间执行的特征,方法调用、循环跳转、异常跳转等都可能设置安全点。优化:int类型的循环 可数循环 Counted Loop 不放置安全点,long类型的循环 不可数循环 Uncounted Loop 放置安全点
5.3 实战:Eclipse运行速度调优
// todo 待完成



浙公网安备 33010602011771号