文章目錄
- 內存調優
- 內存溢出和內存泄漏
- 內存泄露帶來什么問題
- 內存泄露案例演示
- 內存泄漏的常見場景
- 場景一
- 場景二
- 解決內存溢出的方法
- 常用內存監控工具
- 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
啟動程序。
- 創建線程組,在線程組中增加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、設置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一套搞定(豐富的實戰案例及最熱面試題),也有部分內容來自于自己的思考,發布文章是想幫助其他學習的人更方便地整理自己的筆記或者直接通過文章學習相關知識,如有侵權請聯系刪除,最后對 黑馬程序員 的優質課程表示感謝。