【JVM實戰篇】內存調優:內存泄露危害+內存監控工具介紹+內存泄露原因介紹

文章目錄

  • 內存調優
    • 內存溢出和內存泄漏
      • 內存泄露帶來什么問題
      • 內存泄露案例演示
      • 內存泄漏的常見場景
        • 場景一
        • 場景二
    • 解決內存溢出的方法
      • 常用內存監控工具
        • Top命令
          • 優缺點
        • VisualVM
          • 軟件、插件
          • 優缺點
          • 監控本地Java進程
          • 監控服務器的Java進程(生產環境不推薦使用)
        • Arthas
          • 優缺點
          • **使用****`arthas tunnel`****管理所有的需要監控的程序**
        • Prometheus+Grafana(監控)
          • 優缺點
          • 阿里云環境搭建(了解即可)
      • 如何判斷有沒有出現內存泄漏
        • 堆內存狀況的對比
          • 正常情況
          • 出現內存泄漏處于持續增長的情況,即使Minor GC也不能把大部分對象回收手動FULL GC之后的內存量每一次都在增長長時間觀察內存曲線持續增長
        • 查看那些對象導致的內存泄漏
        • 代碼中的內存泄漏(產生內存溢出原因一)
          • 案例1:equals()和hashCode()導致的內存泄漏
            • 問題
            • 測試
            • 原因
            • 解決方案
          • 案例2:內部類引用外部類
            • 非靜態的內部類默認會持有外部類,盡管代碼上不再使用外部類
            • 匿名內部類對象如果在非靜態方法中被創建,會持有調用者對象,垃圾回收時無法回收調用者
          • 案例3:ThreadLocal的使用
          • 案例4:String的intern方法
          • 案例5:通過靜態字段保存對象
          • 案例6:資源沒有正常關閉
        • 并發請求問題(產生內存溢出原因二,關鍵)
          • Jmeter介紹
          • 使用Jmeter進行并發測試,發現內存溢出問題
  • 文章說明

內存調優

內存溢出和內存泄漏

內存泄漏(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<Inner> inners = new ArrayList<>();while (true){if(count++ % 100 == 0){Thread.sleep(10);}inners.add(new Inner());}}
}

【運行】
在這里插入圖片描述

Arthas統計的最后一次內存情況

在這里插入圖片描述

內存泄漏的常見場景

場景一
  • 大型的Java后端應用中,在處理用戶的請求之后,沒有及時將用戶的數據刪除。隨著用戶請求數量越來越多,內存泄漏的對象占滿了堆內存最終導致內存溢出。
  • 內存溢出會直接導致用戶請求無法處理,影響用戶的正常使用。重啟可以恢復應用使用(治標不治本),但是在運行一段時間之后依然會出現內存溢出。

在這里插入圖片描述

【場景演示】

代碼:

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<Long,Object> userCache = new HashMap<>();/*** 登錄接口 放入hashmap中*/@PostMapping("/login")public void login(String name,Long id){// 每次放300MuserCache.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

在這里插入圖片描述

調用幾次之后就會出現內存溢出:

在這里插入圖片描述

場景二
  • 分布式任務調度系統如Elastic-job、Quartz等進行任務調度時,被調度的Java應用在調度任務結束中出現了內存泄漏,導致多次調度之后內存溢出
  • 重啟可以恢復應用使用,但是在調度執行一段時間之后依然會出現內存溢出
    在這里插入圖片描述

開啟定時任務:

在這里插入圖片描述

定時任務代碼:

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<Object> list = new ArrayList<>();// 每隔100毫秒執行一次@Scheduled(fixedRate = 100L)public void test(){System.out.println("定時任務調用" + ++count);// 這行代碼存在內存泄露問題list.add(new Outer().newList());}
}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<String> newList() {List<String> list = new ArrayList<String>() {{add("1");add("2");}};return list;}
}

啟動程序之后很快就出現了內存溢出:

在這里插入圖片描述

解決內存溢出的方法

解決內存溢出的步驟總共分為四個步驟,其中前兩個步驟是最核心的:

在這里插入圖片描述

常用內存監控工具

Top命令
  • top命令是linux下用來查看系統信息的一個命令,它提供給我們去實時地去查看系統的資源,比如執行時的進程、線程和系統參數等信息
  • 進程使用的內存為RES(常駐內存)- SHR(共享內存)
  • 系統負載是可以大于1的,例如銀行中每個窗口都被占用了,但是還有的人在排隊,這些人也會被記錄到負載中

在這里插入圖片描述

上面的列表默認是按照CPU占用率降序排序的,如果想要按照內存來降序排序,需要先按CapsLock鎖定大寫,然后再按下M,通過這個方式可以快速知道哪個進程占用了大量內存

在這里插入圖片描述

優缺點

優點:

  • 操作簡單
  • 無額外的軟件安裝

缺點:

  • 只能查看最基礎的進程信息,無法查看到每個部分的內存占用(堆、方法區、堆外) (top命令只適合做初步的篩查)
VisualVM

VisualVM是多功能合一的Java故障排除工具并且他是一款可視化工具,整合了命令行 JDK 工具和輕量級分析功能,功能非常強大。這款軟件在Oracle JDK 6~8 中發布,但是在 Oracle JDK 9 之后不在JDK安裝目錄下需要單獨下載。下載地址:https://visualvm.github.io/

在這里插入圖片描述

軟件、插件

JDK8自帶

在這里插入圖片描述

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

在這里插入圖片描述

【插件】

安裝插件之后,可以快速啟動VisualVM

在這里插入圖片描述

配置軟件路徑

在這里插入圖片描述

使用

在這里插入圖片描述

優缺點

優點:

  • 功能豐富,實時監控CPU、內存、線程等詳細信息
  • 支持Idea插件,開發過程中也可以使用

缺點:

  • 對大量集群化部署的Java進程(微服務項目)需要手動進行管理,一個一個去添加對應的服務器以及對應的JMX端口
監控本地Java進程

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

如果used逼近max,說明內存快不夠用了

監控服務器的Java進程(生產環境不推薦使用)

如果需要進行遠程監控,可以通過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

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版本號保持一致:

<dependency><groupId>com.taobao.arthas</groupId><artifactId>arthas-spring-boot-starter</artifactId><version>3.7.1</version>
</dependency>

application.yml中添加配置:

arthas:#tunnel地址,目前是部署在同一臺服務器,正式環境需要拆分 /ws是固定路徑tunnel-server: ws://localhost:7777/ws#tunnel顯示的應用名稱,直接引用應用名app-name: ${spring.application.name}#arthas http訪問的端口和遠程連接的端口http-port: 8888telnet-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(監控)

Prometheus+Grafana是企業中運維常用的監控方案

  • 其中Prometheus用來采集系統或者應用的相關數據,同時具備告警功能
  • Grafana可以將Prometheus采集到的數據以可視化的方式進行展示

在這里插入圖片描述

優缺點

優點:

  • 支持系統級別和應用級別的監控,比如linux操作系統、Redis、MySQL、Java進程。(監控范圍廣)
  • 支持告警并允許自定義告警指標,通過郵件、短信等方式盡早通知相關人員進行處理

缺點:

  • 環境搭建較為復雜,一般由專業運維人員完成。java程序員責任:看懂Grafana的圖
阿里云環境搭建(了解即可)

這一小節主要是為了讓同學們更好地去閱讀監控數據,所以提供一整套簡單的環境搭建方式,覺得困難可以直接跳過。企業中環境搭建的工作由運維人員來完成。

1、在pom文件中添加依賴

  • actuator:通過http將一些指標向外暴露
  • micrometer:將java基本信息,包括java虛擬機信息、數據庫連接池信息、磁盤等信息收集起來,并組裝成prometheus可以識別的格式
<dependency><groupId>io.micrometer</groupId><artifactId>micrometer-registry-prometheus</artifactId><scope>runtime</scope>
</dependency>
<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>

2、添加配置項

management:endpoint:metrics:enabled: true #支持metricsprometheus:enabled: true #支持Prometheusmetrics:export:prometheus:enabled: truetags: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、指標內容:

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

如何判斷有沒有出現內存泄漏

堆內存狀況的對比
正常情況
  • 處理業務時會出現上下起伏,業務對象頻繁創建內存會升高,觸發MinorGC之后內存會降下來。
  • 手動執行FULL GC之后,內存大小會驟降,而且每次降完之后的大小是接近的。
  • 長時間觀察內存曲線應該是在一個范圍內。

在這里插入圖片描述

出現內存泄漏處于持續增長的情況,即使Minor GC也不能把大部分對象回收手動FULL GC之后的內存量每一次都在增長長時間觀察內存曲線持續增長

在這里插入圖片描述

查看那些對象導致的內存泄漏

在這里插入圖片描述

在這里插入圖片描述

代碼中的內存泄漏(產生內存溢出原因一)

以下產生內存泄漏的原因,均來自于java代碼的不當處理:

  • equals()和hashCode(),不正確的equals()和hashCode()實現導致內存泄漏
  • ThreadLocal的使用,由于線程池中的線程不被回收導致的ThreadLocal內存泄漏
  • 內部類引用外部類,非靜態的內部類和匿名內部類的錯誤使用導致內存泄漏
  • String的intern方法,由于JDK6中的字符串常量池位于永久代,intern被大量調用并保存產生的內存泄漏
  • 通過靜態字段保存對象,大量的數據在靜態變量中被引用,但是不再使用,成為了內存泄漏
  • 資源沒有正常關閉,由于資源沒有調用close方法正常關閉,導致的內存溢出(不太準確,因為不一定導致內存泄漏)

代碼中的內存泄漏很容易暴露出來,做一次壓力測試就知道了

案例1:equals()和hashCode()導致的內存泄漏
問題

在定義新類時沒有重寫正確的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<Student,Long> 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;}@Overridepublic 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();}@Overridepublic int hashCode() {return new HashCodeBuilder(17, 37).append(id).toHashCode();}
}

【測試】

在這里插入圖片描述

為什么Student對象還是有這么多呢?不應該只有一個嗎?

原因:垃圾回收需要時間,這些對象沒有及時被垃圾回收

案例2:內部類引用外部類
非靜態的內部類默認會持有外部類,盡管代碼上不再使用外部類

所以如果有地方引用了這個非靜態內部類,會導致外部類也被引用,垃圾回收時無法回收這個外部類

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<Inner> 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<Inner> 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<String> newList() {# 這里使用了匿名內部類來初始化 list 變量。匿名內部類沒有顯式的類名,# 它是一個實現了 ArrayList<String> 接口(實際上是繼承自 ArrayList<String> 類)的未命名子類。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);// 這里創建的Outer對象不能被回收objects.add(new Outer().newList());}}
}

在這里插入圖片描述

查看字節碼文件

在這里插入圖片描述

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<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());}}
}

不再持有調用者對象

在這里插入圖片描述

在這里插入圖片描述

案例3:ThreadLocal的使用

問題:

  • ThreadLocal用來存儲線程里面的本地變量,每個線程之間的變量隔離,不會相互影響。
  • 如果僅僅使用手動創建的線程(new的方式),就算沒有調用ThreadLocal的remove方法清理數據,也不會產生內存泄漏。因為當線程被回收時,ThreadLocal也同樣被回收。
  • 但是如果使用線程池就不一定了。

【直接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<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);}}
}

沒有發生內存泄漏

在這里插入圖片描述

【使用線程池】

package com.itheima.jvmoptimize.leakdemo.demo5;import java.util.concurrent.*;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]);});Thread.sleep(10);}}
}

在這里插入圖片描述

解決方案:

線程方法執行完,一定要調用ThreadLocal中的remove方法清理對象。

package com.itheima.jvmoptimize.leakdemo.demo5;import java.util.concurrent.*;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);}}
}
案例4:String的intern方法

問題:

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<String> list = new ArrayList<String>();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<String> list = new ArrayList<String>();int i = 0;while (true) {// 產生了引用關系之后,就不會被回收了list.add(String.valueOf(i++).intern()); //溢出}}}
}

JDK6測試

在這里插入圖片描述

JDK8(字符串常量池放在堆里面)測試

在這里插入圖片描述

解決方案:

1、注意代碼中的邏輯,盡量不要將隨機生成的字符串加入字符串常量池

2、增大永久代空間的大小,根據實際的測試/估算結果進行設置-XX:MaxPermSize=256M

案例5:通過靜態字段保存對象

問題:

如果大量的數據在靜態變量中被長期引用,數據就不會被釋放,如果這些數據不再使用,就成為了內存泄漏。

解決方案:

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<Object, Object> build = Caffeine.newBuilder()// 設置100ms之后就過期.expireAfterWrite(Duration.ofMillis(100)).build();int count = 0;while (true){build.put(count++,new byte[1024 * 1024 * 10]);Thread.sleep(100L);}}
}
案例6:資源沒有正常關閉

問題:

連接和流這些資源會占用內存,如果使用完之后沒有關閉,這部分內存不一定會出現內存泄漏。

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 URLstatic final String JDBC_DRIVER = "com.mysql.cj.jdbc.Driver";static final String DB_URL = "jdbc:mysql:///bank1";//  Database credentialsstatic 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 querystmt = conn.createStatement();String sql;sql = "SELECT id, account_name FROM account_info";ResultSet rs = stmt.executeQuery(sql);//STEP 4: Extract data from result setwhile (rs.next()) {//Retrieve by column nameint id = rs.getInt("id");String name = rs.getString("account_name");//Display valuesSystem.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個請求

在這里插入圖片描述

解決方案:

  • 找到執行時間長、占用內存大的接口,看看怎么優化代碼
Jmeter介紹

使用Apache Jmeter軟件可以進行并發請求測試。Apache Jmeter是一款開源的測試軟件,使用Java語言編寫,最初是為了測試Web程序,目前已經發展成支持數據庫、消息隊列、郵件協議等不同類型內容的測試工具。

  • Jmeter可以模擬Http并發請求
  • Apache Jmeter支持插件擴展,生成多樣化的測試結果(如TPS、響應時間)
使用Jmeter進行并發測試,發現內存溢出問題

背景:

小李的團隊發現有一個微服務在晚上8點左右用戶使用的高峰期會出現內存溢出的問題,于是他們希望在自己的開發環境能重現類似的問題。

步驟:

1、安裝Jmeter軟件,添加線程組。

打開資料中的Jmeter,找到bin目錄,雙擊jmeter.bat啟動程序。

在這里插入圖片描述

  1. 創建線程組,在線程組中增加Http請求,添加隨機參數。

在這里插入圖片描述

添加線程組參數:

  • 線程數:并發線程的數量
  • Ramp-Up時間:上面的線程在多少時間啟動完成。(如果希望所有線程一開始就工作,就設置為0)
  • 循環次數:每個線程調用多少次請求(勾選永遠,就會持續發送請求)

在這里插入圖片描述

在線程組中添加Http請求:

在這里插入圖片描述

添加http參數:

在這里插入圖片描述

接口代碼:

/*** 大量數據 + 處理慢*/
@GetMapping("/test")
public void test1() throws InterruptedException {// 100m(模擬大量數據)byte[] bytes = new byte[1024 * 1024 * 100];// 模擬處理慢Thread.sleep(10 * 1000L);
}
  1. 在線程組中添加監聽器 – 聚合報告,用來展示最終結果。
    在這里插入圖片描述

  2. 啟動程序,運行線程組并觀察程序是否出現內存溢出。

添加虛擬機參數:

在這里插入圖片描述

點擊運行:

在這里插入圖片描述

很快就出現了內存溢出:

在這里插入圖片描述

【內存泄漏案例】

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一套搞定(豐富的實戰案例及最熱面試題),也有部分內容來自于自己的思考,發布文章是想幫助其他學習的人更方便地整理自己的筆記或者直接通過文章學習相關知識,如有侵權請聯系刪除,最后對 黑馬程序員 的優質課程表示感謝。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/44823.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/44823.shtml
英文地址,請注明出處:http://en.pswp.cn/web/44823.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【圖解大數據技術】流式計算:Spark Streaming、Flink

【圖解大數據技術】流式計算&#xff1a;Spark Streaming、Flink 批處理 VS 流式計算Spark StreamingFlinkFlink簡介Flink入門案例Streaming Dataflow Flink架構Flink任務調度與執行task slot 和 task EventTime、Windows、WatermarksEventTimeWindowsWatermarks 批處理 VS 流式…

如何查找電腦的MAC地址

一. 什么是mac地址&#xff1f; mac地址本質上幫助我們連接到我們遇到的大多數本地網絡。每個網絡適配器通常由網絡接口??控制器(NIC) 制造商分配一個唯一的 mac 地址。 二. 如何查找mac地址 1.點擊網絡和Internet設置 2.點擊WLAN點擊硬件屬性 3.即可查看mac地址

智慧城市3d數據可視化系統提升信息匯報的時效和精準度

在信息大爆炸的時代&#xff0c;數據的力量無可估量。而如何將這些數據以直觀、高效的方式呈現出來&#xff0c;成為了一個亟待解決的問題。為此&#xff0c;我們推出了全新的3D可視化數據大屏系統&#xff0c;讓數據“躍然屏上”&#xff0c;助力您洞察先機&#xff0c;決勝千…

從零開始實現大語言模型(五):縮放點積注意力機制

1. 前言 縮放點積注意力機制(scaled dot-product attention)是OpenAI的GPT系列大語言模型所使用的多頭注意力機制(multi-head attention)的核心,其目標與前文所述簡單自注意力機制完全相同,即輸入向量序列 x 1 , x 2 , ? ? , x n x_1, x_2, \cdots, x_n x

pytorch訓練的時候 shm共享內存不足,導致訓練停止

1.查看shm情況 df -h /dev/shm內存已經滿了&#xff0c;因為之前訓練多次訓練意外停止到shm中的緩存不能及時被清理 2、手動清理shm 依然沒被釋放 3、查看關聯的進程&#xff0c;一個一個kill lsof |grep deletedkill -9 46619 44618 44617 。。。。。4、搞定

Spring @Scheduled學習

一. Jdk中的定時任務 我們平時在 Spring 項目中會使用 Scheduled 開啟定時任務&#xff1b; jdk 中其實也提供了定時任務線程池 ScheduledThreadPool&#xff0c;我們可以直接通過 Executors 工具類獲取&#xff1b; // 創建了核心線程數為 2 的 ScheduledThreadPool 對象 S…

ROS2 + 科大訊飛 初步實現機器人語音控制

環境配置&#xff1a; 電腦端&#xff1a; ubuntu22.04實體機作為上位機 ROS版本&#xff1a;ros2-humble 實體機器人&#xff1a; STM32 思嵐A1激光雷達 科大訊飛語音SDK 訊飛開放平臺-以語音交互為核心的人工智能開放平臺 實現步驟&#xff1a; 1. 下載和處理科大訊飛語音模…

開發指南048-前端模塊版本

平臺前端框架內置了一個文件version.vue <template> <div> <br> 應用名稱: {{name}} <br> 當前版本&#xff1a;{{version}} <br> 服務網關: {{gateway}} </div> </template> <scrip…

qt 創建一個包含兩按鈕,且安裝和自定義控件間沒有間距

在 Qt 中創建一個包含兩個按鈕且按鈕之間沒有間距的自定義控件&#xff0c;你可以使用 QHBoxLayout 或 QVBoxLayout&#xff08;取決于你希望按鈕是水平排列還是垂直排列&#xff09;&#xff0c;并設置布局的間距為 0。以下是一個簡單的示例&#xff0c;展示了如何創建一個水平…

Dataset for Stable Diffusion

1.Dataset for Stable Diffusion 筆記來源&#xff1a; 1.Flickr8k數據集處理 2.處理Flickr8k數據集 3.Github&#xff1a;pytorch-stable-diffusion 4.Flickr 8k Dataset 5.dataset_flickr8k.json 1.1 Dataset 采用Flicker8k數據集&#xff0c;該數據集有兩個文件&#xff…

Node.js_mongodb用戶名和密碼操作

mongodb用戶名和密碼操作 查看用戶密碼創建管理員用戶和密碼mongodb的目標是實現快速簡單部署,所以存在很多安全問題 默認配置下沒有用戶和密碼,無需身份驗證即可登錄,不像mysql那樣需要登錄才能操作數據庫本身安全問題:升級3.0以上版本查看用戶密碼 密碼是加密存儲的,并且…

前端工程化10-webpack靜態的模塊化打包工具之各種loader處理器

9.1、案例編寫 我們創建一個component.js 通過JavaScript創建了一個元素&#xff0c;并且希望給它設置一些樣式&#xff1b; 我們自己寫的css,要把他加入到Webpack的圖結構當中&#xff0c;這樣才能被webpack檢測到進行打包&#xff0c; style.css–>div_cn.js–>main…

速盾:ddos高防ip哪里好用?

隨著互聯網的飛速發展&#xff0c;DDoS攻擊問題逐漸突出。DDoS攻擊是一種通過在網絡上創建大量請求&#xff0c;使目標網絡或服務器過載而無法正常工作的攻擊方式。為了應對DDoS攻擊&#xff0c;提高網絡的安全性和穩定性&#xff0c;使用高防IP成為了一種常見的解決辦法。 DD…

Flower花所比特幣交易及交易費用科普

在加密貨幣交易中&#xff0c;選擇一個可靠的平臺至關重要。Flower花所通過提供比特幣交易服務脫穎而出。本文將介紹在Flower花所進行比特幣交易的基礎知識及其交易費用。 什么是Flower花所&#xff1f; Flower花所是一家加密貨幣交易平臺&#xff0c;為新手和資深交易者提供…

【C++】開源:drogon-web框架配置使用

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 這篇文章主要介紹drogon-web框架配置使用。 無專精則不能成&#xff0c;無涉獵則不能通。——梁啟超 歡迎來到我的博客&#xff0c;一起學習&#xff0c;共同進步。 喜歡的朋友可以關注一下&#xff0c;…

Linux系統編程-線程同步詳解

線程同步是指多個線程協調工作&#xff0c;以便在共享資源的訪問和操作過程中保持數據一致性和正確性。在多線程環境中&#xff0c;線程是并發執行的&#xff0c;因此如果多個線程同時訪問和修改共享資源&#xff0c;可能會導致數據不一致、競態條件&#xff08;race condition…

面試題008-Java-SpringBoot

面試題008-Java-SpringBoot 目錄 面試題008-Java-SpringBoot題目自測題目答案1. Spring 和 Spring Boot有什么區別&#xff1f;2. Spring Boot 的主要優點是什么&#xff1f;3. 什么是Spring Boot Starter&#xff1f;4. 介紹一下SpringBootApplication注解&#xff1f;5. Spri…

【密碼學】消息認證

你發送給朋友一條消息&#xff08;內容&#xff1a;明天下午來我家吃飯&#xff09;&#xff0c;這一過程中你不想讓除你朋友以外的人看到消息的內容&#xff0c;這就叫做消息的機密性&#xff0c;用來保護消息機密性的方式被叫做加密機制。 現在站在朋友的視角&#xff0c;某一…

使用PyQt5實現添加工具欄、增加SwitchButton控件

前言&#xff1a;通過在網上找到的“電池電壓監控界面”&#xff0c;學習PyQt5中添加工具欄、增加SwitchButton控件&#xff0c;在滑塊控件右側增加文本顯示、設置界面背景顏色、修改文本控件字體顏色等。 1. 上位機界面效果展示 網絡上原圖如下&#xff1a; 自己使用PyQt5做…

springboot異常(一):springboot自定義全局異常處理

&#x1f337;1. 自定義一個異常類 自定義一個異常&#xff0c;有兩個變量異常代碼、異常消息&#xff0c;定義了兩個構造方法&#xff0c;一個無參構造方法&#xff0c;一個所有參數構造方法。 在構造方法中要掉用父類的構造方法&#xff0c;主要目的是在日志或控制臺打印異…