文章目录
- 一、内存管理
- 1.1 区域划分
- 1.2 垃圾回收
- 1.3 垃圾回收算法
- 二、垃圾回收器
- 2.1 CMS
- 2.2 G1
- 2.3 Shenandoah【暂未用到】
- 2.4 ZGC【暂未用到】
- 三、故障处理工具
- 3.1 jps
- 3.2 jstat
- 3.3 jinfo
- 3.4 jmap + mat
- 3.5 jhat
- 3.6 jstack
一、内存管理
1.1 区域划分
- JVM 内存区域
- 程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的
行号指示器
。每条线程都需要有一个独立的程序计数器。 - 虚拟机栈(Java Virtual Machine Stack)描述的是Java方法执行的
线程内存模型
:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。 - 本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的
本地(Native)方法服务
。 - 堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是
存放对象实例
,Java世界里“几乎”所有的对象实例都在这里分配内存。 - 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
- 运行时常量池(Runtime Constant Pool)是
方法区的一部分
。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。 - 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以
使用Native函数库直接分配堆外内存
,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
- 模拟堆内存溢出
[root@simwor ~]# javac HeapOOM.java
[root@simwor ~]# java -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError HeapOOM
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid3167857.hprof ...
Heap dump file created [27650955 bytes in 0.150 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:267)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:241)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:233)
at java.util.ArrayList.add(ArrayList.java:464)
at HeapOOM.main(HeapOOM.java:10)
1.2 垃圾回收
- 可达性分析
从“GC Roots”的根对象作为起始节点集,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain);如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
- GC Roots
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
- 在方法区中类 静态属性 引用的对象,譬如Java类的引用类型静态变量。
- 在方法区中 常量 引用的对象,譬如字符串常量池(String Table)里的引用。
- 在本地方法栈中JNI(即通常所说的 Native方法 )引用的对象。
- Java 虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
- 所有被 同步锁(synchronized关键字)持有的对象。
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的 回调、本地代码缓存 等。
- 针对某些内存区域进行内存回收的垃圾回收器设定的 区域GC Roots。
- 引用
引用 | Java 类 | 回收时机 |
强引用 | FinalReference | 任何时候都不会被回收 |
软引用 | SoftReference | 内存溢出前被回收 |
弱引用 | WeakReference | 下次垃圾回收时被回收 |
虚引用 | PhantomReference | 仅用于垃圾回收时接收到一个系统通知,无任何意义,下次垃圾回收时被回收 |
- GC
GC | 范围 | 说明 | |
部分收集(Partial GC) | 指目标不是完整收集整个Java堆的垃圾收集 | ||
新生代收集(Minor GC/Young GC) | 指目标只是新生代的垃圾收集 | ||
老年代收集(Major GC/Old GC) | 指目标只是老年代的垃圾收集 | 目前只有CMS收集器会有单独收集老年代的行为 | |
混合收集(Mixed GC) | 指目标是收集整个新生代以及部分老年代的垃圾收集 | 目前只有G1收集器会有这种行为 | |
整堆收集(Full GC) | 收集整个Java堆和方法区的垃圾收集 |
1.3 垃圾回收算法
- 标记清除
- 分为两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
- 它的主要缺点有两个:第一个是 执行效率不稳定,如果Java堆中包含大量需要被回收的对象,这时必须进行大量的标记和清除的动作;第二个是 内存空间的碎片化问题,分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
- 标记复制
- 将可用内存按容量划分为两块区域,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉;
- 优点:解决标记-清除算法面对大量可回收对象时执行效率低的问题;缺点:如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销;代价:缩小了可用内存
- HotSpot虚拟机默认Eden和Survivor的大小比例是 8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会被“浪费”的;
- 任何人都没有办法百分百保证每次回收都只有不多于10%的对象存活,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行 分配担保(Handle Promotion);
- 内存的分配担保也一样,如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便将通过分配担保机制直接进入老年代。
- 标记整理
- 标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低,此外还需要有额外的空间进行分配担保,以应对内存中大部分对象都存活的情况,所以在 老年代一般不能直接选用标记-复制算法。
- 标记-整理(Mark-Compact)算法不是直接对可回收对象进行清理,而是让所有 存活的对象都向内存空间一端移动,然后直接 清理掉边界以外的内存。
- 移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种 对象移动操作必须全程暂停用户应用程序才能进行(Stop The World);但如果跟标记-清除算法那样完全不考虑移动和整理存活对象的话,弥散于堆中的存活对象导致的 空间碎片化问题就只能依赖更为复杂的内存分配器和内存访问器 来解决。
- 是否移动对象都存在弊端,移动则内存回收时会更复杂,不移动停顿时间会更短但内存分配时会更复杂;
- HotSpot 虚拟机里面关注吞吐量(赋值器与收集器的效率总和)的 Parallel Scavenge 收集器是基于标记-整理算法的,而关注延迟的 CMS 收集器则是基于标记-清除算法的。
二、垃圾回收器
收集算法是内存回收的方法论,垃圾收集器就是内存回收的实践者。
如果两个收集器之间存在连线,就说明它们可以搭配使用;收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器。
- Serial
- 单线程 工作的收集器,它进行垃圾收集时必须 暂停其他所有工作线程 直到它收集结束。
- HotSpot 虚拟机运行在客户端模式下的默认新生代收集器,它简单而高效,对于内存资源受限的环境,它是所有收集器里 额外内存消耗(Memory Footprint)最小 的;
- 在用户桌面的应用场景以及近年来流行的部分微服务应用中,分配给虚拟机管理的内存一般来说并不会特别大,收集几十兆甚至一两百兆的新生代(仅仅是指新生代使用的内存,桌面应用甚少超过这个容量),垃圾收集的停顿时间完全可以控制在十几、几十毫秒。
- Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。
- ParNew
ParNew收集器实质上是Serial收集器的多线程并行版本。
- Parallel Scavenge
- CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,Parallel Scavenge 收集器的 目标是达到一个可控制的吞吐量(Throughput)。
- 吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 运行垃圾收集时间 )
- 高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要 适合在后台运算而不需要太多交互的分析任务。
- Parallel Scavenge 收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的 -XX:GCTimeRatio 参数。
- -XX:MaxGCPauseMillis 参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户设定值(以牺牲新生代空间为代价换取的,太小则会频繁GC)。
- -XX:GCTimeRatio 参数的值则应当是一个大于0小于100的整数,默认值为99,即允许最大1%(即1/(1+99))的垃圾收集时间。
- Serial Old
- Serial Old 是 Serial 收集器的老年代版本,它同样是一个 单线程收集器,使用标记-整理算法。
- 这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。
- 如果在服务端模式下,它也可能有两种用途:一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用,另外一种就是作为CMS 收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。
- Parallel Old
- Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。
- 在 注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器这个组合。
2.1 CMS
CMS(Concurrent Mark Sweep)收集器是一种
以获取最短回收停顿时间为目标
的收集器。基于浏览器的B/S系统的服务端上通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非常符合这类应用的需求。
初始标记、重新标记这两个步骤仍然需要“Stop The World”。
- 初始标记(CMS initial mark):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快;
- 并发标记(CMS concurrent mark):从GC Roots的直接关联对象 通过增量更新的方式开始并发遍历整个对象图 的过程;
- 重新标记(CMS remark):修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录;
- 并发清除(CMS concurrent sweep):清理删除掉标记阶段判断的已经死亡的对象。
- 并发的可达性分析
- 白色:表示对象尚未被垃圾收集器访问过;黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过;灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。
- 当且仅当:赋值器插入了一条或多条从黑色对象到白色对象的新引用 && 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用:时,会产生“对象消失”的问题,即原本应该是黑色的对象被误标为白色。
- 增量更新:破坏第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。
- 原始快照:破坏第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。
- 缺点
- CMS收集器 对处理器资源非常敏感,在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说处理器的计算能力)而导致应用程序变慢,降低总吞吐量。
- CMS收集器 无法处理“浮动垃圾”(Floating Garbage):在CMS的并发标记和并发清理阶段用户线程产生的垃圾称为“浮动垃圾”。
- 有 可能出现“Con-current ModeFailure”失败 进而导致另一次完全“Stop The World”的Full GC的产生:
3.1 CMS收集器必须预留一部分空间供并发收集时的用户程序运作使用;
3.2 CMS运行期间预留的内存无法满足程序分配新对象的需要时会出现一次“并发失败”(Concurrent Mode Failure),将临时启用Serial Old收集器来重新进行老年代的垃圾收集;
3.3 参数-XX:CMSInitiatingOccupancyFraction 控制老年代使用了多少的空间后收集器被激活。 - CMS是一款基于“标记-清除”算法实现的收集器,收集结束时会有 大量空间碎片 产生:设计者提供 -XX:+UseCMS-CompactAtFullCollection开关参数用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程、-XX:CMSFullGCsBeforeCompaction 在执行过若干次不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理。
2.2 G1
G1是一款主要面向服务端应用的垃圾收集器,它
开创了收集器面向局部收集的设计思路和基于Region的内存布局形式
,被Oracle官方称为“全功能的垃圾收集器”(Fully-Featured Garbage Collector)。
- 简介
- 作为CMS收集器的替代者和继承人,设计者们希望做出一款 “符合停顿时间模型”(PausePrediction Model)的收集器;
- 停顿时间模型的意思是能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标,这几乎已经是实时Java(RTSJ)的中 软实时垃圾收集器 特征了。
- G1收集器之前所有的其他收集器垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老年代(Major GC),再要么就是整个Java堆(Full GC)。
- G1可以 面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。
- G1 把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间或者老年代空间。
- Region中还有一类特殊的Humongous区域,专门用来存储 大对象(超过半个Region容量,Region大小参数-XX:G1HeapRegionSize);而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待。
- G1收集器之所以能建立可预测的停顿时间模型,是因为它 将Region作为单次回收的最小单元,跟踪各个Region里面的垃圾堆积的“价值” (即回收所获得的空间大小以及回收所需时间)大小,每次根据用户设定允许的收集停顿时间(-XX:MaxGCPauseMillis),优先处理回收价值收益最大的那些Region,有计划地避免在整个Java堆中进行全区域的垃圾收集。
- 细节性问题
- 跨Region的引用对象:使用记忆集(哈希表)避免全堆作为GC Roots扫描,每个Region都维护有自己的记忆集,G1收集器要比其他的传统垃圾收集器有着更高的内存占用负担。
- 收集线程与用户线程互不干扰:G1 通过原始快照的方式开始并发遍历整个对象图;G1为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针划分部分空间用于并发回收过程中的新对象分配,如果内存回收的速度赶不上内存分配的速度会导致Full GC而产生长时间“Stop The World”。
- 停顿预测模型的可靠性:在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。
- 垃圾回收流程
与CMS的“标记-清除”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region之间)上看又是基于“标记-复制”算法实现,无论如何,这两种算法都意味着
G1运作期间不会产生内存空间碎片
,垃圾收集完成之后能提供规整的可用内存。
- 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
- 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
- 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
- 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
2.3 Shenandoah【暂未用到】
2.4 ZGC【暂未用到】
三、故障处理工具
3.1 jps
jps(JVM Process Status Tool):列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main函数所在的类)名称以及这些进程的本地虚拟机唯一 ID(LVMID,Local Virtual Machine Identifier)。
选项 | 作用 |
-q | 只输出LVMID,省略主类的名称 |
-m | 输出虚拟机进程启动时传递给主类 main() 函数的参数 |
-l | 输出主类的全名,如果进程执行的是 JAR 包,则输出 JAR 路径 |
-v | 输出虚拟机进程启动时的 JVM 参数 |
3.2 jstat
jstat(JVM Statistics Monitoring Tool)是用于监视虚拟机各种运行状态信息的命令行工具,它可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据。
命令格式:jstat [ generalOption | outputOptions vmid [ interval[s|ms] [ count ] ]
options | explain |
class | Displays statistics about the behavior of the class loader. |
compiler | Displays statistics about the behavior of the Java HotSpot VM Just-in-Time compiler. |
gc | Displays statistics about the behavior of the garbage collected heap. |
gccapacity | Displays statistics about the capacities of the generations and their corresponding spaces. |
gccause | Displays a summary about garbage collection statistics (same as -gcutil), with the cause of the last andcurrent (when applicable) garbage collection events. |
gcnew | Displays statistics of the behavior of the new generation. |
gcnewcapacity | Displays statistics about the sizes of the new generations and its corresponding spaces. |
gcold | Displays statistics about the behavior of the old generation and metaspace statistics. |
gcoldcapacity | Displays statistics about the sizes of the old generation. |
gcmetacapacity | Displays statistics about the sizes of the metaspace. |
gcutil | Displays a summary about garbage collection statistics. |
printcompilation | Displays Java HotSpot VM compilation method statistics. |
3.3 jinfo
jinfo(Configuration Info for Java)的作用是实时查看和调整虚拟机各项参数。
# 查询CMSInitiatingOccupancyFraction参数值
jinfo -flag CMSInitiatingOccupancyFraction 1444
-XX:CMSInitiatingOccupancyFraction=85
3.4 jmap + mat
jmap(Memory Map for Java)命令用于生成堆转储快照(一般称为heapdump或dump文件)、查询finalize执行队列、Java堆和方法区的详细信息,如空间使用率、当前用的是哪种收集器等。
注:会 “Stop The World”,慎用。
- 其它获取堆转储快照的方式
- -XX:+HeapDumpOnOutOfMemoryError 参数,可以让虚拟机在内存溢出异常出现之后自动生成堆转储快照文件;
- -XX:+HeapDumpOnCtrlBreak 参数则可以使用[Ctrl]+[Break]键让虚拟机生成堆转储快照文件;
- 在Linux系统下通过 kill -3 命令发送进程退出信号“恐吓”一下虚拟机,也能顺利拿到堆转储快照。
3.5 jhat
JDK提供jhat(JVM Heap Analysis Tool)命令与jmap搭配使用,来分析jmap生成的堆转储快照。jhat内置了一个微型的HTTP/Web服务器,生成堆转储快照的分析结果后,可以在浏览器中查看。
[root@simwor ~]# jhat java_pid3167857.hprof
Reading from java_pid3167857.hprof...
Dump file created Wed Jul 28 21:38:35 CST 2021
Snapshot read, resolving...
Resolving 814328 objects...
Chasing references, expect 162 dots..................................................................................................................................................................
Eliminating duplicate references..................................................................................................................................................................
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.
3.6 jstack
jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)。
- 使用场景
- 线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合;
- 生成线程快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等,都是导致线程长时间停顿的常见原因;
- 线程出现停顿时通过jstack来查看各个线程的调用堆栈,就可以获知没有响应的线程到底在后台做些什么事情,或者等待着什么资源。