jvm内存管理
2022-01-06 15:33:16 0 举报
AI智能生成
总结至《深入理解java虚拟机 3》
作者其他创作
大纲/内容
对象内存布局
对象头(Mark word)
运行时数据
存储的与对象定义本身无关的数据,即,
HashCode 、 GC分代年龄、 锁状态标志、 线程持有的锁、 偏向线程ID、 偏向时间戳等
HashCode 、 GC分代年龄、 锁状态标志、 线程持有的锁、 偏向线程ID、 偏向时间戳等
根据对象状态动态存储不同的数据,即,锁状态(无、轻、重)、GC标记、可偏向
类型指针
确定是哪个类的实例
如果是数组,还有数组的size
实例数据
对象定义的字段信息
对齐填充
HotSpot规定对象大小是8字节的整数倍,如果或多或少就填充
对象访问定位
直接指针
直接访问对象数据,快,主流
句柄
对象被移动时,只改变句柄池中的实例指针
程序计数器
线程私有
线程当前执行字节码行号指示器:可以是记录的当前执行的指令地址
不会出现oom错误
虚拟机栈
线程私有
一种线程内存模型,存储栈帧
栈帧:一种描述方法运行时的数据结构
栈帧中的数据:局部变量表、 操作数栈、 动态连接、 方法出口等
局部变量表
存储:基本数据类型
存储:对象引用
用局部变量槽来表示要存储的数据
栈深度既定时,可能会出现sof
栈深动态扩展时,可能会出现oom
本地方法栈
同虚拟机栈,用于native方法
堆
线程共享
存储对象实例、数组
方法区
线程共享
存储:类型信息、 常量、 静态变量、 即时编译器编译后的代码缓存等数据
8之前采用分代设计,数据存储在永久代,8之后存储于元空间
运行时常量池
存储:字面量、符号引用
动态增加常量,String#Intern()
直接内存
NIO使用native函数直接分配的堆外内存
通过DirectByteBuffer对象操作
主要是提高性能,避免java堆和native堆之间的复制
方法区回收
虚拟机可以不实现方法区内存回收,主要是代价大,收益小
回收常量
回收类型
该类所有的实例都已经被回收, 也就是Java堆中不存在该类及其任何派生子类的实例
加载该类的类加载器已经被回收,很难成立
该类对应的java.lang.Class对象没有在任何地方被引用, 无法在任何地方通过反射访问该类的方
法。
法。
对象生死判定
引用计数法
用引用计数器记录对象的引用状态,0表示死亡
弊端:不能识别循环引用产生的死亡对象
可达性分析
通过gc roots 向下搜索对象是否可达
Gc Roots
虚拟机栈中的引用对象,即方法中的参数、 局部变量、 临时变量
方法区中的类静态引用对象、常量引用对象
本地方法栈,即native方法中引用的对象
虚拟机内部的引用,如基本数据类型对应的Class对象,常驻异常对象Npe,Oom等、系统类加载器
被同步锁持有的对象,相当于被锁住的对象
反映Java虚拟机内部情况的JMXBean、 JVMTI中注册的回调、 本地代码缓存等
根据回收算法确定的局部回收所加入的临时对象
java引用定义
如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址, 就称该reference数据是代表某块内存、 某个对象的引用
如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址, 就称该reference数据是代表某块内存、 某个对象的引用
强引用:根据gc算法判定生死是否回收
软引用:根据内存使用情况按需回收,SoftReference
弱引用:每次gc都回收,WeakReference
虚引用:为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。PhantomReference
分代收集理论
弱分代假说:对象朝生夕灭
强分代假说:熬过多次收集的对象难以消亡
回收思路:高频回收易消亡的,低频回收难以消亡的
分代实现难点:对象跨代引用
跨代引用假说:老年代引用新生代,最终都会被拖入老年代
解决方式:记忆集,标识老年代中存在跨代引用的部分区域,minor时该部分才加入扫描
回收算法
标记-清除
执行效率不稳定,随对象数量增加而增加标记清除时间
内存碎片
标记-复制
内存对半交替,有效率,但浪费空间
改进:appel回收,Eden:survivor:survivor,老年代分配担保
标记-整理
移动对象会stop the world,好于操作系统自己整理内存
算法细节
根节点枚举,即gc roots的枚举
必须stw(stop the world)
采用OopMap记录对象引用存储的位置,不需要从方法区等地方遍历查找root
安全点
发生gc时,程序指令暂停的位置
选定标准:“是否具有让程序长时间执行的特征”
具体位置:调用、循环跳转、异常跳转等复用指令处
发生gc时,如何让所有线程都跑到安全点
抢先式中断:发生gc时,直接中断线程,如果没在安全点则让线程跑到安全点为止。几乎弃用
主动式中断:线程主动轮训中断标志位,轮训标志位同安全点重合并加上对象创建和其他分配内存的地方
安全区域
定义:指能够确保在某一段代码片段之中, 引用关系不会发生变化
发生gc时,当线程运行在安全区内则不受影响,当线程离开安全区时,如果还处于gc需要stw的时段,则必须等待直到有可以离开的信号
记忆集
定义:一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。主要用来解决跨带引用问题
常采用卡表实现
实现思路:记录固定大小内存块里面的对象是否含有跨带指针
具体实现:一个字节数组,元素值为0或1,1表示该元素所对应的内存块内有带有跨带指针的对象。元素的位置映射了内存块的地址
扫描非回收区时就只用查询卡表把有跨带指针的区域拿出来扫描,来查找gc roots
写屏障
如何维护卡表,卡表的元素在引用赋值时会出现变脏的情况
经过即时编译后的代码已经是纯粹的机器指令流了, 这就必须找到一个在机器码层面的手段, 把维护卡表的动作放到每一个赋值操作之中
g1之前大部分收集器采用写后屏障,即虚拟机采用类似aop的方式在引用赋值操作后,生成更新卡表的指令
采用判断卡表标记是否已经变脏再更新为未标记来解决伪共享问题
并发的可达性分析
对象消失的两个同时满足条件
赋值器插入了一条或多条从黑色对象到白色对象的新引用
赋值器删除了全部从灰色对象到该白色对象的直接或间接引用
对象消失的解决方案
增量更新:记录黑色对象新增的到白色对象的引用,扫描完时要再次以该黑色对象为根进行扫描
原始快照:当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后, 再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次。(这个咋行呢?再次从灰色对象扫描,已经消失的对象还是消失了啊!)
经典垃圾收集器
Serial收集器
单线程
stw所有线程
客户端模式下的新生代使用,简单高效
ParNew收集器
Serial的多线程版
jdk7之前服务端模式下的新生代使用
与CMS配合
Parallel Scavenge收集器
用于新生代,采用标记-复制实现
JDK 4
关注吞吐量
用户程序时间/(用户时间+垃圾时间)
适合主要处理后台运算任务,而不需要过多交互响应
控制gc吞吐量参数
-XX: MaxGCPauseMillis
过小导致收集频繁
-XX: GCTimeRatio
垃圾时间占比
Serial Old收集器
老年代,标记-整理
客服端模式
如果服务端
jdk5及前 与paralle scavenge搭配
cms model failure 时备用
Parallel Old
Parallel Scaevnge 老年代,标记-整理
JDK 6
配合 Parallel Scavenge
CMS收集器
关注回收停顿时间
互联网服务端主流
老年代
JDK5
标记-清除
步骤
初始标记
stw
标记gc roots 直接关联的对象
并发标记
遍历对象图
重新标记
stw
修正并发标记,前面的增量更新
并发清除
缺点 1:cpu资源敏感
线程数=(核数+3)/4
核数少于4时对用户程序影响较大
资源少时用增量式并发收集器,i-cms改进,即抢占式并发,jdk7开始废弃
缺点2:无法处理“浮动垃圾”
并发过程中用户程序还在不停的生成垃圾
-XX: CMSInitiatingOccu-pancyFraction,控制触发cms回收的老年代内存使用百分比
太低,回收频率高
太高,容易 Concurrent mode failure,使用Serial Old 进行stw
缺点3:标记-清除 产生内存碎片,没有连续空间放大对象执行full gc
-XX: +UseCMS-CompactAtFullCollection,默认开启,full时整理碎片,jdk9 废弃
-XX: CMSFullGCsBeforeCompaction,默认0,JDK 9废弃,n次full gc后进行碎片整理
Garbage First 收集器
JDK 7 Update 4
JDK8 Update 40完成类卸载支持,完全完成
面向堆内任何区域回收,Mixed GC 模式,而非之前的单独面向某个分代
按分代设计,但内存区域按连续固定大小空间设置为一个Region
每个Region按需扮演Eden,Survivor,Old
Humongous Region:专门存放超过0.5Region的大对象,该区域被当成老年代处理
具体思路
维护一个优先级列表,记录每个Region的价值(回收时间,回收内存大小的经验值)
-xx:MaxGCPaulseMills 设定gc停顿时间,根据优先级列表评估要回收的Region,即最先回收价值最高的
关键问题
跨Region引用
记忆集-双向卡表
HashMap,Key是别的Region的起始地址, Value是一个集合, 里面存储的元素是卡表的索引号
需要更多的内存空间,堆的10%-20%
并发标记导致标记错误
原始快照算法进行修正
每个Region计了两个名为TAMS(Top at Mark Start) 的指针,将一部分区域作为新对象的分配空间,此空间上的对象都默认为存活
如果创建比回收快则stw进行full gc
如何实现可靠停顿时间预测模型
衰减均值理论基础,最近的统计结果能预测目前最真实的状态
每个Region的回收耗时、 每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本, 并分析得出平均值、 标准偏差、 置信度等统计信息
根据统计信息决策回收哪些Region
大致步骤
初始标记
标记一下GC Roots能直接关联到的对象
修改 TAMS指针,用于新对象分配
短暂停顿
并发标记
遍历整对象图,并记录并发时发生的对象引用删除
最终标记
原始快照算法修正
短暂停顿
筛选回收
Region统计数据更新
复制回收该Region,即Region间的复制
必须停顿,复制使得对象移动
与cms的对比
1、记忆集实现更复制,每个Region都有,需要更多内存空间
2、还采用写前屏障保证实现最终标记的原始快照。需要更多的计算,因此采用队列进行异步处理
经验上:cms适用小内存应用,g1适用大内存,6-8g之间是两者平衡点,那么这个平衡点后g1比较有优势
低延迟收集器
Shenandoah 收集器
非oracle官方团队,OpenJdk 支持
OpenJdk 12
G1的升级
不采用分代设计
连接矩阵替代记忆集
如c32,表示Region3 有Region2的跨代引用
大致步骤
初始标记
同G1
并发标记
同G1
最终标记
同G1
并发清理
清理完全没有存活对象的Region
并发回收
核心差异
并发复制存活对象到未使用的Region
Brooks pointers:解决并发时的老对象被读写的问题
原来采用内存保护陷阱,即遇到旧地址对象就进入异常处理,然后跳转到新对象地址。需要操作系统支持,用户态核心态频繁切换
具体设计:在每个对象头前新增一个转发指针,在没有被并发移动的情况下指针指向自己
并发访问一定存在线程竞争,此处采用cas来保证转发指针更新和对象写操作的正确同步
缺点:额外转发开销
读屏障:解决并发时的老对象被读的问题
JDK13 将优化为引用访问屏障:所谓“引用访问屏障”是指内存屏障只拦截对象中数据类型为引用类型的读写操作, 而不去管原生数据类型等其他非引用字的读写。
原来的Region成为Immediate Garbage Regions(IGR)了
初始引用更新
所有线程完成对象复制工作
并发引用更新
按内存地址顺序,线性的更新老地址为新地址
最终引用更新
停顿,修正gc roots的引用地址
并发清理
完全回收IGR
吞吐量下降,停顿时间大幅缩减
ZGC收集器
Oracle 研发 JDK11中开始试验
原理几乎和Azul公司的商业收集器 PGC 、C4一样
定义:其基于Region内存布局的, (暂时)不设分代的, 使用了读屏障、 染色指针和内存多重映射等技术来实现可并发的标记-整理算法的, 以低延迟为首要目标的一款垃圾收集器
Region新特征
动态创建与销毁
3种大小
小:2m,存放256kb以下的对象
中:32m,存放256kb至4m的对象
大:大小不固定,动态变化,4m以上(2m的倍数),存一个对象
核心技术
染色指针
标记对象时,用来记录对象的标记信息
利用64位linux系统规定只使用46位地址中的高4位来记录对象的标记状态
一旦某个Region的存活对象被移走之后, 这个Region立即就能够被释放和重用掉, 而不必等待整个堆中所有指向该Region的引用都被修正后才能清理。指针自愈
减少内存屏障使用,没有写屏障
扩展染色指针的位数,利用未开发的18位记录更多的信息
内存多重映射技术
定义:染色指针特定高4位来记录信息,后面14位为真实地址,但操作系统不知道啊,因此发明此方法
将多个不同的地址映射到同一个物理地址,把标志位看做是地址的分段符
大致步骤
并发标记
细节类似于G1,Shenandoah,只是最终标记到染色指针,前者标记到独立的bitmap中
并发预备重分配
根据特定的查询条件统计得出本次收集过程要清理哪些Region
并发重分配
复制存活对象到新的Region
为每个Region维护一个转发表记录新旧对象的转换关系
如果访问旧对象,此时内存读屏障生效,、根据转发表来找新对象,并更新引用值(指针自愈)
并发重应射
并发更新引用值
非紧急任务,合并到下次gc中并发标记中
因为有自愈功能,但更新完引用后可以避免转发且能释放转发表
缺点
整堆回收,对象分配速率低,因为过高的分配速率会导致内存回收不及时,内存越来越不够用
优点
真实的10ms内停顿时间
不可运行在windows下
Epsilon收集器
JDK 11
不进行实际的垃圾回收
同其他收集器一样是一个完整的自动内存管理子系统
堆的管理与布局
对象的分配
与解释器的协作
与编译器的协作
与监控子系统协作等职责
适用于:在堆耗尽之前就会退出的应用
实践经验
大内存应用部署
单虚拟机管理大内存
大内存回收,停顿时间长,可使用G1,ZGC,Shenandoah
使用64位虚拟机,由于压缩指针、处理器缓存行,性能较32位低
程序要求较高,如果出现了内存溢出、泄漏,分析困难
相同的程序因指针膨胀,数据类型补齐等在64位消耗内存较32位大,可使用压缩指针来缓解
多应用逻辑集群
反向代理负载均衡,单应用分配相应比例内存
问题1:全局资源竞争,如磁盘
问题2:很难最高效率地利用某些资源池, 譬如连接池
问题3:多应用造成本地缓存浪费,可以用集中缓存
问题4:32位linux最多4G内存,但不能完全分配给虚拟机
0 条评论
下一页