摘要:垃圾回收及一次內存泄漏處理內存分布上圖展示了的架構圖,本篇我們主要關注,運行時數據區。但是垃圾回收并不能百分百保證不會出現內存泄漏,所以了解垃圾回收,對于我們遇到內存泄漏時能更加清晰的分析原因,也能幫助我們寫出更加安全,可靠的程序。
[toc]
JAVA GC垃圾回收(及一次內存泄漏處理) JVM內存分布上圖展示了JVM的架構圖,本篇我們主要關注,運行時數據區。GC垃圾回收發生在這個區的堆上。
Java使用了垃圾回收機制,極大的減輕了程序員的工作,是程序員能夠更加焦距在業務上。
但是垃圾回收并不能百分百保證不會出現內存泄漏,所以了解垃圾回收,對于我們遇到內存泄漏時能更加清晰的分析原因,也能幫助我們寫出更加安全,可靠的程序。
類加載器加載類之后,把類的信息存儲到方法區(即加載類時需要加載的信息,包括版本、域、方法、接口等信息)。所以方法區是存儲類級別的數據,包括靜態變量。
每個jvm實例只有一個方法區,這里會被jvm下的線程共享,so方法區是線程不安全的。
常量池是方法區的一部分,string對象的引用就存儲在這里。
String s1 = "abc";//這里“abc”就存儲在常量池 s1在棧區指向方法區的一個內存地址
下面看一個面試題來理解一下:
String s=new String("xyz") //創建了幾個String Object?
兩個: "xyz"創建一個對象 new String()創建一個 一個: “xyz”在其他程序中已經創建,并且還沒有死亡, 那么本次只會創建一對象new String()堆區 heap Area
垃圾回收主要集中在這個內存區。
堆區存放對象的實例變量以及數組將被存儲在這里。
堆區和方法區一樣在JVM的實例中只有一個,會被JVM下的線程共享,所以堆區是線程不全的。
堆區分為:新生代和老年代(方法區是持久代)
新生代分為三個區:
eden(伊甸園 新的對象最先在這里產生),to survivor, from survivor
在后面討論GC的時候,再詳細說明這一塊的工作過程。
stack Area也可以叫虛擬機棧
棧區是線程安全的,每個線程都會創建自己私有的棧區。
在每個線程運行的時候會多帶帶創建一個運行時棧,棧區會分為三個實體:
局部變量:存儲方法中的局部變量
操作數棧:即執行的指令,a+b:a入棧+入棧b入棧出棧計算結果
幀數據: 方法所有符號都保存在這里。異常情況下catch塊的信息將會被保存在楨數據中。
程序計數器程序計數器也稱pc寄存器。從寄存器的概念上我們就可以了解到空間很小但是很重要。
程序計數器是一個比較小的內存區域,用于指示當前線程所執行的字節碼執行到第幾行,可以理解為當前線程的行號指示器(字節碼的哦)字節碼解釋器在工作時,會通過改變這個計數器的值來取下一條指令。
還記得在看源碼的時候看到有些方法被聲明為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
如果新生代的對象經過了幾次新生代gc(一般是15次)還沒有被回收,那么新生代的對象會被移到老年代。
老年代存儲的對象比年輕代多得多,而且很多都是大對象,老年代的清理算法采用的是標記清除法: 當老年代進行內存清理額時候,先標記出需要被清理的空間,然后統一進行清理(清除的時候會使程序停止)。
按照內存存儲的數據的生命周期,方法區也被稱為持久代。表示此空間很少被回收,但是不表示不會被回收。
持久代的回收有兩種:
常量池中的常量,常量如果沒有被引用則可以被回收
無用的類信息(同時滿足以下條件):
2.1. 類的所有實例都已經被回收了
2.2. 加載類的ClassLoader已經被回收
2.3. 類對象的class對象沒有被引用(即沒有通過反射引用該類的地方)
通過一下兩個算法,我們可以看到那些引用計數器為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可到達的則為存活對象,不可到達的則為需要清理的對象。
圖中的 Object10,object11,obejct4, object5 為不可達對象。
GC Roots的條件:
虛擬機棧的棧楨的局部變量表所引用的對象
本地方法棧的JNI所引用的對象
方法區的靜態變量和常量所引用的對象
從上圖可以看reference1(滿足上面條件3)、reference2(滿足條件1)、
reference3(滿足條件2)
reference1 引用 對象實例1
reference2 引用 對象實例2
reference3 引用 對象實例4(間接引用 實例對象6)
從上圖中可以看出實例1,2,4,6都具有GC Roots可達性也就是存活對象,不會被GC回收。而實例3,5雖然直接連通,但是由于沒有和GC Roots 連通不是可達對象。在可達性算法中實例3、5是會被GC回收的。
回到引用計數器算法那個示例我們通過可達性分析,最終 object1,object2會被GC回收。
標記-清除算法標記-清除算法分為兩步,第一步:標記從GC Root 根的可達對象。 第二步:清除不可達對象,清除沒有被標記的對象,此時會使程序停止運行,如果不停止程序,那么新產生的可達對象沒有被標記則會被清除。
缺點:會產生不連續的內存空間,并且會暫時停止程序。
復制算法將內存區分為兩部分:空閑區域和活動區域,首先標記可達對象,標記之后把可達對象復制到空閑區,將空閑區變為活動區,同時清除掉之前的活動區,并且變為空閑區。
速度快但是耗費空間。
標記可達對象,清除不可達對象,整理內存空間。
各代使用的算法新生代采用 復制算法
老年代采用 標記-整理算法
YGC:年輕代的GC
FGC: 全范圍的GC
-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); WeakReferencedf = new WeakReference (b.setScale(2,BigDecimal.ROUND_HALF_UP).doubleValue()); WeakReference symbol = new WeakReference ("fcion_"+jsonObject1.getString("symbol")); log.info("{}當前價格:{}", symbol, df+"美元"); redisClient.opsForValue().set(symbol.get(), df.get()+" 美元"); } } } }
這里使用弱引用修飾 symbol,df,是之能夠被釋放,當方法被回調完成執行后,會被回收。
長對象引用短對象:
定位過程快速定位內存泄漏的命令:
jamp -histo:live pid
可以看到哪些類被使用的最多:
(上面使用的是阿里云的提供的服務器網頁版 其本質也是執行的上面的命令)
看到了 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
渴望與你一起成長進步!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/76919.html
摘要:不能滿足被回收的條件,盡管調用也還是不能得到回收這就造成了內存泄漏。種解決單例中的內存泄漏將引用置為銷毀監聽使用弱引用將監聽器放入弱引用中從弱引用中取出回調通過第七小點就能完美的解決單例中回調引起的內存泄漏。我們為什么要優化內存 showImg(https://user-gold-cdn.xitu.io/2019/5/12/16aac64e31d8c501); 在 Android 中我們寫的...
摘要:垃圾回收內存管理實踐先通過一個來看看在中進行垃圾回收的過程是怎樣的內存泄漏識別在環境里提供了方法用來查看當前進程內存使用情況,單位為字節中保存的進程占用的內存部分,包括代碼本身棧堆。 showImg(https://segmentfault.com/img/remote/1460000019894672?w=640&h=426);作者 | 五月君Node.js 技術棧 | https:...
摘要:內存泄漏總結內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。在中,內存泄漏的范圍更大一些。 Android 內存泄漏總結 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏大家都不陌生了,簡單粗俗的講,就是該被釋放的對象沒有釋放,一直被某個或某些實例所持有卻不再被使用導致 GC 不能回收。最近自己閱讀了大量相關的文檔資料,打...
摘要:介紹瀏覽器的具有自動垃圾回收機制,也就是說,執行環境會負責管理代碼執行過程中使用的內存。中的內存泄漏問題程序的內存溢出后,會使某一段函數體永遠失效取決于當時的代碼運行到哪一個函數,通常表現為程序突然卡死或程序出現異常。 showImg(https://segmentfault.com/img/remote/1460000018932880?w=4400&h=3080); 1. 介紹 瀏...
閱讀 1818·2023-04-26 02:51
閱讀 2849·2021-09-10 10:50
閱讀 3026·2021-09-01 10:48
閱讀 3594·2019-08-30 15:53
閱讀 1816·2019-08-29 18:40
閱讀 405·2019-08-29 16:16
閱讀 2024·2019-08-29 13:21
閱讀 1816·2019-08-29 11:07