本文首發於公衆號【why技術】,關注公衆號,有更加優秀的排版方式,閱讀體驗更佳。java
首先,我先說一下我發現的《Java併發編程的藝術》寫錯的地方吧。
我手上這本《Java併發編程的藝術》的版次是:2019年3月第1版第14次印刷。算法
我瀏覽目錄的時候注意到了其中3.6.5小節的標題是:《爲何final引用不能從構造函數內「溢出」》編程
很明顯,做者這裏是一個筆誤。從做者該小節具體的描述也能夠看出來,【溢出】應該是【逸出】。數組
看到這裏,你要說我是一個"可惡的標題黨",我也不反駁。由於這個錯誤,結合上下文來看,確實無傷大雅。安全
可是,只看標題呢?若是隻知道java有內存溢出,不知道java有引用逸出的讀者呢?微信
他們可能摳破腦殼,也想不出"構造函數內的final引用"和"內存溢出"之間有什麼聯繫吧?多線程
好了,這個不重要。併發
由於本文想要闡述的,不是這個筆誤,而是這個筆誤,背後隱藏的兩大知識點:【引用逸出】和【內存溢出】。框架
主要是接合《Java併發編程的藝術》、《Java併發編程實戰》、《深刻理解Java虛擬機》這三本書中的相關內容進行對比,而後展開描述。函數
同時須要強調的是:我認爲,這個小小的筆誤,徹底不妨礙這本書的優秀性。這是一本提高併發編程能力乾貨滿滿的書。
在《Java併發編程實戰》的3.2小節中是這樣定義發佈與逸出的:
「發佈(Publish)」一個對象的意思是指,使對象可以在當前做用域以外的代碼中使用。將一個指向該對象的引用保存到其餘代碼能夠訪問到的地方,或者在某一個非私有的方法中返回該引用,或者將引用傳遞到其餘類的方法中。
當某個不該該發佈的對象被髮布時,這種狀況就被成爲"逸出(Escape)"。
概念讀起來老是讓人摸不着頭腦。
咱們直接看書裏面給出的程序清單3-5:
如程序清單3-5所示:在initialize方法中實例化一個新的HashSet對象,並將對象的引用保存到knownSecrets中以發佈該對象。
這段代碼有什麼問題?
當發佈knownSecrets對象時,間接地發佈了Secret對象。由於任何代碼均可以遍歷這個集合,並得到對這個新Secret對象的引用。因此Secret對象"逸出"了,這是不安全的。
再看書裏給出的另一個程序清單3-6:
若是按照上述方式來發布states,就會出現問題,由於任何調用者都能修改這個數組的內容。在程序清單3-6中,數組states已經"逸出"了它所在的做用域,由於這個本應該是私有的變量已經被髮布了。
當某個對象逸出後,你必須作最壞的打算,必須假設某個類或者線程可能會誤用該對象。
同時書中也說到,這也正是須要使用封裝的最主要的緣由:
封裝可以使得對程序的正確性進行分析變得可能,並使得無心中破壞設計約束條件變得更難。
在《Java併發編程實戰》裏面給出了一個"隱式地使this引用逸出"的例子。以下所示:
ThisEscape在發佈其內部類EventListener時,由於EventListener這個內部類包含了對ThisEscape實例的引用,因此使ThisEscape實例發生了"this引用逸出"。
很差理解對不對?咱們再看看書中的描述:
對於不正確構造,做者給了一個備註說明:
具體來講,只有當構造函數返回時,this引用才應該從線程中逸出。構造函數能夠將this引用保存到某個地方,只要其餘線程不會在構造函數完成以前使用它。
也不太好理解對不對?確實是,由於我以爲這個代碼片斷少了幾個關鍵的引導的地方;而這段話很難提煉出關鍵詞,由於全是關鍵詞。
可是我讀到這段話的時候,有一句話直接吸引了個人注意力,彷彿把手舉得高高的在喊:看我,看我!
即便發佈對象的語句位於構造函數的最後一行也是如此
做者爲何要感受是輕描淡寫,其實是在強調"最後一行"呢?
做者沒有明說,可是答案是重排序,由於有了重排序,因此一行代碼看起來是在最後一行,實際上不是最後一行。
這裏咱們接合《Java併發編程的藝術》發生筆誤的這一章節裏面的例子,來講明【this引用逸出】和【即便發佈對象的語句位於構造函數的最後一行也是如此】這兩個問題,代碼以下:
假設一個線程A執行writer()方法,另外一個線程B執行reader()方法。
這裏的操做2(obj=this)使得對象還未完成構造前就爲線程B可見。即便這裏的操做2(obj=this)是構造函數的最後一步。
且在程序中操做2(obj=this)排在操做1(i=1)後面,執行read()方法的線程仍然可能沒法看到final域被初始化後的值。
由於這裏的操做1(i=1)和操做2(obj=this)之間可能被重排序。實際的執行時序可能以下圖所示:
因此《Java併發編程的藝術》裏面的示例代碼和多線程下代碼的執行時序圖就很好的說明了【this引用逸出帶來的問題(線程不安全)】,解答了【《Java併發編程實戰》中沒有明說的爲何"即便最後一行"也不行(重排序)】。
這一小節就是我讀完《Java併發編程實戰》、《Java併發編程的藝術》以後,取出書中部份內容再加上本身對於對象&引用逸出的理解的總結、輸出。
其實《深刻理解Java虛擬機》裏面也有對逃逸描述的相關內容,有興趣的能夠翻閱一下。以下:
《深刻理解Java虛擬機》目錄
若是前面說的引用逸出讓你雲裏霧裏,快要瞌睡了。那接下咱們要談的內存溢出,你們應該都是耳熟能詳的了。
先上一個來自《深刻理解Java虛擬機》中第2章【Java內存區域與內存溢出異常】中的一張清晰的、牛逼的、經典的、一應俱全的大圖:
Java 虛擬機運行時數據區
這個圖包含的知識點能夠說是很是多,全是"內功心法",咱們只討論其中的一大分支---內存溢出。
因此,本小節內容的目的有兩個:
第一,經過代碼驗證Java虛擬機規範中描述的各個運行時區域存儲的內容。
第二,但願讀者在工做中遇到實際的內存溢出異常時,能根據異常的信息快速判斷是哪一個區域的內存溢出,知道什麼樣的代碼可能會致使這些區域內存溢出,以及出現這些異常後該如何處理。
對於每一個區域具體的職能,就不鋪開講了,一鋪開,又是一個萬字長篇。我在保證質量的前提下,儘可能精簡字數,讓你們讀起來不要那麼耗時(實在耗時的話,說明我真的用心在寫,能夠收藏起來或者轉發朋友圈慢慢看呀),一進來,一看完,半小時過去了。
話很少說,精彩繼續。
此內存區域是惟一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域。
因爲沒有OutOfMemoryError的狀況,因此不作模擬。
關於虛擬機棧和本地方法棧,在Java虛擬機規範中描述了兩種異常:
若是線程請求的棧深度大於虛擬機所容許的最大深度,將拋出StackOverflowError異常。
若是虛擬機在擴展棧時沒法申請到足夠的內存空間,則拋出OutOfMemoryError異常。
這裏把異常分紅兩種狀況,看似更加嚴謹,但卻存在着一些互相重疊的地方:當棧空間沒法繼續分配時,究竟是內存過小,仍是已使用的棧空間太大,其本質上只是對同一件事情的兩種描述而已。
常言說的好:Talk is cheap.Show me the code(光說不練假把式)。咱們用代碼說話:
在《深刻理解Java虛擬機》筆者的實驗中,將實驗範圍限制於單線程中的操做,嘗試了下面兩種方法均沒法讓虛擬機產生OutOfMemoryError異常,嘗試的結果都是得到StackOverflowError異常,測試代碼以下所示。
使用-Xss參數減小棧內存容量。結果:拋出StackOverflowError異常,異常出現時輸出的堆棧深度相應縮小。
定義了大量的本地變量,增大此方法幀中本地變量表的長度。結果:拋出StackOverflowError異常時輸出的堆棧深度相應縮小。
虛擬機棧和本地方法棧OOM測試(僅做爲第一點測試程序)
運行結果:
在單線程下,不管因爲棧幀太大仍是虛擬機棧容量過小,當內存沒法分配的時候,虛擬機拋出的都是StackOverflowError異常。
那咱們怎麼去模擬OutOfMemoryError異常呢?
我查閱了一些其餘的文章,他們的測試不限於單線程,經過不斷地建立線程的方式產生內存溢出異常。舉出的例子也是書中的例子,以下:
運行結果:
Exception in thread"main"java.lang.OutOfMemoryError:unable to create new native thread
可是不少文章中沒有把書中的特殊說明擺出來,我以爲這裏是混淆概念的問題,應該進行特殊說明,以下:
這樣產生的內存溢出異常與棧空間是否足夠大並不存在任何聯繫,或者準確地說,在這種狀況下,爲每一個線程的棧分配的內存越大,反而越容易產生內存溢出異常。
那做者爲何說這樣產生的內存溢出異常與棧空間是否足夠大並不存在任何聯繫呢?
其實緣由不難理解,操做系統分配給每一個進程的內存是有限制的,譬如32位的Windows限制爲2GB。虛擬機提供了參數來控制Java堆和方法區的這兩部份內存的最大值。剩餘的內存爲2GB(操做系統限制)減去Xmx(最大堆容量),再減去MaxPermSize(最大方法區容量),程序計數器消耗內存很小,能夠忽略掉。若是虛擬機進程自己耗費的內存不計算在內,剩下的內存就由虛擬機棧和本地方法棧"瓜分"了。每一個線程分配到的棧容量越大,能夠創建的線程數量天然就越少,創建線程時就越容易把剩下的內存耗盡。
因此,書中提醒讀者須要在開發多線程的應用時特別注意,若是是創建過多線程致使的內存溢出,在不能減小線程數或者更換64位虛擬機的狀況下(如今用32位的應該是極少數了吧),就只能經過減小最大堆和減小棧容量來換取更多的線程。若是沒有這方面的處理經驗,這種經過"減小內存"的手段來解決內存溢出的方式會比較難以想到。
怎麼讓方法區溢出?
咱們不妨先換個問法,方法區裏面放的是什麼東西?
這樣一問,你們都知道:方法區用於存放Class的相關信息,好比類名、 訪問修飾符、 常量池、 字段描述、 方法描述等。
知道它存放的東西是Class相關信息了,那咱們不停的往裏面放入類,不就溢出了嗎。
接下來問題又來了,咱們怎麼在運行時產生大量的類去往方法區裏面放呢?
在書中做者給出的示例代碼,是藉助CGLib直接操做字節碼運行時生成了大量的動態類。 以下:
須要多說一句的是,書中的JDK版本是1.7,個人JDK版本是1.8。由於JDK1.8中用Metaspace代替了Permsize,所以在咱們設置VM Args的時候須要有所變化,正如上面圖片展現的那樣。
JDK1.8運行結果:
JDK1.7運行結果:
方法區溢出也是一種常見的內存溢出異常,一個類要被垃圾收集器回收掉,斷定條件是比較苛刻的。在常常動態生成大量Class的應用中,須要特別注意類的回收情況。
這類場景常見有以下幾種:
1.上面提到的程序使用了CGLib字節碼加強和動態語言
2.大量JSP或動態產生JSP文件的應用(JSP第一次運行時須要編譯爲Java類)
3.基於OSGi的應用(即便是同一個類文件,被不一樣的加載器加載也會視爲不一樣的類)等。
而對於使用CGLib字節碼加強技術的這種場景,能夠說是很是常見了。咱們經常使用的Spring框架中就有大量的CGLib技術的應用。隨便截個源碼的圖片,好比這個CglibAopProxy。
這塊區域的OOM異常,能夠說是咱們在實際開發的過程當中最多見的內存溢出異常狀況。
衆所周知,Java堆裏面放的是對象實例,按照以前的想法,咱們只要不斷的建立對象,這樣當建立的對象數量足夠多的時候,就會產生內存溢出異常。
再讀一讀上面的話,這個描述對嗎?
這樣說是不徹底正確的。若是咱們建立的時對象被垃圾回收機制清除了呢?
因此書中給出的完整的描述是這樣的:
java堆用於存儲對象實例,只要不斷地建立對象,而且保證GC Roots到對象之間有可達的路徑來避免垃圾回收機制清除這些對象,那麼在對象數量到達最大的容量限制後就會產生內存溢出異常。
這裏涉及到的GC Root和可達性分析算法也是很是重要的知識點。不展開講了,若是不瞭解的讀者,建議瞭解一下,都是知識點啊,朋友們。
咱們再看書中給出的示例代碼:
運行結果(多麼熟悉、親切、辨識度高的異常啊):
Java堆內存的OOM異常是實際應用中常見的內存溢出異常狀況。當出現Java堆內存溢出時,異常堆棧信息"java.lang.OutOfMemoryError"會跟着進一步提示"Java heap space"。
要解決這個區域的異常,通常的手段是先經過內存映像分析工具(如Eclipse Memory Analyzer)對Dump出來的堆轉儲快照進行分析,重點是確認內存中的對象是不是必要的,也就是要先分清楚究竟是出現了內存泄漏( Memory Leak)仍是內存溢出( Memory Overflow) 。
內存泄漏解決思路:
若是是內存泄露,可進一步經過工具查看泄露對象到GC Roots的引用鏈。因而就能找到泄露對象是經過怎樣的路徑與GC Roots相關聯並致使垃圾收集器沒法自動回收它們的。掌握了泄露對象的類型信息及GC Roots引用鏈的信息, 就能夠比較準確地定位出泄露代碼的位置。
內存溢出解決思路:
若是不存在泄露,換句話說,就是內存中的對象確實都還必須存活着,那就應當檢查虛擬機的堆參數(-Xmx與-Xms),與機器物理內存對比看是否還能夠調大,從代碼上檢查是否存在某些對象生命週期過長、持有狀態時間過長的狀況,嘗試減小程序運行期的內存消耗。
通過上面的對各個區域的一頓操做後,再來細細品味這一張清晰的、牛逼的、經典的、一應俱全的大圖:
Java 虛擬機運行時數據區
每次讀完《深刻理解Java虛擬機》都會耐人尋味。對於其做者周志明先生:在下佩服!
第一點:文章提到的《Java併發編程實戰》、《Java併發編程的藝術》、《深刻理解Java虛擬機》這三本書,我認爲都是很是優秀的,值得反覆翻閱的技術書籍。能夠關注我後在後臺回覆關鍵字【Java】,便可得到這三本書的電子版。可是,對於這類工具書,強烈建議購買實體書,以便作讀書筆記和隨手翻閱。
因此,我打算自掏腰包送一本書給個人讀者。讀者能夠關注我公衆號後在後臺回覆關鍵字【書籍】,便可參與抽獎。中獎後的讀者能夠從《Java併發編程的藝術》《Java併發編程實戰》《深刻理解Java虛擬機》三本中任選一本。
第二點:由於加我我的微信的人愈來愈多,不少人的問題都具備類似性,因此我建立了一個技術分享的羣,咱們能夠在這裏交流技術,品味生活,感悟人生。歡迎你進來一塊兒學習,相互交流,共同進步,願你我一塊兒早日成爲真正的大佬。
若羣二維碼失效,能夠加我我的微信(公衆號菜單欄有個人微信二維碼),我拉你入羣。
謝謝您的閱讀,感謝您的關注。我的能力有限,文章中不免有紕漏,錯誤的地方,若是您發現了,煩請指出,我對其加以修改。若是你以爲文章寫的不錯,你的點贊、轉發、讚揚就是對我最大的鼓勵。
以上。
PS:說出來你可能不信,這篇文章我已經很收斂的在寫了,仍是有6359個字,真的是我用心在寫。
這篇文章特別耗時,由於在寫以前我把文中提到的三本書的相關章節又仔細的閱讀了一次,寫的過程當中也在反覆翻閱。快餐時代下,修煉內功心法,仍是須要細嚼慢嚥。
歡迎關注公衆號【why技術】。在這裏我會分享一些技術相關的東西,主攻java方向,用匠心敲代碼,對每一行代碼負責。偶爾也會荒腔走板的聊一聊生活,寫一寫書評,影評。願你我共同進步。