虛擬機部分(JVM)

 

第一部分:JVM的內存區域劃分(堆是用來存放對象而棧是用來執行程序的)java

 

一、 線程獨有的內存區域程序員

1PROGRAM COUNTER REGISTER,程序計數器算法

這塊內存區域很小,它是當前線程所執行的字節碼的行號指示器,字節碼解釋器經過改變這個計數器的值來選取下一條須要執行的字節碼指令。因爲程序計數器中存儲的數據所佔空間的大小不會隨程序的執行而發生改變,所以,對於程序計數器是不會發生內存溢出現象(OutOfMemory)的。數據庫

2JAVA STACK,虛擬機棧數組

Java棧也稱做虛擬機棧,Java棧是Java方法執行的內存模型tomcat

Java棧中存放的是一個個的棧幀,每一個棧幀對應一個被調用的方法,在棧幀中包括局部變量表(Local Variables)、操做數棧(Operand Stack)、指向當前方法所屬的類的運行時常量池(運行時常量池的概念在方法區部分會談到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些額外的附加信息。安全

下圖表示了一個Java棧的模型:網絡

 棧幀中的各部分:局部變量表,顧名思義,就是用來存儲方法中的局部變量(包括在方法中聲明的非靜態變量以及函數形參)。對於基本數據類型的變量,則直接存儲它的值,對於引用類型的變量,則存的是指向對象的引用。局部變量表的大小在編譯器就能夠肯定其大小了,所以在程序執行期間局部變量表的大小是不會改變的。操做數棧,棧最典型的一個應用就是用來對錶達式求值。程序中的全部計算過程都是在藉助於操做數棧來完成的指向運行時常量池的引用,由於在方法執行的過程當中有可能須要用到類中的常量,因此必須要有一個引用指向運行時常量方法返回地址,當一個方法執行完畢以後,要返回以前調用它的地方,所以在棧幀中必須保存一個方法返回地址數據結構

棧的執行:多線程

當線程執行一個方法時,就會隨之建立一個對應的棧幀,並將創建的棧幀壓棧。當方法執行完畢以後,便會將棧幀出棧。所以可知,線程當前執行的方法所對應的棧幀一定位於Java棧的頂部。棧區的空間不用程序員去管理,由於Java有本身的垃圾回收機制,這部分空間的分配和釋放都是由系統自動實施的。

因爲每一個線程正在執行的方法可能不一樣,所以每一個線程都會有一個本身的Java棧,互不干擾;生命週期和線程相同。每一個方法執行的同時都會建立一個棧幀,用於存儲局部變量表、操做數棧、動態連接、方法出口等信息,每個方法從調用直至執行完畢的過程,就對應着一個棧幀在虛擬機中入棧到出棧的過程。

 

3NATIVE METHOD STACK,本地方法棧

本地方法棧與Java棧的做用和原理很是類似。區別只不過是Java棧是爲執行Java方法服務的,而本地方法棧則是爲執行本地方法(Native Method)服務的。

 

2、線程間共享的內存區域

1HEAP,堆

堆是Java虛擬機所管理的內存中最大的一塊,它在虛擬機啓動時建立,此內存惟一的目的就是存放對象實例。因爲如今垃圾收集器採用的基本都是分代收集算法,因此堆還能夠細分爲新生代和老年代,再細緻一點還有Eden區、From Survivior區、To Survivor區。

2METHOD AREA,方法區

這塊區域用於存儲虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據.

 

2、對象的建立過程

1   NEW指令:定位類(初始化)

2       類加載 ,虛擬機從堆內存中分配內存   垃圾收集器選擇的是Serial、ParNew

           內存規整:指針碰撞    內存不規整:空閒列表   垃圾收集器選擇的是CMS

           線程安全性:CAS +  TLAB

3       分配到的內存初始化爲零(不包括對象頭)

4       對象頭設置

5       初始化

 

對象的訪問定位

Java程序須要經過棧上的reference(引用)數據來操做堆上的具體對象

 

 

 

 

第二部分:JVM分代垃圾回收策略

1、爲何要分代

不一樣的對象的生命週期是不同的。所以,不一樣生命週期的對象能夠採起不一樣的收集方式,以便提升回收效率。

2、如何分代

虛擬機中的共劃分爲三個代:年輕代(Young Generation)、年老代(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java類的類信息,與垃圾收集要收集的Java對象關係不大。年輕代和年老代的劃分是對垃圾收集影響比較大的。

年輕代:

全部新生成的對象首先都是放在年輕代的。年輕代的目標就是儘量快速的收集掉那些生命週期短的對象。年輕代分三個區。一個Eden區,兩個Survivor區(通常而言)。大部分對象在Eden區中生成。當Eden區滿時,還存活的對象將被複制到Survivor區(兩個中的一個),當這個Survivor區滿時,此區的存活對象將被複制到另一個Survivor區,當這個Survivor區也滿了的時候,從第一個Survivor區複製過來的而且此時還存活的對象,將被複制「年老區(Tenured)」。

年老代:

在年輕代中經歷了N次垃圾回收後仍然存活的對象,就會被放到年老代中。所以,能夠認爲年老代中存放的都是一些生命週期較長的對象。GC過程當中,當某些對象通過屢次GC都沒有被回收,可能會進入到年老代。或者,當新生代沒有足夠的空間來爲對象分配內存時,可能會直接在年老代進行分配。

持久代:

用於存放靜態文件,現在Java類、方法等。持久代對垃圾回收沒有顯著影響,大部分對象在運行都不要進行GC,可是有些應用可能動態生成或者調用一些class,持久代大小經過-XX:MaxPermSize=<N>進行設置。永久代實際上對應着虛擬機運行時數據區的「方法區」,這裏主要存放類信息、靜態變量、常量等數據。 

 

3、什麼狀況下觸發垃圾回收

GC有兩種類型:Scavenge GCFull GC

通常狀況下,當新對象生成,而且在Eden申請空間失敗時,就會觸發Scavenge GC,對Eden區域進行GC,清除非存活對象,而且把尚且存活的對象移動到Survivor區。而後整理Survivor的兩個區。這種方式的GC是對年輕代的Eden區進行,不會影響到年老代。由於大部分對象都是從Eden區開始的,同時Eden區不會分配的很大,因此Eden區的GC會頻繁進行。於是,通常在這裏須要使用速度快、效率高的算法,使Eden去能儘快空閒出來。對整個堆進行整理,包括Young、Tenured和Perm。

 

Full GC由於須要對整個塊進行回收,因此比Scavenge GC要慢,所以應該儘量減小Full GC的次數。在對JVM調優的過程當中,很大一部分工做就是對於FullGC的調節。有以下緣由可能致使Full GC:

· 年老代(Tenured)被寫滿

· 持久代(Perm)被寫滿

· System.gc()被顯示調用

·上一次GC以後Heap的各域分配策略動態變化

 

 

 

 第三部分:JVM以內存分配與回收策略

TLAB,內存分配的動做,能夠按照線程劃分在不一樣的空間之中進行,即每一個線程在Java堆中預先分配一小塊內存,稱爲本地線程分配緩衝.

1、 對象優先在Eden區分配

對象一般在新生代的Eden區進行分配,當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC

Minor GC:指發生在新生代的垃圾收集動做,很是頻繁,速度較快。

Major GC:指發生在老年代的GC,出現Major GC,常常會伴隨一次Minor GC,同時Minor GC也會引發Major GC,通常在GC日誌中統稱爲GC,不頻繁。

Full GC:指發生在老年代和新生代的GC,速度很慢,須要Stop The World。

2、大對象直接進入老年代

須要大量連續內存空間的Java對象稱爲大對象,大對象的出現會致使提早觸發垃圾收集以獲取更大的連續的空間來進行大對象的分配。虛擬機提供了-XX:PretenureSizeThreadshold參數來設置大對象的閾值,超過閾值的對象直接分配到老年代。

3、長期存活的對象進入老年代

每一個對象有一個對象年齡計數器,與前面的對象的存儲佈局中的GC分代年齡對應。對象出生在Eden區、通過一次Minor GC後仍然存活,並可以被Survivor容納,設置年齡爲1,對象在Survivor區,每次通過一次Minor GC,年齡就加1,當年齡達到必定程度(默認15),就晉升到老年代,虛擬機提供了-XX:MaxTenuringThreshold來進行設置。

4、動態對象年齡判斷

對象的年齡到達了MaxTenuringThreshold能夠進入老年代,同時,若是在survivor區中相同年齡全部對象大小的總和大於survivor區的一半,年齡大於等於該年齡的對象就能夠直接進入老年代。無需等到MaxTenuringThreshold中要求的年齡。

5、空間分配擔保在發生Minor GC

在發生Minor GC時,虛擬機會檢查老年代連續的空閒區域是否大於新生代全部對象的總和,若成

立,則說明Minor GC是安全的.

 

 

 

 

第四部分 Java垃圾回收(GC)機制詳解

 

1、爲何須要垃圾回收

若是不進行垃圾回收,內存早晚都會被消耗空

2、哪些內存須要回收?

所謂「要回收的垃圾」無非就是那些不可能再被任何途徑使用的對象。那麼如何找到這些對象?

1引用計數法 XXXXXX

引用計數法的實現是,給對象中添加一個引用計數器,每當一個地方引用這個對象時,計數器值+1;當引用失效時,計數器值-1。任什麼時候刻計數值爲0的對象就是不可能再被使用的。XXXXXX

2可達性分析法 111111

可達性分析法的基本思想是經過一系列稱爲「GC Roots」的對象做爲起始點,從這些節點向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈(即GC Roots到對象不可達)時,則證實此對象是不可用的。111111

 

3、四種引用狀態 對象的回收---堆

Java中引用的定義:若是引用類型的數據中存儲的數值表明的是另外一塊內存的起始地址,就稱這塊內存表明着一個引用。

 

4、方法區的垃圾回收

方法區的垃圾回收主要回收兩部份內容:1. 廢棄常量。2. 無用的類。

廢棄常量

若是一個字符串「abc」已經進入常量池,可是當前系統沒有任何一個String對象引用了叫作abc的字面量,那麼,若是發生垃圾回收而且有必要時,「abc」就會被系統移出常量池。常量池中的其餘類(接口)、方法、字段的符號引用也與此相似。

無用的類:

1. 該類的全部實例都已經被回收,即Java堆中不存在該類的任何實例。

2. 加載該類的ClassLoader已經被回收

3. 該類對應的java.lang.Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。

 

 

第五部分:垃圾收集算法

1、標記-清除(Mark-Sweep)算法

分爲「標記」和「清除」兩個階段:首先標記出全部須要回收的對象,標記完成後統一回收全部被標記的對象

這種算法的不足主要體如今效率和空間,從效率的角度講,標記和清除兩個過程的效率都不高;從空間的角度講,標記清除後會產生大量不連續的內存碎片, 內存碎片太多可能會致使之後程序運行過程當中在須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發一次垃圾收集動做。

標記-清除算法執行過程如圖:

2、複製(Copying)算法

複製算法是爲了解決效率問題而出現的,它將可用的內存分爲兩塊,每次只用其中一塊,當這一塊內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已經使用過的內存空間一次性清理掉。這樣每次只須要對整個半區進行內存回收,內存分配時也不須要考慮內存碎片等複雜狀況,只須要移動指針,按照順序分配便可。

複製算法的執行過程如圖:

這種算法有個缺點,內存縮小爲了原來的一半,這樣代價過高了如今的商用虛擬機都採用這種算法來回收新生代,不過研究代表1:1的比例很是不科學,所以新生代的內存被劃分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor

每次回收時,EdenSurvivor中還存活着的對象一次性複製到另一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。HotSpot虛擬機默認Eden區和Survivor區的比例爲8:1,意思是每次新生代中可用內存空間爲整個新生代容量的90%。固然,咱們沒有辦法保證每次回收都只有很少於10%的對象存活,當Survivor空間不夠用時,須要依賴老年代進行分配擔保。

 

3、標記-整理(Mark-Compact)算法

複製算法在對象存活率較高的場景下要進行大量的複製操做,效率很低。萬一對象100%存活,那麼須要有額外的空間進行分配擔保。老年代都是不易被回收的對象,對象存活率高,所以通常不能直接選用複製算法。根據老年代的特色,有人提出了另一種標記-整理算法,過程與標記-清除算法同樣,不過不是直接對可回收對象進行清理,而是讓全部存活對象都向一端移動,而後直接清理掉邊界之外的內存

標記-整理算法的工做過程如圖:

4、分代收集算法

現代商用虛擬機基本都採用分代收集算法來進行垃圾回收。上面內容的結合罷了,根據對象的生命週期的不一樣將內存劃分爲幾塊,而後根據各塊的特色採用最適當的收集算法。大批對象死去、少許對象存活的(新生代),使用複製算法,複製成本低;對象存活率高、沒有額外空間進行分配擔保的(老年代),採用標記-清理算法或者標記-整理算法

 

 

第六部分、垃圾收集器

不一樣虛擬機所提供的垃圾收集器可能會有很大差異,咱們使用的是HotSpot。

HotSpot這個虛擬機所包含的全部收集器如圖:

 

 

 

上圖展現了7種做用於不一樣分代的收集器,若是兩個收集器之間存在連線,那說明它們能夠搭配使用。

一、 Serial收集器

  這個收集器是一個採用複製算法的單線程的收集器,單線程一方面意味着它只會使用一個CPU或一條線程去完成垃圾收集工做,另外一方面也意味着它進行垃圾收集時必須暫停其餘線程的全部工做,直到它收集結束爲止。不過實際上到目前爲止,Serial收集器依然是虛擬機運行在Client模式下的默認新生代收集器,由於它簡單而高效。

說明:1. 須要STW(Stop The World),停頓時間長。2. 簡單高效,對於單個CPU環境而言,Serial收集器因爲沒有線程交互開銷,能夠獲取最高的單線程收集效率。

2Serial Old收集器

Serial收集器的老年代版本,一樣是一個單線程收集器,使用標記-整理算法,這個收集器的主要意義也是在於給Client模式下的虛擬機使用。

3ParNew收集器

ParNew收集器其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集外,其他行爲和Serial收集器徹底同樣,包括使用的也是複製算法。ParNew收集器除了多線程之外和Serial收集器並無太多創新的地方,可是它倒是Server模式下的虛擬機首選的新生代收集器,其中有一個很重要的和性能無關的緣由是,除了Serial收集器外,目前只有它能與CMS收集器配合工做(看圖)。

CMS收集器是一款幾乎能夠認爲有劃時代意義的垃圾收集器,由於它第一次實現了讓垃圾收集線程與用戶線程基本上同時工做。ParNew收集器運行過程以下圖所示:

4Parallel Scavenge收集器

一個新生代收集器,也是用複製算法的收集器,也是並行的多線程收集器,可是它的特色是它的關注點和其餘收集器不一樣。CMS等收集器的關注點是儘量縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量 < 吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間) >另外,Parallel Scavenge收集器是虛擬機運行在Server模式下的默認垃圾收集器

虛擬機提供了-XX:MaxGCPauseMillis和-XX:GCTimeRatio兩個參數來精確控制最大垃圾收集停頓時間和吞吐量大小。

前者小點好, 後者是一個開關參數,這個參數打開以後,就不須要手動指定新生代大小、Eden區和Survivor參數等細節參數了,虛擬機會根據當前系統的運行狀況手機性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量。

5Parallel Old收集器

Parallel Scavenge收集器的老年代版本,使用多線程和標記-整理算法。 「吞吐量優先收集器」終於有了比較名副其實的應用組合,在注重吞吐量以及CPU資源敏感的場合,均可以優先考慮Parallel Scavenge收集器+Parallel Old收集器的組合。

運行過程以下圖所示:

 

 

 

6CMS收集器

CMSConrrurent Mark Sweep)收集器是以獲取最短回收停頓時間爲目標的收集器。使用標記- 清除算法,收集過程分爲以下四步:

(1). 初始標記,標記GCRoots能直接關聯到的對象,時間很短。

(2). 併發標記,進行GCRoots Tracing(可達性分析)過程,時間很長。

(3). 從新標記,修正併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,時間較長。

(4). 併發清除,回收內存空間,時間很長。

其中,併發標記與併發清除兩個階段耗時最長,可是能夠與用戶線程併發執行。運行過程以下圖所示:

7G1收集器

HotSpot開發團隊賦予它的使命是將來能夠替換掉JDK1.5中發佈的CMS收集器。與其餘GC收集器相比,G1收集器有如下特色:

(1). 並行和併發。使用多個CPU來縮短Stop The World停頓時間,與用戶線程併發執行。

(2). 分代收集。獨立管理整個堆,可是可以採用不一樣的方式去處理新建立對象和已經存活了一段時間、熬過屢次GC的舊對象,以獲取更好的收集效果。

(3). 空間整合。基於標記 - 整理算法,無內存碎片產生。

(4). 可預測的停頓。能簡歷可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片斷內,消耗在垃圾收集上的時間不得超過N毫秒。

在G1以前的垃圾收集器,收集的範圍都是整個新生代或者老年代,而G1再也不是這樣。使用G1收集器時,Java堆的內存佈局與其餘收集器有很大差異,它將整個Java堆劃分爲多個大小相等的獨立區域,雖然還保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔離的了,它們都是一部分(能夠不連續)Region的集合。

 

 

 

第七部分:理解GC日誌

 

[GC [DefNew: 310K->194K(2368K), 0.0269163 secs] 310K->194K(7680K), 0.0269513 secs]

[Times: user=0.00 sys=0.00, real=0.03 secs]

[Full GC (System) [Tenured: 2241K->193K(5312K), 0.0056517 secs] 4289K->193K(7680K),

[Perm : 2950K->2950K(21248K)], 0.0057094 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

Heap

def new generation total 2432K, used 43K [0x00000000052a0000, 0x0000000005540000,

0x0000000006ea0000)

eden space 2176K, 2% used [0x00000000052a0000, 0x00000000052aaeb8,

0x00000000054c0000)

from space 256K, 0% used [0x00000000054c0000, 0x00000000054c0000,

0x0000000005500000)

to space 256K, 0% used [0x0000000005500000, 0x0000000005500000,

0x0000000005540000)

tenured generation total 5312K, used 193K [0x0000000006ea0000, 0x00000000073d0000,

0x000000000a6a0000)

the space 5312K, 3% used [0x0000000006ea0000, 0x0000000006ed0730,

0x0000000006ed0800, 0x00000000073d0000)

compacting perm gen total 21248K, used 2982K [0x000000000a6a0000,

0x000000000bb60000, 0x000000000faa0000)

the space 21248K, 14% used [0x000000000a6a0000, 0x000000000a989980,

0x000000000a989a00, 0x000000000bb60000)

No shared spaces configured.

一、日誌的開頭GC」、「Full GC」表示此次垃圾收集的停頓類型,若是有Full,則說明本次GC中止了其餘全部工做線程這說明是調用System.gc()方法所觸發的GC。

二、GC」中接下來的「DefNew」表示GC發生的區域,這裏顯示的區域名稱與使用的GC收集器是密切相關的,例如上面樣例所使用的Serial收集器中的新生代名爲「Default New Generation」,因此顯示的是DefNew。老年代和永久代同理,名稱也是由收集器決定的。

3、後面方括號內部的310K->194K(2368K)」,指的是該區域已使用的容量->GC後該內存區域已使用的容量(該內存區總容量)方括號外面的310K->194K(7680K)」」則指的是GCJava堆已使用的容量->GCJava堆已使用的容量(Java堆總容量)

4、再日後「0.0269163 secs」表示該內存區域GC所佔用的時間,單位是秒。最後的「[Times:user=0.00 sys=0.00 real=0.03 secs]」則更具體了,user表示用戶態消耗的CPU時間、內核態消耗的CPU時間、操做從開始到結束通過的牆鍾時間。後面兩個的區別是,牆鍾時間包括各類非運算的等待消耗,好比等待磁盤I/O、等待線程阻塞,而CPU時間不包括這些耗時,但當系統有多CPU或者多核的話,多線程操做會疊加這些CPU時間,因此若是看到user或sys時間超過real時間是徹底正常的。

五、Heap」後面就列舉出堆內存目前各個年代的區域的內存狀況。

 

 

 

第八部分:JVM調優-eclipse開始

 

eclipse 默認配置:eclipse.ini

在配置的末尾處添加以下配置文件:

-XX:+PrintGCDetails // 輸出GC的詳細日誌

-XX:+PrintGCDateStamps // 輸出GC的時間戳(以日期的形式)

-Xloggc:gc.log // 輸出GC的詳細日誌

 

至此優化結束,附最終的eclipse.ini文件:

-startup

plugins/org.eclipse.equinox.launcher_1.3.100.v20150511-1540.jar

--launcher.library

plugins/org.eclipse.equinox.launcher.win32.win32.x86_1.1.300.v20150602-1417

-clean

-product

org.eclipse.epp.package.jee.product

--launcher.defaultAction

openFile

--launcher.XXMaxPermSize

256M

-showsplash

org.eclipse.platform

--launcher.XXMaxPermSize

256m

--launcher.defaultAction

openFile

--launcher.appendVmargs

-vmargs

-Dosgi.requiredJavaVersion=1.7

-Xms1024m

-Xmx1024m

-Xverify:none

-XX:+PrintGCDetails

-XX:+PrintGCDateStamps

-Xloggc:gc.log

 

 

 

 

第九部分:內存溢出與內存泄漏

 

1、基本概念

內存溢出:簡單地說內存溢出就是指程序運行過程當中申請的內存大於系統可以提供的內存,致使沒法申請到足夠的內存,因而就發生了內存溢出。out of memory

內存泄漏:內存泄漏指程序運行過程當中分配內存給臨時變量,用完以後卻沒有被GC回收,始終佔用着內存,既不能被使用也不能分配給其餘程序,因而就發生了內存泄漏 memory leak

memory leak會最終會致使out of memory!

 

2、內存溢出的常見狀況(內存區域的劃分中只有程序計數器不會發生泄漏或者溢出)

1java.lang.OutOfMemoryError: PermGen space (持久帶溢出) 「方法區

jvm經過持久帶實現了java虛擬機規範中的方法區,而運行時常量池就是保存在方法區中的,所以發生這種溢出多是運行時常量池溢出或是因爲程序中使用了大量的jar或class,使得方法區中保存的class對象沒有被及時回收或者class信息佔用的內存超過了配置的大小。

2java.lang.OutOfMemoryError: Java heap space (堆溢出) 

發生這種溢出的緣由通常是建立的對象太多,在進行垃圾回收以前對象數量達到了最大堆的容量限制。

內存泄漏,可進一步經過工具查看泄漏對象到GC Roots的引用鏈,定位出泄漏代碼的位置,修改程序或算法

內存溢出 ,就是說內存中的對象確實都還必須存活,那就應該檢查虛擬機的堆參數-Xmx(最大堆大小)-Xms(初始堆大小),與機器物理內存對比看是否能夠調大。

3、虛擬機棧和本地方法棧溢出

若是線程請求的棧深度大於虛擬機所容許的最大深度,將拋出StackOverflowError。

若是虛擬機在擴展棧時沒法申請到足夠的內存空間,則拋出OutOfMemoryError。

 

3、內存泄漏的常見狀況

內存泄漏的根本緣由是長生命週期的對象持有短生命週期對象的引用,儘管短生命週期的對象已經再也不須要,但因爲長生命週期對象持有它的引用而致使不能被回收

內存泄漏能夠分爲4類:常發性內存泄漏/偶發性內存泄漏/一次性內存泄漏/隱式內存泄漏

從用戶使用程序的角度來看,內存泄漏自己不會產生什麼危害,做爲通常的用戶,根本感受不到內存泄漏的存在。真正有危害的是內存泄漏的堆積,這會最終消耗盡系統全部的內存。從這個角度來講,一次性內存泄漏並無什麼危害,由於它不會堆積,而隱式內存泄漏危害性則很是大,由於較之於常發性和偶發性內存泄漏它更難被檢測到。

下面總結幾種常見的內存泄漏:

1靜態集合類引發的內存泄漏

HashMapVector等的使用最容易出現內存泄露,這些靜態變量的生命週期和應用程序一致,他們所引用的全部的對象Object也不能被釋放,從而形成內存泄漏,由於他們也將一直被Vector等引用着。

Vector<Object> v=new Vector<Object>(100);

for (int i = 1; i<100; i++)

{

Object o = new Object();

v.add(o);

o = null;

}

在這個例子中,循環申請Object 對象,並將所申請的對象放入一個Vector 中,若是僅僅釋放引用自己(o=null),那麼Vector 仍然引用該對象,因此這個對象對GC 來講是不可回收的。所以,若是對象加入到Vector 後,還必須從Vector 中刪除,最簡單的方法就是將Vector對象設置爲null

2、修改HashSet中對象的參數值,且參數是計算哈希值的字段

當一個對象被存儲到HashSet集合中之後,修改了這個對象中那些參與計算哈希值的字段後,這個對象的哈希值與最初存儲在集合中的就不一樣了,這種狀況下,用contains方法在集合中檢索對象是找不到的,這將會致使沒法從HashSet中刪除當前對象,形成內存泄漏。 

三、監聽器

一般一個應用當中會用到不少監聽器,咱們會調用一個控件的諸如addXXXListener()等方法來增長監聽器,但每每在釋放對象的時候卻沒有記住去刪除這些監聽器,從而增長了內存泄漏的機會。

4各類鏈接

好比數據庫鏈接,網絡鏈接和io鏈接,除非其顯式的調用了其close() 方法將其鏈接關閉,不然是不會自動被GC 回收的。對於Resultset 和Statement 對象能夠不進行顯式回收,但Connection 必定要顯式回收,由於Connection 在任什麼時候候都沒法自動回收,而Connection一旦回收,Resultset 和Statement 對象就會當即爲NULL。

可是若是使用鏈接池,狀況就不同了,除了要顯式地關閉鏈接,還必須顯式地關閉Resultset Statement 對象(關閉其中一個,另一個也會關閉),不然就會形成大量的Statement 對象沒法釋放,從而引發內存泄漏。這種狀況下通常都會在try裏面去鏈接,在finally裏面釋放鏈接。

5單例模式

若是單例對象持有外部對象的引用,那麼這個外部對象將不能被jvm正常回收,致使內存泄露。不正確使用單例模式是引發內存泄露的一個常見問題,單例對象在被初始化後將在JVM的整個生命週期中存在(以靜態變量的方式),若是單例對象持有外部對象的引用,那麼這個外部對象將不能被jvm正常回收,致使內存泄露。

避免內存泄漏的幾點建議:

一、儘早釋放無用對象的引用。

二、避免在循環中建立對象。

三、使用字符串處理時避免使用String,應使用StringBuffer。

四、儘可能少使用靜態變量,由於靜態變量存放在永久代,基本不參與垃圾回收。

 

 

 

第十部分:內存溢出及解決方案   OutOfMemoryError

 

1.JVM Heap(堆)溢出:java.lang.OutOfMemoryError: Java heap space        

JVM在啓動的時候會自動設置JVM Heap的值, 能夠利用JVM提供的-Xmn -Xms -Xmx等選項可進行設置,決方法:手動設置JVM Heap(堆)的大小。eclipse中tomcat中 VM arguments :   -Xmx512m  -Xms512M

 

2.PermGen space溢出: java.lang.OutOfMemoryError: PermGen space         方法區

PermGen space是指內存的永久保存區域。因爲這塊內存主要是被JVM存放Class和Meta信息的,Class在被Load的時候被放入PermGen space區域,它和存放Instance的Heap區域不一樣,sun的 GC不會在主程序運行期對PermGen space進行清理,因此若是你的APP會載入不少CLASS的話,就極可能出現PermGenspace溢出。通常發生在程序的啓動階段。

解決方法: 經過-XX:PermSize-XX:MaxPermSize設置永久代大小便可 -XX:PermSize=128M -XX:MaxPermSize=512m

 

3.棧溢出: java.lang.StackOverflowError : Thread Stack space              

棧溢出了,JVM依然是採用棧式的虛擬機。函數的調用過程都體如今堆棧和退棧上了。調用構造函數的 「層」太多了,以至於把棧區溢出了。

解決方法:1:修改程序。2:經過 -Xss: 來設置每一個線程的Stack大小便可。在Java虛擬機規範中,對這個區域規定了兩種異常情況:StackOverflowError和OutOfMemoryError異常。

1StackOverflowError異常

每當java程序代碼啓動一個新線程時,Java虛擬機都會爲它分配一個Java棧。Java棧以幀爲單位保存線程的運行狀態。當線程調用java方法時,虛擬機壓入一個新的棧幀到該線程的java棧中。只要這個方法尚未返回,它就一直存在。若是線程的方法嵌套調用層次太多(如遞歸調用),隨着java棧中幀的逐漸增多,最終會因爲該線程java棧中全部棧幀大小總和大於-Xss設置的值,而產生StackOverflowError內存溢出異常

2OutOfMemoryError異常

java程序代碼啓動一個新線程時,沒有足夠的內存空間爲該線程分配java(一個線程java棧的大小由-Xss參數肯定),jvm則拋出OutOfMemoryError異常。

 

4. Server容器啓動的時候咱們常常關心和設置JVM的幾個參數以下:

-Xmsjava Heap初始大小, 默認是物理內存的1/64

-Xmxjava Heap最大值,不可超過物理內存。

-Xmn:young generation的heap大小,通常設置爲Xmx的三、4分之一 。增大年輕代後,將會減少

年老代大小,能夠根據監控合理設置。

-Xss:每一個線程的Stack大小,而最佳值應該是128K,默認值好像是512k

-XX:PermSize:設定內存的永久保存區初始大小,缺省值爲64M

-XX:MaxPermSize:設定內存的永久保存區最大大小,缺省值爲64M

-XX:SurvivorRatio:Eden區與Survivor區的大小比值,設置爲8,則兩個Survivor區與一個Eden區

的比值爲2:8,一個Survivor區佔整個年輕代的1/10。

-XX:+UseParallelGC:F年輕代使用併發收集,而年老代仍舊使用串行收集。

-XX:+UseParNewGC:設置年輕代爲並行收集,JDK5.0以上,JVM會根據系統配置自行設置,所無需再設置此值。

-XX:ParallelGCThreads:並行收集器的線程數,值最好配置與處理器數目相等 一樣適用於CMS。

-XX:+UseParallelOldGC:年老代垃圾收集方式爲並行收集(Parallel Compacting)。

-XX:MaxGCPauseMillis:每次年輕代垃圾回收的最長時間(最大暫停時間),若是沒法知足此時間,

JVM會自動調全年輕代大小,以知足此值。

-XX:+ScavengeBeforeFullGC:Full GC前調用YGC,默認是true。

 

 

第十一部分 :線上環境上查看JVM的參數並進行調優

1、什麼是類的加載

類的加載指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,而後在堆區建立一個java.lang.Class對象,用來封裝類在方法區內的數據結構。類的加載的最終產品是位於堆區中的Class對象,Class對象封裝了類在方法區內的數據結構,而且向Java程序員提供了訪問方法區內的數據結構的接口。

類加載器並不須要等到某個類被「首次主動使用」時再加載它,JVM規範容許類加載器在預料某個類將要被使用時就預先加載它,若是在預先加載的過程當中遇到了.class文件缺失或存在錯誤,類加載器必須在程序首次主動使用該類時才報告錯誤(LinkageError錯誤)若是這個類一直沒有被程序主動使用,那麼類加載器就不會報告錯誤。

 

2、類的生命週期

類加載的過程包括了加載、驗證、準備、解析、初始化五個階段。在這五個階段中,加載、驗證、準備和初始化這四個階段發生的順序是肯定的(按順序開始,而不是按順序進行或完成),而解析階段則不必定,它在某些狀況下能夠在初始化階段以後開始,這是爲了支持Java語言的運行時綁定。

1、加載:查找並加載類的二進制數據

加載是類加載過程的第一個階段,在加載階段,虛擬機須要完成如下三件事情:

(1)經過一個類的全限定名來獲取其定義的二進制字節流。

(2)將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。

(3)在Java堆中生成一個表明這個類的java.lang.Class對象,做爲對方法區中這些數據的訪問入口。

加載階段完成後,虛擬機外部的 二進制字節流就按照虛擬機所需的格式存儲在方法區之中,並且在Java堆中也建立一個java.lang.Class類的對象,這樣即可以經過該對象訪問方法區中的這些數據。

 

二、 鏈接包括驗證/準備/解析

1)驗證:確保被加載的類的正確性

驗證是鏈接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。驗證階段大體會完成4個階段的檢驗動做:

文件格式驗證:驗證字節流是否符合Class文件格式的規範;例如:是否以0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理範圍以內、常量池中的常量是否有不被支持的類型。

元數據驗證對字節碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object以外。

字節碼驗證經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的。符號引用驗證:確保解析動做能正確執行。

驗證階段是很是重要的,但不是必須的,它對程序運行期沒有影響,若是所引用的類通過反覆驗證,那麼能夠考慮採用-Xverifynone參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。

2)準備:爲類的靜態變量分配內存,並將其初始化爲默認值。

準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。對於該

階段有如下幾點須要注意:

① 這時候進行內存分配的僅包括類變量(static,而不包括實例變量,實例變量會在對象實例化時隨着對象一塊分配在Java堆中。

② 這裏所設置的初始值一般狀況下是數據類型默認的零值(如00Lnullfalse等),而不是被在Java代碼中被顯式地賦予的值,把value賦值爲3的動做將在初始化階段纔會執行

這裏還須要注意以下幾點:

基本數據類型來講,對於類變量(static)和全局變量,若是不顯式地對其賦值而直接使用,則系統會爲其賦予默認的零值,而對於局部變量來講,在使用前必須顯式地爲其賦值,不然編譯時不經過。

對於同時被static和final修飾的常量,必須在聲明的時候就爲其顯式地賦值,不然編譯時不經過;而只被final修飾的常量則既能夠在聲明時顯式地爲其賦值,也能夠在類初始化時顯式地爲其賦值,總之,在使用前必須爲其顯式地賦值,系統不會爲其賦予默認零值。

對於引用數據類型reference來講,如數組引用、對象引用等,若是沒有對其進行顯式地賦值而直接使用,系統都會爲其賦予默認的零值,即null。若是在數組初始化時沒有對數組中的各元素賦值,那麼其中的元素將根據對應的數據類型而被賦予默認的零值。

③ 若是類字段的字段屬性表中存在ConstantValue屬性,即同時被finalstatic修飾,那麼在準備階段變量value就會被初始化爲ConstValue屬性所指定的值

假設上面的類變量value被定義爲: public static final int value = 3;編譯時Javac將會爲value生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue的設置將value賦值爲3。咱們能夠理解爲static final常量在編譯期就將其結果放入了調用它的類的常量池中。

3)解析:把類中的符號引用轉換爲直接引用

解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程,解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。符號引用就是一組符號來描述目標,能夠是任何字面量。直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。

 

3、初始化

初始化,爲類的靜態變量賦予正確的初始值,JVM負責對類進行初始化,主要對類變量進行初始化。

在Java中對類變量進行初始值設定有兩種方式:

② 明類變量時指定初始值

②使用靜態代碼塊爲類變量指定初始值

類初始化時機:只有當對類主動使用的時候纔會致使類的初始化,類的主動使用包括如下四種:

– 使用new關鍵字實例化對象、讀取或者設置一個類的靜態字段(被final修飾的靜態字段除外)、調用一個類的靜態方法的時候。

– 使用java.lang.reflect包中的方法對類進行反射調用的時候。

– 初始化一個類,發現其父類尚未初始化過的時候。

– 虛擬機啓動的時候,虛擬機會先初始化用戶指定的包含main()方法的那個類。以上四種狀況稱爲主動使用,其餘的狀況均稱爲被動使用,被動使用不會致使初始化。

 

4、結束生命週期

在以下幾種狀況下,Java虛擬機將結束生命週期

– 執行了System.exit()方法

– 程序正常執行結束

– 程序在執行過程當中遇到了異常或錯誤而異常終止

– 因爲操做系統出現錯誤而致使Java虛擬機進程終止

 

4、類的加載

類加載有三種方式:

一、命令行啓動應用時候由JVM初始化加載

二、經過Class.forName()方法動態加載

三、經過ClassLoader.loadClass()方法動態加載

 

5、雙親委派模型

雙親委派模型的工做流程是:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把請求委託給父加載器去完成,依次向上,所以,全部的類加載請求最終都應該被傳遞到頂層的啓動類加載器中,只有當父加載器在它的搜索範圍中沒有找到所需的類時,即沒法完成該加載,子加載器纔會嘗試本身去加載該類。

一、當AppClassLoader加載一個class時,它首先不會本身去嘗試加載這個類,而是把類加載請求委派給父類加載器ExtClassLoader去完成。

二、當ExtClassLoader加載一個class時,它首先也不會本身去嘗試加載這個類,而是把類加載請求委派給BootStrapClassLoader去完成。

三、若是BootStrapClassLoader加載失敗(例如在$JAVA_HOME/jre/lib裏未查找到該class),會使用ExtClassLoader來嘗試加載;

四、ExtClassLoader也加載失敗,則會使用AppClassLoader來加載,若是AppClassLoader也加載失敗,則會報出異常ClassNotFoundException

雙親委派模型意義:-系統類防止內存中出現多份一樣的字節碼     -保證Java程序安全穩定運行

 

 

 

 

 

常見知識問答

jvm內存機制

JAVA虛擬機運行時的區域區域劃分爲(線程獨有的內存區域)堆,方法區和(線程間共享的內存區域)程序計數器,java棧和本地服務棧。

 

介紹下垃圾收集機制,垃圾收集有哪些算法,各自的特色

垃圾收集算法包括:

標記-清楚算法,首先標記出全部須要回收的對象,標記完成後統一回收全部被標記的對象。從效率的角度講,標記和清除兩個過程的效率都不高;從空間的角度講,標記清除後會產生大量不連續的內存碎片,須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發一次垃圾收集動做。

複製法,它將可用的內存分爲兩塊,每次只用其中一塊,當這一塊內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已經使用過的內存空間一次性清理掉。所以新生代的內存被劃分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。

標記-整理算法,過程與標記-清除算法同樣,不過不是直接對可回收對象進行清理,而是讓全部存活對象都向一端移動,而後直接清理掉邊界之外的內存。

分代收集算法:結合以上算法,大批對象死去、少許對象存活的(新生代),使用複製算法,複製成本低;對象存活率高、沒有額外空間進行分配擔保的(老年代),採用標記-清理算法或者標記-整理算法。

 

 

3,聊聊GC,談談Major GC ,FullGc的區別,垃圾收集器有哪些,他們的區別?

Minor GC:指發生在新生代的垃圾收集動做,很是頻繁,速度較快。

Major GC:指發生在老年代的GC,出現Major GC,常常會伴隨一次Minor GC,同時Minor GC也會引發Major GC,通常在GC日誌中統稱爲GC,不頻繁。

Full GC:指發生在老年代和新生代的GC,速度很慢,須要Stop The World。

垃圾收集器包括:

Serial收集器是採用複製算法的單線程的收集器,須要STW(Stop The World),停頓時間長; 簡單高效,對於單個CPU環境而言。

ParNew收集器其實就是Serial收集器的多線程版本;

Serial Old收集器是Serial收集器的老年代版本,一樣是一個單線程收集器,使用「標記-整理算法」;

 

Parallel Scavenge收集器 是一個新生代收集器,也是用複製算法的收集器,也是並行的多線程收集器,Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量  

Parallel Old收集器Parallel Scavenge收集器的老年代版本,使用多線程和「標記-整理」算法。在注重吞吐量以及CPU資源敏感的場合,均可以優先考慮Parallel Scavenge收集器+Parallel Old收集器的組合

 

CMS收集器是以獲取最短回收停頓時間爲目標的收集器。使用標記- 清除算法

G1,使用G1收集器時,將整個Java堆劃分爲多個大小相等的獨立區域,雖然還保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔離的了,它們都是一部分的集合。

 

4,OutOfMemoryError這個錯誤你遇到過嗎?你是怎麼解決處理的?

遇到過

若是是JVM Heap(堆)溢出:JVM在啓動的時候會自動設置JVM Heap的值, 能夠利用JVM提供的-Xmn -Xms -Xmx等選項可進行設置;若是是PermGen space(方法區)溢出:經過-XX:PermSize和-XX:MaxPermSize設置永久代大小便可;若是是棧溢出 1:修改程序。2:經過 -Xss: 來設置每一個線程的Stack大小便可。

 

5,JVM調優有哪些參數,介紹下,線上環境上,你是怎麼查看JVM的參數並進行調優的?

-Xmsjava Heap初始大小, 默認是物理內存的1/64

-Xmxjava Heap最大值,不可超過物理內存。

-Xmn:young generation的heap大小,通常設置爲Xmx的三、4分之一 。增大年輕代後,將會減少

年老代大小,能夠根據監控合理設置。

-Xss:每一個線程的Stack大小,而最佳值應該是128K,默認值好像是512k

-XX:PermSize:設定內存的永久保存區初始大小,缺省值爲64M

-XX:MaxPermSize:設定內存的永久保存區最大大小,缺省值爲64M

 

6,能不能本身寫一個類叫java.lang.String(類加載的過程,雙親委派模型)

雙親委派模型的工做流程是:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把請求委託給父加載器去完成,依次向上,所以,全部的類加載請求最終都應該被傳遞到頂層的啓動類加載器中,只有當父加載器在它的搜索範圍中沒有找到所需的類時,即沒法完成該加載,子加載器纔會嘗試本身去加載該類。

相關文章
相關標籤/搜索