导图社区 深入理解虚拟机---第二部分自动内存管理
这是一篇关于深入理解虚拟机---第二部分自动内存管理的思维导图,理解Java的内存区域和如何避免内存溢出是每个Java开发者的基本技能。通过合理地管理和使用内存,可以确保应用程序的稳定性和性能。
编辑于2023-12-21 08:38:27第二部分 自动内存管理 第2章 java内存区域与内存溢出
2.3 HostSport 虚拟机对象探秘
对象的创建
1虚拟机碰到new 首先在常量池中是否能确定是一个类的符号引用 并且检查这个类是否加载 解析和初始化,如果没有执行类加载过程
2 类加载完成后开始分配内存 ,对象占用的内存量就基本可以确定
指针碰撞
如果java堆有足够大的内存 并且是连续 那么我就指针偏移来 分配内存
空闲列表
如果java堆的内存是交叉的,那么需要维护一个列表 说明内存情况,再从列表中找到一个够用的内存进行分配
3 多线程情况下内存的分配是不安全的
1 采用 cas同步机制 保证原子性
2 先给每个线程分配一块内存, 对象的内存在该区域进行划分 这块区域叫做本地线程缓冲区 只有这块区域 使用完 重新分配新的区域的时候才会需要同步锁定 可以通过 tlab设置
4 分配后开始赋值 默认值是0 保证对象可用性
5 开始对对象进行数据赋值 对象头赋值
6 虚拟机的角度对象已经创建完了 但是从程序的角度 还没有 值现在都是0 只有执行了init() 才会算真正的创建对象
对象的内存布局
对象在堆内的内存布局可以划分为三部分
对象头
存储数据的第一类 用于存储对象自身的运行数据 如 hash GC 分带年龄 锁状态 线程持有的锁 偏向线程ID 等
对象头 存储数据的第二类 是类型指针 及类型的元数据指针 java通过指针来确定这个对象数据那个类的实例 如果java是一个数组 还会存在 数组长度 如果数组长度是不确定的 无法在元数据内推断 数组的大小
实例数据
真正的对象内个字段内容
相同宽度的字段会放到一起存放
对齐填充
没有特殊意义 就是要求是字节8 的整数倍
对象的访问定位
句柄:能标示所有东西统称为对象
访问方式是由虚拟机决定的 一般有两种方式
句柄访问
句柄访问需要维护一个句柄池 通过句柄池中句柄到对象的指针
通过指针 找到堆中对象的实例数据,和方法区的对象类型
直接指针访问
指针是直接指向的是对象的地址 如果是只访问对象本身的话 就会少一次访问的开销
hostSport 采用的是直接指针 访问 就在很大程度上减少了对象的访问开销
2.4 OutOfMemoryError异常
java 堆溢出
-XX:+HeapDumpOnOutOf-MemoryError
内存溢出的时候会生成当时内存堆转储快照
工具
通过 Eclipse Memory Analyzer 工具 进行分析 来确认是内存溢出 还是内存泄漏
内存泄漏
通过工具查看泄漏的 GC ROOT 引用链 与哪些对象相关导致无法回收
内存溢出
判断对象是否必须存活,在判断 Xmx 是否有向上扩充的可能,在进行优化 对象的声明周期 存储结构有没有不合理的地方
虚拟机栈和本地方法栈溢出
虚拟机规范 是允许动态扩展栈大小的 但是 hostsport 是不允许的
报错
当 线程请求的栈深大于虚拟机允许的深度 抛出 StackOverflowError 如果栈内存允许 动态扩展有可能 抛出 OutOfMemmerError
方法区和运行时常量池溢出
虚拟机进程配置
-XX:MaxMetaspaceSize
元空间最大值 默认-1 不受限制 受本地内存限制
-XX:+HeapDumpOnOutOf-MemoryError
内存溢出的时候会生成当时内存堆转储快照
-XX:MetaspaceSize
元空间初始大小,字节为单位 打到该值就会触发垃圾回收器对其类型卸载 和垃圾回收 并对此值进行调整
-XX :MinMetaspaceFreeRatio
控制垃圾回收器之后 最小元空间百分比 可调整垃圾回收因为元空间不足调整垃圾回收平率
-Xmx
设置最大堆内存
-XX:MaxDirectMemorySize
指定最大直接内存大小 如果不指定 则默认是堆内存大小
如果 内存溢出看不出什么特殊情况 并且Heap.Dump 很小 就要考虑堆内存问题
第3章 垃圾收集器与内存分配策略
对象已死(对象 方法区 判断可死算法)
引用计数算法
原理:在对象上做个标志 有引用加1 引用失效减1
问题:如果两个对象互相引用a=b b=a 除此之外在无其他引用 计数器上有数据 也无法回收
javaGC 没有使用该方法
可达性分析算法
原理:做个GC roots 起始节点集 重这些节点 向下搜索 如果搜索的引用链没有对象那么 证明这个对象不可达 无引用可以回收
GCroots 包含
虚拟机栈中引用的对象
方法去中类静态属性引用的对象
本地方发栈引用的对象
java虚拟机呢内部的引用 如一些常用的异常等
同步锁 synchronized 关键字持有的对象
等
再谈引用
强引用
最普遍的应用关系
软引用
描述一些还有用 单并非必要的引用关系 在内存快要溢出前 进行回收,如果这些回收还没有释放出足够内存 就会oom
弱引用
比弱引用 更弱一些 只能存活到下次垃圾回收
虚引用
最弱的一种引用关系 通过这种引用关系无法获取对方的实例,只能在垃圾回收的时候收到回收的通知
生存还是死亡
java对象回收流程
1 在分析可达性后发现与 GCRoots没有关联引用链也不是必死无疑
2 对象死亡前 会经过两次标记 如果可达性分析后 没有GCRoot 引用链 会进行第一份标记
3 随后进行一次筛选是否调用finalize() 函数 最后决定是否回收的方法
是否覆盖了 finalize 方法
是
执行
否
不执行
是否虚拟机调用过该方法
是
不再执行
否
正常执行
只有两种情况有一种符合 那么就不再回收资源范围内
4 如果对象没有被finalize 执行 就会把对象放到F_Queue 队列中去
队列会开一个低效率线程去给对象标记 但是不保证一定要等待结果 因为可能会出现 等待时间过长等待 导致一些其他问题
队列开始第二次标记 如果这个时候对象还没有拯救自己(重新引用) 并且被标记 那么对象就基本上确定要被杀死了
回收方法区
方法区的回收一般回收两种类型 废弃的常量 和无引用的类型
常量
如果该常量无人引用 并且没有类引用字面量 而且虚拟机判断确实必要的话就会清楚该常量,接口 方法 字段符号等也与此类似
类
类的卸载通常要满足三个条件 而且满足条件也只是被允许 卸载 被卸载还要看具体的虚拟机设置,现在大量使用反射 动态代理等 通常虚拟机都有类卸载能力 防止 方法区过大 造成压力
1 该类的实例都被回收 ,并且虚拟机上没有存在该类派生的子类信息
2 该类的类加载器已经被回收 一般除非经过特殊设计 否则不会达成
3 该类的 java.lang,class对象没有任何地方被引用,无法再任何地方通过反射获取到该类的方法
垃圾收集算法
通常有两种 引入计数式垃圾算法 追踪式垃圾算法 通常 java 虚拟机采用的是追踪式垃圾算法
分代收集理论
强弱分代理论
弱分代假说
强分代假说
设计原则
1 收集器应该把java堆划分出不同的区域来 对象依据其年龄(熬过垃圾回收的次数)在不同的区域内存储
2 如果一个区内大多数都是招生夕死难以熬垃圾回收过程的话,那么久把这写对象放在一起 每次垃圾回收只关注如何保留少量的存活,那么较低的代价回收大量的内存空间
3 难以消亡的对象 把他们放在一起 已较低的频率来回收这个区域 那么久同时兼顾了垃圾回收器的时间开销和 内存空间有效的利用
4 垃圾回收器可以每次回收不同的分代,根据不同的分才采用不同的垃圾回收算法
5 虚拟机中 对象 会根据年龄分到不同的分代 虚拟机本身也有分代式垃圾收集器框架,但是虚拟机部不断的更新 分代理论的绝对性 反而不方便 因为对象不是孤立的 有可能存在不同分代之间的互相引用
跨代引用假说
1 一个新生代对象被老年代引用 因此新生代不能被消灭 而会慢慢晋升到老年代 那么跨代引用也会随之消除
2 会在新生代上增加一个 记忆集 只有在记忆集上的对象才会被 GCroots扫描 这种会增加一个引用关系的消耗 但是对比访扫描整个老年代来说 仍然是划算的
标记-清除算法
原理:标记要回收的对象在清楚回收的对象 或者 标记要保留的对象 在清楚没有被标记的对象
缺点
1 执行效率不稳定 会随着 对象数据的增加而降低
2 会生成大量 碎片化的内存 导致 大的对象无法被创建 触发一次GC收集
标记-复制算法
原理:把内存分成两块,每次只用一块 这样 每次回收的时候只需要把存货的对象复制到另一快上 重新分配资源的时候只需要移动指针就好了
缺点
1 如果有大量存活的对象,那么就会消耗大量的时间进行对象复制,如果对象存活的少,就会很快
2 可用的内存为原有的一半 空间浪费太大
Appel式回收
把新生代分为 一块Eden空间 和两个Survivor空间 每次只使用一个Eden 和Survivor空间 然后直接清理 Eden 和Survivior空间 把其他存活的资源放到另一个survivor 上 三个空间的比例是 8:1:1原则上新生代每次垃圾回收会打到98% 以上 所以百分之十够用 但是也有例外的情况 ,当存货的对象 在剩余的百分之十空间无法存储的时候那么就要设计一个逃生门 会把剩余的资源放到其他内存区域 实际上基本上是老年代,
标记-整理算法
一般老年代会使用此算法 因为 整理算法需要存活的对象移动到一边并更新引用 这种复杂的操作 必须暂停整个程序。而老年代 更关注吞吐量 所以采用此方式 因为 虽然会暂停整个程序 但是相比 标记-清除的算法 产生内存随便 每次访问内存 都需要利用更复杂的方式去处理内存问题 显然 标记整理来的更为划算
HotSpot的细节算法实现
根节点枚举
收集器在根节点枚举这一块必须做到进程暂停已保证一致性的快照中才能进行(保证在可达性分析的过程中保证对象的引用关系不变)所以枚举跟节点的时候必须暂停 但是因为 GCROOT 会很大 导致回收的效率 及和时间不可控因而 hotsport引入一个oopmap算法
OOPMap 当类加载完 就会把有引用的对象关系放到这个map里去从而 访问 扫描的时候在这个map里扫描就行不需要再方法区一个不拉的进行扫描
安全点
因为程序的运行可能会导致引用的变化 如果每一次变化指定都要告知oopmap 那就会占用大量的内存,这样垃圾回收器 伴随着大量的空间成本是无法忍受的 那么 就只有在特定的位置 记录这些信息 这些特定的位置就叫做安全点
解决 上述大量化指令的方法有两种
抢占式中断
主动式中断
在垃圾回收需要中断的线程这种设置一个垃圾位,各个先生不停的轮训这个标志 一点发现这个标志是真的时候就在最近的安全点挂起 轮训标志和安全点重合的 同时还要加上锁创建的对象和 其他内存分配的地方 这是为了检查 单GC执行时会有足够的内存分配对象
安全区域
背景
当线程 进入 sleep blocked 状态的时候 无法响应虚拟机内的中断请求 不能再走到安全点上来挂起自己 虚拟机也不能等待线程重新激活分配处理时间 如实就引入安全区域的概念
说明
安全区域 是指确保在某一段代码区域内 引用关系不会发生改变 因此线程在这个区域任意一点时在收集垃圾时都是安全的。 也可以把安全区域看成 安全点的延伸
流程
当线程进入安全区域的时候要标示自己已经进入安全区域 ,这时线程的GC 进行垃圾回收的时候 就不用考虑安全区域内的线程,当线程要离开安全区域的时候要检查虚拟机是否完成根节点枚举或者其他需要暂停用户线程的阶段,如果已经完成了 那正常运行 否则就要一直等待 直到通知可以离开安全区域的通知
记忆集和卡表
背景
当出现夸代引用的时候 会把非收集区域的对象指向收集区域的指针 记录在记忆集里 这样就会减少 整个区域的扫描 只需要扫描记忆集中的对象就可以了
记忆集中的记录精度
字长精度
每个记录精确到机器的字长,该字包含跨代引用指针
对象精度
每个记录里精确到一个对象 对象里包含跨代指针
卡精度
每个记录为一个内存区域 区域内包含对象的跨代指针
hotSpot 中简单的就是是一个字节数组, 每一个元素称为卡页 卡也里存的是一个特定大小的内存块,一个卡业内 通常包含多个对象 如果对象内有存在跨代引用的情况下 那么我们就做一个标志为1 否则为0 1 这种情况称之为元素变脏, 当垃圾回收的情况下 只需要把 脏了的卡页加入到 GC roots 中一并扫描就可以了 减少了扫描的广度
写屏障
背景
卡表是记忆集的一种实现,当跨代引用的时候 会把元素变脏(1) 那么什么时候会变脏 是谁实现了 卡表的变脏 这里就涉及到 写屏障
设计原理
假如解释执行的字节码 相对好处理 虚拟机负责每条指令的实现 有足够的介入空间 但是
并发的可达性分析
背景
可达性分析的依据是锁喉用线程全部冻结才能进行分析
设计原理
三色标记
白色
对象没有被垃圾回收期访问过,在可达性分析前期应该都是白色
黑色
表示这个对象被垃圾回收期以及所有引用都访问过表示它是安全存活的,如果其他对象引用了黑色对象无绪重新扫描
灰色
表示这个对象被垃圾回收器访问过,但是至少一个引用没有被引用过
对象消失
原因
如果是并发情况下,那么一个引用有可能是动态的那么有两种方案处理这个对象一种是给它标记为黑色也就是让他可以逃离这次回收,是可以容忍的,另一种是把它标记成已回收 但是有可能会遇见问题,对象被回收系统出现异常.
如果现在有一个黑色对象扫描了 但是现在被打断了扫描 导致又重新扫描它引用的对象是白色的.而且黑色的对象又不会被重新扫描所有有可能不会被发现而是被回收了
赋值器插入一条或者多条黑色对象到白色对象的引用
赋值器删除所有灰色对象到白色对象的引用
解决
当并发扫描的时候新的引用关系要先记录下来,等并发访问关系结束后在将黑色对象作为根在重新扫描一次
当灰色对象要删除白色访问关系时候,记录下来一灰色为根在重新扫描一次
经典垃圾回收器
serial收集器
是最老的垃圾回收期 单线程的,它需要所有的线程都停止,对于现在大部分程序来说是不可接受的可是它效率高,稳定性好,内存消耗也是最小的.
ParNew收集器
他是serial收集器的多线程版本,适合回收新生代,一般配合cms 老年代共同使用
线程数量
默认的线程数量和处理器核心数量相同
java -XX:+PrintGCDetails -version
查看当前Java使用的垃圾回收器
设置线程数量
-XX: ParallelGCThreads 20
并发
说明垃圾回收线程,和用户线程同时运行
并行
多个同样的线程同时运行
Parallel Scavenge 收集器
新生代垃圾回收器 采用 标记-复制算法的多线程垃圾回收器
目标
可控的吞吐量 比如运行代码是+垃圾回收用了一百分钟 垃圾回收用了99分钟那么吞吐量就是99%
参数控制
-XXMaxGcPauseMillis
最大垃圾回收停顿时间
这个字段并不是越小越好,因为如果时间停顿越短也就是新生代可能没有回收的垃圾越多,也就会导致垃圾回收器越频繁
-XX GCtimeRatio
设置吞吐量大小
-XX UseAdaptiveSiezPolicy
不需要自己指定指定新生代相关空间的额大小了 系统会自动调节 自适应调节也是 Parallel Scavenge 收集器与parnew的一个重要区别
serial old 收集器
采用标记整理算法的单线程老年代收集齐 现在一般都不用了
parallel old 收集器
多线程 老年代收集器 一般配合 parallel scavenge 年轻代收集器配合使用 因为使用了多线程 而且经过多年沉淀稳定性也特别好
CMS收集器
基础说明
基于标记清除算法,年老代并发低低停顿垃圾回收器
工作原理
初识标记
stop the world
已GCroot 为根标记一下直接关联的对象,速度很快
并发标记
重初识标记的对象开始 遍历整个对象图,耗时长,但是不需要停顿用户线程
重新标记
stop the world
用户线程的变动导致部分对象标记产生变化,使用三色标记方式进行重新标记
并发清除
清理删除掉标记阶段死亡的对象
局限性和缺点
浮游垃圾
因为垃圾清除阶段没有停止用户线程,所以可能会有一些浮游垃圾只能等到下一次GC的时候才能清除
并发标记阶段引起的资源抢占
因为并发标记采用多线程方式,所以会导致GC线程和用户线程进行抢占的问题
空间碎片
因为CMS 收集器 采用的标记-清除的算法,虽然收集的过程不需要用户线程的停止,但是会有大量的空间碎片,即使空间够用,如果有大对象需要连续的内存空间无法存储的时候就要触发一次full gc 导致整个用户线程暂停
G1 垃圾回收器
说明
G1垃圾回收器旨在为运行在多核处理器上的具有大量内存的应用程序提供高吞吐量和低延迟。
工作原理
区域化堆内存
把堆内存分为多个区域,这样可以提高垃圾回收的效率,先回收价值最大的区域
并发和并行处理
G1 可以使用多核心并发垃圾回收 这样增加了回收的效率
可预测的停顿时间模型
因为是按区域回收,所以可以机会可以提前预测出来回收的时间,所以可以根据用户的停顿时间来设置停顿时间
增量式清理
因为把内存分为多个分区 ,所以不必等到垃圾回收满了之后在进行回收,也就是采用增量回收
回收整理
因为采用的是标记-整理,标记-复制的 算法,所以可以支持垃圾回收整理,这样也不会出现CMS 那种大的对象无法存放的问题
工作过程
初时标记
stop the world
已GCroot 为根标记一下直接关联的对象,速度很快
并发标记
重初识标记的对象开始 遍历整个对象图,耗时长,但是不需要停顿用户线程
最终标记
这个阶段完成了标记过程中的一些遗留工作,如处理剩余的SATB(Snapshot-At-The-Beginning)记录和重新标记某些区域。这个阶段可能会引起短暂的停顿。
筛选回收
在这个阶段,G1将会回收那些已经被确定为没有存活对象的区域,并且在必要时,它也会移动对象以减少碎片化。这是一个需要停顿的阶段。
参数设置
开启G1垃圾回收器
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
这是一个目标参数,用于指定垃圾收集器尝试达到的最大停顿时间,单位是毫秒。G1将尽量控制垃圾收集的停顿时间以不超过这个值,但这不是一个硬性保证
如果这个参数设置的太小,垃圾产生的速度大于回收的速度,最后有可能触发一次fullGc 就更影响 了
-XX:GCPauseIntervalMillis
两次垃圾回收中间的间隔 这个不是一个常用变量,一般可以用来微调性能
XX:InitiatingHeapOccupancyPercent (IHOP)
G1垃圾回收器启动标记周期的堆占用的百分比。当堆中使用超过此百分比的内存时,G1将开始并发标记周期。默认值根据JVM版本可能有所不同,但通常设置在堆的40-50%左右
-XX:ConcGCThreads
并发收集的线程数量根CPU的数量相关
XX:G1HeapRegionSize
每个区域占用的大小通常是1-32M 必须是2的幂
缺点
资源消耗
G1垃圾回收器在并行回收和后台并发回收期间可能会使用更多的CPU资源。如果应用程序本身就要求高CPU利用率,G1的资源消耗可能会与应用程序的性能产生竞争
记忆集管理
G1垃圾回收器维护了一个记忆集(Remembered Set),用于记录跨区域引用的对象。记忆集的管理会增加额外的内存开销和处理时间
预热时间
对于一些特定的使用场景,如短任务或者低延迟的交互式应用程序,G1回收器在启动的时候可能需要一段时间来预热,并达到最优的垃圾回收效率
配置复杂化
G1垃圾回收器提供了大量的可调参数,这既是一个优点也是一个缺点。对于未经优化的默认设置,可能会发现它的性能没有预期的好,因此需要进行仔细的调优以达到最佳效果
内存占用
为了实现其功能,G1可能比其他垃圾回收器有更大的内存占用。对于内存资源受限的环境来说,这可能是一个问题。
停顿时间不一致
尽管G1旨在提供可预测的停顿时间,但在实践中,停顿时长可能因为多种原因而有所不同,有时可能会超出设定的目标。
碎片处理
G1执行回收整理以减少内存碎片,但在堆内存非常大的情况下,碎片整理的效率可能会降低,有时可能会导致较长的停顿。
适应性
G1的许多决策都是自适应的,它根据先前的垃圾收集性能来调整策略。在某些情况下,这种自适应机制可能不会如预期工作,导致性能波动。
低延迟垃圾回收器
说明
判断一个垃圾回收器的指标有三个,低延迟,吞吐量,内存占用,随着硬件的发展,内存的占用越来约被允许,但是延迟缺越来越不被允许,所以低延迟垃圾回收期就营运而生
shenandoah回收器
主要特性
回收过程
大部分时间都是并发的包标记 清理,压缩,回收都是并发的 这意味着减少停顿时间
回收过程
初识标记
已GCroot 为根标记一下直接关联的对象,速度很快
并发标记
重初识标记的对象开始 遍历整个对象图,耗时长,但是不需要停顿用户线程
最终标记
处理剩余的标记工作,包括处理并发阶段产生的变化。这个阶段可能会有短暂的STW
并发清理
清理空闲区域
垃圾回收器会识别那些完全没有被使用的内存区域,也就是说这些区域中的对象全都是垃圾。然后,它会将这些区域标记为可用,以便未来的对象分配时可以重用。
更新内存管理数据结构
清理过程还包括更新内存管理的元数据,比如空闲列表或者位图,这样系统就知道哪些内存是空闲的。
处理引用
一些垃圾回收器可能需要处理最终性引用(finalizer references),软引用(soft references),弱引用(weak references)和虚引用(phantom references)。并发清理阶段可能会涉及识别这些特殊类型的引用,并根据它们的使用情况做出相应的清理工作。
准备压缩阶段
如果垃圾回收器接下来会进行内存压缩(也就是将存活的对象移动到堆的一端以减少内存碎片),并发清理可能会准备相关的任务,以便压缩能够高效进行。
并发回收
对象移动
存活的对象被移动到新的位置。这通常涉及到更新所有指向被移动对象的引用,确保它们指向对象的新位置。
读写屏障
写屏障是在对象的引用发生变化时自动执行的代码片段,它记录那些在并发标记期间被修改的对象信息,以便垃圾回收器在最终标记阶段可以迅速找到这些变化并更新其标记信息
转发指针
在对象被移动时,原来的空间通常会存放一个转发指针,指向对象新的位置,这样即使在并发回收期间应用程序访问了旧的引用,也能通过转发指针找到对象的新位置。
同步点
尽管大部分工作是并发进行的,但在某些时候可能需要短暂的同步点(这不是完整的STW暂停),以确保所有线程的视图是一致的,特别是在并发回收接近完成时。
读写屏障
Shenandoah利用了读写屏障(Read/Write Barriers)技术,在并发阶段处理对象引用,以确保内存的一致性。
已空间换时间
内存压缩
通过并发移动对象,Shenandoah可以有效地解决内存碎片问题,保持内存的高效利用。
低停顿时间
Shenandoah的设计是为了实现尽可能低的停顿时间,即使是在大内存堆上也能保持这一特性。