JAVA GC垃圾回收(及一次內存泄漏處理)

[toc]html

JAVA GC垃圾回收(及一次內存泄漏處理)

20170610165140237

JVM內存分佈

上圖展現了JVM的架構圖,本篇咱們主要關注,運行時數據區。GC垃圾回收發生在這個區的堆上。java

Java使用了垃圾回收機制,極大的減輕了程序員的工做,是程序員可以更加焦距在業務上。
可是垃圾回收並不能百分百保證不會出現內存泄漏,因此瞭解垃圾回收,對於咱們遇到內存泄漏時能更加清晰的分析緣由,也能幫助咱們寫出更加安全,可靠的程序。程序員

memorypic

方法區 Method Area

methodarea

類加載器加載類以後,把類的信息存儲到方法區(即加載類時須要加載的信息,包括版本、域、方法、接口等信息)。因此方法區是存儲類級別的數據,包括靜態變量。
每一個jvm實例只有一個方法區,這裏會被jvm下的線程共享,so方法區是線程不安全的。web

常量池是方法區的一部分,string對象的引用就存儲在這裏。面試

String s1 = "abc";//這裏「abc」就存儲在常量池 s1在棧區指向方法區的一個內存地址

下面看一個面試題來理解一下:redis

String s=new String("xyz")
//建立了幾個String Object?
兩個:
    "xyz"建立一個對象
    new String()建立一個
一個:
 「xyz」在其餘程序中已經建立,而且尚未死亡,
 那麼本次只會建立一對象new String()

堆區 heap Area

垃圾回收主要集中在這個內存區。算法

堆區存放對象的實例變量以及數組將被存儲在這裏。
堆區和方法區同樣在JVM的實例中只有一個,會被JVM下的線程共享,因此堆區是線程不全的。spring

堆區分爲:新生代和老年代(方法區是持久代)
新生代分爲三個區:
eden(伊甸園 新的對象最早在這裏產生),to survivor, from survivor
在後面討論GC的時候,再詳細說明這一塊的工做過程。json

heapArea

棧區 Stack Area

stack Area也能夠叫虛擬機棧
棧區是線程安全的,每一個線程都會建立本身私有的棧區。
在每一個線程運行的時候會單首創建一個運行時棧,棧區會分爲三個實體:api

  1. 局部變量:存儲方法中的局部變量
  2. 操做數棧:即執行的指令,a+b:a入棧+入棧b入棧出棧計算結果
  3. 幀數據: 方法全部符號都保存在這裏。異常狀況下catch塊的信息將會被保存在楨數據中。

程序計數器

程序計數器也稱pc寄存器。從寄存器的概念上咱們就能夠了解到空間很小可是很重要。
程序計數器是一個比較小的內存區域,用於指示當前線程所執行的字節碼執行到第幾行,能夠理解爲當前線程的行號指示器(字節碼的哦)字節碼解釋器在工做時,會經過改變這個計數器的值來取下一條指令。

本地方法棧 native method stack

還記得在看源碼的時候看到有些方法被聲明爲navite嗎?
navite的聲明方法爲本地方法,通常是C語言實現。
本地方法棧在做用,運行機制、異常類型等方面都與虛擬機相同,惟一的區別是:虛擬機棧是執行Java方法,而本地方法棧使用執行navite方法的。在不少虛擬機(hotspot)會將本地方法棧與虛擬機棧放在一塊兒使用。

直接內存

內存一部分被jvm管理,一部分沒有被jvm管理,沒有的那部分就是直接內存。

Object o = new Object()的jvm分佈

Object o 表示一個本地引用,存儲在jvm棧的本地變量表裏,表示一個reference類型數據,
new Object():做爲實例對象存儲在對堆中。
類的信息(即加載類時須要加載的信息、包括版本、file、方法、接口等信息)存儲在方法區。

三個代(新生、老年、持久代)

堆內存 = 新生代 + 老年代

新生代(年輕代 yong :so s1 eden)

對象被建立以後會被存儲在新生代(新生代空間足夠,不然會放在老年代,若是老年代內存滿了,則會拋出 out of memory異常)
新生代分爲3個區:eden,to survivor, from survivor
servivor永遠有一個是沒有被使用的(空閒的),由於新生代的垃圾回收算法使用的是複製算法,因此永遠有一個survivor是沒有被使用的。
複製算法過程:

當新生代須要垃圾回收的時候, 把eden和其中一個survivor存活的對象複製到另外一個survivor,而後進行清理,以後在使用存放存活的對象的survivor和eden,下次再按照本次的複製算法進行復制。

新生代的三個區的默認空間比例是(因爲絕大多數對象都是短命的,因此eden相比survivor會比較大):
eden: from: to = 8:1 :1

老年代(old)

若是新生代的對象通過了幾回新生代gc(通常是15次)尚未被回收,那麼新生代的對象會被移到老年代。
老年代存儲的對象比年輕代多得多,並且不少都是大對象,老年代的清理算法採用的是標記清除法: 當老年代進行內存清理額時候,先標記出須要被清理的空間,而後統一進行清理(清除的時候會使程序中止)。

持久代(永久代 perm)

按照內存存儲的數據的生命週期,方法區也被稱爲持久代。表示此空間不多被回收,可是不表示不會被回收。
持久代的回收有兩種:

  1. 常量池中的常量,常量若是沒有被引用則能夠被回收
  2. 無用的類信息(同時知足如下條件):
    2.1. 類的全部實例都已經被回收了
    2.2. 加載類的ClassLoader已經被回收
    2.3. 類對象的class對象沒有被引用(即沒有經過反射引用該類的地方)

GC算法

GC是在清除那些對象?

經過一下兩個算法,咱們能夠看到那些引用計數器爲0或着不具備可達性的對象會被清除回收。

引用計數法

在對象中記錄一個引用計數器,若是對象被引用則計數器加一,若是引用被釋放則計數器減一。當引用計數器爲0的是不然對象被回收,可是這個算法有一個問題若是,兩個對象相互引用,則一直都不會被回收,致使內存泄漏

內存泄漏:是指程序中已動態分配的堆內存因爲某種緣由程序未釋放或沒法釋放,形成系統內存的浪費,致使程序運行速度減慢甚至系統崩潰等嚴重後果

內存溢出:通俗的說就是系統內存不夠,致使程序崩潰,通常內存泄漏很嚴重會致使內存溢出。

/**
    *引用計數器算法致使內存泄漏示例
    * @author: xuelongjiang 
    **/
    
    public class countDemo{
    
    public static void main(String [] args){
        
        DemoObject object1 = new DemoObject();//(1) object1引用計數器 = 1
        DemoObject object2 = new DemoObject();//(2) obejct2 引用計數器 = 1
        object1.instance = object2;//(3) object2引用計數器 = 2
        object2.instance = object1;//(4) object1引用計數 = 2
        object1 = null;//(5) object1引用計數器 = 1
        object2 = null // (6) obejct2引用計數器 = 1
       //到程序結束obejct1,object2的引用計數器都沒有被置爲0 
    }
}

public class DemoObject{
    public Object instance = null;
}

so hotspot虛擬機並無採用引用計數器算法。

可達性算法

如今咱們來看可達性分析是如何避免上面循環引用致使內存泄漏

可達性算法核心是從GC Roots對象做爲起始點,GC Roots可到達的則爲存活對象,不可到達的則爲須要清理的對象。

GC2017080101

圖中的 Object10,object11,obejct4, object5 爲不可達對象。

GC Roots的條件:

  1. 虛擬機棧的棧楨的局部變量表所引用的對象
  2. 本地方法棧的JNI所引用的對象
  3. 方法區的靜態變量和常量所引用的對象

圖 可達性實例圖。參考知乎答案

從上圖能夠看reference1(知足上面條件3)、reference2(知足條件1)、
reference3(知足條件2)

reference1 引用 對象實例1
reference2 引用 對象實例2
reference3 引用 對象實例4(間接引用 實例對象6)

從上圖中能夠看出實例1,2,4,6都具備GC Roots可達性也就是存活對象,不會被GC回收。而實例3,5雖然直接連通,可是因爲沒有和GC Roots 連通不是可達對象。在可達性算法實例三、5是會被GC回收的。

回到引用計數器算法那個示例咱們經過可達性分析,最終 object1,object2會被GC回收。

標記-清除算法

標記-清除算法分爲兩步,第一步:標記從GC Root 根的可達對象。 第二步:清除不可達對象,清除沒有被標記的對象,此時會使程序中止運行,若是不中止程序,那麼新產生的可達對象沒有被標記則會被清除。

缺點:會產生不連續的內存空間,而且會暫時中止程序。

複製算法

將內存區分爲兩部分:空閒區域和活動區域,首先標記可達對象,標記以後把可達對象複製到空閒區,將空閒區變爲活動區,同時清除掉以前的活動區,而且變爲空閒區。
速度快可是耗費空間。

標記-整理算法

標記可達對象,清除不可達對象,整理內存空間。

各代使用的算法

新生代採用 複製算法
老年代採用 標記-整理算法

GC中的一些值解釋

YGC:年輕代的GC
FGC: 全範圍的GC

JVM的一些參數說明

-XmsxxM : -Xms64M 設置最小堆內存爲64MB
-Xmxxxm : -XMx128M 設置最大堆內存128MB

若是以上參數設置的過於小會致使頻繁的發生GC,致使應用的性能極大降低。如沒必要要使用默認就能夠。
通常JVM調優調整以上兩個參數就能夠。
還能夠設置的更加詳細:

-XX:NewSize :設置年輕代的大小
-XX:NewRatio : 設置年輕代和老年代的比值,如:3 表示年輕代與老年代的比值爲1:3
-XX:SurvivorRatio :年輕代中eden區與兩個survivor區的比值
-XX:MaxPermSize : 設置持久代的大小

一次線上內存泄漏解決

最新線上生產的項目發生了內存泄漏,整個排查思路是這樣的:

事故背景

使用websocket(基於netty實現)客戶端實時獲取其餘網站的數據,把返回的數據使用redis緩存起來。
websocket只在程序啓動的時候運行一次,以後定時任務(timer)接管websocket的ping,斷線重連。

事故緣由

因爲websocket的消息處理使用了redisClient,onReceive方法中調用redisClient。
redisClient的生命週期是整個應用的生命週期是一致。

redisClient.opsForValue().set(symbol.get(), df.get()+" 美圓");//redisClient引用了 symbol 和df 致使symbol,df沒有被釋放,而且他倆引用了其餘的致使都沒有被釋放,發生了內存泄漏

內存泄漏代碼

@SpringBootApplication
@EnableScheduling
public class WalleInt2Application {

public static void main(String[] args) {
        SpringApplication.run(WalleInt2Application.class, args);
    }
    
    
    @Bean
    public TaskRunnerFcion taskRunnerFcion(){
        return new TaskRunnerFcion();
    }
}
/**
* 只在項目啓動的時候運行一次(run()方法)
 * @author xuelongjiang
 */
@Order(value = 1)
public class TaskRunnerFcion implements ApplicationRunner {


    private static Logger logger = LoggerFactory.getLogger(TaskRunnerFcion.class);

   /* @Autowired
    @Qualifier("redisClient")
    private StringRedisTemplate redisClient;*/


    @Autowired
    @Qualifier("fcionWebSocketServiceImpl")
    private  WebSocketService fcionWebSocketServiceImpl;



    String fcionUri = "wss://ws.fcoin.com/api/v2/ws";
    String fcion_ping = "{\"cmd\":\"ping\",\"args\":[1532070885000]}";
    String fcion_getData = "{\"id\":\"tickers\",\"cmd\":\"sub\",\"args\":[\"all-tickers\"]}";

    @Override
    public void run(ApplicationArguments args) throws Exception {
        logger.info("啓動fcion websocket客戶端............");
       // WebSocketService service = new FcionWebSocketServiceImpl(redisClient);
        WebSocketFcionClient client = new WebSocketFcionClient(fcionUri,fcionWebSocketServiceImpl,fcion_ping);
        client.start();
        client.addChannel(fcion_getData);
        logger.info("啓動fcion websocket客戶端  完成............");

    }
}
@Service
public class FcionWebSocketServiceImpl implements WebSocketService{

    private Logger log = LoggerFactory.getLogger(FcionWebSocketServiceImpl.class);


    private String get_rate_usedToCNY = "https://www.fcoin.com/api/common/get_rate?from=USD&to=CNY";

    @Autowired
    @Qualifier("redisClient")
    private StringRedisTemplate redisClient;

    public FcionWebSocketServiceImpl() {
    }

    public FcionWebSocketServiceImpl(StringRedisTemplate redisClient) {
        this.redisClient = redisClient;
    }


    @Override
    public void onReceive(String msg){

        log.info("WebSocket fcion Client 接收到消息:{} ",  msg);
        JSONObject jsonObject = JSONObject.parseObject(msg);
        String topic = jsonObject.getString("topic");
        if(topic != null &&topic.equals("all-tickers")){
            JSONArray jsonArray = jsonObject.getJSONArray("tickers");
            for(int i =0; i< jsonArray.size(); i++){

                JSONObject jsonObject1 = jsonArray.getJSONObject(i);
                Double usdPrice =jsonObject1.getJSONArray("ticker").getDouble(0);
                if(usdPrice == null){
                    continue;
                }
                BigDecimal b = new BigDecimal(usdPrice);
                df=b.setScale(2,BigDecimal.ROUND_HALF_UP).doubleValue();
                String symbol="fcion_"+jsonObject1.getString("symbol");
                log.info("{}當前價格:{}", symbol, df+"美圓");
                redisClient.opsForValue().set(symbol, df+" 美圓");//redisClient至關於單例模式沒有被釋放,致使器引用的symbol,df沒有被釋放,symbol引用JSONObject, df引用了BigDecimal致使都沒有被釋放,發生了內存泄漏
            }
        }
    }
}

redisClient至關於單例模式沒有被釋放,致使引用的symbol,df沒有被釋放,symbol引用JSONObject, df引用了BigDecimal致使都沒有被釋放,發生了內存泄漏

修復後的代碼

@Service
public class FcionWebSocketServiceImpl implements WebSocketService{

    private Logger log = LoggerFactory.getLogger(FcionWebSocketServiceImpl.class);


    private String get_rate_usedToCNY = "https://www.fcoin.com/api/common/get_rate?from=USD&to=CNY";

    @Autowired
    @Qualifier("redisClient")
    private StringRedisTemplate redisClient;

    public FcionWebSocketServiceImpl() {
    }

    public FcionWebSocketServiceImpl(StringRedisTemplate redisClient) {
        this.redisClient = redisClient;
    }


    @Override
    public void onReceive(String msg){

        log.info("WebSocket fcion Client 接收到消息:{} ", msg);
        JSONObject jsonObject = JSONObject.parseObject(msg);
        String topic = jsonObject.getString("topic");
        if(topic != null &&topic.equals("all-tickers")){
            JSONArray jsonArray = jsonObject.getJSONArray("tickers");
            for(int i =0; i< jsonArray.size(); i++){

                JSONObject jsonObject1 = jsonArray.getJSONObject(i);
                Double usdPrice =jsonObject1.getJSONArray("ticker").getDouble(0);
                if(usdPrice == null){
                    continue;
                }
                BigDecimal b = new BigDecimal(usdPrice);
                WeakReference<Double> df = new WeakReference<Double>(b.setScale(2,BigDecimal.ROUND_HALF_UP).doubleValue());
                WeakReference<String> symbol = new WeakReference<String>("fcion_"+jsonObject1.getString("symbol"));
                log.info("{}當前價格:{}", symbol, df+"美圓");
                redisClient.opsForValue().set(symbol.get(), df.get()+" 美圓");


            }
        }

    }
}

這裏使用弱引用修飾 symbol,df,是之可以被釋放,當方法被回調完成執行後,會被回收

長對象引用短對象:

longObjectShortObejct

定位過程

快速定位內存泄漏的命令:

jamp -histo:live pid

能夠看到哪些類被使用的最多:

watchmemory
(上面使用的是阿里雲的提供的服務器網頁版 其本質也是執行的上面的命令)

看到了 byte[]佔用的內存比較大,開始懷疑是否是使用netty的handler的channelRead0方法裏致使的內存泄漏,由於這裏處理返回的流,使用到了byte [],以後註釋掉onReceive方法的業務處理(因爲這個項目只是完成websocket客戶端獲取三個網站的數據)。跑了四五個小時,再次查看內存使用狀況,發現沒有發生泄漏。此時定位到問題發生在onReceive方法中。

經過分析redisClient 沒有被釋放,致使引用的對象沒有被釋放,發生了內存泄露。
最後使用弱引用來進行釋放。

以上是問題解決的時候的步驟(其實當時是直接停掉了websocket,只是跑springboot)

實際排錯,比較曲折。最好是用過jmap命令,看到輸出對象裏有BigDecimal就以爲有問題,由於按照代碼BigDecimal的對象不可能20多兆。可是也以爲多是redisClient致使沒有釋放對象 。
把symbol , df置爲null以後運行了幾個小時內存仍是泄漏。因此就使用以上關閉部分代碼的方法來準肯定位(因爲這一塊理論知識的不足纔會致使排錯走了不少彎路)。

單例模式引起的內存泄漏

因爲單例對象的生命週期是伴隨着應用的生命週期的,因此若是單例對象引用了其餘對象,會致使其餘對象很難被回收(長生命週期對象持有短生命週期對象)。

幾種引用方式

強引用

代碼中廣泛存在的相似 Obejct o = new Object() 這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象

弱引用

非必須對象,被弱引用關聯的對象只能生存到下一次垃圾回收以前,垃圾收集器工做以後,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。Java中的類WeakReference表示弱引用。

軟引用

還有用但非必須對象。在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍進行二次回收。若是此次回收尚未足夠的內存,纔會跑出了內存溢出異常。java 中的類 SoftReference表示軟引用。

虛引用

這個引用存在的惟一目的就是這個對象被收集器回收時收到一個系統通知,被虛引用關聯的對象,和其生存時間徹底沒有關係。Java中的類PhantomReference表示虛引用。

參考:

https://www.cnblogs.com/first...
https://www.cnblogs.com/study...
https://blog.csdn.net/aijiudu...
https://www.cnblogs.com/xiaox...
https://www.cnblogs.com/yydcd...
http://baijiahao.baidu.com/s?...
https://www.zhihu.com/questio...
https://www.cnblogs.com/soari...
https://www.cnblogs.com/my-ki...
https://blog.csdn.net/u012167...

關注個人公衆號第一時間閱讀有趣的技術故事
掃碼關注:

能夠在微信搜索公衆號便可關注我:codexiulian

渴望與你一塊兒成長進步!

相關文章
相關標籤/搜索