堆内存调优
堆内存的大小设置及打印
package com.example.springboot.test.jvm;
public class HeapSizeTest {
//-Xmx10m -Xms8m -XX:+PrintGCDetails
public static void main(String[] args) {
//堆内存最大大小
System.out.println(Runtime.getRuntime().maxMemory()/1024/1024 + "M");
//堆内存初始化大小
System.out.println(Runtime.getRuntime().totalMemory()/1024/1024 + "M");
}
}
堆内存的初始化大小一般为物理内存的1/64,最大大小一般为物理内存的1/4。
年轻代和老年代加一起的内存和初始化内存一样大,在此也证明了元空间是逻辑上存在,其实不在jvm内存中,在本地内存中。
OOM异常
package com.example.springboot.test.jvm;
import java.util.Random;
public class OomTest1 {
//-Xms1m -Xmx1m -XX:+PrintGCDetails
public static void main(String[] args) {
String str = "oom";
while (true) {
str += (new Random().nextInt(999999999)
+ new Random().nextInt(999999999)
+ new Random().nextInt(999999999)
+ new Random().nextInt(999999999));
}
}
}
最终报出异常:
Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
dump内存快照
使用的工具:
- jconsole
- idea debug
- eclipse插件 mat插件
- jprofiler插件
其中jprofiler是一款性能瓶颈分析插件,能很好集成在idea上,只需在idea工具的插件中安装jprofiler,并在电脑安装jprofiler软件,最后在idea中绑定jprofiler.exe启动即可。
package com.example.springboot.test.jvm;
import java.util.ArrayList;
import java.util.List;
//-Xmx2m -Xms2m -XX:+HeapDumpOnOutOfMemoryError
public class Oomtest2 {
static byte[] bytes = new byte[1*1024*1024];
public static void main(String[] args) {
List list = new ArrayList<>();
while (true) {
list.add(bytes);
}
}
}
会生成java_pid1584.hprof文件,用jprofiler打开分析。
GC
我们平时所说的JVM调优其实就是在调堆。
关于垃圾回收,针对不同的年代区,使用不同的算法,即分代收集算法。
- Young代: GC频繁区域
- Old代:GC次数较少
- Perm代:不会产生GC!
一个对象产生的历程:
JVM在进行GC时,并非每次都是对三个区域进行扫描的!大部分的时候都是指的新生代!
两个类型:
普通GC:只针对新生代 【GC】
全局GC:主要是针对老年代,偶尔伴随新生代! 【Full GC】
JVM如何识别垃圾
引用计数法

特点:每个对象都有一个引用计数器,每当对象被引用一次,计数器就+1,如果引用失效,则计数器-1,如果为0,则GC可以清理;
缺点:
- 计数器维护麻烦!
- 循环引用无法处理!
可达性算法
可达性算法的原理是以一系列叫做 GC Root 的对象为起点出发,引出它们指向的下一个节点,再以下个节点为起点,引出此节点指向的下一个结点,这样通过 GC Root 串成的一条线就叫引用链,直到所有的结点都遍历完毕,如果相关对象不在任意一个以 GC Root 为起点的引用链中,则这些对象会被判断为垃圾,会被 GC 回收。
那么这些 GC Roots 到底是什么东西呢,哪些对象可以作为 GC Root 呢,有以下几类:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI(Native 方法)引用的对象
虚拟机栈中的引用对象
public class Test {
public static void main(String[] args) {
Test a = new Test();
a = null;
}
}
a 是栈帧中的本地变量,当 a = null 时,由于此时 a 充当了 GC Root 的作用,a 与原来指向的实例 new Test() 断开了连接,所以对象会被回收.
静态属性引用的对象
public class Test {
public static Test s;
public static void main(String[] args) {
Test a = new Test();
a.s = new Test();
a = null;
}
}
当栈帧中的本地变量 a = null 时,由于 a 原来指向的对象与 GC Root (变量 a) 断开了连接,所以 a 原来指向的对象会被回收,而由于我们给 s 赋值了变量的引用,s 在此时是类静态属性引用,充当了 GC Root 的作用,它指向的对象依然存活。
常量引用的对象
public class Test {
public static final Test s = new Test();
public static void main(String[] args) {
Test a = new Test();
a = null;
}
}
常量 s 指向的对象并不会因为 a 指向的对象被回收而回收。
GC回收算法
复制算法
复制算法把堆内存分为两块区域A和B,区域 A 负责分配对象,区域 B 不分配, 对区域 A 使用以上所说的标记法把存活的对象标记出来(下图有误无需清除),然后把区域 A 中存活的对象都复制到区域 B(存活对象都依次紧邻排列)最后把 A 区对象全部清理掉释放出空间,这样就解决了内存碎片的问题了。
1、一般普通GC 之后,差不多Eden几乎都是空的了!
2、每次存活的对象,都会被从 from 区和 Eden区等复制到 to区,from 和 to 会发生一次交换;记住一个点就好,谁空谁是to,每当幸存一次,就会导致这个对象的年龄+1;如果这个年龄值大于15,就会进入养老区!
优点:没有标记和清除的过程!效率高!没有内存碎片!
缺点:需要浪费双倍的空间
Eden 区,对象存活率极低! 统计:99% 对象都会在使用一次之后,引用失效!推荐使用 复制算法
标记清除算法

先根据可达性算法标记出相应的可回收对象,然后对可回收的对象进行回收。
优点:不需要额外的空间!
缺点:两次扫描,耗时较为严重,会产生内存碎片,不连续!
老年代一般使用这个,但是会和我们后面的整理压缩一起使用
标记整理算法(标记清除压缩算法)

先根据可达性算法标记出相应的可回收对象,然后对可回收的对象进行回收,最后压缩整理,移动存活的对象。
减少了上面标记清除的缺点:没有内存碎片!但是增加了移动存活对象的操作,耗时也较为严重!
小结
内存效率:复制算法 > 标记清除算法 > 标记整理(时间复杂度!)
内存整齐度:复制算法=标记整理>标记清除算法
内存利用率:标记整理 = 标记清除算法 > 复制算法
那么上述算法哪一种最优呢?其实没有最优算法,只有最合适算法,每个分代使用不同的算法。
年轻代:
相对于老年区,对象存活率低!
Eden 区,对象存活率极低! 统计:99% 对象都会在使用一次之后,引用失效!推荐使用 复制算法
老年代:
区域比较大,对象存活率较高!
推荐使用:标记清除压缩
对象如何晋升到老年代
- 幸存区对象年龄达到15,则进入老年区。
- 大对象 当某个对象分配需要大量的连续内存时,此时对象的创建不会分配在 Eden 区,会直接分配在老年代,因为如果把大对象分配在 Eden 区, Minor GC 后再移动到 S0,S1 会有很大的开销(对象比较大,复制会比较慢,也占空间),也很快会占满 S0,S1 区,所以干脆就直接移到老年代.
- 在 S0(或S1) 区相同年龄的对象大小之和大于 S0(或S1)空间一半以上时,则年龄大于等于该年龄的对象也会晋升到老年代。
JVM参数
jvm 只有三种参数类型:标配参数、X参数,XX参数;
标配参数
在各种版本之间都很稳定,很少有变化
-version
-help
-showversion
X参数
-Xint # 解释执行
-Xcomp # 第一次使用就编译成本地的代码
-Xmixed # 混合模式(Java默认)
正常查看jvm版本时,使用java -version,如果想修改执行模式,则使用
java -Xint version
java -Xcomp version
XX参数
XX参数之布尔型
-XX: + 或者 - 某个属性值, + 代表开启某个功能,- 表示关闭了某个功能!;
-XX:+printGCDetails
XX 参数之 key = value型
修改元空间大小
-XX:MetaspaceSize=128m
修改进入老年区的存活年限
-XX:MaxTenuringThreshold=15
还有常用的语法糖:
-Xms初始堆的大小,等价:-XX:InitialHeapSize
-Xmx最大堆的大小 ,等价:-XX:MaxHeapSize
-XX:+PrintFlagsInitial 查看 java 环境初始默认值
其中=为默认值,:=为修改过的值
java -XX:+PrintCommandLineFlags -version 打印出用户手动选项的 XX 选项
常用的调优参数
-Xms 初始化堆内存大小
-Xmx 最大堆内存大小
-Xss 线程栈大小设置,默认 512k~1024k
-Xmn 设置年轻代的大小,一般不用动!
-XX:MetaspsaceSize 设置元空间的大小,这个在本地内存中!
-XX:+PrintGCDetails 打印出GC详情
-XX:SurvivorRatio
设置新生代中的 s0/s1 空间的比例;
uintx SurvivorRatio = 8Eden:s0:s1 = 8:1:1
uintx SurvivorRatio = 4Eden:s0:s1 = 4:1:1
-XX:NewRatio
设置年轻代与老年代的占比:
NewRatio = 2新生代1,老年代是2,默认新生代整个堆的 1/3;
NewRatio = 4新生代1,老年代+是4,默认新生代整个堆的 1/5;
-XX:MaxTenuringThreshold
进入老年区的存活阈值;
MaxTenuringThreshold = 15
垃圾回收器
如果说GC算法(引用计数、复制、标记清除、标记整理算法)方法论,那么垃圾收集器就是对应的落地的实现!
四种垃圾回收器:
串行(STW:Stop the World)单线程

什么是 STW ?所谓的 STW, 即在 GC(minor GC 或 Full GC)期间,只有垃圾回收器线程在工作,其他工作线程则被挂起。
并行垃圾回收器(多线程工作,也会导致 STW)

并发垃圾回收器
在回收垃圾的同时,可以正常执行线程,并行处理,但是如果是单核CPU,只能交替执行!
G1垃圾回收器
G1(Garbage First)垃圾回收器将堆内存分割成不同的区域,然后并发的对其进行垃圾回收
查看默认的垃圾回收器java -XX:+PrintCommandLineFlags -version
java的垃圾回收器有哪些?
DefNew : 默认的新一代 【Serial 串行】
Tenured : 老年代 【Serial Old】
ParNew : 并行新一代 【并行ParNew】
PSYoungGen : 并行清除年轻代 【Parallel Scavcegn】
ParOldGen: 并行老年区
如何选择垃圾回收器
1、单CPU,单机程序,内存小
-XX:UseSerialGC
2、多CPU,大的吞吐量、后台计算!
XX:+UseParallelGC
3、多CPU,但是不希望有时间停顿,快速响应!
-XX:+UseParNewGC或者XX:+UseParallelGC
Serial 收集器
Serial 收集器是工作在新生代的,单线程的垃圾收集器,单线程意味着它只会使用一个 CPU 或一个收集线程来完成垃圾回收,不仅如此,还记得我们上文提到的 STW 了吗,它在进行垃圾收集时,其他用户线程会暂停,直到垃圾收集结束,也就是说在 GC 期间,此时的应用不可用。
ParNew 收集器
ParNew 收集器是 Serial 收集器的多线程版本,除了使用多线程,其他像收集算法,STW,对象分配规则,回收策略与 Serial 收集器完成一样。
Parallel Scavenge 收集器
Parallel Scavenge 收集器也是一个使用复制算法,多线程,工作于新生代的垃圾收集器,看起来功能和 ParNew 收集器一样,它有啥特别之处吗
关注点不同,CMS 等垃圾收集器关注的是尽可能缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 目标是达到一个可控制的吞吐量(吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间)),也就是说 CMS 等垃圾收集器更适合用到与用户交互的程序,因为停顿时间越短,用户体验越好,而 Parallel Scavenge 收集器关注的是吞吐量,所以更适合做后台运算等不需要太多用户交互的任务。
Parallel Scavenge 收集器提供了两个参数来精确控制吞吐量,分别是控制最大垃圾收集时间的 -XX:MaxGCPauseMillis 参数及直接设置吞吐量大小的 -XX:GCTimeRatio(默认99%)
除了以上两个参数,还可以用 Parallel Scavenge 收集器提供的第三个参数 -XX:UseAdaptiveSizePolicy,开启这个参数后,就不需要手工指定新生代大小,Eden 与 Survivor 比例(SurvivorRatio)等细节,只需要设置好基本的堆大小(-Xmx 设置最大堆),以及最大垃圾收集时间与吞吐量大小,虚拟机就会根据当前系统运行情况收集监控信息,动态调整这些参数以尽可能地达到我们设定的最大垃圾收集时间或吞吐量大小这两个指标。自适应策略也是 Parallel Scavenge 与 ParNew 的重要区别!
Serial Old 收集器
Serial Old 是工作于老年代的单线程收集器,此收集器的主要意义在于给 Client 模式下的虚拟机使用,如果在 Server 模式下,则它还有两大用途:一种是在 JDK 1.5 及之前的版本中与 Parallel Scavenge 配合使用,另一种是作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用,它与 Serial 收集器配合使用示意图如下:
Parallel Old 收集器
Parallel Old 是相对于 Parallel Scavenge 收集器的老年代版本,使用多线程和标记整理法,这两者的组合由于都是多线程收集器,真正实现了「吞吐量优先」的目标。
CMS 收集器
CMS 收集器是以实现最短 STW 时间为目标的收集器,如果应用很重视服务的响应速度,希望给用户最好的体验,则 CMS 收集器是个很不错的选择!只有Serial收集器能与 CMS 收集器配合工作,CMS 是一个划时代的垃圾收集器,是真正意义上的并发收集器,它第一次实现了垃圾收集线程与用户线程(基本上)同时工作,它采用的是传统的 GC 收集器代码框架,与 Serial、ParNew 共用一套代码框架,所以能与这两者一起配合工作,而后文提到的 Parallel Scavenge 与 G1 收集器没有使用传统的 GC 收集器代码框架,而是另起炉灶独立实现的,另外一些收集器则只是共用了部分的框架代码,所以无法与 CMS 收集器一起配合工作。
G1垃圾回收器
以前的垃圾回收器的特点
1、年轻代和老年代是各自独立的内存区域
2、年轻代使用 eden+s0+s1 复制算法
3、老年代收集器必须扫描整个老年代的区域;
4、垃圾回收器原则:尽可能少而快的执行GC为设计原则!
G1 收集器是面向服务端的垃圾收集器,被称为驾驭一切的垃圾回收器,主要有以下几个特点:
1、像 CMS 收集器一样,能与应用程序线程并发执行。
2、整理空闲空间更快。
3、需要 GC 停顿时间更好预测。
4、不会像 CMS 那样牺牲大量的吞吐性能。
5、不需要更大的 Java Heap

与 CMS 相比,它在以下两个方面表现更出色
1、运作期间不会产生内存碎片,G1 从整体上看采用的是标记-整理法,局部(两个 Region)上看是基于复制算法实现的,两个算法都不会产生内存碎片,收集后提供规整的可用内存,这样有利于程序的长时间运行。
2、在 STW 上建立了可预测的停顿时间模型,用户可以指定期望停顿时间,G1 会将停顿时间控制在用户设定的停顿时间以内。
为什么G1能建立可预测的停顿模型呢,主要原因在于 G1 对堆空间的分配与传统的垃圾收集器不一器,传统的内存分配就像我们前文所述,是连续的,分成新生代,老年代,新生代又分 Eden、S0、S1,而 G1 各代的存储地址不是连续的,每一代都使用了 n 个不连续的大小相同的 Region,每个Region占有一块连续的虚拟内存地址。
G1 收集器的工作步骤如下:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
如何使用
-XX:+UseG1GC
这个还不是它最大的亮点,它增加了一些参数,可以自定义垃圾回收的时间!
-XX:MaxGCPauseMillis=100 最大的GC停顿时间单位:毫秒,JVM尽可能的保证停顿小于这个时间!
G1 优点
1、没有内存碎片!
2、可以精准空垃圾回收时间!
OOM异常
java.lang.StackOverflowError
public class StackTest1 {
//-Xss1m 把线程栈大小设为1M
public static void main(String[] args) {
addStack();
}
//一直压栈
private static void addStack() {
addStack();
}
}
报出异常:Exception in thread “main” java.lang.StackOverflowError
java.lang.OutOfMemoryError: Java heap space
public class HeapTest {
static String str = "heap";
//-Xms1m -Xmx1m 设置堆内存的初始化和最大值为1m
public static void main(String[] args) {
while (true) {
str += UUID.randomUUID();
}
}
}
报出异常:Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: GC overhead limit exceeded
GC 回收时间过长也会导致 OOM;
可能CPU占用率一直是100%,GC 但是没有什么效果!
public class GcOverHeadTest {
public static void main(String[] args) {
//-Xms5m -Xmx5m -XX:MaxDirectMemorySize=1m -XX:+PrintGCDetails
List<String> list = new ArrayList<String>();
while (true) {
//把字符串写入字符串常量池
list.add(UUID.randomUUID().toString().intern());
}
}
}
报出异常:Exception in thread “main” java.lang.OutOfMemoryError: GC overhead limit exceeded
java.lang.OutOfMemoryError: Direct buffer memory
基础缓冲区的错误
public class BufferTest {
//-Xms5m -Xmx5m -XX:MaxDirectMemorySize=1m
public static void main(String[] args) {
System.out.println("MaxDirectMemorySize大小:" + (VM.maxDirectMemory()/1024/1024));
// ByteBuffer.allocate(); 分配JVM的堆内存,属于GC管辖
// ByteBuffer.allocateDirect() ; // 分配本地OS内存,不属于GC管辖
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(2 * 1024 * 1024);
}
}
报出异常:Exception in thread “main” java.lang.OutOfMemoryError: Direct buffer memory
java.lang.OutOfMemoryError: unable to create native Thread
在高并发时,unable to create native Thread这个错误更多的时候和平台有关!
1、应用创建的线程太多!
2、服务器不允许你创建这么多线程!
服务器线程不够了,超过了限制,也会爆出OOM异常!
public class NativeThreadTest {
public static void main(String[] args) {
for (int i = 1; ; i++) {
new Thread(()->{
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
报出异常:Exception in thread “main” java.lang.OutOfMemoryError: unable to create new native thread
java.lang.OutOfMemoryError: Metaspace
元空间报错,java8 之后使用元空间代替永久代;本地内存!
1、虚拟机加载类信息
2、常量池
3、静态变量
4、编译后的代码
模拟元空间溢出、不断的生成类即可!
public class MetaSpaceTest {
//-XX:MetaspaceSize=5m -XX:MaxMetaspaceSize=5m
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(test3.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return method.invoke(o,args);
}
});
enhancer.create();
}
}
class test3 { }
报出异常:
Exception in thread “main” java.lang.ExceptionInInitializerError
Caused by: java.lang.IllegalStateException: Unable to load cache item
Caused by: java.lang.OutOfMemoryError: Metaspace