什么是內(nèi)存泄漏?該如何檢測(cè)?又該如何解決?
前言
這個(gè)問題是我之前翻看面經(jīng)的時(shí)候見到的。那位小姐姐把內(nèi)存泄漏當(dāng)成了內(nèi)存溢出問題去解答的,結(jié)果當(dāng)場(chǎng)掛掉了。為此總結(jié)一下,之前和一位老哥也討論過這個(gè)問題??梢姴还苁敲嬖囘€是工作這都是一個(gè)極為重要的點(diǎn)。
我也曾在面阿里的時(shí)候也遇到過原題,題目是寫出倆內(nèi)存泄漏案例,然后問如何排查?如何解決?
本篇文章大體結(jié)構(gòu)來(lái)自外國(guó)大佬baeldung;
一、介紹
1、什么是內(nèi)存泄漏
java的優(yōu)勢(shì)之一就是內(nèi)置了垃圾回收器GC,它幫助我們實(shí)現(xiàn)了自動(dòng)化內(nèi)存管理。但是GC再好,也有老馬失前蹄的時(shí)候,它不能保證提供一個(gè)解決內(nèi)存泄漏的萬(wàn)無(wú)一失的解決方案。什么是內(nèi)存泄漏?可以看看下面這張圖,
也就是一部分內(nèi)存空間我明明已經(jīng)使用了,卻沒有引用指向這部分空間。造成這片已經(jīng)使用的空間無(wú)法處理的情況。
正規(guī)點(diǎn)的理解:動(dòng)態(tài)開辟的空間,在使用完畢后未釋放,結(jié)果導(dǎo)致一直占據(jù)該內(nèi)存單元。直到程序結(jié)束。
2、內(nèi)存泄漏的危害
- 長(zhǎng)時(shí)間運(yùn)行,程序變卡,性能嚴(yán)重下降
- 程序莫名其妙掛掉
- OutOfMemoryError錯(cuò)誤
- 亂七八糟的錯(cuò)誤,還不易排查
反正內(nèi)存泄漏不是好事。
二、內(nèi)存泄漏原因
內(nèi)存泄漏原因太多了。說不定就是某一行代碼不對(duì)就會(huì)出現(xiàn)這種情況,因此這里給出最常見的幾種。關(guān)鍵的還是如何找出哪個(gè)地方出現(xiàn)了內(nèi)存泄漏,代碼好修改,錯(cuò)誤不易查。
1、大量使用靜態(tài)變量
靜態(tài)變量的生命周期與程序一致。因此常駐內(nèi)存。
- public class StaticTest {
- public static List<Integer> list = new ArrayList<>();
- public void populateList() {
- for (int i = 0; i < 10000000; i++) {
- list.add((int)Math.random());
- }
- System.out.println("running......");
- }
- public static void main(String[] args) {
- System.out.println("before......");
- new StaticTest().populateList();
- System.out.println("after......");
- }
- }
現(xiàn)在可以使用jvisualvm運(yùn)行一邊,看看內(nèi)存效果。
- 帶static關(guān)鍵字(使用靜態(tài)變量)
從上圖可以看到,堆內(nèi)存從一開始的135M左右飆升了到了200M。直接占據(jù)了65M的內(nèi)存。
- 不使用static關(guān)鍵字(不使用靜態(tài)變量)
由于全局變量與程序周期不一致,因此不使用時(shí),就會(huì)進(jìn)行回收。此時(shí)內(nèi)存最高150M。
總結(jié):由于靜態(tài)變量與程序生命周期一致,因此對(duì)象常駐內(nèi)存,造成內(nèi)存泄漏
2、連接資源未關(guān)閉
每當(dāng)建立一個(gè)連接,jvm就會(huì)為這么資源分配內(nèi)存。比如數(shù)據(jù)庫(kù)連接、文件輸入輸出流、網(wǎng)絡(luò)連接等等。
- public class FileTest {
- public static void main(String[] args) throws IOException {
- File f=new File("G:\\nginx配套資料\\筆記資料.zip");
- System.out.println(f.exists());
- System.out.println(f.isDirectory());
- }
- }
依然使用jvisualvm運(yùn)行一邊,看看內(nèi)存效果。
可以看出,在連接文件資源時(shí),jvm會(huì)為本資源分配內(nèi)存。
3、equals()和hashCode()方法使用不當(dāng)
定義新類時(shí),如果沒有重新equals()和hashCode()方法,也有可能會(huì)造成內(nèi)存泄漏。主要原因是沒有這兩個(gè)方法時(shí),很容易造成重復(fù)的數(shù)據(jù)添加??蠢樱?/p>
- public class User{
- public String name;
- public int age;
- public User(String name, int age) {
- this.name = name;
- this.age = age;
- }
- }
- public class EqualTest {
- public static void main(String[] args) {
- Map<User, Integer> map = new HashMap<>();
- for(int i=0; i<100; i++) {
- map.put(new User("", 1), 1);
- }
- System.out.println(map.size() == 1);//輸出為false
- }
- }
然后運(yùn)行一下,看看內(nèi)存情況:
內(nèi)存從150M一下子飆升到225M,可見飆升的厲害。輸出為false,說明user對(duì)象被重復(fù)添加了。我們知道像HashMap在添加新的對(duì)象時(shí),會(huì)對(duì)其hashcode進(jìn)行比較,如果一樣,那就不插入。如果一樣那就插入。此時(shí)說明這100個(gè)User其hashcode不同。
現(xiàn)在重寫這倆方法再運(yùn)行一邊:
- public class User{
- public static String name;
- public User(String name) {
- this.name = name;
- }
- @Override
- public boolean equals(Object o) {
- if (o == this) return true;
- if (!(o instanceof User)) {
- return false;
- }
- User user = (User) o;
- return User.name.equals(name);
- }
- @Override
- public int hashCode() {
- return name.hashCode();
- }
- }
在EqualTest類再測(cè)試一遍,首先看看內(nèi)存變化:
上圖可以看到上升幅度沒那么大。而且輸出為true,這是肯定的,由于重寫了hashcode和equal,所以HashMap添加的肯定是同一個(gè)對(duì)象。
4、內(nèi)部類持有外部類
這個(gè)場(chǎng)景和上面類似。
5、finalize方法
這個(gè)方法之前曾經(jīng)專門花過文章寫過,這個(gè)問題很簡(jiǎn)單。看一張圖
這就是整個(gè)過程。不過在這里我們主要看的是finalize方法對(duì)垃圾回收的影響,其實(shí)就是在第三步,也就是這個(gè)對(duì)象含有finalize,進(jìn)入了隊(duì)列但一直沒有被調(diào)用的這段時(shí)間,會(huì)一直占用內(nèi)存。造成內(nèi)存泄漏。
6、ThreadLocal的錯(cuò)誤使用
ThreadLocal主要用于創(chuàng)建本地線程變量,不合理的使用也有可能會(huì)造成內(nèi)存泄漏。
上面這張圖詳細(xì)的揭示了ThreadLocal和Thread以及ThreadLocalMap三者的關(guān)系。
1、Thread中有一個(gè)map,就是ThreadLocalMap
2、ThreadLocalMap的key是ThreadLocal,值是我們自己設(shè)定的。
3、ThreadLocal是一個(gè)弱引用,當(dāng)為null時(shí),會(huì)被當(dāng)成垃圾回收
4、重點(diǎn)來(lái)了,突然我們ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此時(shí)我們的ThreadLocalMap生命周期和Thread的一樣,它不會(huì)回收,這時(shí)候就出現(xiàn)了一個(gè)現(xiàn)象。那就是ThreadLocalMap的key沒了,但是value還在,這就造成了內(nèi)存泄漏。
解決辦法:使用完ThreadLocal后,執(zhí)行remove操作,避免出現(xiàn)內(nèi)存溢出情況。
現(xiàn)在介紹了幾種常見的內(nèi)存泄漏情況,上面的知識(shí)點(diǎn)比較常見,最主要的是如何檢測(cè)出來(lái)。
三、檢測(cè)內(nèi)存泄漏
檢測(cè)的目的是定位內(nèi)存泄漏出現(xiàn)的位置,常見的有以下幾種方法:
1、工具分析
這個(gè)工具比較多,比如說JProfiler、YourKit、Java VisualVM和Netbeans Profiler。他可以幫助我們分析是哪一個(gè)對(duì)象或者是類內(nèi)存的飆升。也可以看到內(nèi)存CPU的等等各種情況。上面多次演示到了。
2、垃圾回收分析
這個(gè)其實(shí)也可以用工具進(jìn)行分析。上面的VisualVM中,可以打印堆。也可以從外部導(dǎo)入dump文件進(jìn)行分析。
如果不用工具的話,我們可以通過IDE看到。JVM配置添加-verbose:gc。然后就會(huì)打印出相關(guān)信息。下面這張圖非原創(chuàng),來(lái)自Baeldung。
3、基準(zhǔn)測(cè)試
也就是使用科學(xué)的方式進(jìn)行分析java代碼的性能。進(jìn)而判斷分析。
四、結(jié)論
內(nèi)存泄漏是個(gè)很嚴(yán)重的問題,也比較常見。最主要的原因是動(dòng)態(tài)開辟的空間,在使用完畢后未釋放,結(jié)果導(dǎo)致一直占據(jù)該內(nèi)存單元。直到程序結(jié)束。因此良好的代碼規(guī)范,可以有效地避免這些錯(cuò)誤。
本文轉(zhuǎn)載自微信公眾號(hào)「愚公要移山」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系愚公要移山公眾號(hào)。