什么是內存泄漏?該如何檢測?又該如何解決?
前言
這個問題是我之前翻看面經的時候見到的。那位小姐姐把內存泄漏當成了內存溢出問題去解答的,結果當場掛掉了。為此總結一下,之前和一位老哥也討論過這個問題。可見不管是面試還是工作這都是一個極為重要的點。
我也曾在面阿里的時候也遇到過原題,題目是寫出倆內存泄漏案例,然后問如何排查?如何解決?
本篇文章大體結構來自外國大佬baeldung;
一、介紹
1、什么是內存泄漏
java的優勢之一就是內置了垃圾回收器GC,它幫助我們實現了自動化內存管理。但是GC再好,也有老馬失前蹄的時候,它不能保證提供一個解決內存泄漏的萬無一失的解決方案。什么是內存泄漏?可以看看下面這張圖,
也就是一部分內存空間我明明已經使用了,卻沒有引用指向這部分空間。造成這片已經使用的空間無法處理的情況。
正規點的理解:動態開辟的空間,在使用完畢后未釋放,結果導致一直占據該內存單元。直到程序結束。
2、內存泄漏的危害
- 長時間運行,程序變卡,性能嚴重下降
- 程序莫名其妙掛掉
- OutOfMemoryError錯誤
- 亂七八糟的錯誤,還不易排查
反正內存泄漏不是好事。
二、內存泄漏原因
內存泄漏原因太多了。說不定就是某一行代碼不對就會出現這種情況,因此這里給出最常見的幾種。關鍵的還是如何找出哪個地方出現了內存泄漏,代碼好修改,錯誤不易查。
1、大量使用靜態變量
靜態變量的生命周期與程序一致。因此常駐內存。
- 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......");
- }
- }
現在可以使用jvisualvm運行一邊,看看內存效果。
- 帶static關鍵字(使用靜態變量)
從上圖可以看到,堆內存從一開始的135M左右飆升了到了200M。直接占據了65M的內存。
- 不使用static關鍵字(不使用靜態變量)
由于全局變量與程序周期不一致,因此不使用時,就會進行回收。此時內存最高150M。
總結:由于靜態變量與程序生命周期一致,因此對象常駐內存,造成內存泄漏
2、連接資源未關閉
每當建立一個連接,jvm就會為這么資源分配內存。比如數據庫連接、文件輸入輸出流、網絡連接等等。
- 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運行一邊,看看內存效果。
可以看出,在連接文件資源時,jvm會為本資源分配內存。
3、equals()和hashCode()方法使用不當
定義新類時,如果沒有重新equals()和hashCode()方法,也有可能會造成內存泄漏。主要原因是沒有這兩個方法時,很容易造成重復的數據添加??蠢樱?/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
- }
- }
然后運行一下,看看內存情況:
內存從150M一下子飆升到225M,可見飆升的厲害。輸出為false,說明user對象被重復添加了。我們知道像HashMap在添加新的對象時,會對其hashcode進行比較,如果一樣,那就不插入。如果一樣那就插入。此時說明這100個User其hashcode不同。
現在重寫這倆方法再運行一邊:
- 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類再測試一遍,首先看看內存變化:
上圖可以看到上升幅度沒那么大。而且輸出為true,這是肯定的,由于重寫了hashcode和equal,所以HashMap添加的肯定是同一個對象。
4、內部類持有外部類
這個場景和上面類似。
5、finalize方法
這個方法之前曾經專門花過文章寫過,這個問題很簡單??匆粡垐D
這就是整個過程。不過在這里我們主要看的是finalize方法對垃圾回收的影響,其實就是在第三步,也就是這個對象含有finalize,進入了隊列但一直沒有被調用的這段時間,會一直占用內存。造成內存泄漏。
6、ThreadLocal的錯誤使用
ThreadLocal主要用于創建本地線程變量,不合理的使用也有可能會造成內存泄漏。
上面這張圖詳細的揭示了ThreadLocal和Thread以及ThreadLocalMap三者的關系。
1、Thread中有一個map,就是ThreadLocalMap
2、ThreadLocalMap的key是ThreadLocal,值是我們自己設定的。
3、ThreadLocal是一個弱引用,當為null時,會被當成垃圾回收
4、重點來了,突然我們ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此時我們的ThreadLocalMap生命周期和Thread的一樣,它不會回收,這時候就出現了一個現象。那就是ThreadLocalMap的key沒了,但是value還在,這就造成了內存泄漏。
解決辦法:使用完ThreadLocal后,執行remove操作,避免出現內存溢出情況。
現在介紹了幾種常見的內存泄漏情況,上面的知識點比較常見,最主要的是如何檢測出來。
三、檢測內存泄漏
檢測的目的是定位內存泄漏出現的位置,常見的有以下幾種方法:
1、工具分析
這個工具比較多,比如說JProfiler、YourKit、Java VisualVM和Netbeans Profiler。他可以幫助我們分析是哪一個對象或者是類內存的飆升。也可以看到內存CPU的等等各種情況。上面多次演示到了。
2、垃圾回收分析
這個其實也可以用工具進行分析。上面的VisualVM中,可以打印堆。也可以從外部導入dump文件進行分析。
如果不用工具的話,我們可以通過IDE看到。JVM配置添加-verbose:gc。然后就會打印出相關信息。下面這張圖非原創,來自Baeldung。
3、基準測試
也就是使用科學的方式進行分析java代碼的性能。進而判斷分析。
四、結論
內存泄漏是個很嚴重的問題,也比較常見。最主要的原因是動態開辟的空間,在使用完畢后未釋放,結果導致一直占據該內存單元。直到程序結束。因此良好的代碼規范,可以有效地避免這些錯誤。
本文轉載自微信公眾號「愚公要移山」,可以通過以下二維碼關注。轉載本文請聯系愚公要移山公眾號。