内存泄漏(memory leak):在Java中如果不再使用一个对象,但是该对象依然在GC ROOT的引用链上,这个对象就不会被垃圾回收器回收,这种情况就称之为内存泄漏。
内存泄漏绝大多数情况都是由堆内存泄漏引起的,所以后续没有特别说明则讨论的都是堆内存泄漏。
比如图中,如果学生对象1不再使用

可以选择将ArrayList到学生对象1的引用删除,即调用remove方法

如果整个集合都不再使用,将对象A对ArrayList的引用删除(A = null),这样所有的学生对象包括ArrayList都可以回收:

但是如果不移除这两个引用中的任何一个,学生对象1就属于内存泄漏了。
少量的内存泄漏可以容忍,但是如果发生持续的内存泄漏,就像滚雪球雪球越滚越大,不管有多大的内存迟早会被消耗完,最终导致的结果就是内存溢出。
产生内存溢出并不是只有内存泄漏这一种原因。

这些学生对象如果都不再使用,越积越多,就会导致超过堆内存的上限出现内存溢出。
正常情况的内存结构图如下:

内存溢出出现时如下:

内存泄漏的对象和依然在GC ROOT引用链上需要使用的对象加起来占满了内存空间,无法为新的对象分配内存。
Arthas中使用dashboard -i 1000,可以每隔一秒统计一下堆内存的使用情况

package com.itheima.jvmoptimize.leakdemo.demo3;  import java.io.IOException; import java.util.ArrayList;  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 static void main(String[] args) throws IOException, InterruptedException { //        System.in.read();         int count = 0;         ArrayList inners = new ArrayList<>();         while (true){             if(count++ % 100 == 0){                 Thread.sleep(10);             }             inners.add(new Inner());         }     } }  【运行】
Arthas统计的最后一次内存情况


【场景演示】
代码:
package com.itheima.jvmoptimize.controller;  import com.itheima.jvmoptimize.entity.UserEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;  import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map;  @RestController @RequestMapping("/leak2") public class LeakController2 {     private static Map userCache = new HashMap<>();      /**      * 登录接口 放入hashmap中      */     @PostMapping("/login")     public void login(String name,Long id){         // 每次放300M         userCache.put(id,new byte[1024 * 1024 * 300]);     }       /**      * 登出接口,删除缓存的用户信息      */     @GetMapping("/logout")     public void logout(Long id){         userCache.remove(id);     }  }  设置虚拟机参数,将最大堆内存设置为1g:

在Postman中测试,登录id为1的用户:

调用logout接口,id为1那么数据会正常删除:

连续调用login传递不同的id,但是不调用logout

调用几次之后就会出现内存溢出:


开启定时任务:

定时任务代码:
package com.itheima.jvmoptimize.task;  import com.itheima.jvmoptimize.leakdemo.demo4.Outer; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component;  import java.util.ArrayList; import java.util.List;  @Component public class LeakTask {      private int count = 0;     private List启动程序之后很快就出现了内存溢出:

解决内存溢出的步骤总共分为四个步骤,其中前两个步骤是最核心的:


上面的列表默认是按照CPU占用率降序排序的,如果想要按照内存来降序排序,需要先按CapsLock锁定大写,然后再按下M,通过这个方式可以快速知道哪个进程占用了大量内存

优点:
缺点:
VisualVM是多功能合一的Java故障排除工具并且他是一款可视化工具,整合了命令行 JDK 工具和轻量级分析功能,功能非常强大。这款软件在Oracle JDK 6~8 中发布,但是在 Oracle JDK 9 之后不在JDK安装目录下需要单独下载。下载地址:https://visualvm.github.io/

JDK8自带

更高版本的JDK,直接下载最新版即可

【插件】
安装插件之后,可以快速启动VisualVM

配置软件路径

使用

优点:
缺点:





如果used逼近max,说明内存快不够用了
如果需要进行远程监控,可以通过jmx方式进行连接。在启动java程序时添加如下参数:
-Djava.rmi.server.hostname=服务器ip地址 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9122 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false 
右键点击remote

填写服务器的ip地址:

右键添加JMX连接

填写ip地址和端口号,勾选不需要SSL安全验证:

双击成功连接

生产环境不建议使用VisualVM来连接远程服务器,因为其中的Perform GC(手动GC)和Heap Dump(生成内存快照)功能都会停止进程功能,影响用户体验
Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率。

优点:
缺点:
arthas tunnel**管理所有的需要监控的程序背景:
小李的团队已经普及了arthas的使用,但是由于使用了微服务架构,生产环境上的应用数量非常多,使用arthas还得登录到每一台服务器上再去操作非常不方便。为了解决这个问题,可以使用tunnel来管理所有需要监控的程序。

步骤:
1、在Spring Boot程序中添加arthas的依赖(只支持Spring Boot2.几的版本),在配置文件中添加tunnel服务端的地址,便于tunnel去监控所有的程序
2、将tunnel服务端程序部署在某台服务器上并启动(如果是生产环境,尽量将tunnel服务部署单独的服务器上,避免tunnel服务对线上业务造成影响)
3、启动java程序(微服务)
4、打开tunnel的服务端页面,查看所有的进程列表,并选择进程进行arthas的操作。
在微服务的pom.xml添加依赖,版本最好和使用的arthas版本号保持一致:
     com.taobao.arthas      arthas-spring-boot-starter      3.7.1    application.yml中添加配置:
arthas:   #tunnel地址,目前是部署在同一台服务器,正式环境需要拆分 /ws是固定路径   tunnel-server: ws://localhost:7777/ws   #tunnel显示的应用名称,直接引用应用名   app-name: ${spring.application.name}   #arthas http访问的端口和远程连接的端口   http-port: 8888   telnet-port: 9999 在资料中找到arthas-tunnel-server.3.7.1-fatjar.jar上传到服务器,并使用
nohup java -jar -Darthas.enable-detail-pages=true arthas-tunnel-server.3.7.1-fatjar.jar &  命令启动tunnel服务。-Darthas.enable-detail-pages=true这个参数的作用是让tunnel提供一个页面展示内容,默认是不提供的。服务器ip地址:8080/apps.html打开页面,目前没有注册上来任何应用。
启动spring boot应用,如果在一台服务器上,注意区分端口。
-Dserver.port=tomcat端口号 -Darthas.http-port=arthas的http端口号 -Darthas.telnet-port=arthas的telnet端口号端口号 

最终就能看到两个应用:
单击应用就可以进入操作arthas了。

如果有服务没有注册上来,查看nohup的日志文件,看看启动有没有报错

Prometheus+Grafana是企业中运维常用的监控方案

优点:
缺点:
这一小节主要是为了让同学们更好地去阅读监控数据,所以提供一整套简单的环境搭建方式,觉得困难可以直接跳过。企业中环境搭建的工作由运维人员来完成。
1、在pom文件中添加依赖
     io.micrometer      micrometer-registry-prometheus      runtime         org.springframework.boot      spring-boot-starter-actuator                             org.springframework.boot              spring-boot-starter-logging                    2、添加配置项
management:   endpoint:     metrics:       enabled: true #支持metrics     prometheus:       enabled: true #支持Prometheus   metrics:     export:       prometheus:         enabled: true     tags:       application: jvm-test #实例名采集   endpoints:     web:       exposure:         include: '*' #开放所有端口,即设置向外暴露的指标 这两步做完之后,启动程序。
3、通过地址:ip地址:端口号/actuator/prometheus访问之后可以看到jvm相关的指标数据。

查看普罗米修斯相关内容

查看内存信息

查看所有bean对象
4、创建阿里云Prometheus实例

5、选择ECS服务

6、在自己的ECS服务器上找到网络和交换机

7、选择对应的网络:

填写内容,与ECS里边的网络设置保持一致

安全组和服务器里面的安全组保持一致

8、选中新的实例,选择MicroMeter

想监控什么,就安装相关的插件

9、给服务器ECS添加标签;


10、填写内容,注意ECS的标签

11、点击大盘就可以看到指标了


打开Grafana页面,查看所有指标

12、指标内容:








以下产生内存泄漏的原因,均来自于java代码的不当处理:
代码中的内存泄漏很容易暴露出来,做一次压力测试就知道了
在定义新类时没有重写正确的equals()和hashCode()方法,默认使用Object的实现。在使用HashMap的场景下,如果使用这个类对象作为key,HashMap在判断key是否已经存在时会使用这些方法,如果重写方式不正确,会导致相同的数据被保存多份。
Student类没有重写equal和hashcode方法
package com.itheima.jvmoptimize.leakdemo.demo2;  import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder;  import java.util.Objects;  public class Student {     private String name;     private Integer id;     private byte[] bytes = new byte[1024 * 1024];      public String getName() {         return name;     }      public void setName(String name) {         this.name = name;     }      public Integer getId() {         return id;     }      public void setId(Integer id) {         this.id = id;     }  } package com.itheima.jvmoptimize.leakdemo.demo2;  import java.util.HashMap; import java.util.Map;  public class Demo2 {     public static long count = 0;     public static Map map = new HashMap<>();     public static void main(String[] args) throws InterruptedException {         while (true){             if(count++ % 100 == 0){                 // 休眠一下,让VisualVM获取数据,不然CPU可能忙于执行程序                 Thread.sleep(10);             }             Student student = new Student();             student.setId(1);             student.setName("张三");             map.put(student,1L);         }     } }  运行之后通过visualvm观察:

预测是大量学生对象加入hashmap中产生的问题

正常情况:
1、以JDK8为例,首先调用hash方法计算key的哈希值,hash方法中会使用到key(这里是Student的对象)的hashcode方法。根据hash方法的结果决定存放的数组中位置。
2、如果没有元素,直接放入。如果有元素,先判断key是否相等,会用到equals方法,如果key相等,直接替换value;key不相等,走链表或者红黑树查找逻辑,其中也会使用equals比对是否相同。

异常情况:
1、hashCode方法实现不正确,按照Object默认实现(采用一个随机数+三个确定的值运算出来),会导致相同id的学生对象计算出来的hash值不同,可能会被分到不同的槽中。

2、equals方法实现不正确,会导致key在比对时,即便学生对象的id是相同的,也被认为是不同的key。

3、长时间运行之后HashMap中会保存大量相同id的学生数据。

1、在定义新实体时,始终重写equals()和hashCode()方法。
2、重写时一定要确定使用了唯一标识去区分不同的对象,比如用户的id等。
3、hashmap使用时尽量使用编号id等数据作为key(效率更高),不要将整个实体类对象作为key存放。


equals方法用哪些字段来判断

代码:
package com.itheima.jvmoptimize.leakdemo.demo2;  import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder;  import java.util.Objects;  public class Student {     private String name;     private Integer id;     private byte[] bytes = new byte[1024 * 1024];      public String getName() {         return name;     }      public void setName(String name) {         this.name = name;     }      public Integer getId() {         return id;     }      public void setId(Integer id) {         this.id = id;     }      @Override     public boolean equals(Object o) {         if (this == o) {             return true;         }          if (o == null || getClass() != o.getClass()) {             return false;         }          Student student = (Student) o;          return new EqualsBuilder().append(id, student.id).isEquals();     }      @Override     public int hashCode() {         return new HashCodeBuilder(17, 37).append(id).toHashCode();     } } 【测试】

为什么Student对象还是有这么多呢?不应该只有一个吗?
原因:垃圾回收需要时间,这些对象没有及时被垃圾回收
所以如果有地方引用了这个非静态内部类,会导致外部类也被引用,垃圾回收时无法回收这个外部类
package com.itheima.jvmoptimize.leakdemo.demo3;  import java.io.IOException; import java.util.ArrayList;  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 inners = new ArrayList<>();         while (true){             if(count++ % 100 == 0){                 Thread.sleep(10);             }             inners.add(new Outer().new Inner());         }     } }  
外部类对象为内存泄漏对象,运行一段时间,就溢出了

为什么外部类对象会一直被保留下来

这个外部类对象在GC Root引用链上面,所以不会被回收
【解决方案】
这个案例中,使用内部类的原因是可以直接获取到外部类中的成员变量值,简化开发。如果不想持有外部类对象,应该使用静态内部类

package com.itheima.jvmoptimize.leakdemo.demo3;  import java.io.IOException; import java.util.ArrayList;  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 static void main(String[] args) throws IOException, InterruptedException { //        System.in.read();         int count = 0;         ArrayList inners = new ArrayList<>();         while (true){             if(count++ % 100 == 0){                 Thread.sleep(10);             }             inners.add(new Inner());         }     } }  
package com.itheima.jvmoptimize.leakdemo.demo4;  import java.io.IOException; import java.util.ArrayList; import java.util.List;  public class Outer {     private byte[] bytes = new byte[1024 * 1024 * 10];     public List newList() {         # 这里使用了匿名内部类来初始化 list 变量。匿名内部类没有显式的类名,         # 它是一个实现了 ArrayList 接口(实际上是继承自 ArrayList 类)的未命名子类。         List list = new ArrayList() {{             add("1");             add("2");         }};         return list;     }      public static void main(String[] args) throws IOException {         System.in.read();         int count = 0;         ArrayList     
查看字节码文件

Outer$1:匿名内部类

【解决方案】
使用静态方法,可以避免匿名内部类持有调用者对象。

package com.itheima.jvmoptimize.leakdemo.demo4;  import java.io.IOException; import java.util.ArrayList; import java.util.List;  public class Outer {     private byte[] bytes = new byte[1024 * 1024 * 10];     public static List newList() {         List list = new ArrayList() {{             add("1");             add("2");         }};         return list;     }      public static void main(String[] args) throws IOException {         System.in.read();         int count = 0;         ArrayList   不再持有调用者对象


问题:
【直接new】
package com.itheima.jvmoptimize.leakdemo.demo5;  import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit;  public class Demo5_1 {     public static ThreadLocal没有发生内存泄漏

【使用线程池】
package com.itheima.jvmoptimize.leakdemo.demo5;  import java.util.concurrent.*;  public class Demo5 {     public static ThreadLocal
解决方案:
线程方法执行完,一定要调用ThreadLocal中的remove方法清理对象。
package com.itheima.jvmoptimize.leakdemo.demo5;  import java.util.concurrent.*;  public class Demo5 {     public static ThreadLocal问题:
JDK6中字符串常量池位于堆内存中的Perm Gen永久代中,如果不同字符串的intern方法被大量调用,字符串常量池会不停的变大超过永久代内存上限之后就会产生内存溢出问题。
package com.itheima.jvmoptimize.leakdemo.demo6;  import org.apache.commons.lang3.RandomStringUtils;  import java.util.ArrayList; import java.util.List;  public class Demo6 {     public static void main(String[] args) {         while (true){             List list = new ArrayList();             int i = 0;             while (true) {                 // 每次循环创建一个字符串,放到常量池中                 String.valueOf(i++).intern(); //JDK1.6 perm gen 不会溢出             }         }     } }   测试发现,上述代码永久代内存不会溢出,因为内存满的话,会执行垃圾回收
package com.itheima.jvmoptimize.leakdemo.demo6;  import org.apache.commons.lang3.RandomStringUtils;  import java.util.ArrayList; import java.util.List;  public class Demo6 {     public static void main(String[] args) {         while (true){             List list = new ArrayList();             int i = 0;             while (true) {                 // 产生了引用关系之后,就不会被回收了                 list.add(String.valueOf(i++).intern()); //溢出             }         }     } }   JDK6测试

JDK8(字符串常量池放在堆里面)测试

解决方案:
1、注意代码中的逻辑,尽量不要将随机生成的字符串加入字符串常量池
2、增大永久代空间的大小,根据实际的测试/估算结果进行设置-XX:MaxPermSize=256M
问题:
如果大量的数据在静态变量中被长期引用,数据就不会被释放,如果这些数据不再使用,就成为了内存泄漏。
解决方案:
1、尽量减少将对象长时间的保存在静态变量中,如果不再使用,必须将对象删除(比如在集合中)或者将静态变量设置为null。
2、使用单例模式时,尽量使用懒加载(如果该类没有使用,不会创建对象),而不是立即加载。
package com.itheima.jvmoptimize.leakdemo.demo7;  import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component;  @Lazy //懒加载 @Component public class TestLazy {     private byte[] bytes = new byte[1024 * 1024 * 1024]; } 将内存上限设置为500,一旦使用这个对象,就会报错;如果没有添加@Lazy注解,不使用也会报错

3、Spring的Bean中不要长期存放大对象,如果是缓存用于提升性能,尽量设置过期时间定期失效。
package com.itheima.jvmoptimize.leakdemo.demo7;  import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine;  import java.time.Duration;  public class CaffineDemo {     public static void main(String[] args) throws InterruptedException {         Cache问题:
连接和流这些资源会占用内存,如果使用完之后没有关闭,这部分内存不一定会出现内存泄漏。
package com.itheima.jvmoptimize.leakdemo.demo1;  import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLConnection; import java.sql.*;  //-Xmx50m -Xms50m public class Demo1 {      // JDBC driver name and database URL     static final String JDBC_DRIVER = "com.mysql.cj.jdbc.Driver";     static final String DB_URL = "jdbc:mysql:///bank1";      //  Database credentials     static final String USER = "root";     static final String PASS = "123456";      public static void leak() throws SQLException {         //Connection conn = null;         Statement stmt = null;         Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);          // executes a valid query         stmt = conn.createStatement();         String sql;         sql = "SELECT id, account_name FROM account_info";         ResultSet rs = stmt.executeQuery(sql);          //STEP 4: Extract data from result set         while (rs.next()) {             //Retrieve by column name             int id = rs.getInt("id");             String name = rs.getString("account_name");              //Display values             System.out.print("ID: " + id);             System.out.print(", Name: " + name + "\n");         }      }      public static void main(String[] args) throws InterruptedException, SQLException {         while (true) {             leak();         }     } } 同学们可以测试一下这段代码会不会产生内存泄漏,应该是不会的,因为后面conn对象不使用了,不再处于GC Root引用链中,会被回收。同理,rs这些也会被回收。但是这个结论不是确定的,所以建议编程时养成良好的习惯,尽量关闭不再使用的资源。
解决方案:
1、为了防止出现这类的资源对象泄漏问题,必须在finally块中关闭不再使用的资源。
2、从 Java 7 开始,使用try-with-resources语法可以用于自动关闭资源。

通过发送请求向Java应用获取数据,正常情况下Java应用将数据返回之后,这部分数据就可以在内存中被释放掉。
接收到请求时创建对象:

响应返回之后,对象就可以被回收掉:

并发请求问题指的是由于用户的并发请求量有可能很大,同时处理数据的时间很长,导致大量的数据存在于内存中,最终超过了内存的上限,导致内存溢出。这类问题的处理思路和内存泄漏类似,首先要定位到对象产生的根源。SpringBoot里面的tomcat线程池的线程最多只有200个,所以同时只能处理200个请求

解决方案:
使用Apache Jmeter软件可以进行并发请求测试。Apache Jmeter是一款开源的测试软件,使用Java语言编写,最初是为了测试Web程序,目前已经发展成支持数据库、消息队列、邮件协议等不同类型内容的测试工具。
背景:
小李的团队发现有一个微服务在晚上8点左右用户使用的高峰期会出现内存溢出的问题,于是他们希望在自己的开发环境能重现类似的问题。
步骤:
1、安装Jmeter软件,添加线程组。
打开资料中的Jmeter,找到bin目录,双击jmeter.bat启动程序。


添加线程组参数:

在线程组中添加Http请求:

添加http参数:

接口代码:
/**  * 大量数据 + 处理慢  */ @GetMapping("/test") public void test1() throws InterruptedException {     // 100m(模拟大量数据)     byte[] bytes = new byte[1024 * 1024 * 100];     // 模拟处理慢     Thread.sleep(10 * 1000L); } 在线程组中添加监听器 – 聚合报告,用来展示最终结果。
启动程序,运行线程组并观察程序是否出现内存溢出。
添加虚拟机参数:

点击运行:

很快就出现了内存溢出:

【内存泄漏案例】
1、设置线程池参数:

2、设置http接口参数

3、代码:
/**  * 登录接口 传递名字和id,放入hashmap中  */ @PostMapping("/login") public void login(String name,Long id){     // userCache是一个hashMap (静态变量中存放大量数据)     userCache.put(id,new UserEntity(id,name)); } 4、我们想生成随机的名字和id,选择函数助手对话框

5、选择Random随机数生成器
6、让随机数生成器生效,值中直接ctrl + v就行,已经被复制到粘贴板了。

7、字符串也是同理的设置方法:

8、添加name字段:

9、点击测试,一段时间之后同样出现了内存溢出:

该文章是本人学习 黑马程序员 的学习笔记,文章中大部分内容来源于 黑马程序员 的视频黑马程序员JVM虚拟机入门到实战全套视频教程,java大厂面试必会的jvm一套搞定(丰富的实战案例及最热面试题),也有部分内容来自于自己的思考,发布文章是想帮助其他学习的人更方便地整理自己的笔记或者直接通过文章学习相关知识,如有侵权请联系删除,最后对 黑马程序员 的优质课程表示感谢。