🌸個人主頁:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343
🏵?熱門專欄:
🧊 Java基本語法(97平均質量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm=1001.2014.3001.5482
🍕 Collection與數據結構 (93平均質量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482
🧀線程與網絡(97平均質量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482
🍭MySql數據庫(95平均質量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482
🍬算法(97平均質量分)https://blog.csdn.net/2301_80050796/category_12676091.html?spm=1001.2014.3001.5482
🍃 Spring(97平均質量分)https://blog.csdn.net/2301_80050796/category_12724152.html?spm=1001.2014.3001.5482
🎃Redis(97平均質量分)https://blog.csdn.net/2301_80050796/category_12777129.html?spm=1001.2014.3001.5482
🐰RabbitMQ(97平均質量分) https://blog.csdn.net/2301_80050796/category_12792900.html?spm=1001.2014.3001.5482
感謝點贊與關注~~~
目錄
- 1. 內存溢出和內存泄漏
- 1.1 基本概念
- 1.2 內存泄漏的常見場景
- 1.3 常見的虛擬機參數
- 2. 解決內存溢出的辦法
- 2.1 常用的監控工具
- 2.1.1 Top命令:
- 2.2.2 VisualVM
- 2.2.3 Arthas
- 2.2.4 使用阿里arthas tunnel管理所有的需要監控的程序
- 2.2.5 Prometheus+Grafana
- 2.2 堆內存情況的對比
- 2.3 產生內存溢出的原因一: 代碼中的內存泄漏
- 2.3.1 equals和hashCode導致的內存泄漏
- 2.3.2 內部類引用外部類
- 2.3.3 ThreadLocal的使用
- 2.3.4 String的intern方法
- 2.3.5 通過靜態字段保存對象
- 2.3.6 資源沒有正常關閉
- 2.4 產生內存溢出的原因二: 并發請求問題
- 2.5 診斷
- 2.5.1 內存快照
- 2.5.2 MAT內存泄漏檢測原理
- 2.5.3 服務器上的內存快照導出和分析
- 背景:
- 思路:
1. 內存溢出和內存泄漏
1.1 基本概念
內存泄漏: 在java中如果不再使用一個對象,但是該對象依然在GC Root的引用鏈上,這個對象就不會被垃圾回收器回收,這種情況就稱之為內存泄漏.
內存泄漏絕大多數都是由對內存泄漏引起的,所以后續沒有特別說明則討論的都是堆內存泄漏.
比如圖中,如果學生對象1不再使用
可以選擇將ArrayList到學生對象1的引用刪除:
或者將對象A堆ArrayList的引用刪除,這樣所有的學生包括ArrayList都可以回收:
但是如果不移除這兩個引用中的任何一個,學生對象1就屬于內存泄漏了.
少量的內存泄漏可以容忍,但是如果發生持續的內存泄漏,就像滾雪球一樣越滾越大,不管由多大的內存遲早會被消耗完,最終導致內存溢出,但是產生的內存溢出并不只有內存泄漏這一種原因.
這些學生對象如果都不再使用,越積越多,就會導致超過堆內存的上限出現內存溢出.
正常情況的內存結構圖如下:
內存溢出出現時如下:
內存泄漏的對象依然在GC Root引用鏈上需要使用的對象加起來沾滿了內存空間,無法為新的對象分配內存.
1.2 內存泄漏的常見場景
- 內存泄漏導致溢出的常見場景是大型的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){userCache.put(id,new byte[1024 * 1024 * 300]);}/*** 登出接口,刪除緩存的用戶信息*/@GetMapping("/logout")public void logout(Long id){userCache.remove(id);}}
設置虛擬機參數,將最大堆內存設置為1g:
PostMan連續調用login傳遞不同的id,但是不調用logout.
調用幾次之后就會出現內存溢出:
2. 第二種常見的是分布式任務調度系統如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<>();@Scheduled(fixedRate = 100L)public void test(){System.out.println("定時任務調用" + ++count);list.add(new Outer().newList());}
}
啟動程序之后很快就出現了內存溢出:
1.3 常見的虛擬機參數
2. 解決內存溢出的辦法
首先要熟悉一些常用的監控工具:
2.1 常用的監控工具
2.1.1 Top命令:
top命令是Linux下用來查看系統信息的一個命令,它提供我們去實地地去查看系統的資源,比如執行時的進程,線程和系統參數等信息,進程使用的內存為res(常駐內存)-shr(共享內存)
優點:
- 操作簡單
- 無額外的軟件安裝
缺點:
- 只能查看最基礎的進程信息,無法查看到每個部分的內存占用(堆,方法區,堆外).
2.2.2 VisualVM
==VisualVM是多功能合一的java故障排除工具并且他是一款可視化工具,==整合了命令行JDK工具和輕量級分析功能,功能非常強大.這款軟件在Oracle JDK 6~8 中發布,但是在 Oracle JDK 9 之后不在JDK安裝目錄下需要單獨下載.下載地址:
https://visualvm.github.io/
優點:
- 功能豐富,實時監控CPU,內存,線程等詳細信息.
- 支持idea插件,開發過程中也可以使用.
缺點:
- 對大量集群化部署的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安全驗證:
雙擊成功連接:
2.2.3 Arthas
Arthas是一款線上監控診斷產品,通過全局視角實時查看應用Load,內存,gc,線程的狀態信息,并能在不修改引用代碼的情況下,對業務問題進行診斷,包括查看方法調用的出入參,異常,檢測方法執行耗時,類加載信息等,大大提升線上問題的排查效率.
優點:
- 功能強大,不止于監控基礎信息,還可以監控單個方法的執行耗時等細節內容.
- 支持應用的集群管理
缺點:
部分高級功能使用門檻較高
2.2.4 使用阿里arthas tunnel管理所有的需要監控的程序
背景:
小李的團隊已經普及了arthas的使用,但是由于使用了微服務框架,生產環境上的應用數量非常多,使用arthas還得登錄到每一臺服務器上再去操作非常不方便,他看到官方文檔上可以使用tunnel來管理所有需要監控的程序.
步驟:
- 在SpringBoot程序中添加了Arthas的依賴(支持SpringBoot2),在配置文件中添加了tunnel服務端的地址,便于tunnel去監控所有的程序.
- 將tunnel服務端程序部署在某臺服務器上并啟動.
- 啟動java程序
- 打開tunnel的服務端頁面,查看所有的進程列表,并選擇進程進行arthas的操作.
pom.xml添加依賴:
<dependency><groupId>com.taobao.arthas</groupId><artifactId>arthas-spring-boot-starter</artifactId><version>3.7.1</version>
</dependency>
application.yml中添加配置:
arthas:#tunnel地址,目前是部署在同一臺服務器,正式環境需要拆分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 &
命令啟動該程序. -Darthas.enable-detail-pages=true
參數作用是可以有一個頁面展示內容.通過服務器ip地址:8080/apps.html打開頁面,目前沒有注冊上來任何應用.
啟動SpringBoot應用,如果在一臺服務器上,注意區分端口.
-Dserver.port=tomcat端口號
-Darthas.http-port=arthas的http端口號
-Darthas.telnet-port=arthas的telnet端口號端口號
最終就能看到兩個應用:
單擊應用就可以進入操作arthas了.
2.2.5 Prometheus+Grafana
Prometheus+Grafana是企業中運維常用的監控方案,其中Prometheus用來采集系統或者應用的相關數據,同時具備告警功能,Grafana可以將Prometheus采集到的數據以可視化的方式進行展示.
優點:
- 支持系統級別和應用級別的監控,比如Linux操作系統,Redi,MySQL,java進程.
- 支持告警運行定義告警指標,通過郵件,短信等方式盡早通知相關人員進行處理.
缺點:
環境搭建較為復雜,一般有運維人員完成
2.2 堆內存情況的對比
- 正常情況
- 處理業務時會出現上下起伏,業務對象頻繁創建內存會升高,觸發MinorGC之后會降下來.
- 手動執行FULLGC之后,內存大小會驟降,而且每次降完之后的大小是接近的.
- 長時間觀察內存曲線應該是在一個范圍內.
- 出現內存泄漏
- 處于持續增長的情況,即使MinorGC也不能把大部分對象回收
- 手動FullGC之后的內存量每一次都在增長
- 長時間觀察內存曲線持續增長
2.3 產生內存溢出的原因一: 代碼中的內存泄漏
總結了6種產生的內存泄漏的原因,均來自java代碼的處理不當:
- equals()和hashCode(),不正確的equals()和hashCode()實現導致內存泄漏
- ThreadLocal的使用,由于線程池中的線程不被回收導致的ThreadLocal內存泄漏
- 內部類引用外部類,非靜態的內部類和匿名內部類的錯誤使用導致內存泄漏.
- String的Intern方法,由于JDK6中的字符串常量池位于永久代,intern被大量的調用并保存產生的內存泄漏
- 通過靜態字段保存的對象,大量的數據在靜態變量中被引用,但是不再使用,成為了內存泄漏.
- 資源沒有正常關閉,由于資源沒有調用close方法正常關閉,導致的內存溢出.
2.3.1 equals和hashCode導致的內存泄漏
問題:
在定義新類時沒有重寫正確的equals()和hashCode()方法,在使用HashMap的場景下,如果使用這個類對象作為key,HashMap在判斷key是否已經存在時會使用這些方法,如果重寫方式不正確,會導致相同的數據被保存多份.
正常情況:
- 以JDK8為例,首先調用hash方法計算key的哈希值,hash方法中會使用到key的hashCode方法,根據hash方法的結果決定存放的數組中的位置.
- 如果沒有元素,直接放入,如果有元素,先判斷key是否相等,會用到equals方法,如果key相等,直接替換value,key不相等,走鏈表或者是紅黑樹查找邏輯,其中也會使用equals對比是否相等.
異常情況: - hashCode方法實現不正確,會導致相同id的學生對象計算出來的hash值不同,可能會被分到不同的槽中.
- equals方法實現不正確,會導致key在對比時,即便學生對象的id是相同的,也被認為是不同的key.
5. 長時間運行之后HashMap中會保存大量相同的id的學生的數據.
比如下面的代碼,就會發生內存泄漏,我們在student中不重寫hashCode和equals方法,我們直接
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){Thread.sleep(10);}Student student = new Student();student.setId(1);student.setName("張三");map.put(student,1L);}}
}
運行之后觀察VisualVM,出現了內存泄漏的現象.
解決方案:
- 在定義實體時,始終重寫equals()和hashCode()方法.
- 重寫時一定要確定使用了唯一標識去區分不同的對象,比如用戶的id等.
- HashMap使用時盡量使用使用編號id等數據作為key,不要將整個實體類對象作為key存放.
2.3.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 static String name = "測試";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 Outer().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<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(new Outer().newList());}}
}
解決方案:
- 這個案例中,使用內部類的原因是可以直接獲取到外部類中的成員變量值,簡化開發,如果不想持有外部類的對象,應該使用靜態內部類.
- 使用靜態方法,可以避免匿名內部類持有調用者對象.
2.3.3 ThreadLocal的使用
問題:
如果僅僅使用手動創建線程,就算沒有調用ThreadLocal的remove方法清理數據,也不會產生內存泄漏,因為當線程被回收時,ThreadLocal也同樣被回收,但是如果使用線程池就不一定了.
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);}}
}
2.3.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 不會溢出list.add(String.valueOf(i++).intern()); //溢出}}}
}
2.3.5 通過靜態字段保存對象
問題:
如果大量的數據在靜態變量中被長期引用,而靜態字段的生命周期與類本身不同相同,數據就不會被釋放,如果這些數據不再使用,就會成為內存泄漏.
public class Cache {private static Map<String, Object> cache = new HashMap<>();public static void addToCache(String key, Object value) {cache.put(key, value);}public static Object getFromCache(String key) {return cache.get(key);}
}
解決方案:
- 盡量減少將對象長時間保存在靜態變量中,如果不再使用,必須將對象刪除(比如在集合中)或者將靜態變量攝者為null.
- 使用單例模式時,盡量使用懶加載,而不是立即加載.
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];
}
- 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);}}
}
2.3.6 資源沒有正常關閉
問題:
連接和流這些資源會占用內存,如果使用完之后沒有關閉,這部分內存不一定會出現內存泄漏,但是會導致close方法不被執行.
解決方案:
- 為了防止出現這類的資源對象泄漏問題,必須在finally塊中關閉不再使用的資源.
- 從java7開始,使用try-with-resource語法可以用戶自動關閉資源.
2.4 產生內存溢出的原因二: 并發請求問題
通過發送請求向java應用中獲取數據,正常情況下java應用將數據返回之后,這部分數據就可以在內存中被釋放掉.
接收到請求是創建對象:
響應返回之后,對象就可以被回收掉:
并發請求問題指的是由于用戶的并發請求量可能很大,同時處理數據的時間很長,導致大量的數據存在于內存中,最終超過了內存的上限,導致內存溢出,這類問題的處理思路和內存泄漏類似,首先要定位到對象產生的根源.
接下來我們就來模擬一下這種情況:
接口代碼如下:
/*** 大量數據 + 處理慢*/
@GetMapping("/test")
public void test1() throws InterruptedException {byte[] bytes = new byte[1024 * 1024 * 100];//100mThread.sleep(10 * 1000L);
}
- 接下來我們使用Jmeter進行并發測試,把線程參數設置為100.
- 添加虛擬機參數
- 點擊運行進行壓測,很快就出現了內存溢出:
比如我們再來看一個案例: - 代碼如下:
/*** 登錄接口 傳遞名字和id,放入hashmap中*/
@PostMapping("/login")
public void login(String name,Long id){userCache.put(id,new UserEntity(id,name));
}
- 在壓測工具中添加name字段,id字段設置為指定區間的隨機數
- 點擊測試,一段時間之后同樣出現了內存溢出:
2.5 診斷
2.5.1 內存快照
當堆內存溢出時,需要在堆內存溢出時將整個堆內存保存下來,生成快照文件.
使用MAT打開hprof文件,并選擇內存泄漏檢測功能,MAT會自動根據內存快照中保存的數據分析內存泄漏的根源.
生成內存快照的java虛擬機參數:
-XX:+HeapDumpOnOutOfMemoryError
: 發生OutOfMemoryError錯誤時,自動生成hprof內存快照文件.
-XX:HeapDumpPath=<path>
: 指定hprof文件的輸出路徑.
使用MAT打開hprof文件,并選擇內存泄漏檢測功能,MAT會自動根據內存快照中保存的數據分析內存泄漏的根源.
在程序中添加jvm參數:
-Xmx256m -Xms256m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\jvm\dump\test1.hprof
運行程序之后:
使用MAT打開hprof文件,首頁就顯示了MAT檢測出來的內存泄漏問題的原因.
點擊Detail查看詳情,這個線程持有了大量的字節數組:
2.5.2 MAT內存泄漏檢測原理
MAT提供了稱為支配樹的對象圖.支配樹展示的是對象實例間支配關系,在對象引用中,所有指向對象的B路徑都經過A,則認為對象A支配對象B.
如下圖,A引用BC,BC引用D,C引用ED,E引用F,轉成支配樹之后,由于E只有C引用,所以E掛在C上,接下來BCDF都由其他至少1個對象引用,所以追溯上去,只有A滿足支配它們的條件.
支配樹中對象本身占用的空間稱之為淺堆,支配樹種對象的子樹就是所有被該對象支配的內容,這些內容組成了對象的深堆,也稱之為保留集==,深堆的大小表示該對象如果可以被回收,能釋放多大的內存空間==.
如下圖: C自身包含一個淺堆,而C地下掛了E,所以C+E占用的空間大小代表C的深堆.
在MAT中,也有查看MAT支配樹的功能:
輸入main進行搜索,可以看到支配樹的整體結構:
同時也可以看到字符串的淺堆大小和深堆大小:
2.5.3 服務器上的內存快照導出和分析
剛才我們都是在本地導出內存快照的,并且是程序已經出現了內存溢出,接下來我們要做到防患于未然,一點看到大量內存增長就去分析內存快照,此時內存還沒有溢出,怎么樣去獲得內存快照文件呢?
背景:
小李的團隊通過監控系統發現有一個服務的內存在持續增長,希望盡快通過內存快照分析增長的原因,由于并未產生內存溢出所以不能通過HeapDumpOnOutOfMemoryError
參數生成內存快照.
思路:
導出運行中系統的內存快照,比較簡單的方式有兩種,注意只需要導出標記為存活的對象:
- 通過JDK自帶的jmap命令導出,格式為:
jmap -dump:live,format=b,file=文件路徑和文件名 進程ID
- 通過arthas的heapdump命令導出,格式為:
heapdump --live
文件路徑和文件名.
先使用jps
或者ps -ef
查看進程ID:
通過jmap命令導出內存快照文件,live代表值保存存活對象,format=b用二進制方式保存:
也可以在arthas中輸出heapdump命令:
接下來下載到本地分析即可.