Java內(nèi)存泄漏問(wèn)題排查與解決
前言
Java 最牛逼的一個(gè)特性就是垃圾回收機(jī)制,不用像 C++ 需要手動(dòng)管理內(nèi)存,所以作為 Java 程序員很幸福,只管 New New New 即可,反正 Java 會(huì)自動(dòng)回收過(guò)期的對(duì)象。。。
那么 Java 都自動(dòng)管理內(nèi)存了,那怎么會(huì)出現(xiàn)內(nèi)存泄漏,難道 Jvm 有 bug? 不要急,且聽我慢慢道來(lái)。。
1. 怎么判斷可以被回收
先了解一下 Jvm 是怎么判斷一個(gè)對(duì)象可以被回收。一般有兩種方式,一種是引用計(jì)數(shù)法,一種是可達(dá)性分析。
引用計(jì)數(shù)法:每個(gè)對(duì)象有一個(gè)引用計(jì)數(shù)屬性,新增一個(gè)引用時(shí)計(jì)數(shù)加1,引用釋放時(shí)計(jì)數(shù)減1,計(jì)數(shù)為0時(shí)可以回收。
這個(gè)辦法看起來(lái)挺簡(jiǎn)單的,但是如果出現(xiàn) A 引用了 B,B 又引用了 A,這時(shí)候就算他們都不再使用了,但因?yàn)橄嗷ヒ?計(jì)算器=1 永遠(yuǎn)無(wú)法被回收。
此方法簡(jiǎn)單,無(wú)法解決對(duì)象相互循環(huán)引用的問(wèn)題。

可達(dá)性分析(Reachability Analysis):從 GC Roots 開始向下搜索,搜索所走過(guò)的路徑稱為引用鏈。當(dāng)一個(gè)對(duì)象到 GC Roots 沒有任何引用鏈相連時(shí),則證明此對(duì)象是不可用的,那么虛擬機(jī)就判斷是可回收對(duì)象。

可達(dá)性分析可以解決循環(huán)引用的問(wèn)題。
那么 gc roots 對(duì)象是哪些呢
虛擬機(jī)棧中引用的對(duì)象
方法區(qū)中類靜態(tài)屬性引用的對(duì)象
方法區(qū)中常量引用的對(duì)象
本地方法棧中JNI[即一般說(shuō)的Native]引用的對(duì)象
目前主流的虛擬機(jī)中大多使用可達(dá)性分析的方式來(lái)判定對(duì)象是否可被 GC 回收。
2. 什么情況下會(huì)出現(xiàn)內(nèi)存泄漏
既然可達(dá)性分析好像已經(jīng)很牛逼的樣子了,怎么可能還會(huì)出現(xiàn)內(nèi)存泄漏呢,那我們?cè)賮?lái)看一下內(nèi)存泄漏的定義。
內(nèi)存泄露就是指一個(gè)不再被程序使用的對(duì)象或變量一直被占據(jù)在內(nèi)存中。
有可能此對(duì)象已經(jīng)不使用了,但是還有其它對(duì)象保持著此對(duì)象的引用,就會(huì)導(dǎo)致 GC 不能回收此對(duì)象,這種情況下就會(huì)出現(xiàn)內(nèi)存泄漏。
寫一個(gè)程序讓出現(xiàn)內(nèi)存泄漏
①長(zhǎng)生命周期的對(duì)象持有短生命周期對(duì)象的引用就很可能發(fā)生內(nèi)存泄露,盡管短生命周期對(duì)象已經(jīng)不再需要,但是因?yàn)殚L(zhǎng)生命周期對(duì)象持有它的引用而導(dǎo)致不能被回收。
public class Simple {
Object object;
public void method1(){
object = new Object();
//...其他代碼
}
}這里的 object 實(shí)例,其實(shí)我們期望它只作用于 method1() 方法中,且其他地方不會(huì)再用到它,但是,當(dāng)method1()方法執(zhí)行完成后,object 對(duì)象所分配的內(nèi)存不會(huì)馬上被認(rèn)為是可以被釋放的對(duì)象,只有在 Simple 類創(chuàng)建的對(duì)象被釋放后才會(huì)被釋放,嚴(yán)格的說(shuō),這就是一種內(nèi)存泄露。
解決方法就是將 object 作為 method1() 方法中的局部變量。
public class Simple {
Object object;
public void method1(){
object = new Object();
//...其他代碼
object = null;
}
}當(dāng)然大家有可能會(huì)想就這一個(gè)方法也不會(huì)有多大影響,但如果在某些項(xiàng)目中,一個(gè)方法在一分鐘之內(nèi)調(diào)用上萬(wàn)次的時(shí)候,就會(huì)出現(xiàn)很明顯的內(nèi)存泄漏現(xiàn)象。
②集合中的內(nèi)存泄漏,比如 HashMap、ArrayList 等,這些對(duì)象經(jīng)常會(huì)發(fā)生內(nèi)存泄露。比如當(dāng)它們被聲明為靜態(tài)對(duì)象時(shí),它們的生命周期會(huì)跟應(yīng)用程序的生命周期一樣長(zhǎng),很容易造成內(nèi)存不足。
下面給出了一個(gè)關(guān)于集合內(nèi)存泄露的例子。
Vector v=new Vector(10);
for (int i=1;i<100; i++)
{
Object o=new Object();
v.add(o);
o=null;
}
//此時(shí),所有的Object對(duì)象都沒有被釋放,因?yàn)樽兞縱引用這些對(duì)象。在這個(gè)例子中,我們循環(huán)申請(qǐng) Object 對(duì)象,并將所申請(qǐng)的對(duì)象放入一個(gè) Vector 中,如果我們僅僅釋放引用本身,那么 Vector 仍然引用該對(duì)象,所以這個(gè)對(duì)象對(duì) GC 來(lái)說(shuō)是不可回收的。
因此,如果對(duì)象加入到 Vector 后,還必須從 Vector 中刪除,最簡(jiǎn)單的方法就是將 Vector 對(duì)象設(shè)置為 null。
以上兩種是最常見的內(nèi)存泄漏案例。當(dāng)然還有一些內(nèi)存泄漏的例子,這里就不再一一例舉了,感興趣的同學(xué)可以在網(wǎng)上找找資料。
3. 如何檢測(cè)內(nèi)存泄漏
3.1 模擬內(nèi)存泄漏代碼
package com.wenxiaowu.solution;
import java.util.ArrayList;
import java.util.List;
/**
* Java 內(nèi)存泄露模擬
*/
public class TestMemoryLeak {
public static void main(String[] args) throws InterruptedException {
List<SimpleObject> list = new ArrayList<>();
Runtime run = Runtime.getRuntime();
int i = 1;
while (true) {
SimpleObject simpleObject = new SimpleObject();
list.add(simpleObject);
simpleObject = null;
if (i++ % 1000 == 0) {
System.out.print(i + ": 最大內(nèi)存=" + run.maxMemory() / 1024 / 1024 + "M,");
System.out.print("已分配內(nèi)存=" + run.totalMemory() / 1024 / 1024 + "M,");
System.out.print("剩余空間內(nèi)存=" + run.freeMemory() / 1024 / 1024 + "M");
System.out.println("最大可用內(nèi)存=" + (run.maxMemory() - run.totalMemory() + run.freeMemory()) / 1024 / 1024 + "M");
}
Thread.sleep(1);
}
}
}
class SimpleObject {
// 初始化占用1M內(nèi)存的數(shù)組
private int[] arr = new int[1024 * 8];
public int[] getArr() {
return arr;
}
}3.2 生成dump文件
java -jar -Xmx1G -Xms1G -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp wenxiaowu-java.jar

3.3 用JProfile加載dump文件

可以看到,紅框處的int數(shù)組占用空間最大
3.4 定位最大占用內(nèi)存的原始位置




最終找到這個(gè)對(duì)象是在main方法中創(chuàng)建的。

解決辦法
使用完之后,先刪除對(duì)應(yīng)的引用,再將對(duì)象置為null,即可正常進(jìn)行內(nèi)存回收。
總結(jié)
到此這篇關(guān)于Java內(nèi)存泄漏問(wèn)題排查與解決的文章就介紹到這了,更多相關(guān)Java內(nèi)存泄漏內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java數(shù)據(jù)結(jié)構(gòu)之List的使用總結(jié)
List是Java中比較常用的集合類,指一系列存儲(chǔ)數(shù)據(jù)的接口和類,可以解決復(fù)雜的數(shù)據(jù)存儲(chǔ)問(wèn)題,本文就來(lái)拿實(shí)際案例總結(jié)介紹一下List的使用方法,感興趣的朋友快來(lái)看看吧2021-11-11
通過(guò)入門demo簡(jiǎn)單了解netty使用方法
這篇文章主要介紹了通過(guò)入門demo簡(jiǎn)單了解netty使用方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12
MQ的分類組成優(yōu)缺點(diǎn)測(cè)試點(diǎn)入門教程
這篇文章主要為大家介紹了MQ的分類組成優(yōu)缺點(diǎn)測(cè)試點(diǎn)入門教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05
Spring?Boot集成etcd的詳細(xì)過(guò)程
etcd是一個(gè)分布式鍵值存儲(chǔ)數(shù)據(jù)庫(kù),用于共享配置和服務(wù)發(fā)現(xiàn),etcd采用Go語(yǔ)言編寫,具有出色的跨平臺(tái)支持,很小的二進(jìn)制文件和強(qiáng)大的社區(qū),這篇文章主要介紹了SpringBoot集成etcd,需要的朋友可以參考下2023-08-08
深入解析Java的Hibernate框架中的持久對(duì)象
Hibernate的持久對(duì)象在數(shù)據(jù)庫(kù)數(shù)據(jù)操作中有著重要作用,這里我們就來(lái)深入解析Java的Hibernate框架中的持久對(duì)象,首先必須從理解持久化對(duì)象的生命周期開始:2016-07-07
關(guān)于Java集合框架Collection接口詳解
java使用zookeeper實(shí)現(xiàn)的分布式鎖示例

