【实战JVM】-实战篇-05-内存泄漏及分析
- 1 内存溢出和内存泄漏
- 1.1 常见场景
- 1.2 解决内存溢出的方法
- 1.2.1 发现问题
- 1.2.1.1 top
- 1.2.1.2 ViusalVM
- 1.2.1.3 arthas
- 1.2.1.4 Prometheus+Grafana
- 1.2.2 堆内存状况对比
- 1.2.3 内存泄漏原因-代码中
- 1.2.3.1 equals()-hashCode()
- 1.2.3.2 内部类引用外部类
- 1.2.3.3 ThreadLocal的使用
- 1.2.3.4 String的intern方法
- 1.2.3.5 通过静态字段保存对象
- 1.2.3.6 资源没有正常关闭
- 1.2.4 内存泄漏原因-并发请求
- 1.2.4.1 内存溢出
- 1.2.4.2 内存泄漏
- 2 诊断内存
- 2.1 使用MAT诊断内存
- 2.2 MAT检测内存泄漏原理
- 2.2.1 生成内存快照观察深堆浅堆
- 2.3 导出运行中内存的快照并分析
- 2.3.1 jmap导出
- 2.3.2 arthas导出
- 2.4 分析超大堆内存快照
- 3 实战
- 3.1 分页查询文章接口的内存溢出
- 3.1.1 设置参数重启jar包
- 3.1.2 准备数据
- 3.1.3 jmeter中导入测试脚本
- 3.1.4 定位问题
- 3.1.5 解决思路
- 3.2 Mybatis导致的内存溢出
- 3.2.1 定位问题
- 3.2.2 解决思路
- 3.3 导出大文件内存溢出
- 3.3.1 k8s部署
- 3.3.2 解决思路
- 3.4 ThreadLocal使用时占用大量内存
- 3.4.1 定位问题
- 3.4.2 解决思路
- 3.5 文章内容审核接口的内存问题
- 3.5.1 定位问题
- 3.5.1.1 线程池
- 3.5.1.2 生产者-消费者使用队列
- 3.5.1.3 消息中间件
- 4 诊断和解决问题
- 4.1 离线分析
- 4.2 在线分析-arthas
- 4.3 在线分析-btrace追踪
- 4.3.1 添加依赖
- 4.3.2 编写btrace脚本
- 4.3.3 上传btrace工具及其脚本
1 内存溢出和内存泄漏
1.1 常见场景
- 内存泄露导致溢出的常见场景是大型的Java后端应用中,在处理用户请求之后,没有及时将用户数据删除。随着用户请求的数量越来越多,内存泄漏的对象占满了堆内存,最终导致内存溢出。
1.2 解决内存溢出的方法
1.2.1 发现问题
1.2.1.1 top
top
按M选择内存从大到小排序
1.2.1.2 ViusalVM
启动微服务com.itheima.jvmoptimize.JvmOptimizeApplication
远程连接查看visualvm
java -jar -Djava.rmi.server.hostname=182.92.117.86 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9122 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false SpringBoot-demo-1.0-SNAPSHOT.jar
访问不成功的话记得添加阿里云的安全组
生产环境禁止通过visualVM连接!只能在测试环境中查看。
1.2.1.3 arthas
启动arthas tutorial
nohup java -jar -Darthas.enable-detail-pages=true arthas-tunnel-server-3.7.1-fatjar.jar &
默认端口号8080,默认访问地址
http://182.92.117.86:8080/apps.html
微服务引入依赖并打包
<dependency>
<groupId>com.taobao.arthas</groupId>
<artifactId>arthas-spring-boot-starter</artifactId>
<version>3.7.1</version>
</dependency>
先启动一个
nohup java -jar -Dserver.port=9527 -Darthas.http-port=9528 -Darthas.telnet-port=9529 jvm-optimize-0.0.1-SNAPSHOT.jar &
再启动一个
nohup java -jar -Dserver.port=9530 -Darthas.http-port=9531 -Darthas.telnet-port=9532 jvm-optimize-0.0.1-SNAPSHOT.jar &
一个tunnel,两个微服务
查看http://182.92.117.86:8080/apps.html
一点名字就进入arthas的页面
1.2.1.4 Prometheus+Grafana
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<exclusions><!-- 去掉springboot默认配置 -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
暴露所有端口
management:
endpoint:
metrics:
enabled: true #支持metrics
prometheus:
enabled: true #支持Prometheus
metrics:
export:
prometheus:
enabled: true
tags:
application: jvm-test #实例名采集
endpoints:
web:
exposure:
include: '*' #开放所有端口
查询localhost:8881/actuator
查看所有bean属性
现在是通过监控springboot中的属性,也可以将jvm中其他的指标暴露出来。
引入依赖
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<scope>runtime</scope>
</dependency>
management:
endpoint:
metrics:
enabled: true #支持metrics
prometheus:
enabled: true #支持Prometheus
metrics:
export:
prometheus:
enabled: true
tags:
application: jvm-test #实例名采集
打开http://localhost:8881/actuator/prometheus
查看堆信息
为我们的主机在阿里云中配置Prometheus监控
访问http://182.92.117.86:9527/actuator/prometheus
添加microMeter,监控jvm
Micrometer大盘监控成功:
堆内存使用情况
堆中具体使用情况
Prometheus负责将服务器上的信息通过接口的方式收集到,并且把信息传输给Grafana,最后通过Grafana的仪表盘可视化出来
1.2.2 堆内存状况对比
1.2.3 内存泄漏原因-代码中
1.2.3.1 equals()-hashCode()
解决方法:
- 在定义新实体类时始终重写equals()和hashCode()方法。
- 重写时一定要确定使用了唯一的标识去区分不同的对象,比如用户的ID等。
- hashmap使用时尽量使用编号ID等数据作为key,不要将整个实体类对象作为key存放。
- 采用@Data来注释实体类避免出现此类情况
1.2.3.2 内部类引用外部类
问题一:
非静态的内部类默认会持有外部类,尽管代码上不使用外部类,所以如果有地方引用了这个非静态内部类,会导致外部类也会被引用,垃圾回收时无法回收这个外部类。
public class Outer{
private byte[] bytes = new byte[1024 * 1024]; //外部类持有数据
private String name = "测试";
class Inner{
private String name;
public Inner() {
this.name = Outer.this.name;
}
}
public static void main(String[] args) throws IOException, InterruptedException {
// System.in.read();
int count = 0;
ArrayList<Inner> inners = new ArrayList<>();
while (true){
if(count++ % 100 == 0){
Thread.sleep(10);
}
inners.add(new Inner());
}
}
}
如果要用就要改为静态的内部类,并且引用外部类的静态属性
public class Outer{
private byte[] bytes = new byte[1024 * 1024]; //外部类持有数据
private static String name = "测试";
static class Inner{
private String name;
public Inner() {
this.name = Outer.name;
}
}
可以理解为:非静态的内部类会保存自己是由哪个外部类对象所创建的,所以会持有该外部类对象的引用,而静态内部类属于类而不属于对象,所以可以直接创建。
问题二:
匿名内部类对象如果在非静态方法中被创建,则会持有调用者对象,垃圾回收时无法回收调用者。
public class Outer {
private byte[] bytes = new byte[1024];
public List<String> newList() {
List<String> list = new ArrayList<String>() {{
add("1");
add("2");
}};
return list;
}
public static void main(String[] args) throws IOException {
System.in.read();
int count = 0;
ArrayList<Object> objects = new ArrayList<>();
while (true){
System.out.println(++count);
objects.add(newList());
}
}
}
使用内部类或匿名类,尽量改为静态类或静态方法
public class Outer {
private byte[] bytes = new byte[1024];
public static List<String> newList() {
List<String> list = new ArrayList<String>() {{
add("1");
add("2");
}};
return list;
}
1.2.3.3 ThreadLocal的使用
public class Demo5_1 {
public static ThreadLocal<Object> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
while (true) {
new Thread(() -> {
threadLocal.set(new byte[1024 * 1024 * 10]);
}).start();
Thread.sleep(10);
}
}
}
单纯使用 new Thread(() -> {
创建线程即使不会回收也不会内存泄漏,但是使用线程池就不一样了。
public class Demo5 {
public static ThreadLocal<Object> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(Integer.MAX_VALUE, Integer.MAX_VALUE,
0, TimeUnit.DAYS, new SynchronousQueue<>());
int count = 0;
while (true) {
System.out.println(++count);
threadPoolExecutor.execute(() -> {
threadLocal.set(new byte[1024 * 1024]);
//threadLocal.remove();
});
Thread.sleep(10);
}
}
}
使用完线程池的ThreadLocal之后记得remove移除
1.2.3.4 String的intern方法
1.2.3.5 通过静态字段保存对象
public class CaffineDemo {
public static void main(String[] args) throws InterruptedException {
Cache<Object, Object> build = Caffeine.newBuilder()
.build();
int count = 0;
while (true){
build.put(count++,new byte[1024 * 1024 * 10]);
Thread.sleep(100L);
}
}
}
为缓存设置一个时间 .expireAfterWrite(Duration.ofMillis(100))
1.2.3.6 资源没有正常关闭
1.2.4 内存泄漏原因-并发请求
1.2.4.1 内存溢出
在Jmeter中添加线程组,每秒触发100次http请求
增加http请求
@GetMapping("/test")
public void test1() throws InterruptedException {
byte[] bytes = new byte[1024 * 1024 * 100];//100m
Thread.sleep(10 * 1000L);
}
修改启动参数
启动后大量报错
1.2.4.2 内存泄漏
粘贴到id的值那一行。
name同理,选择随机字符串RandomString,长度1000,哪些字符用于生成:26个字母
启动测试,每秒能处理8000个
2 诊断内存
2.1 使用MAT诊断内存
先把环境变量的jdk设为17再启动memoryanalyzer
添加虚拟机参数
-Xmx256m -Xms256m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\File\StudyJavaFile\JavaStudy\JVM\shizhan\day05\resource\dump\test1.hprof
然后启动时用jmeter测试,控制台输出
用mat打开这个内存快照
点击detail查看详情
综合报告中同样能看到此类信息
2.2 MAT检测内存泄漏原理
2.2.1 生成内存快照观察深堆浅堆
public class HeapDemo {
public static void main(String[] args) {
TestClass a1 = new TestClass();
TestClass a2 = new TestClass();
TestClass a3 = new TestClass();
String s1 = "itheima1";
String s2 = "itheima2";
String s3 = "itheima3";
a1.list.add(s1);
a2.list.add(s1);
a2.list.add(s2);
a3.list.add(s3);
//System.out.print(ClassLayout.parseClass(TestClass.class).toPrintable());
s1 = null;
s2 = null;
s3 = null;
System.gc();
}
}
class TestClass {
public List<String> list = new ArrayList<>(10);
}
转换为支配树
添加虚拟机参数
-XX:+HeapDumpBeforeFullGC -XX:HeapDumpPath=D:/File/StudyJavaFile/JavaStudy/JVM/shizhan/day05/resource/dump/mattest.hprof
启动,控制台输出
Dumping heap to D:/File/StudyJavaFile/JavaStudy/JVM/shizhan/day05/resource/dump/mattest.hprof ...
Heap dump file created [2507919 bytes in 0.010 secs]
用mat打开,并且打开支配树
打印类的信息
引入
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
public class StringSize {
public static void main(String[] args) {
//使用JOL打印String对象
System.out.print(ClassLayout.parseClass(String.class).toPrintable());
}
}
输出
java.lang.String object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 char[] String.value N/A
16 4 int String.hash N/A
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
0-11 是类头,12-15是char[] String.value,16-19是int String.hash,因为用的是64位系统,所以需要和8字节对齐,所以20-23是补齐8字节。
2.3 导出运行中内存的快照并分析
2.3.1 jmap导出
服务器中有3个,一个是arthas,两个是jvm-optimize
运行jmap命令输出9527的内存快照
jmap -dump:live,format=b,file=/home/jvm/dump/jvm-optimize-jmap.hprof 29317
2.3.2 arthas导出
访问182.92.117.86:8080/apps.html
随便进入一个
输入
heapdump --live /home/jvm/dump/jvm-optimize-arthas.hprof
得到俩,下载到本地后用mat打开
再看柱状图
要是一个对象的深堆过于的大的时候就可能发生内存泄漏。现在都在一个数量级。还算正常
2.4 分析超大堆内存快照
3 实战
3.1 分页查询文章接口的内存溢出
3.1.1 设置参数重启jar包
重新添加参数启动9527
nohup java -jar -Dserver.port=9527 -Darthas.http-port=9528 -Darthas.telnet-port=9529 -Xmx512m -Xms512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/jvm/dump/jvm9527.hprof jvm-optimize-0.0.1-SNAPSHOT.jar &
打开Prometheus查看运行情况,现在只有100M还算ok
3.1.2 准备数据
在docker的mysql中生成了11w数据,方便后面查询
3.1.3 jmeter中导入测试脚本
这样等于150个线程×10000条数据保存到内存中
先查询一个
没什么问题,启动jmeter测试脚本
运行了五分钟都没溢出。
3.1.4 定位问题
直接看老师的视频吧,用mat分析内存结构,打开直方图和支配树,占用大的一个是rowData,一个是tomcat的线程。
先从线程入手,选择线程
发现是DemoQueryController在执行queryByPage这个方法
@GetMapping
public ResponseEntity<Page<TbArticle>> queryByPage(TbArticle tbArticle, int page,int size) {
//size = Math.min(100,size);
return ResponseEntity.ok(this.articleService.queryByPage(tbArticle, PageRequest.of(page,size)));
}
很多TbArticle对象只是被创建了,但是还没有返回,大量存在于内存中,在开发环境重新复现这个问题。
3.1.5 解决思路
选用一进行解决。
size = Math.min(100,size);
限制查询个数
3.2 Mybatis导致的内存溢出
3.2.1 定位问题
用3.1一样的分析方法,HandlerMethod->List->with outgoing references,定位到这里
@GetMapping
public ResponseEntity countIfAbsent(int size) {
//随机生成批量id
List<Integer> ids = new Random().ints(0, 1000000).
limit(size).boxed().collect(Collectors.toList());
return ResponseEntity.ok(this.articleService.countIfAbsent(ids));
}
会生成一个供mybatis去foreach的hashmap数组,占据大量空间
3.2.2 解决思路
3.3 导出大文件内存溢出
3.3.1 k8s部署
k8s搞不定,光听算了
3.3.2 解决思路
3.4 ThreadLocal使用时占用大量内存
3.4.1 定位问题
每次拦截用户信息都存在threadlocal中
public class UserInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
UserDataContextHolder.userData.set(new UserDataContextHolder.UserData());
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
UserDataContextHolder.userData.remove();
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
但是如果出现异常,postHandle
中remove方法无法执行,用户信息就永远留在threadlocal中,所以需要吧remove移到afterCompletion中。
并且和tomcat的配置也有关系,最低保留100个线程进行处理,即使没有请求也有很多线程并不能被回收。
server:
port: 8881
tomcat:
threads:
min-spare: 100
max: 500
修改为min-spare: 10,最小值为10,不能为0。
3.4.2 解决思路
3.5 文章内容审核接口的内存问题
3.5.1 定位问题
3.5.1.1 线程池
如果使用线程池处理异步请求
3.5.1.2 生产者-消费者使用队列
3.5.1.3 消息中间件
4 诊断和解决问题
4.1 离线分析
为jmeter添加插件
添加插件后,查看响应时间
大概30s就能响应
通过
jmap -dump:live,format=b,file=/home/jvm/dump/offline-jamp.hprof 28865
会导致较高的响应时间
4.2 在线分析-arthas
jmap -histo:live 28865 > /home/jvm/dump/online-jamp-histo.txt
监控一下UserEntity,在arthas中输入
stack com.itheima.jvmoptimize.entity.UserEntity -n 1
4.3 在线分析-btrace追踪
4.3.1 添加依赖
<dependencies>
<dependency>
<groupId>org.openjdk.btrace</groupId>
<artifactId>btrace-agent</artifactId>
<version>${btrace.version}</version>
<scope>system</scope>
<systemPath>D:\Software\software_with_code\btrace-v2.2.4-bin\libs\btrace-agent.jar</systemPath>
</dependency>
<dependency>
<groupId>org.openjdk.btrace</groupId>
<artifactId>btrace-boot</artifactId>
<version>${btrace.version}</version>
<scope>system</scope>
<systemPath>D:\Software\software_with_code\btrace-v2.2.4-bin\libs\btrace-boot.jar</systemPath>
</dependency>
<dependency>
<groupId>org.openjdk.btrace</groupId>
<artifactId>btrace-client</artifactId>
<version>${btrace.version}</version>
<scope>system</scope>
<systemPath>D:\Software\software_with_code\btrace-v2.2.4-bin\libs\btrace-client.jar</systemPath>
</dependency>
</dependencies>
4.3.2 编写btrace脚本
@BTrace
public class TracingUserEntity {
@OnMethod(
clazz="com.itheima.jvmoptimize.entity.UserEntity",
method="/.*/")
public static void traceExecute(){
jstack();
}
}
method="/.*/"
中/
表示开始和结束,.*
表示监控clazz的所有方法jstack();
当调用当前类时,会打印当前所有栈信息
4.3.3 上传btrace工具及其脚本
上传btrace工具及其脚本,并且把他的bin目录放到环境变量中。
btrace 3785 TracingUserEntity.java
已经挂载到当前进程中,我们用jmeter发送请求。
但是加了btrace后,响应时间不是一般的长,平均都在300ms以上