一、JVM内存管理如图
下面介绍下这些区域
1、程序计数器
内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成
如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
2、虚拟机栈
线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表
、操作数栈
、动态链接
、方法出口
等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。
3、本地方法栈
区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。
4、堆
对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。
5、方法区
属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
6、运行时常量池
属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError。
7、直接内存
非虚拟机运行时数据区的部分
在 JDK 1.4 中新加入 NIO (New Input/Output) 类,引入了一种基于通道(Channel)和缓存(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。可以避免在 Java 堆和 Native 堆中来回的数据耗时操作。
OutOfMemoryError:会受到本机内存限制,如果内存区域总和大于物理内存限制从而导致动态扩展时出现该异常。
二、垃圾回收与分配策略
1、概述
程序计数器、虚拟机栈、本地方法栈 3 个区域随线程生灭(因为是线程私有),栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。而 Java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期才知道那些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收期所关注的就是这部分内存。
2、如何判断对象该回收了
2.1引用计数法
给对象添加一个引用计数器。但是难以解决循环引用问题。
从图中可以看出,如果不下小心直接把 Obj1-reference 和 Obj2-reference 置 null。则在 Java 堆当中的两块内存依然保持着互相引用无法回收。
2.2、可达性分析法
通过一系列的 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用。
可作为 GC Roots 的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI(即一般说的 Native 方法) 引用的对象
2.3、引用
强引用
类似于 Object obj = new Object();
创建的,只要强引用在就不回收。
弱引用
WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。
虚引用
PhantomReference 类实现虚引用。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
3、垃圾回收算法
3.1、标记清除法
直接标记清除就可。
两个不足:
- 效率不高
- 空间会产生大量碎片
3.2、复制算法
把空间分成两块,每次只对其中一块进行 GC。当这块内存使用完时,就将还存活的对象复制到另一块上面。
解决前一种方法的不足,但是会造成空间利用率低下。因为大多数新生代对象都不会熬过第一次 GC。所以没必要 1 : 1 划分空间。可以分一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另一块 Survivor 上,最后清理 Eden 和 Survivor 空间。大小比例一般是 8 : 1 : 1,每次浪费 10% 的 Survivor 空间。但是这里有一个问题就是如果存活的大于 10% 怎么办?这里采用一种分配担保策略:多出来的对象直接进入老年代。
3.3、标记-整理算法
不同于针对新生代的复制算法,针对老年代的特点,创建该算法。主要是把存活对象移到内存的一端。
3.4、分代回收
根据存活对象划分几块内存区,一般是分为新生代和老年代。然后根据各个年代的特点制定相应的回收算法。
新生代
每次垃圾回收都有大量对象死去,只有少量存活,选用复制算法比较合理。
老年代
年代中对象存活率较高、没有额外的空间分配对它进行担保。所以必须使用 标记 —— 清除
或者 标记 —— 整理
算法回收。
4、垃圾回收器
集算法是内存回收的理论,而垃圾回收器是内存回收的实践。
说明:如果两个收集器之间存在连线说明他们之间可以搭配使用。
4.1、Serial 收集器
4.2、 ParNew 收集器
可以认为是 Serial 收集器的多线程版本。
并行:Parallel 指多条垃圾收集线程并行工作,此时用户线程处于等待状态
并发:Concurrent 指用户线程和垃圾回收线程同时执行(不一定是并行,有可能是交叉执行),用户进程在运行,而垃圾回收线程在另一个 CPU 上运行。
4.3、 Parallel Scavenge 收集器
这是一个新生代收集器,也是使用复制算法实现,同时也是并行的多线程收集器。
CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程所停顿的时间,而 Parallel Scavenge 收集器的目的是达到一个可控制的吞吐量(Throughput = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间))。
作为一个吞吐量优先的收集器,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整停顿时间。这就是 GC 的自适应调整策略(GC Ergonomics)。
4.4、Serial Old 收集器
收集器的老年代版本,单线程,使用 标记 —— 整理
。
4.5、Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本。多线程,使用 标记 —— 整理
4.6、CMS 收集器
CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。基于 标记 —— 清除
算法实现。
运作步骤:
1 初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象
2 并发标记(CMS concurrent mark):进行 GC Roots Tracing
3 重新标记(CMS remark):修正并发标记期间的变动部分
4 并发清除(CMS concurrent sweep)
缺点:对 CPU 资源敏感、无法收集浮动垃圾、标记 —— 清除
算法带来的空间碎片
4.7、G1收集器
面向服务端的垃圾回收器。优点:并行与并发、分代收集、空间整合、可预测停顿。
运作步骤:
- 初始标记(Initial Marking)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选回收(Live Data Counting and Evacuation)
5、内存分配与回收策略
5.1对象优先在 Eden 分配
对象主要分配在新生代的 Eden 区上,如果启动了本地线程分配缓冲区,将线程优先在 (TLAB) 上分配。少数情况会直接分配在老年代中。
一般来说 Java 堆的内存模型如下图所示:
新生代 GC (Minor GC) 发生在新生代的垃圾回收动作,频繁,速度快。
老年代 GC (Major GC / Full GC) 发生在老年代的垃圾回收动作,出现了 Major GC 经常会伴随至少一次 Minor GC(非绝对)。Major GC 的速度一般会比 Minor GC 慢十倍以上。
5.2、大对象直接进入老年代
5.3、长期存活的对象将进入老年代
5.4、动态对象年龄判定
5.5、空间分配担保
三、类加载机制
虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、装换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。
在 Java 语言中,类型的加载、连接和初始化过程都是在程序运行期间完成的。
1、类加载时机
类的生命周期( 7 个阶段)
双亲委派模型
从 Java 虚拟机角度讲,只存在两种类加载器:一种是启动类加载器(C++ 实现,是虚拟机的一部分);另一种是其他所有类的加载器(Java 实现,独立于虚拟机外部且全继承自 java.lang.ClassLoader)
-
-
启动类加载器
加载 lib 下或被 -Xbootclasspath 路径下的类 -
扩展类加载器
加载 lib/ext 或者被 java.ext.dirs 系统变量所指定的路径下的类 -
引用程序类加载器
ClassLoader负责,加载用户路径上所指定的类库。
-
除顶层启动类加载器之外,其他都有自己的父类加载器。
工作过程:如果一个类加载器收到一个类加载的请求,它首先不会自己加载,而是把这个请求委派给父类加载器。只有父类无法完成时子类才会尝试加载。
四、启动jar参数
1、实例
nohup java -Xms500m -Xmx500m -Xmn250m -Xss256k -server -XX:+HeapDumpOnOutOfMemoryError -jar $JAR_PATH/test-0.0.1-SNAPSHOT.jar --spring.profiles.active=daily -verbose:class &
java -jar -Xms256m -Xmx512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Xloggc:C:\demo.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps demo.jar &
2、说明
1、--spring.profiles.active=daily, 这个可以在spring-boot启动中指定系统变量,多环境(测试、预发、线上配置)的区分
在排查jar包冲突时,可以指定启动的-verbose:class 打印出启动的应用实际加载类的路径,来排查来源。
2、jvm堆设值: -Xms500m -Xmx500m -Xmn250m -Xss256k
3、nohup 不挂断地运行命令;& 在后台运行 ,一般两个一起用。 eg:nohup command &
4、-server:服务器模式,在多个CPU时性能佳,启动慢但性能好,能合理管理内存。
5、-XX:+HeapDumpOnOutOfMemoryError:在堆溢出时保存快照
6、GC回收器类型:
使用SerialGC添加参数: -XX:+UseSerialGC
使用ParallelGC添加参数: -XX:+UseParallelGC
使用CMSGC添加参数: -XX:+UseConcMarkSweepGC
使用G1GC添加参数: -XX:+UseG1GC
3、GC调优思路
- 分析场景,如:启动速度慢,偶尔出现响应慢于平均水平或出现卡顿
- 确定目标,如:内存占用,低延时,吞吐量
- 收集日志,如:通过参数配置收集GC日志,通过JDK工具查看GC状态
- 分析日志,如:使用工具辅助分析日志,查看GC次数,GC时间
- 调整参数,如:切换垃圾收集器或者调整垃圾收集器参数
常用GC参数
参数 | 描述 |
---|---|
-XX:ParallelGCThreads | 并行GC线程数量 |
-XX:ConcGcThreads | 并发GC线程数量 |
-XX:MaxGCPauseMillis | 最大停顿时间,单位毫秒,GC尽力保证回收时间不超过设定值 |
-XX:GCTimeRatio | 垃圾收集时间占总时间的比值,取值0-100,默认99,即最大允许1%的时间做GC |
-XX:SurvivorRatio | 设置eden区大小和survivor区大小的比例,8表示两个survivor:eden=2:8,即一个survivor占年轻代的1/10 |
-XX:NewRatio | 新生代和老年代的比,4表示新生代:老年代=1:4,即年轻代占堆的1/5 |
-verbose:gc,-XX:+PrintGC | 打印GC的简要信息 |
-XX:+PrintGCDetails | 打印GC详细信息(JDK9之后不再使用) |
-XX:+PrintGCTimeStamps | 打印GC发生的时间戳(JDK9之后不再使用) |
-Xloggc:log/gc.log | 指定GC log的位置,以文件输出 |
-XX:PrintHeapAtGC | 每次GC后都打印堆信息 |
垃圾收集器Parallel参数调优
Parallel垃圾收集器在JDK8中是JVM默认的垃圾收集器,它是以吞吐量优先的垃圾收集器。其可调节的参数如下:
参数 | 描述 |
---|---|
-XX:+UseParallelGC | 新生代使用并行垃圾收集器 |
-XX:+UseParallelOldGC | 老年代使用并行垃圾收集器 |
-XX:ParallelGCThreads | 设置用于垃圾回收的线程数 |
-XX:+UseAdaptiveSizePolicy | 打开自适应GC策略 |
垃圾收集器CMS参数调优
CMS垃圾收集器是一个响应时间优先的垃圾收集器,Parallel收集器无法满足应用程序延迟要求时再考虑使用CMS垃圾收集器,从JDK9开始CMS收集器已不建议使用,默认用的是G1垃圾收集器。
-XX:+UseConcMarkSweepGC | 新生代使用并行收集器,老年代使用CMS+串行收集器 |
-XX:+UseParNewGC | 新生代使用并行收集器,老年代CMS收集器默认开启 |
-XX:CMSInitiatingOccupanyFraction | 设置触发GC的阈值,默认68%,如果内存预留空间不够,就会引起concurrent mode failure |
-XX:+UseCMSCompactAtFullCollection | Full GC后,进行一次整理,整理过程是独占的,会引起停顿时间变长 |
-XX:+CMSFullGCsBeforeCompaction | 设置进行几次Full GC后进行一次碎片整理 |
-XX:+CMSClassUnloadingEnabled | 允许对类元数据进行回收 |
-XX:+UseCMSInitiatingOccupanyOnly | 表示只在达到阈值的时候才进行CMS回收 |
-XX:+CMSIncrementalMode | 使用增量模式,比较适合单CPU |
垃圾收集器G1参数调优
G1收集器是一个兼顾吞吐量和响应时间的收集器,如果是大堆(如堆的大小超过6GB),堆的使用率超过50%,GC延迟要求稳定且可预测的低于0.5秒,建议使用G1收集器。
参数 | 描述 |
---|---|
-XX:G1HeapRegionSize | 设置Region大小,默认heap/2000 |
-XX:G1MixedGCLiveThresholdPercent | 老年代依靠Mixed GC, 触发阈值 |
-XX:G1OldSetRegionThresholdPercent | 定多包含在一次Mixed GC中的Region比例 |
-XX:+ClassUnloadingWithConcurrentMark | G1增加默认开启,在并发标记阶段结束后,JVM即进行类型卸载 |
-XX:G1NewSizePercent | 新生代的最小比例 |
-XX:G1MaxNewSizePercent | 新生代的最大比列 |
-XX:G1MixedGCCountTraget | Mixed GC数量控制 |
4、垃圾回收调优示例
@SpringBootApplication
public class SpringBootDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootDemoApplication.class, args);
Executors.newScheduledThreadPool(1)
.scheduleAtFixedRate(
() -> {
new Thread(
() -> {
for (int i = 0; i < 150; i++) {
try {
byte[] temp = new byte[1024 * 512];
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
})
.start();
},
100,
100,
TimeUnit.MILLISECONDS);
}
}
GC分析主要查看GC导致的Stop The World的时间,它会导致程序的延时增加。
示例代码运行的时候建议指定其堆内存的最大值,启动时添加JVM参数-Xmx1024m。程序运行起来之后可以利用jps或者jcmd产看运行的程序进程号。
拿到进程号之后利用jstat命令查看GC信息,如动态监控GC统计信息,间隔1000毫秒统计一次,每10行数据后输出列标题:
上述两个步骤也可以合并成一个 jstat -gc -h10 $(jcmd | grep “com.example.springbootdemo.SpringBootDemoApplication” | awk ‘{print $1}’) 1000
当然除了动态监控GC信息,也可以将GC日志信息打印到文件,然后利用GC分析工具进行分析。
在程序启动时添加JVM参数”-Xmx1024m -Xloggc:/gc.log“,则可以可以将GC日志打印到gc.log文件,然后可以利用GCViewer工具辅助分析GC日志文件,参考地址:https://github.com/chewiebug/GCViewer
GCViewer下载后双击gcviewer-x.xx-SNAPSHOT.jar文件即可运行,然后将gc.log日志文件导入即可观察GC信息。
GC调优之前,我们需要了解当前JVM参数的信息。命令 java -XX:+PrintFlagsFinal -version 会打印所有的JVM参数,如需查看指定参数,如查看UseAdaptiveSizePolicy的值可以使用 java -XX:+PrintFlagsFinal -version | grep UseAdaptiveSizePolicy
调整-XX:ParallelGCThreads的值可以指定GC并发的线程数,如在JVM启动参数中可以添加 “-Xmx1024m -XX:ParallelGCThreads=4”,调节GC并发的线程数,观察GC的信息,如Full GC次数FGC,Full GC的总时间FGCT,GC的总时间GCT等进行调优。
同样我们可以在JVM启动参数中指定-XX:MaxGCPauseMills,使用G1收集器-XX:+UseG1GC等,调节JVM启动参数,收集GC日志,更具监控进行相应的调节,进而找到最优值。