相對於C、C++這些高性能語言,Java有着讓此類程序員羨慕的功能:內存自動管理。彷佛這樣,Java程序員不用再關心內存,也不用去了解相關知識。但結果然的是這樣嗎?特別對於咱們這種Android程序員來講,對內存但是吃得死死的,一旦出現較爲複雜的內存泄露和溢出方面的問題,簡直就是噩夢。所以,對Java內存管理有個大致的瞭解彷佛已經成爲一個合格的Android程序員必備的技能,就算是新進的Kotlin一樣是基於JVM的。不如趁此機會,你們一塊兒來揭開它的面紗。git
Java是一門面向對象的編程語言,江湖一直流傳着這麼一句話:萬物皆對象。所以,Java的內存管理也能夠理解成爲對象的建立與釋放。那麼,對象究竟是什麼?男友?女友?仍是?對象和內存究竟是什麼關係?這裏的問題太多,咱們一步一步來。程序員
Tips1:全文以經常使用的虛擬機HotSpot、經常使用的內存區域Java堆和普通Java對象爲例。
Tips2:若是深讀過《深刻理解Java虛擬機》的同窗能夠不用看了,請右上角,若是忘了,請繼續!
複製代碼
男友或者說女友你均可以理解成對象,對象是實實在在存在的,好比老爸,老媽,同時伴隨着一個抽象的概念,類:它是對對象的抽象,無論是男友和女友都是人,屬於人類。概念差很少就介紹到這,感受本身在大學上課同樣。。。個人天(捂臉)。github
程序員沒媳婦怎麼辦?new一個。老簡單了,高的,矮的,瘦的,胖的,想要啥就有啥,今生最不後悔的就是當程序員了,雖然頭有點冷。算法
new一個就是一個對象的建立,那麼到底是怎樣的一個過程呢?JVM遇到一條new指令的時候,首先將去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,而且檢查這個符號引用表明的類是否已被加載、解析和初始化過。若是沒有,那必須先執行相應的類加載過程,類加載檢查經過後,能夠說一個對象的模型已經出來了,但Java畢竟只是編程語言,仍是得分配內存不是?否則怎麼操做?編程
對象內存的分配和現實不少場景都是同樣的,好比停車,有些地方可能只有100個車位,先到的停在最前的空位上,就這樣按順序一輛一輛的停下來。這樣的分配稱爲「指針碰撞」。還有一種你想停哪就停哪,只要你插得進去。這樣的分配稱爲「空閒列表」。無論是前者仍是後者,停車咱們是靠眼睛看的,哪裏有空位才停,那麼JVM如何「看」的呢?前者是靠一個指針做爲指示器,分配多大內存的對象就日後移多大距離,後者會維護一個列表來記錄可用內存(可插車位)。bash
對於併發敏感的同窗確定會提出疑問,在併發的時候如何能正確分配到相應位置? 通常也有兩種解決方案,一種是一輛一輛停,保證前一輛停完,下一輛纔開始停;另外一種是你們說好要停哪一片區域,好比A,B,C停在A區域,那麼A,B,C每次去停A區域就好了,跟其它區域不要緊(區域指的是線程),若是他們邀了朋友D,那對不起,只能等其餘區域人停完,你再停。所以,對象的建立並非原子操做,切記,切記。併發
車停哪裏,咱們已經知道了,那麼怎麼停?有人喜歡正着停,有人喜歡橫着停,有人喜歡倒着停。一樣的,對象在內存中是怎麼擺放的呢?大致分爲3個部分:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。編程語言
簡單地來介紹這3位,畢竟這概念性太強。佈局
對象頭包括兩部分信息,第一部分用於存儲對象自身的運行時數據、如哈希碼、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等;另外一部分是類型指針,即對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的示例。對上面部分名詞不理解的,我在後續文章可能會解釋,畢竟本身也在學習當中,若是想急於知道的同窗能夠查閱相關資料,姑且當它是概念記住便可。性能
實例數據就比較好理解了,它是對象真正存儲的有效信息,也是在程序代碼中所定義的各類類型的字段內容。
對齊填充並非必然存在的,因爲內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說,就是對象的大小必須是8字節的整數倍。對象頭大小是8字節的整數倍,因此實例數據大小不是8字節的整數倍時,就須要對齊填充來補齊。
你停完車,幹完事,總得開車回家吧,那總得找到本身的車吧?怎麼找?本身停在哪一個車位總記得吧?本身的牌照總記得吧?那麼咱們如何在內存中訪問咱們的對象呢?你們來看一組圖:
前者稱爲句柄訪問,優勢很明顯,對象移動了只要修改句柄中的指針就好了,不會牽涉到reference;後者稱爲直接指針訪問,優勢也很明顯,就是快,直接少了句柄這一層。而本文中討論的HotSpot採用後者。
車炸了怎麼辦?固然是買輛新的(手動壞笑)。那麼咱們如何斷定一個對象屎沒屎呢?在此以前介紹兩種引用算法:第一種是引用計數算法,很好理解,給對象一個計數器,初始值爲0,有地方引用就加1,失效就減1,計數器爲0的說明都是屎了的;第二種是可達性分析算法,也很好理解,從GC Roots開始,向下引用對象,若是一個對象存在一條從GC Roots到自己的路徑,那麼說明這個對象還活着,不然就屎了。以下圖object567就是屎的:
那麼哪些能夠做爲GC Roots呢?
咱們的HotSpot是採用後者,那麼爲啥沒采用前者呢?由於它很難解決對象之間相互循環引用的問題。例如:
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
複製代碼
那麼問題來了,不可達的對象真的屎了嗎?固然不會,至少通過2次標記纔會宣告一個對象的屎亡。第一次標記是發現對象不可達,同時篩選出沒有覆蓋finalize()方法或者finalize()方法已經被虛擬機調用過,那麼這些能夠認爲屎了,能夠回收(那麼這時候不是隻標記一次嗎?有沒有大佬解答);剩下的對象會被放置F-Quenue的隊列中而且GC會對這些對象進行第二次標記,在執行finalize()方法的時候也是拯救本身的時候(只要在方法中合從新創建與引用鏈上其它對象的關聯便可)。你們最好忘記這個方法的存在。它的運行代價高昂,不肯定性大,沒法保證各個對象的調用順序等。《Effective Java》中也有提到避免此方法。
對象的簡單分析差很少就到這裏結束了,你覺得到這裏所有結束了?太天真。
像上面碰到的名詞,諸如虛擬機棧、方法區、Java堆等究竟是什麼玩意?
國際慣例,No picture,say a J8!
看到這張圖,你們確定知道我要幹什麼了。。。我也不肯意啊,寫到這感受是篇說明文了,個人天,賊尷尬。
程序計數器是一塊較小的內存空間,它能夠看做是當前線程所執行的字節碼的行號指針。例如平時的分支、循環、跳轉、異常處理、線程恢復等基礎功能要依賴這個計數器完成。從圖上咱們可知,它是線程私有的,也就說每一個線程都會有一個獨立的程序計數器且互不影響。並且它是惟一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域。
虛擬機棧描述的是Java方法執行的內存模型:每一個方法在執行的同時會建立一個棧幀用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。細心的朋友,會發現局部變量表在對象的訪問章節圖中出現過,重要的是當進入一個方法時,這個方法須要在幀中分配多大的局部變量空間是徹底肯定的,換句話說,局部變量表所需的內存空間是在編譯期間就完成分配的。
在Java虛擬機規範中,對這個區域規定了兩種異常情況:若是線程請求的棧深度大於虛擬機所容許的深度,將拋出StackOverflowError異常;若是虛擬機棧能夠動態擴展(當前大部分的Java虛擬機均可動態擴展,只不過Java虛擬機規範中也容許固定長度的虛擬機棧),若是沒法擴展時沒法申請到足夠的內存,就會拋出OutOfMemoryError異常。
本地方法棧與虛擬機棧所發揮的做用是很是類似的,它們之間的區別不過是虛擬機棧爲虛擬機執行Java(也就說字節碼)服務,而本地方法棧則爲虛擬機使用到的Native方法服務。所以與Java虛擬機棧拋出的異常情況也是同樣的。
你能夠認爲幾乎全部的對象實例都在堆上分配的。難道不是全部的?這是一個優化技術,試想一下,若是一個對象沒法被別的方法或者線程經過任何途徑訪問到,爲什麼不直接分配在棧上呢?
根據Java虛擬機規範的規定,Java堆能夠處於物理上不連續的內存空間中,只要邏輯上連續便可,這也意味着,若是邏輯上沒有足夠的內存完成分配且堆也沒法擴展,那麼將會拋出OutOfMemoryError異常。
方法區與Java堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。但它除了和Java堆同樣不須要連續的內存和沒法選擇固定大小或者可擴展內存將拋出OutOfMemoryError異常外,還能夠選擇不實現垃圾收集。
運行時數據區介紹的差很少了,這裏補充一個概念叫直接內存,在jdk1.4加入的NIO有用到,感興趣的能夠看看。你們確定注意到每一個區域(除程序計數器)都有拋內存溢出的情況,之後有人問到, 什麼時候會產生OOM,就不要再說內存不夠的時候了,很傷感情。
上面提到的Java堆能夠說是虛擬機管理的內存中最大的一塊了,是GC光顧的常客,所以也叫「GC堆」。GC顧名思義就是垃圾回收,這也是Java一大優點,不用的內存能夠自動回收。既然是垃圾回收能夠有垃圾回收裝置啊,掃地的還用掃帚呢。
圖中是咱們HotSpot的垃圾收集器,上邊是新生代,下邊是老年代,具體的垃圾收集器來歷做用我就是不介紹,沒有必要,本文但願讀者有個大概的瞭解。那麼,有垃圾器,總得有方法吧,喝飲料還用吸管呢,吸管什麼原理你們沒點13數嗎?那麼在這裏大致介紹幾種算法的思想。見名知義啊,先標記須要回收的對象,而後一次性清除標記的對象。它能夠說是最基礎的收集算法,就算是後面介紹的算法都是在它基礎上加以改進的。既然改進,那麼確定有沒法忍受的缺點,它除了效率不高外,還有個嚴重的問題,就算會產生大量不連續的內存碎片,從剛剛咱們提到的Java對OOM的緣由可知,很是容易沒法分配而第二次執行垃圾回收,或者直接OOM。執行過程如圖所示:
這個算法很好理解,將可用內存化爲兩塊,每次只用其中一塊,當要回收的時候,把可用的對象複製到另一塊,而後把原先那塊一次性清理掉,可用說在效率上大大的提升,但有個致命的弱點就是內存減半。
複製算法執行過程如圖所示:
複製算法理論上效率很高,可是你想一想若是存在100個對象,其中98個均可用的,那麼你得複製98個對象,極端狀況100個都存活,你還得複製所有一遍,這是沒法接受的。該算法針對標記-清楚算法產生大量內存碎片作了改進,先把可用對象移到一端,而後直接清理掉端邊界之外的內存。執行過程如圖所示:
從咱們剛剛分析來看,複製算法貌似更適合朝生夕屎的對象,而剩餘的兩個算法更適合「百歲」對象。前者那些對象所在區域咱們就叫作新生代,後者對象所在區域就叫作老年代。咱們的分代算法就是根據新生代和老年代採用不一樣的算法而已。
那麼,這裏有個問題,老年代的對象到底怎麼來?換句話問,怎樣才能進入老年代?首先,分析一個特例:大對象直接進入老年代;而後是正常步驟:對象A在分配的時候優先分配在新生代的Eden空間,當Eden空間不夠分配內存的時候,將進行一次Minor GC,此後對象A仍然存活且能被Survivor空間容納,那麼將移至Survivor空間,並將其年齡計數器置爲1,此後,對象A每度過一次Minor GC且存活,年齡就加1,當達到最大年齡(MaxTenuringThreshold)時,將被榮升到老年代(鼓掌鼓掌)。固然這也不是絕對的,若是Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就能夠直接晉級。
關於本文主要內容差很少就到這了,最後留下一個很關鍵的問題,垃圾回收器到底何時進行垃圾回收,又是如何進行的?這裏有個很牛逼的名詞叫「Stop The World」。
首先,我想說深刻理解Java虛擬機(第2版)真的是一本不錯的書,我這種小菜雞根本沒機會認識這種大神,也談不上打廣告,看過的同窗應該都知道。其次,本文全部的內容均來自於該書,甚至有一字不差的一段話。本文能夠說是我讀完該書第二部分:自動內存管理機制的筆記。本文不少都屬於概念性知識,就好比地球爲何叫地球?這種屬於約定俗成的東西,但對於咱們Android程序員來講,最好是可以對其有個大概的瞭解,但不是全部同窗都看過該書(買了,也不必定看),所以我分享了該文章,其中有部分是本身的理解,若是有問題我及時改正,最好你們仍是買原著仔細閱讀,我這裏拋磚引玉一下- -!
天天都學習一點點也是極好的。既然是學習,對象確定是有前輩已經總結了的,你應該作的是將其理解,並轉爲本身的東西(用本身的思想把它翻譯出來,本質不變),否則就叫作探索。還有一句話就是好記性不如爛筆頭,老師確定說過這句話,當時一句都沒進我法耳。
最後,感謝一直支持個人人!
在這裏,提早祝你們新年快樂!
Github:github.com/crazysunj/