JVM (Java Virtual Machine),在java生态圈中具有重要地位,不仅支持Java还支持Kotlin、Groovy、Scala、Jython等多种其他语言, 封装各种操作系统和底层硬件的操作为Native代码(大部分为C/C++代码),编程时只需调用Java包中提供的接口即可。从而做到“一次编译,到处运行”(当然得是有JRE环境的机器)。本篇主要总结JVM的内存分配与垃圾回收机制,主要内容如下:
一、 内存分配回收之历史
二、 “垃圾”识别算法
三、 JVM内存模型
四、 垃圾回收(Garbage Collection)
五、 JVM调优思路
JVM内存分配与垃圾回收
一、 内存分配回收之历史
在最先的编程语言中,大对象的分配是需要人工指定分配(malloc)和 回收(free)内存的,如C语言的代码片段:
// Code within C
int send_request() {
size_t n = read_size();
int *elements = malloc(n * sizeof(int));
if(read_elements(n, elements) < n) {
// elements is not freed!
return -1;
}
// There are a lot of code here!
free(elements)
return 0;
}
上述代码,在read_elements(n, elements) < n
条件满足时,直接返回-1,并未回收elements指向的内存,从而造成内存泄漏。事实上,若是代码量足够大,且并非一蹴而就,是很容易出现上述疏忽,从而造成内存泄漏。
为了解决上述问题,C++里引入了智能指针的概念:
int send_request() {
size_t n = read_size();
auto elements = make_shared<vector<int>>();
// read elements
store_in_cache(elements);
// process elements further
return 0;
}
智能指针采用引用计数(稍后介绍)的方式判断内存是否依然还在使用,即每次引用该内存(本例elements所指的内存),引用计数加一,每个引用离开作用区域引用计数则减一。引用计数为0时,内存被自动回收。
智能指针相对于原先的手动分配和回收,先进了很多,但其依赖程序员指明调用,仍不够"智能"。
二、“垃圾”识别算法
为了让程序员更专注于业务逻辑,能不能将内存分配和回收完全交给第三方机制呢?
要解决这个问题,首先要区分哪部分内存还在使用,哪部分内存已经没用了(垃圾)。先驱者们提出了用于识别“垃圾”的引用计数和 可达性分析方法。
-
引用计数:标识每个对象(内存区域)被引用的次数,引用计数为0的对象以及只被其引用的对象,则为垃圾。引用计数简单好用,但是有个循环引用问题,如图1-4所示红色区域的对象,虽理论上为垃圾,但其中每个对象的引用计数均不为0,因而无法被标识。不过不用担心,各个采用引用计数区分垃圾的高级语言,如Python等,使用了如弱引用等方法处理该的问题。(Java并没有采用这种方法,因此不会继续展开说明)
图1-4 引用计数的循环依赖问题
-
可达性分析:由GC Root出发,依次扫描并标识引用到的对象,扫描结束后,所有未被标识的对象则为垃圾。Java采用的即使可达性分析方法,可达性分析如图1-5所示。
图1-5 可达性分析
GC Root 在不同编程语言中实现不同。在Java中 ,GC Root为: - 局部变量(Local variables) - 活动线程(Active threads) - 静态变量(Static fields) - JNI(Java Native Inteface)引用的对象。
三、JVM内存模型
JVM内存模型见图1-1,其中绿色方框为线程私有,红色部分为线程公用。运行时数据区主要分为:
- 程序计数器
- 虚拟机栈(VM Stack)
- 堆(Heap)
- 本地方法栈
- 元空间(Metaspace )
1. 程序计数器
记录当前线程执行的 字节码行号指示器(告诉线程,程序执行到哪了),每个线程独立储存,互不干涉(私有)。
2. 本地方法栈
与虚拟机栈的功能相似,只不过专为Native方法服务。
3. 虚拟机栈
每个线程私有,每个方法执行时,生成一个栈帧,将局部变量表、操作数栈、动态链接、方法返回地址保存,并且入栈,执行完成后,该栈帧返回并出栈。
4. 堆
GC(Garbage Colletion)的主要区域,存放各种对象实例,可分为年轻代(Young)和 年老代(Tenured/Old),年轻代又分为一个伊甸区(Eden)和两个幸存者区(Survivor),其中Virtual是由JVM控制分配但未使用的内存。堆结构的详细结构如图1-2所示(G1回收器的堆内存分配除外)。
5. 元空间(Metaspace)
JDK1.8将之前的方法区(Method Area)移除后,加入了元空间,用以加载的类信息和常量池。元空间并不占用
-Xmx
设置的虚拟内存,而是占用额外的直接内存,(如:使用-Xmx1G
配置了JVM最大内存为1G,最终App可能占用了1.5G内存,而多余部分就是被Metaspace占用了)。
四、垃圾回收(Garbage Collection)
垃圾回收器可以分为三大类:
- Serial GC
- Parallel GC
- Mostly Concurrent GC
1. 单线程(Serial)回收器
在JVM面世之后,Serial回收器就开始服役,回收机制开始工作时,会暂停JVM中APP的工作线程(Stop The World),开始标记
- 清扫
- 压缩
的清理过程。在多核处理器盛行的今天,也许Serial回收器看起来需要淘汰了,但该回收器依然适用于客户端内存回收和内存需求小(100M)的应用。
2. 并行(Parallel)回收器
并行回收器,依然会暂停JVM中APP的工作线程,但其利用多核处理器,能够高效的完成回收工作,有着极高的吞吐量
表现。一般注重·数据计算以及强调CPU利用率的应用适合使用。
(吞吐量:若工作CPU时钟周期为T,垃圾回收CPU始终周期为t,吞吐量 = T/(T + t))
3. 并发(Mostly Concurrent)回收器
在APP的工作线程运行的同时,占用少量CPU资源并发的回收垃圾。CMS回收器(Concurrent Mark Sweep)和G1回收器(Garbage First)是该类回收器的代表。
为什么是Mostly Concurrent?因为CMS和G1在回收过程中,依然会有2次短暂STW(Stop The World)。CMS和G1主要适用于交互式低延时应用。
(2次短暂STW: First Mark(标识出GC Root)和 Remark(重新标记并发标记过程中进入引用链的对象))
CMS在清理Tenured过程中并没有对内存的压缩
的操作,而是维护了一张记录空闲内存的表,但这也造成了内存的碎片化,这是CMS一个弊端,我猜测这也是官方后续不推荐使用CMS的一个原因。
对应的新生代(Young)和年老代(Tenured)的回收器使用组合和命令如下表所示:
Young | Tenured | JVM options |
---|---|---|
Incremental | Incremental | -Xincgc |
Serial | Serial | -XX:+UseSerialGC |
Parallel Scavenge | Serial | -XX:+UseParallelGC -XX:-UseParallelOldGC |
Parallel New | Serial | N/A |
Serial | Parallel Old | N/A |
Parallel Scavenge | Parallel Old | -XX:+UseParallelGC -XX:+UseParallelOldGC |
Parallel New | Parallel Old | N/A |
Serial | CMS | -XX:-UseSerialGC -XX:+UseConcMarkSweepGC |
Parallel Scavenge | CMS | N/A |
Parallel New | CMS | -XX:+UseParNewGC -XX:+UseConcMarkSweepGC |
G1 | G1 | -XX:+UseG1GC |
乍一看很可怕,这怎么记啊!大多数组合要么无法使用,要么不切合实际(多核处理器环境下,已经有一个回收代用了Parallel GC或者CMS,那另一个也就没必要用Serial GC了),因此只用记住粗体字部分就好,总共4条(分别对应本章开始的3类):
- Serial GC (
-XX:+UseSerialGC
) - Parallel GC (
-XX:+UseParallelGC -XX:+UseParallelOldGC
) - Mostly Concurrent GC
- CMS (
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
) - G1 (
-XX:+UseG1GC
) - ZGC (
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
):JDK11加入的新GC,适用场景:超大内存(TB级),极低响应延迟(10ms内),STW时间不随内存增加而增大。JDK12中依然属于实验性质,非常强大的GC,拭目以待吧。
- CMS (
五、 JVM调优思路
JVM三大重要指标:
- 延时(Latency)
- 吞吐量(Throughput)
- 内存容量(Footprint)
延时
针对注重系统响应时间、延迟低的交互式应用,可以考虑使用CMS(JDK1.8以后不推荐使用)或G1(JDK1.9的默认回收器)。
吞吐量
针对注重计算以及CPU利用率的应用,可以考虑使用Parallel GC。
内存容量
若并不清楚应用所需的内存容量,可以将该配置交给JVM的Ergonomics机制自动配置(将在1/64 ~ 1/4 的系统内存之间调控)
JDK GC的默认配置(新生代:年老代 = 1:3,Eden : Survivor = 8 : 1),针对的是大量朝生暮死对象场景,若是有大量中期或者长期存活的对象,就不适用了。根据使用场景和对象存活时间调整Young和Tenured的大小比例。
JVM 的GC日志相关配置:
JVM options | Description |
---|---|
-Xloggc:${LogFilePath}
|
重定向GC日志至指定地址 |
-XX:+PrintGCDetails | 打印GC细节 |
-XX:+PrintGCDateStamps | 打印GC时间戳 |
-XX:+UseGCLogFileRotation | GC日志循环打印 |
-XX:NumberOfGCLogFiles=${Num}
|
保存GC日志文件数 |
-XX:GCLogFileSize=${LogFileSize}
|
GC日志文件最大值 |
----施工中,未完待续
GC Root
- Local variables
- Active threads
- Static fields
- JNI references
- Serial (STW)
- Parallel (PSYoung-Ergonomics、 Parallel Old) - 吞吐量优先
- Mostly Concurrent (ParNew+CMS、G1) - 延时优先
- Concurrent (ZGC) - 大内存、低延迟
网友评论