荐 2 自动内存管理机制(一)运行时数据区域、垃圾回收算法和垃圾回收器
程序员文章站
2022-07-10 21:09:25
文章目录自动内存管理机制(一)运行时数据区域、垃圾回收算法和垃圾回收器运行时数据区域垃圾收集算法(方法论)垃圾收集器(具体实现)附录响应时间和吞吐量参考自动内存管理机制(一)运行时数据区域、垃圾回收算法和垃圾回收器运行时数据区域Java堆定义:Java虚拟机所管理的内存区域,用于存放实例数据(对象)。特点:线程共享。空间不足会抛出OutOfMemoryError异常。Java虚拟机所管理的最大的一块内存,几乎所有的实例对象都在Java堆中分配内存。为了方便无用数据(主要是无用对象)的回...
自动内存管理机制(一)运行时数据区域、垃圾回收算法和垃圾回收器
运行时数据区域
Java堆
- 定义:Java虚拟机所管理的内存区域,用于存放实例数据(对象)。
-
特点:
- 线程共享。
- 空间不足会抛出OutOfMemoryError异常。
- Java虚拟机所管理的最大的一块内存,几乎所有的实例对象都在Java堆中分配内存。
- 为了方便无用数据(主要是无用对象)的回收,Java堆和方法区又划分为:
- 年轻代 Young generation(属于Java堆的一部分区域,又细分为Eden区(新生代),From Survivor区,To Survivor区)
- 年老代 Tenured/Old generation(属于Java堆的一部分区域)
- 永久代 Permanent generation(方法区)
-
实例数据
- 对象头
- 对象自身运行时数据,官方称为“Mark Word”,包括哈希码、GC分代年龄和锁标志位。
- 指向方法区对应类型数据的指针(HotSpot虚拟机采用直接指针访问方式的对象访问)。
- 数组对象还会有一个额外的部分用于存储数组长度。
- 对象的字段值。
- 对象头
-
其他:
- class文件中的类型数据被加载到方法区后,会在Java堆中创建一个类对象,作为外部访问类型数据的入口。
- 程序运行时,遇到new关键字才会创建对象并开始分配内存。
方法区
- 定义:方法区不全被Java虚拟机所管理,用于存放类型数据。
-
特点:
- 线程共享。
- 空间不足会抛出OutOfMemoryError异常。
- 每个被加载后的类在方法区都有一个类私有的存储类型数据的内存区域。
- JDK7及其之前,方法区存在于一块被Java虚拟机所管理的内存区域(称为“永久代”);JDK7将永久代中的字符串池(见参考[2])和静态变量转移到Java堆中存储;JDK8将剩余的部分转移到一块本地内存中(称为“元数据区”)存储。
-
类型数据
- 类信息,包括类版本信息,类、父类、接口、字段和方法描述信息。
- 常量,存储在方法区的运行时常量池,包括字面量(文本字符串、常量)和符号引用(类和接口的全限定名、字段的名称和描述符、方法的名称和描述符)。
- 即时编译器编译后的代码。
虚拟机栈
- 定义:Java虚拟机所管理的内存区域,用于辅助执行Java方法。
-
特点:
- 线程隔离(私有),生命周期与线程相同。
- 内存不足会先抛出*Error异常,且扩展失败时抛出OutOfMemoryError异常。
- 每个方法被执行时都会创建一个栈帧(由局部变量表、操作栈、动态链接、方法出口组成),方法被调用直至执行完成的过程对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
本地方法栈
- 定义:Java虚拟机所管理的内存区域,用于辅助执行本地方法。
-
特点:
- 线程隔离(私有),生命周期与线程相同。
- 内存不足会先抛出*Error异常,且扩展失败时抛出OutOfMemoryError异常。
- 虚拟机规范对本地方法栈中方法使用的语言、使用方式与数据结构没有作强制规定,HotSpot虚拟机直接把本地方法栈和虚拟机栈合二为一。
程序计数器
- 定义:Java虚拟机所管理的一块较小的内存区域,用于存储当前所执行的字节码的地址。
-
特点:
- 线程隔离(私有),生命周期与线程相同。
- 不会出现内存不足的情况,所以不会抛出OutOfMemoryError异常。
- 字节码解释器就是通过改变程序计数器的值来选取下一条要执行的字节码指令。
直接内存
- 定义:直接内存是本地内存的一块区域,不被Java虚拟机所管理,也经常被用到。
-
特点:
- 线程共享。
- 空间不足(即本地剩余内存不足)时会抛出OutOfMemoryError异常。
- JDK1.4中新加入了NIO(New Input/Output)类,可以使用本地函数库直接分配本地内存,并通过一个存储在Java堆里的DirectByteBuffer对象作为该内存的引用进行操作。
垃圾收集算法(方法论)
说明:“垃圾”指“无用对象”。
标记-清除算法
- 过程:标记无用对象,清理被标记对象。
- 缺点:无用对象清除后会产生大量不连续的内存碎片。可能导致在分配较大对象时,虽然还有很多空闲内存,但却由于无法找到足够的连续内存而不得不触发一次垃圾收集动作。
复制算法
- 过程:将内存划分为两块,每次只使用其中的一块。当一块内存用完时,将存活的对象复制到另一块内存中,再把原来使用的那块内存一次性清理掉。
- 适用场景:存活对象比较少时(复制动作开销少)。
- 缺点:① 会有一块内存处于空闲状态。 ② 不适用于存活对象比较多的场景(复制动作开销大)。
标记-整理算法
- 过程:标记无用对象,将存活对象向一边移动,清理存活对象边界以外的内存。
- 适用场景:存活对象比较多时(移动动作开销少)。
- 缺点:不适用于存活对象比较少的场景(移动动作开销大)。
垃圾收集器(具体实现)
Serial收集器
- 年轻代收集器
- 采用复制算法
- 单线程(停止其他所有的工作线程-Stop The World后,开启一条线程进行垃圾收集)
- 适用于桌面级应用场景(即虚拟机运行再Client模式下)
- 最基本最悠久的收集器,JDK1.3.1之前是虚拟机年轻代收集器的唯一选择
ParNew收集器(Serial收集器的多线程版本)
- 年轻代收集器
- 采用复制算法
- 多线程并行(并行,停止其他所有的工作线程后,开启多条线程进行垃圾收集)
- 适用于虚拟机运行再Server模式下,并且追求响应速度
- 单CPU下不一定比Serial收集器好,多CPU下具有优势
Parallel Scavenge收集器
- 年轻代收集器
- 采用复制算法
- 多线程(并行,停止其他所有的工作线程后,开启多条线程进行垃圾收集)
- 适用于虚拟机运行在Server模式下,并且追求吞吐量(通过参数可以精确控制吞吐量,响应时间和吞吐量的解释见附录)
- 具有GC自适应调节功能——设置好堆内存大小,并设定好目标(最大停顿时间或吞吐量)后开启GC自适应调节策略(GC Ergonomics),虚拟机会根据当前系统的运行情况收集性能监控信息,动态的调整Eden区大小,Eden区和Survivor区比例,晋升年老代对象年龄等细节参数,以达到设定目标。
Serial Old收集器(Serial收集器的年老代版本)
- 年老代收集器
- 采用标记-整理算法
- 单线程(停止其他所有的工作线程-Stop The World后,开启一条线程进行垃圾收集)
- 适用于桌面级应用场景(即虚拟机运行再Client模式下);作为CMS收集器的后备预案。
Parallel Old收集器(Parallel Scanvage收集器的年老代版本)
- 年老代收集器
- 采用标记-整理算法
- 多线程(并行,停止其他所有的工作线程后,开启多条线程进行垃圾收集)
- 适用于虚拟机运行在Server模式下,并且追求吞吐量
CMS收集器(Concurrent Mark Sweep)
- 年老代收集器
- 采用标记-清除算法
- 多线程(并行,不停止其他工作线程,开启多条线程进行垃圾收集)
- 适用于虚拟机运行再Server模式下,并且追求响应速度
- 过程分析:
- 初始标记:标记一下GC Roots能直接关联到的对象,停顿很短。
- 并发标记:即进行GC Roots Tracing的过程,时间相对于初始标记较长。
- 重新标记:修正并发标记期间由于程序继续运行而导致的标记产生变动的那部分对象的标记记录,停顿时间略长于初始标记,但远比并发标记短。
- 并发清理:清理标记的无用对象,时间相对于初始标记和重新标记较长。
- 缺陷分析:
- 对CPU资源敏感。并发执行时会占用一部分CPU资源导致用户线程执行时间延长,会降低吞吐量。
- 可能由于无法处理浮动垃圾出现“Concurrent Mode Failure”导致一次Full GC的产生,并临时启用Serial Old收集器进行年老代垃圾收集,这样停顿时间就很长了。并发清理时产生的垃圾称为浮动垃圾,若并发清理时剩余的内存无法满足浮动垃圾,便会出现上述问题。因此,CMS收集器并不是等年老代快满时触发Minor GC,默认设置阈值为68%(可通过参数设置),即默认预留了32%的老年代内存存放浮动垃圾。
- 由于采用标记-清除算法,收集结束后可能会产生大量空间碎片。因此,CMS收集器可通过参数设置执行多少次Full GC后进行一次碎片整理。
G1收集器(Garbage First)
- Java堆收集器(包括年老代和年轻代)
- 采用标记-整理算法
- 可以精确的控制垃圾回收时间(毫秒级),基本不牺牲吞吐量
- 没有采用分代收集算法,而是采用区域垃圾优先算法:将整个Java堆划分为多个大小固定的独立区域(Regian);跟踪这些区域里面的垃圾堆积程度,并维护一个优先列表;每次根据允许的收集时间,优先回收垃圾最多的区域。
附录
响应时间和吞吐量
响应时间:用户交互场景下追求响应时间。大部分垃圾收集器在收集垃圾的时候会停止其他所有工作线程,工作线程停止的时间长了用户会感到明显的停顿,降低并尽量做到让用户感受不到这种停顿即追求响应时间,可通过缩短GC停顿时间来实现。
吞吐量:后台任务运行场景下追求吞吐量。吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。吞吐量大则CPU利用率高,完成任务的总时间就短。
响应时间和吞吐量的平衡:追求响应时间是以牺牲吞吐量和新生代空间来换取的,需要根据实际场景合理的平衡和选择。
- 如原来新生代大小500M,每10s发生一次GC,每次GC停顿时间100ms,此时吞吐量throughput1=10/(10+0.1)=0.990;
- 为了缩短GC停顿时间,将新生代大小设置为300M,现在每5s发生一次GC,每次GC停顿时间70ms,此时吞吐量throughput2=5/(5+0.07)=0.986。停顿时间虽然减少了,但新生代空间也变小了,吞吐量也下降了。
参考
[1] 《深入理解Java虚拟机》
[2] 理解Java字符串常量池与intern()方法
本文地址:https://blog.csdn.net/ChenTianyu666/article/details/107367525