JVM 之内存管理
一. 概述
在 c 语言开发,内存的申请以及释放,都由开发人员自己控制;而在 java 开发过程中,交由 JVM 来自行控制,开发人员无须关心内存的申请以及释放;
那么 JVM 对内存是如何管理的?JVM 的内存管理主要包含内存的分配以及释放等模块。
二. 内存管理
在讲述 JVM 内存管理之前,需要讲解的 JVM 的内存组成结构。
1. 内存组成结构
程序计数器 用来记录当前线程所执行的字节码的行号指示器
虚拟机栈 线程运行期间,所使用的内存区域;每个方法在执行的同时会创建一个栈帧,用于记录局部变量表、操作数栈、动态链接、方法出口等信息;通过-Xss 参数来这是虚拟机栈的大小
本地方法栈 线程调用 Native 方法服务开辟出的一个内存区域;
堆 该区域主要保存对象信息以及数组信息;通过-Xms 参数设置堆最小值,-Xmx 参数设置堆最大值
方法区 该区域主要是保存类信息、常量、静态变量、即时编译器编译后的代码数据(主要是字面量、符号引用,该区域叫做运行时常量池),通过-XX:PermSize 和-XX:MaxPermSize 参数来设置方法区大小;
另外还有一个区域,不归 JVM 管理的,直接内存。我们通过 ByteBuffer.allocateDirect 在该区域分配一块内存使用;同时不使用该内存时,也是需要手动释放掉,通过调用 DirectByteBuffer.cleaner 方法获取 Cleaner 对象,接着调用 Cleaner 对象的 clean 方法进行内存释放;
虚拟机栈的结构,如下图:
布局变量表 用于存放方法参数和方法内部定义的局部变量;
操作数栈 记录着指令往操作数栈中写入和提取内容,也就是出栈/入栈操作;
动态链表 记录着指向运行时常量池中该栈帧所属方法的引用;
方法返回地址
在对堆中,被划分为两个不同的区域:新生代(Young Generation)和老年代(Old Generation-Tenured Generation),通过-Xmn 参数来设置新生代大小;-XX:NewRatio=4,调整新生代与老年代的比例,例子的意思是:新生代:老年代=1:4;新生代又被划分为三个区域:Eden、From Survivor、To Survivor;比例通过-XX:SurvivorRatio=8 进行设置,例子的意思为:Eden:From Survivor: To Survivor=8:1:1。如下图:
PS. 后续会补充真实结构,代码层面上的介绍;这里只介绍概念;
2. 内存分配
一个对象被创建过程中,首先交由类加载进行加载 Class 信息到方法区中,然后在堆中的分配一块内存来保存被创建对象信息;常规情况下,会在 Eden 区域分配一块内存保存被创建对象的信息;如果该对象所需要的内存比较大时,会在老年代区域分配;通过 JVM 参数-XX:PretenureSizeThreshold 来控制超过这么大容量的对象直接保存到老年代。
3. 内存释放
3.1 哪些内存需要回收
程序计数器、虚拟机栈、本地方法栈与线程生命周期一致,JVM 自行回收;而堆和方法区都是在运行期间动态分配的,所以需要针对该内存区域进行回收,我们常说的垃圾回收器主要是针对该内存区域进行回收。
在堆中,只有当对象不再被引用时,才会被回收;如何定义对象不再被引用?书上讲了有两种模式,引用计数算法和可达性分析算法;可达性分析算法,通过 GC Roots 起始点,可以知道有哪些对象被引用着;那些对象没在 GC Roots 链路中的,则会被回收。
GC Roots 对象包括下面几种:
虚拟机栈中引用的对象
方法区中类静态属性引用的对象
方法区中的常量引用的对象
本地方法栈中共 JNI 引用的对象;
PS. 引用有分为四种,强引用、软引用、弱引用、虚引用;
强引用 -> 不会被垃圾回收器回收;
软引用 -> 在将要发生内存溢出时,才会将其回收;SoftReference
弱引用 -> 存活到下一次垃圾回收发生之前;WeakReference
虚引用 -> 之所以存在该类型,主要目的是在这个对象被收集器回收时收到一个系统通知;PhantomReference
在方法区中,主要回收的是废弃常量和无用的类;废弃常量:当堆中的对象中没有引用该常量时,就会被释放;无用的类,当满足下列三个条件时才会被释放;
该类的所有实例都已经被回收
加载该类的 ClassLoader 已经被回收
该类对应的 Class 对象没有被引用
PS.困惑:什么情况下,ClassLoader 会被回收?
3.2 什么时候回收
当新生代中的 Eden 区没有足够空间区进行分配时虚拟机将其发起 Minor GC,Minor GC 主要是新生代内存进行回收。
当老年代没有足够空间进行分配时,虚拟机将其发起 Full GC,Full GC 对老年代进行内存回收。
PS. 这只是我个人的理解,供参考
3.3 如何回收
在新生代回收(Minor GC)时,普遍采用“复制”算法;会将 Eden 和 Survivoor 中还存活的对象一次性复制到另外一个 Survivor 空间上;当对象经历 n 次 Minir GC 时依然存活时,会将该存活的对象保存到老年代;我们通过-XX:MaxTenuringThreshold 来设置这个 n 的值;
在老年代,采用“标志-清除”算法或“标志-整理”算法。
三. 垃圾回收算法
1. 标志-清除算法
标志-清除(Mark-Sweep)算法:首先标志出所有需要回收的对象,在标志完成后统一回收所有被标志的对象;
2. 复制算法
将可用内存按容量划分大小相等的两块,每次只使用其中一块。当这一块的内存被用完了,就将还存活者的对象复制到另外一块内存上,然后再把已使用过的内存空间一次清理掉;如下图
在 JVM 中,新生代不会用相同大小的内存进行复制算法;因为存活的对象都会很小,所以在新生代里又分为 Eden、From Survivor、To Survivor 区;每次做 Minior GC 时,会将 Eden 和 From Survivor 区存活的对象复制到 To Suvivor 区域;当 To Suvivor 内存不够时,会将老年代做担保;
参数-XX:HandlePromotioinFailure 设置值是否允许担保失败;
3. 标志-整理算法
标志-整理(Mark-Compact)算法:首先是标志需要回收的对象,在标志完成后,将还存活的对象都向一端移动,然后直接清理掉端边界以外的内存;
4. 分代收集算法
根据对象存活周期的不同,将内存划分为几块;然后根据内存存放对象的特点采用上述的算法进行回收;新生代采用“复制”算法,而老年代采用”标记-清理“算法或”标记-整理“算法进行回收;
四. 垃圾回收器
1. 新生代
1.1 Serial
特点:单线程、简单高效(与其他收集器的单线程相比),对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。
应用场景:适用于 Client 模式下的虚拟机。
1.2 ParNew
特点:多线程、ParNew 收集器默认开启的收集线程数与 CPU 的数量相同,在 CPU 非常多的环境中,可以使用-XX:ParallelGCThreads 参数来限制垃圾收集的线程数。和 Serial 收集器一样存在 Stop The World 问题
应用场景:ParNew 收集器是许多运行在 Server 模式下的虚拟机中首选的新生代收集器,因为它是除了 Serial 收集器外,唯一一个能与 CMS 收集器配合工作的。
1.3 Parallel Scavenge
与吞吐量关系密切,故也称为吞吐量优先收集器。
特点:属于新生代收集器也是采用复制算法的收集器,又是并行的多线程收集器(与 ParNew 收集器类似)。
该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC 自适应调节策略(与 ParNew 收集器最重要的一个区别)
GC 自适应调节策略:Parallel Scavenge 收集器可设置-XX:+UseAdptiveSizePolicy 参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为 GC 的自适应调节策略。
Parallel Scavenge 收集器使用两个参数控制吞吐量:
-XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间
-XX:GCRatio 直接设置吞吐量的大小。
2. 老年代
2.1 CMS
一种以获取最短回收停顿时间为目标的收集器。
特点:基于标记-清除算法实现。并发收集、低停顿。
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如 web 程序、b/s 服务。
CMS 收集器的运行过程分为下列 4 步:
初始标记:标记 GC Roots 能直接到的对象。速度很快但是仍存在 Stop The World 问题。
并发标记:进行 GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在 Stop The World 问题。
并发清除:对标记的对象进行清除回收。
CMS 收集器的内存回收过程是与用户线程一起并发执行的。
CMS 收集器的工作过程图:
2.2 Serial Old(MSC)
Serial Old 是 Serial 收集器的老年代版本。
特点:同样是单线程收集器,采用标记-整理算法。
应用场景:主要也是使用在 Client 模式下的虚拟机中。也可在 Server 模式下使用。
Server 模式下主要的两大用途
在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用。
作为 CMS 收集器的后备方案,在并发收集 Concurent Mode Failure 时使用。
2.3 Parallel Old
是 Parallel Scavenge 收集器的老年代版本。
特点:多线程,采用标记-整理算法。
应用场景:注重高吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge+Parallel Old 收集器。
3. G1 回收器
具体参考 JVM G1 源代分析和调优书籍
4. ZGC 回收器
具体参考新一代垃圾回收器 ZGC 设计与实现书籍
5. 垃圾回收器参数总结
UseSerialGC 虚拟机运行在 client 模式下的默认值,打开此开关后,使用 Serial+Serial Old 的收集器组合进行内存回收
UseParNewGC 打开此开关后,使用 ParNew + Serial Old 的收集器组合进行内存回收
UseConcMarkSweepGC 打开此开关后,使用 ParNew + CMS + Serial Old 的收集器组合进行内存回收;Serial Old 收集器将作为 CMS 收集器出现 Concurrent Mode Failure 失败后的后备收集器使用
UseParallelGC 虚拟机运行在 Server 模式下的默认值,打开此开关后,使用 Parallel Scavenge + Serial Old 的收集器组合进行内存回收
UseParallelOldGC 打开此开关后,使用 Parallel Scavenge + Parallel Old 的收集器组合进行内存回收
SurvivorRatio 新生代中 Eden 区域中与 Survivor 区域的容量比值,默认为 8;
PretenureSizeThreshold 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配
MaxTenureingThreshold 晋升到老年代的对象年龄。每个对象在坚持过一次 Minor GC 之后,年龄困就增加 1,当超过这个参数值时就进入老年代
UseAdaptiveSizePolicy 动态调整 java 堆中各个区域的大小以及进入老年代的年龄
HandlePremotionFailure 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个 Eden 和 Survivor 区的所有对象都存活的极端情况
ParallelGCThreads 设置并行时内存回收的线程数
GCTimeRatio GC 时间占总时间的比率,默认值为 99,即允许 1%的 GC 时间。仅在使用 Parallel Scavenge 收集器时生效
MaxGCPauseMillis 设置 GC 最大的停顿时间。仅在使用 Parallel Scavenge 收集器生效
CMSInitiatingOccupancyFraction 设置 CNS 收集器在老年代空间被使用多少后触发垃圾收集。默认值为 68%,仅在使用 CMS 收集器时生效
UseCMSCompactAtFullCollection 设置 CMS 收集器在完成垃圾收集后是否要进行一次内存碎片整理。仅在使用 CMS 收集器时生效
CMSFullGCsBeforeCompaction 设置 CMS 收集器在进行若干次垃圾收集后在启动一次内存碎片整理。仅在使用 CMS 收集器时生效
待补充
参考资料
深入理解 Java 虚拟机 ,作者周志明
JVM G1 源代分析和调优,作者彭成寒
新一代垃圾回收器 ZGC 设计与实现 作者彭成寒
版权声明: 本文为 InfoQ 作者【邱学喆】的原创文章。
原文链接:【http://xie.infoq.cn/article/859327fad080697697ab488d8】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论