持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第11天,点击查看活动详情
1. JVM 内存结构
要求
-
掌握 JVM 内存结构划分
-
尤其要知道方法区、永久代、元空间的关系
结合一段 java 代码的执行理解内存划分
-
执行 javac 命令编译源代码为字节码
-
执行 java 命令
1. 创建 JVM,调用类加载子系统加载 class,将类的信息存入方法区
2. 创建 main 线程,使用的内存区域是 JVM 虚拟机栈,开始执行 main 方法代码
3. 如果遇到了未见过的类,会继续触发类加载过程,同样会存入方法区
4. 需要创建对象,会使用堆内存来存储对象
5. 不再使用的对象,会由垃圾回收器在内存不足时回收其内存
6. 调用方法时,方法内的局部变量、方法参数所使用的是 JVM 虚拟机栈中的栈帧内存
7. 调用方法时,先要到方法区获得到该方法的字节码指令,由解释器将字节码指令解释为机器码执行
8. 调用方法时,会将要执行的指令行号读到程序计数器,这样当发生了线程切换,恢复时就可以从中断的位置继续
9. 对于非 java 实现的方法调用,使用内存称为本地方法栈(见说明)
10. 对于热点方法调用,或者频繁的循环代码,由 JIT 即时编译器将这些代码编译成机器码缓存,提高执行性能
说明
-
加粗字体代表了 JVM 虚拟机组件
-
对于 Oracle 的 Hotspot 虚拟机实现,不区分虚拟机栈和本地方法栈
会发生内存溢出的区域
-
不会出现内存溢出的区域 – 程序计数器
-
出现 OutOfMemoryError 的情况
* 堆内存耗尽 – 对象越来越多,又一直在使用,不能被垃圾回收
* 方法区内存耗尽 – 加载的类越来越多,很多框架都会在运行期间动态产生新的类
* 虚拟机栈累积 – 每个线程最多会占用 1 M 内存,线程个数越来越多,而又长时间运行不销毁时
- 出现 StackOverflowError 的区域
* JVM 虚拟机栈,原因有方法递归调用未正确结束、反序列化 json 时循环引用
方法区、永久代、元空间
-
方法区是 JVM 规范中定义的一块内存区域,用来存储类元数据、方法字节码、即时编译器需要的信息等
-
永久代是 Hotspot 虚拟机对 JVM 规范的实现(1.8 之前)
-
元空间是 Hotspot 虚拟机对 JVM 规范的另一种实现(1.8 以后),使用本地内存作为这些信息的存储空间
从这张图学到三点
-
当第一次用到某个类是,由类加载器将 class 文件的类元信息读入,并存储于元空间
-
X,Y 的类元信息是存储于元空间中,无法直接访问
-
可以用 X.class,Y.class 间接访问类元信息,它们俩属于 java 对象,我们的代码中可以使用
从这张图可以学到
-
堆内存中:当一个类加载器对象,这个类加载器对象加载的所有类对象,这些类对象对应的所有实例对象都没人引用时,GC 时就会对它们占用的对内存进行释放
-
元空间中:内存释放以类加载器为单位,当堆中类加载器内存释放时,对应的元空间中的类元信息也会释放
2. JVM 内存参数
要求
- 熟悉常见的 JVM 参数,尤其和大小相关的
堆内存,按大小设置
解释:
-
-Xms 最小堆内存(包括新生代和老年代)
-
-Xmx 最大对内存(包括新生代和老年代)
-
通常建议将 -Xms 与 -Xmx 设置为大小相等,即不需要保留内存,不需要从小到大增长,这样性能较好
-
-XX:NewSize 与 -XX:MaxNewSize 设置新生代的最小与最大值,但一般不建议设置,由 JVM 自己控制
-
-Xmn 设置新生代大小,相当于同时设置了 -XX:NewSize 与 -XX:MaxNewSize 并且取值相等
-
保留是指,一开始不会占用那么多内存,随着使用内存越来越多,会逐步使用这部分保留内存。下同
堆内存,按比例设置
解释:
-
-XX:NewRatio=2:1 表示老年代占两份,新生代占一份
-
-XX:SurvivorRatio=4:1 表示新生代分成六份,伊甸园占四份,from 和 to 各占一份
元空间内存设置
解释:
-
class space 存储类的基本信息,最大值受 -XX:CompressedClassSpaceSize 控制
-
non-class space 存储除类的基本信息以外的其它信息(如方法字节码、注解等)
-
class space 和 non-class space 总大小受 -XX:MaxMetaspaceSize 控制
注意:
- 这里 -XX:CompressedClassSpaceSize 这段空间还与是否开启了指针压缩有关,这里暂不深入展开,可以简单认为指针压缩默认开启
代码缓存内存设置
解释:
-
如果 -XX:ReservedCodeCacheSize < 240m,所有优化机器代码不加区分存在一起
-
否则,分成三个区域(图中笔误 mthod 拼写错误,少一个 e)
* non-nmethods - JVM 自己用的代码
* profiled nmethods - 部分优化的机器码
* non-profiled nmethods - 完全优化的机器码
线程内存设置
****** **官方参考文档 ****
3. JVM 垃圾回收
要求
-
掌握垃圾回收算法
-
掌握分代回收思想
-
理解三色标记及漏标处理
-
了解常见垃圾回收器
三种垃圾回收算法
标记清除法
解释:
-
找到 GC Root 对象,即那些一定不会被回收的对象,如正执行方法内局部变量引用的对象、静态变量引用的对象
-
标记阶段:沿着 GC Root 对象的引用链找,直接或间接引用到的对象加上标记
-
清除阶段:释放未加标记的对象占用的内存
要点:
-
标记速度与存活对象线性关系
-
清除速度与内存大小线性关系
-
缺点是会产生内存碎片
标记整理法
解释:
-
前面的标记阶段、清理阶段与标记清除法类似
-
多了一步整理的动作,将存活对象向一端移动,可以避免内存碎片产生
特点:
-
标记速度与存活对象线性关系
-
清除与整理速度与内存大小成线性关系
-
缺点是性能上较慢
标记复制法
解释:
-
将整个内存分成两个大小相等的区域,from 和 to,其中 to 总是处于空闲,from 存储新创建的对象
-
标记阶段与前面的算法类似
-
在找出存活对象后,会将它们从 from 复制到 to 区域,复制的过程中自然完成了碎片整理
-
复制完成后,交换 from 和 to 的位置即可
特点:
-
标记与复制速度与存活对象成线性关系
-
缺点是会占用成倍的空间
GC 与分代回收算法
GC 的目的在于实现无用对象内存自动释放,减少内存碎片、加快分配速度
GC 要点:
-
回收区域是堆内存,不包括虚拟机栈
-
判断无用对象,使用可达性分析算法,三色标记法标记存活对象,回收未标记对象
-
GC 具体的实现称为垃圾回收器
-
GC 大都采用了分代回收思想
* 理论依据是大部分对象朝生夕灭,用完立刻就可以回收,另有少部分对象会长时间存活,每次很难回收
* 根据这两类对象的特性将回收区域分为新生代和老年代,新生代采用标记复制法、老年代一般采用标记整理法
- 根据 GC 的规模可以分成 Minor GC,Mixed GC,Full GC
分代回收
- 伊甸园 eden,最初对象都分配到这里,与幸存区 survivor(分成 from 和 to)合称新生代,
- 当伊甸园内存不足,标记伊甸园与 from(现阶段没有)的存活对象
- 将存活对象采用复制算法复制到 to 中,复制完毕后,伊甸园和 from 内存都得到释放
- 将 from 和 to 交换位置
- 经过一段时间后伊甸园的内存又出现不足
- 标记伊甸园与 from(现阶段没有)的存活对象
- 将存活对象采用复制算法复制到 to 中
- 复制完毕后,伊甸园和 from 内存都得到释放
- 将 from 和 to 交换位置
- 老年代 old,当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)
GC 规模
-
Minor GC 发生在新生代的垃圾回收,暂停时间短
-
Mixed GC 新生代 + 老年代部分区域的垃圾回收,G1 收集器特有
-
Full GC 新生代 + 老年代完整垃圾回收,暂停时间长,应尽力避免
三色标记
即用三种颜色记录对象的标记状态
-
黑色 – 已标记
-
灰色 – 标记中
-
白色 – 还未标记
- 起始的三个对象还未处理完成,用灰色表示
- 该对象的引用已经处理完成,用黑色表示,黑色引用的对象变为灰色
- 依次类推
- 沿着引用链都标记了一遍
- 最后为标记的白色对象,即为垃圾
并发漏标问题
比较先进的垃圾回收器都支持并发标记,即在标记过程中,用户线程仍然能工作。但这样带来一个新的问题,如果用户线程修改了对象引用,那么就存在漏标问题。例如:
- 如图所示标记工作尚未完成
- 用户线程同时在工作,断开了第一层 3、4 两个对象之间的引用,这时对于正在处理 3 号对象的垃圾回收线程来讲,它会将 4 号对象当做是白色垃圾
- 但如果其他用户线程又建立了 2、4 两个对象的引用,这时因为 2 号对象是黑色已处理对象了,因此垃圾回收线程不会察觉到这个引用关系的变化,从而产生了漏标
- 如果用户线程让黑色对象引用了一个新增对象,一样会存在漏标问题
因此对于并发标记而言,必须解决漏标问题,也就是要记录标记过程中的变化。有两种解决方法:
- Incremental Update 增量更新法,CMS 垃圾回收器采用
* 思路是拦截每次赋值动作,只要赋值发生,被赋值的对象就会被记录下来,在重新标记阶段再确认一遍
- Snapshot At The Beginning,SATB 原始快照法,G1 垃圾回收器采用
* 思路也是拦截每次赋值动作,不过记录的对象不同,也需要在重新标记阶段对这些对象二次处理
* 新加对象会被记录
* 被删除引用关系的对象也被记录
垃圾回收器 - Parallel GC
-
eden 内存不足发生 Minor GC,采用标记复制算法,需要暂停用户线程
-
old 内存不足发生 Full GC,采用标记整理算法,需要暂停用户线程
-
注重吞吐量
垃圾回收器 - ConcurrentMarkSweep GC
- 它是工作在 old 老年代,支持并发标记的一款回收器,采用并发清除算法
* 并发标记时不需暂停用户线程
* 重新标记时仍需暂停用户线程
-
如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC
-
注重响应时间
垃圾回收器 - G1 GC
-
响应时间与吞吐量兼顾
-
划分成多个区域,每个区域都可以充当 eden,survivor,old, humongous,其中 humongous 专为大对象准备
-
分成三个阶段:新生代回收、并发标记、混合收集
-
如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC
G1 回收阶段 - 新生代回收
- 初始时,所有区域都处于空闲状态
- 创建了一些对象,挑出一些空闲区域作为伊甸园区存储这些对象
- 当伊甸园需要垃圾回收时,挑出一个空闲区域作为幸存区,用复制算法复制存活对象,需要暂停用户线程
- 复制完成,将之前的伊甸园内存释放
- 随着时间流逝,伊甸园的内存又有不足
- 将伊甸园以及之前幸存区中的存活对象,采用复制算法,复制到新的幸存区,其中较老对象晋升至老年代
- 释放伊甸园以及之前幸存区的内存
G1 回收阶段 - 并发标记与混合收集
- 当老年代占用内存超过阈值后,触发并发标记,这时无需暂停用户线程
- 并发标记之后,会有重新标记阶段解决漏标问题,此时需要暂停用户线程。这些都完成后就知道了老年代有哪些存活对象,随后进入混合收集阶段。此时不会对所有老年代区域进行回收,而是根据暂停时间目标优先回收价值高(存活对象少)的区域(这也是 Gabage First 名称的由来)。
- 混合收集阶段中,参与复制的有 eden、survivor、old,下图显示了伊甸园和幸存区的存活对象复制
- 下图显示了老年代和幸存区晋升的存活对象的复制
- 复制完成,内存得到释放。进入下一轮的新生代回收、并发标记、混合收集