1. 程式人生 > >深入解析OutOfMemoryError(上)

深入解析OutOfMemoryError(上)

在Java中,所有物件都儲存在堆中。他們通過new關鍵字來進行分配,JVM會檢查是否所有執行緒都無法在訪問他們了,並且會將他們進行回收。在大多數時候程式設計師都不會有一絲一毫的察覺,這些工作都被靜悄悄的執行。但是,有時候在釋出前的最後一天,程式掛了。

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

OutOfMemoryError是一個讓人很鬱悶的異常。它通常說明你幹了寫錯誤的事情:沒必要的長時間儲存一些沒必要的資料,或者同一時間處理了過多的資料。有些時候,這些問題並不一定受你的控制,比如說一些第三方的庫對一些字串做了快取,或者一些應用伺服器在部署的時候並沒有進行清理。並且,對於堆中已經存在的物件,我們往往拿他們沒辦法。

這篇文章分析了導致OutOfMemoryError的不同原因,以及你該怎樣應對這種原因的方法。以下分析僅限於Sun Hotspot虛擬機器,但是大多數結論都適用於其他任何的JVM實現。它們大多數基於網上的文章以及我自己的經驗。我沒有直接做JVM開發的工作,因此結論並不代表JVM的作者。但是我確實曾經遇到過並解決了很多記憶體相關的問題。

垃圾回收介紹

我在這篇文章中已經詳細介紹了垃圾回收的過程。簡單的說,標記-清除演算法(mark-sweep collect)以garbage collection roots作為掃描的起點,並對整個物件圖進行掃描,對所有可達的物件進行標記。那些沒有被標記的物件會被清除並回收。

Java的垃圾回收演算法過程意味著如果出現了OOM,那麼說明你在不停的往物件圖中新增物件並且沒有移除它們。這通常是因為你在往一個集合類中添加了很多物件,比如Map,並且這個集合物件是static的。或者,這個集合類被儲存在了ThreadLocal物件中,而這個對應的Thread卻又長時間的執行,一直不退出。

這與C和C++的記憶體洩露完全不一樣。在這些語言中,如果一些方法呼叫了malloc()或者new,並且在方法退出的時候沒有呼叫相應的free()或者delete,那麼記憶體就會產生洩露。這些是真正意義上得洩露,你在這個程序範圍內不可能再恢復這些記憶體,除非使用一些特定的工具來保證每一個記憶體分配方法都有其對應的記憶體釋放操作相對應。

在java中,“洩露”這個詞往往被誤用了。因為從JVM的角度來說,所有的記憶體都是被良好管理的。問題僅僅是作為程式設計師的你不知道這些記憶體是被哪些物件佔用了。但是幸運的是,你還是有辦法去找到和定位它們。

在深入探討之前,你還有最後一件關於垃圾收集的知識需要了解:JVM會盡最大的能力去釋放記憶體,直到發生OOM。這就意味著OOM不能通過簡單的呼叫System.gc()來解決,你需要找到這些“洩露”點,並自己處理它們。

設定堆大小

學院派的人非常喜歡說Java語言規範並沒有對垃圾收集器進行任何約定,你甚至可以實現一個從來不釋放記憶體的JVM(實際是毫無意義的)。Java虛擬機器規範中提到堆是由垃圾回收器進行管理,但是卻沒有說明任何相關細節。僅僅說了我剛才提到的那句話:垃圾回收會發生在OOM之前。

實際上,Sun Hotspot虛擬機器使用了一個固定大小的堆空間,並且允許在最小空間和最大空間之間進行自動增長。如果你沒有指定最小值和最大值,那麼對於’client’模式將會預設使用2Mb最為最小值,64Mb最為最大值;對於’server’模式,JVM會根據當前可用記憶體來決定預設值。2000年後,預設的最大堆大小改為了64M,並且在當時已經認為足夠大了(2000年前的時候預設值是16M),但是對於現在的應用程式來說很容易就用完了。

這意味著你需要顯示的通過JVM引數來指定堆的最小值和最大值:

java -Xms256m -Xmx512m MyClass

這裡有很多經驗上得法則來設定最大值和最小值。顯然,堆的最大值應該設定為足以容下整個應用程式所需要的全部物件。但是,將它設定為“剛剛好足夠大”也不是一個很好的注意,因為這樣會增加垃圾回收器的負載。因此,對於一個長時間執行的應用程式,你一般需要保持有20%-25%的空閒堆空間。(你得應用程式可能需要不同的引數設定,GC調優是一門藝術,並且不在該文章討論範圍內)

讓你奇怪的時,設定合適的堆的最小值往往比設定合適的最大值更加重要。垃圾回收器會盡可能的保證當前的的堆大小,而不是不停的增長堆空間。這會導致應用程式不停的建立和回收大量的物件,而不是獲取新的堆空間,相對於初始(最小)堆空間。Java堆會盡量保持這樣的堆大小,並且會不停的執行GC以保持這樣的容量。因此,我認為在生產環境中,我們最好是將堆的最小值和最大值設定成一樣的。

你可能會困惑於為什麼Java堆會有一個最大值上限:作業系統並不會分配真正的實體記憶體,除非他們真的被使用了。並且,實際使用的虛擬記憶體空間實際上會比Java堆空間要大。如果你執行在一個32位系統上,一個過大的堆空間可能會限制classpath中能夠使用的jar的數量,或者你可以建立的執行緒數。

另外一個原因是,一個受限的最大堆空間可以讓你及時發現潛在的記憶體洩露問題。在開發環境中,對應用程式的壓力往往是不夠的,如果你在開發環境中就擁有一個非常大得堆空間,那麼你很有可能永遠不會發現可能的記憶體洩露問題,直到進入產品環境。

在執行時跟蹤垃圾回收

所有的JVM實現都提供了-verbos:gc選項,它可以讓垃圾回收器在工作的時候打印出日誌資訊:

java -verbose:gc com.kdgregory.example.memory.SimpleAllocator
[GC 1201K->1127K(1984K), 0.0020460 secs]
[Full GC 1127K->103K(1984K), 0.0196060 secs]
[GC 1127K->1127K(1984K), 0.0006680 secs]
[Full GC 1127K->103K(1984K), 0.0180800 secs]
[GC 1127K->1127K(1984K), 0.0001970 secs]
...

Sun的JVM提供了額外的兩個引數來以記憶體帶分類輸出,並且會顯示垃圾收集的開始時間:

java -XX:+PrintGCDetails -XX:+PrintGCTimeStamps com.kdgregory.example.memory.SimpleAllocator
0.095: [GC 0.095: [DefNew: 177K->64K(576K), 0.0020030 secs]0.097: [Tenured: 1063K->103K(1408K), 0.0178500 secs] 1201K->103K(1984K), 0.0201140 secs]
0.117: [GC 0.118: [DefNew: 0K->0K(576K), 0.0007670 secs]0.119: [Tenured: 1127K->103K(1408K), 0.0392040 secs] 1127K->103K(1984K), 0.0405130 secs]
0.164: [GC 0.164: [DefNew: 0K->0K(576K), 0.0001990 secs]0.164: [Tenured: 1127K->103K(1408K), 0.0173230 secs] 1127K->103K(1984K), 0.0177670 secs]
0.183: [GC 0.184: [DefNew: 0K->0K(576K), 0.0003400 secs]0.184: [Tenured: 1127K->103K(1408K), 0.0332370 secs] 1127K->103K(1984K), 0.0342840 secs]
...

從上面的輸出我們可以看出什麼?首先,前面的幾次垃圾回收發生的非常頻繁。每行的第一個欄位顯示了JVM啟動後的時間,我們可以看到在一秒鐘內有上百次的GC。並且,還加入了每次GC執行時間的開始時間(在每行的最後一個欄位),可以看出垃圾蒐集器是在不停的執行的。

但是在實時系統中,這會造成很大的問題,因為垃圾蒐集器的執行會奪走很多的CPU週期。就像我之前提到的,這很可能是由於初始堆大小設定的太小了,並且GC日誌顯示了:每次堆的大小達到了1.1Mb,它就開始執行GC。如果你得系統也有類似的現象,請在改變自己的應用程式之前使用-Xms來增大初始堆大小。

對於GC日誌還有一些很有趣的地方:除了第一次垃圾回收,沒有任何物件是存放在了新生代(“DefNew”)。這說明了這個應用程式分配了包含大量資料的陣列,在顯示世界裡這是很少出現的。如果在一個實時系統中出現這樣的狀況,我想到的第一個問題是“這些陣列拿來幹什麼用?”。

堆轉儲(Heap Dumps)

一個堆轉儲可以顯示你在應用程式說使用的所有物件。從基礎上講,它僅僅反映了物件例項的數量和類檔案所佔用的位元組數。當然你也可以將分配這些記憶體的程式碼一起dump出來,並且對比歷史存貨物件。但是,如果你要dump的資料資訊越多,JVM的負載就會越大,因此這些技術僅僅應該使用在開發環境中。

怎樣獲得一個記憶體轉儲

命令列引數-XX:+HeapDumpOnOutOfMemoryError是最簡單的方式生成記憶體轉儲。就像它的名字所說的,它會在記憶體被用完的時候(發生OOM)進行轉儲,這在產品環境非常好用。但是由於這個是一種事後轉儲(已經發生了OOM),它只能提供一種歷史性的資料。它會產生一個二進位制檔案,你可以使用jhat來操作該檔案(這個工具在JDK1.6中已經提供,但是可以讀取JDK1.5產生的檔案)。

你可以使用jmap(JDK1.5之後就自帶了)來為一個執行中得java程式產生堆轉儲,可以產生一個在jhat中使用的dump檔案,或者是一個存文字的統計檔案。統計圖可以在進行分析時優先使用,特別是你要在一段時間內多次轉儲堆並進行分析和對比歷史資料。

從轉儲內容和JVM的負荷的擴充套件性上考慮的話,可以使用profilers。Profiles使用JVM的除錯介面(debuging interface)來蒐集物件的記憶體分配資訊,包括具體的程式碼行和方法呼叫棧。這個是非常有用的:不僅僅可以知道你分配了一個數GB的陣列,你還可以知道你在一個特定的地方分配了950MB的物件,並且直接忽略其他的物件。當然,這些結果肯定會對JVM有開銷,包括CPU的開銷和記憶體的開銷(儲存一些原始資料)。你不應該在產品環境中使用profiles。

堆轉儲分析:live objects

Java中的記憶體洩露是這樣定義的:你在記憶體中分配了一些物件,但是並沒有清除掉所有對它們的引用,也就是說垃圾蒐集器不能回收它們。使用堆轉儲直方圖可以很容易的查詢這些洩露物件:它不僅僅可以告訴你在記憶體中分配了哪些物件,並且顯示了這些物件在記憶體中所佔用的大小。但是這種直方圖最大的問題是:對於同一個類的所有物件都被聚合(group)在一起了,所以你還需要進一步做一些檢測來確定這些記憶體在哪裡被分配了。

使用jmap並且加上-histo引數可以為你產生一個直方圖,它顯示了從程式執行到現在所有物件的數量和記憶體消耗,並且包含了已經被回收的物件和記憶體。如果使用-histo:live引數會顯示當前還在堆中得物件數量及其記憶體消耗,不論這些物件是否要被垃圾蒐集器進行回收。

也就是說,如果你要得到一個當前時間下得準確資訊,你需要在使用jmap之前強制執行一次垃圾回收。如果你的應用程式是執行在本地,最簡單的方式是直接使用jconsole:在’Memory’標籤下,有一個’Perform GC’的按鈕。如果應用程式是執行在服務端環境,並且JMX beans被暴露了,MemoryMXBean有一個gc()操作。如果上述的兩種方案都沒辦法滿足你得要求,你就只有等待JVM自己觸發一次垃圾蒐集過程了。如果你有一個很嚴重的記憶體洩露問題,那麼第一次major collection很可能預示著不久後就會OOM。

有兩種方法使用jmap產生的直方圖。其中最有效的方法,適用於長時間執行的程式,可以使用帶live的命令列引數,並且在一段時間內多次使用該命令,檢查哪些物件的數量在不斷增長。但是,根據當前程式的負載,該過程可能會花費1個小時或者更多的時間。

另外一個更加快速的方式是直接比較當前存活的物件數量和總的物件數量。如果有些物件佔據了總物件數量的大部分,那麼這些物件很有可能發生記憶體洩露。這裡有一個例子,這個應用程式已經連續幾周為100多個使用者提供了服務,結果列舉了前12個數量最多的物件。據我所知,這個程式沒有記憶體洩露的問題,但是像其他應用程式一樣做了常規性的記憶體轉儲分析操作。

~, 510> jmap -histo 7626 | more
 num     #instances         #bytes  class name
----------------------------------------------
   1:        339186       63440816  [C
   2:         84847       18748496  [I
   3:         69678       15370640  [Ljava.util.HashMap$Entry;
   4:        381901       15276040  java.lang.String
   5:         30508       13137904  [B
   6:        182713       10231928  java.lang.ThreadLocal$ThreadLocalMap$Entry
   7:         63450        8789976  <constMethodKlass>
   8:        181133        8694384  java.lang.ref.WeakReference
   9:         43675        7651848  [Ljava.lang.Object;
  10:         63450        7621520  <methodKlass>
  11:          6729        7040104  <constantPoolKlass>
  12:        134146        6439008  java.util.HashMap$Entry
~, 511> jmap -histo:live 7626 | more
 num     #instances         #bytes  class name
----------------------------------------------
   1:        200381       35692400  [C
   2:         22804       12168040  [I
   3:         15673       10506504  [Ljava.util.HashMap$Entry;
   4:         17959        9848496  [B
   5:         63208        8766744  <constMethodKlass>
   6:        199878        7995120  java.lang.String
   7:         63208        7592480  <methodKlass>
   8:          6608        6920072  <constantPoolKlass>
   9:         93830        5254480  java.lang.ThreadLocal$ThreadLocalMap$Entry
  10:        107128        5142144  java.lang.ref.WeakReference
  11:         93462        5135952  <symbolKlass>
  12:          6608        4880592  <instanceKlassKlass>

當我們要嘗試尋找記憶體洩露問題,可以從消耗記憶體最多的物件著手。這聽上去很明顯,但是往往它們並不是記憶體洩露的根源。但是,它們任然是應該最先下手的地方,在這個例子中,最佔用記憶體的是一些char[]的陣列物件(總大小是60MB,基本上沒有任何問題)。但是很奇怪的是當前存貨(live)的物件竟然佔了歷史分配的總物件大小的三分之二。

一般來說,一個應用程式會分配物件,並且在不久之後就會釋放它們。如果儲存一些物件的應用過長的時間,就很有可能會導致記憶體洩露。但是雖然是這麼說的,實際上還是要具體情況具體分析,主要還是要看這個程式到底在做什麼事情。字元陣列物件(char[])往往和字串物件(String)同時存在,大部分的應用程式都會在整個執行過程中一直保持著一些字串物件的引用。例如,基於JSP的web應用程式在JSP頁面中定義了很多HTML字串表示式。這種特殊的應用程式提供HTML服務,但是它們需要保持字串引用的需求卻不一定那麼清晰:它們提供的是目錄服務,並不是靜態文字。如果我遇到了OOM,我就會嘗試找到這些字串在哪裡被分配,為什麼沒有被釋放。

另一個需要關注的是位元組陣列([B)。在JDK中有很多類都會使用它們(比如BufferedInputStream),但是卻很少在應用程式程式碼中直接看到它們。通常它們會被用作快取(buffer),但是快取的生命週期不會很長。在這個例子中我們看到,有一半的位元組陣列任然保持存活。這個是令人擔憂的,並且它凸顯了直方圖的一個問題:所有的物件都按照它的型別被分組聚合了。對於應用程式物件(非JDK型別或者原始型別,在應用程式程式碼中定義的類),這不是一個問題,因為它們會在程式的一個部分被集中分配。但是位元組陣列有可能會在任何地方被定義,並且在大多數應用程式中都被隱藏在一些庫中。我們是否應當搜尋呼叫了new byte[]或者new ByteArrayOutputStream()的程式碼?

堆轉儲分析:相關的原因和影響分析

為了找到導致記憶體洩露的最終原因,僅僅考慮按照類別(class)的分組的記憶體佔用位元組數是不夠的。你還需要將應用程式分配的物件和記憶體洩露的物件關聯起來考慮。一個方法是更加深入檢視物件的數量,以便將具有關聯性的物件找出來。下面是一個具有嚴重記憶體問題的程式的轉儲資訊:

num     #instances         #bytes  class name
----------------------------------------------
   1:       1362278      140032936  [Ljava.lang.Object;
   2:         12624      135469922  [B
  ...
   5:        352166       45077248  com.example.ItemDetails
  ...
   9:       1360742       21771872  java.util.ArrayList
  ...
  41:          6254         200128  java.net.DatagramPacket

如果你僅僅去看資訊的前幾行,你可能會去定位Object[]或者byte[],這些都是徒勞的。真正的問題出在ItemDetails和DatagramPacket上:前者分配了大量的ArrayList,進而又分配了大量的Object[];後者使用了大量的byte[]來儲存從網路上接收到的資料。

第一個問題,分配了大量的陣列,實際上不是記憶體洩露。ArrayList的預設建構函式會分配容量是10的陣列,但是程式本身一般只使用1個或者2個槽位,這對於64位JVM來說會浪費62個位元組的記憶體空間。一個更好的涉及方案是僅僅在有需要的時候才使用List,這樣對每個例項來說可以節約額外的48個位元組。但是,對於這種問題也可以很輕易的通過加記憶體來解決,因為現在的記憶體非常便宜。

但是對於datagram的洩露就比較麻煩(如同定位這個問題一樣困難):這表明接收到的資料沒有被儘快的處理掉。

為了跟蹤問題的原因和影響,你需要知道你的程式是怎樣在使用這些物件。不多的程式才會直接使用Object[]:如果確實要使用陣列,程式設計師一般都會使用帶型別的陣列。但是,ArrayList會在內部使用。但是僅僅知道ArrayList的記憶體分配是不夠的,你還需要順著呼叫鏈往上走,看看誰分配了這些ArrayList。

其中一個方法是對比相關的物件數量。在上面的例子中,byte[]和DatagramPackage的關係是很明顯的:其中一個基本上是另外一個的兩倍。但是ArrayList和ItemDetails的關係就不那麼明顯了。(實際上一個ItemDetails中會包含多個ArrayList)

這往往是個陷阱,讓你去關注那麼數量最多的一些物件。我們有數百萬的ArrayList物件,並且它們分佈在不同的class中,也有可能集中在一小部分class中。儘管如此,數百萬的物件引用是很容易被定位的。就算有10來個class可能會包含ArrayList,那麼每個class的實體物件也會有十萬個,這個是很容易被定位的。

從直方圖中跟蹤這種引用關係鏈是需要花費大量精力的,幸運的是,jmap不僅僅可以提供直方圖,它還可以提供可以瀏覽的堆轉儲資訊。

堆轉儲分析:跟蹤引用鏈

瀏覽堆轉儲引用鏈具有兩個步驟:首先需要使用-dump引數來使用jmap,然後需要用jhat來使用轉儲檔案。如果你確定要使用這種方法,請一定要保證有足夠多的記憶體:一個轉儲檔案通常都有數百M,jhat需要好幾個G的記憶體來處理這些轉儲檔案。

tmp, 517> jmap -dump:live,file=heapdump.06180803 7626
Dumping heap to /home/kgregory/tmp/heapdump.06180803 ...
Heap dump file created
tmp, 518> jhat -J-Xmx8192m heapdump.06180803
Reading from heapdump.06180803...
Dump file created Sat Jun 18 08:04:22 EDT 2011
Snapshot read, resolving...
Resolving 335643 objects...
Chasing references, expect 67 dots...................................................................
Eliminating duplicate references...................................................................
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.

提供給你的預設URL顯示了所有載入進系統的class,但是我覺得並不是很有用。相反,我直接使用http://localhost:7000/histo/,這個地址是一個直方圖的視角來進行顯示,並且是按照物件數量和佔用的記憶體空間進行排序了的。

這個直方圖裡的每個class的名稱都是一個連結,點選這個連結可以檢視關於這個型別的詳細資訊。你可以在其中看到這個類的繼承關係,它的成員變數,以及很多指向這個類的實體變數資訊的連結。我不認為這個詳細資訊頁面非常有用,而且實體變數的連結列表很佔用很多的瀏覽器記憶體。

為了能夠跟蹤你的記憶體問題,最有用的頁面是’Reference by Type’。這個頁面含有兩個表格:入引用和出引用,他們都被引用的數量進行排序了。點選一個類的名字可以看到這個引用的資訊。

你可以在類的詳細資訊(class details)頁面中找到這個頁面的連結。

堆轉儲分析:記憶體分配情況

在大多數情況下,知道了是哪些物件消耗了大量的記憶體往往就可以知道它們為什麼會發生記憶體洩露。你可以使用jhat來找到所有引用了他們的物件,並且你還可以看到使用了這些物件的引用的程式碼。但是在有些時候,這樣還是不夠的。

比如說你有關於字串物件的記憶體洩露問題,那麼就很有可能會花費你好幾天的時間去檢查所有和字串相關的程式碼。要解決這種問題,你就需要能夠顯示記憶體在哪裡被分配的堆轉儲。但是需要注意的是,這種型別的堆轉儲會對你的應用程式產生更多的負載,因為負責轉儲的代理需要記錄每一個new操作符。

有許多互動式的程式可以做到這種級別的資料記錄,但是我找到了一個更簡單的方法,那就是使用內建的hprof代理來啟動JVM。

java -Xrunhprof:heap=sites,depth=2 com.kdgregory.example.memory.Gobbler

hprof有許多選項:不僅僅可以用多種方式輸出記憶體使用情況,它還可以跟蹤CPU的使用情況。當它執行的時候,我指定了一個事後的記憶體轉儲,它記錄了哪些物件被分配,以及分配的位置。它的輸出被記錄在了java.hprof.txt檔案中,其中關於堆轉儲的部分如下:

SITES BEGIN (ordered by live bytes) Tue Sep 29 10:43:34 2009
          percent          live          alloc'ed  stack class
 rank   self  accum     bytes objs     bytes  objs trace name
    1 99.77% 99.77%  66497808 2059  66497808  2059 300157 byte[]
    2  0.01% 99.78%      9192    1     27512    13 300158 java.lang.Object[]
    3  0.01% 99.80%      8520    1      8520     1 300085 byte[]
SITES END

這個應用程式沒有分配多種不同型別的物件,也沒有將它們分配到很多不同的地方。一般的轉儲有成百上千行的資訊,顯示了每一種型別的物件被分配到了哪裡。幸運的是,大多數問題都會出現在開頭的幾行。在這個例子中,最突出的是64M的存活著的位元組陣列,並且每一個平均32K。

大多數程式中都不會一直持有這麼大得資料,這就表明這個程式沒有很好的抽取和處理這些資料。你會發現這常常發生在讀取一些大的字串,並且儲存了substring之後的字串:很少有人知道String.substring()後會共享原始字串物件的位元組陣列。如果你按照一行一行地讀取了一個檔案,但是卻使用了每行的前五個字元,實際上你任然儲存的是整個檔案在記憶體中。

轉儲檔案也顯示出這些陣列被分配的數量和現在存活的數量完全相等。這是一種典型的洩露,並且我們可以通過搜尋’trace’號來找到真正的程式碼:

TRACE 300157:
    com.kdgregory.example.memory.Gobbler.main(Gobbler.java:22)

好了,這下就足夠簡單了:當我在程式碼中找到指定的程式碼行時,我發現這些陣列被存放在了ArrayList中,並且它也一直沒有出作用域。但是有時候,堆疊的跟蹤並沒有直接關聯到你寫的程式碼上:

TRACE 300085:
    java.util.zip.InflaterInputStream.<init>(InflaterInputStream.java:71)
    java.util.zip.ZipFile$2.<init>(ZipFile.java:348)

在這個例子中,你需要增加堆疊跟蹤的深度,並且重新執行你的程式。但是這裡有一個需要平衡的地方:當你獲取到了更多的堆疊資訊,你也同時增加了profile的負載。預設地,如果你沒有指定depth引數,那麼預設值就會是4。我發現當堆疊深度為2的時候就可以發現和定位我程式中得大部分問題了,當然我也使用過深度為12的引數來執行程式。

另外一個增大堆疊深度的好處是,最後的報告結果會更加細粒度:你可能會發現你洩露的物件來自兩到三個地方,並且它們都使用了相同的方法。

堆轉儲分析:位置、地點

當很多物件在分配的不久後就被丟棄時,分代垃圾蒐集器就會開始執行。你可以使用同樣的原則來找發現記憶體洩露:使用偵錯程式,在物件被分配的地方打上斷點,並且執行這段程式碼。在大多數時候,當它們被分配不久後就會加入到長時間存活(long-live)的集合中。

相關推薦

深入解析OutOfMemoryError

在Java中,所有物件都儲存在堆中。他們通過new關鍵字來進行分配,JVM會檢查是否所有執行緒都無法在訪問他們了,並且會將他們進行回收。在大多數時候程式設計師都不會有一絲一毫的察覺,這些工作都被靜悄悄的執行。但是,有時候在釋出前的最後一天,程式掛了。 Exception

VUE2中文文檔:深入組件

支持 導出 urn opener org 表達式 hub 修改 activate 組件註冊 組件名稱 命名方案 可以通過兩種可選方式,定義組件名稱: 串聯式命名(kebab-case) Pascal 式命名(PascalCase)使用串聯式命名(kebab-cas

深入解析病毒理論篇

amp obj ice 不可 表現 當前 text 系統調用 空白 豬年送安康,祝大家新一年健康、快樂。願大家都做一個勤奮努力、真誠奉獻的人,幸運會永遠的眷顧你們。?引子:?某一天饒有興趣在卡飯上瀏覽著帖子,故事的相遇就那麽簡單。當時一條評論勾起我的好奇心,那麽好逆向開始。

深入理解deferdefer基礎

深入理解 defer 分上下兩篇文章,本文為上篇,主要介紹如下內容: 為什麼需要 defer; defer 語法及語義; defer 使用要點; defer 語句中的函式到底是在 return 語句之後被呼叫還是 return 語句之前被呼叫。 為什麼需要 d

深入web workers

前段時間,為了優化某個有點複雜的功能,我採用了shared workers + indexDB,構建了一個高效能的多頁面共享的服務。由於是第一次真正意義上的運用workers,比以前單純的學習有更多體會,所以這裡就分享出來! 各種worker概要 有三種worker:普通的worker、shared work

深入解析SQL Server並行執行原理及實踐

在成熟領先的企業級資料庫系統中,並行查詢可以說是一大利器,在某些場景下它可以顯著地提升查詢的相應時間,提升使用者體驗。如SQL Server、Oracle等, MySQL目前還未實現,而PostgreSQL在2015實現了並行掃描,相信他們也在朝著更健壯的企業級資料庫邁進。RDBMS中並行執行的實現

【Android深入解析】Manifest配置檔案解析英文版

<action> 語法規則: <action android:name="string"/> 描述 : Adds an action to an intentfilter. An elementmust contain one or more

Android 屬性動畫Property Animation 全然解析

顏色 valid 全部 加速度 ext target ng- 點擊 save 轉載請標明出處:http://blog.csdn.net/lmj623565791/article/details/380674751、概述Android提供了幾種動畫類型:View Anima

聊聊高並發二十四解析java.util.concurrent各個組件 深入理解AQS

sar 成功 通知 ati help write ng- ads 同步 近期總體過了下AQS的結構。也在網上看了一些講AQS的文章,大部分的文章都是泛泛而談。又一次看了下AQS的代碼,把一些新的要點拿出來說一說。 AQS是一個管程。提供了一個主要的同步器的

深入理解Java集合框架】紅黑樹講解

時間復雜度 row lee tel framework 關系 eight logs return 來源:史上最清晰的紅黑樹講解(上) - CarpenterLee 作者:CarpenterLee(轉載已獲得作者許可,如需轉載請與原作者聯系) 文中所有圖片點擊之後均可查看大

深入理解計算機系統》閱讀筆記--程序的機器級表示

還要 所有 執行文件 命令 不同的 指向 local 變量 section 一、為什麽要學習和了解匯編 編譯器基於編程語言的規則,目標機器的指令集和操作系統遵循的慣例,經過一系列的階段生成機器代碼。GCC c語言編譯器以匯編代碼的形式產生輸出,匯編代碼是機器代碼的文

Android Fragment 真正的完全解析

watermark 展示 near 主界面 ddt comm 講解 超級 pro 版權聲明:本文為博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/lmj623565791/article/details/37970961 轉載請標明出處:

Java程式設計師從笨鳥到菜鳥之九十三深入java虛擬機器——類載入器詳解

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

深入理解 Java 記憶體模型》讀書筆記乾貨,萬字長文

0. 前提 1. 基礎 2. 重排序 3. 順序一致性 4. Volatile 5. 鎖 6. final 7. 總結 0. 前提 《深入理解 Java 記憶體模型》 程曉明著,該書在以前看過一

一篇文章徹底讀懂HashMap之HashMap原始碼解析

就身邊同學的經歷來看,HashMap是求職面試中名副其實的“明星”,基本上每一加公司的面試多多少少都有問到HashMap的底層實現原理、原始碼等相關問題。 在秋招面試準備過程中,博主閱讀過很多關於HashMap原始碼分析的文章,漫長的拼湊式閱讀之後,博主沒有看到過

深入拆解虛擬機器JVM是如何執行方法呼叫的?

過載與重寫 (1)Java程式裡,如果同一個類出現多個名字相同,並且引數型別相同的方法,那麼它無法通過編譯 (2)在正常情況下,如果我們想要在同一個類中定義名字相同的方法,那麼它們的引數型別必須不同。這些方法之間的關係,我們稱之為過載 (3)選取過載方法的三個階段:

深入拆解虛擬機器垃圾回收

引用計數法 (1)它的做法是為每個物件新增一個引用計數器,用來統計指向該物件的引用個數。一旦某個物件 的引用計數器為0,則說明該物件已經死亡,便可以被回收了。 (2)具體實現:如果有一個引用,被賦值為某一個物件,那麼該物件的引用計數器+1。如果指向某一個物件的引用,被賦值為其他值

web開發excel檔案傳及解析

前言 在web開發的時候,我們經常有遇到檔案的上傳以及解析的需求,比如我們將excel檔案上傳並且解析,最後將解析的excel的內容插入到資料庫中,今天就實踐一下檔案的上傳以及excel的解析,這篇部落格主要是實踐檔案上傳功能,對應的excel檔案的解析,將在下一篇部落格實踐。 準備工作:

Android 屬性動畫Property Animation 完全解析 【轉】

轉載請標明出處:http://blog.csdn.net/lmj623565791/article/details/38067475 1、概述 Android提供了幾種動畫型別:View Animation 、Drawable Animation 、Property Anima

第18課 - 深入特權級轉移

初識任務狀態段(Task State Segment)     處理器所提供的硬體資料結構, 用於實現多工解決方案     TSS中儲存了 關鍵暫存器的值以及 不同特權級使用的棧     參考:紫陌  ht