1. 程式人生 > >jvm的學習

jvm的學習

一、jvm的位置

JVM是執行在作業系統之上的,它與硬體沒有直接的互動

二、jvmt體系結構

①程式計數器

(1)程式計數器是一塊較小的記憶體空間,是當前執行緒所執行的位元組碼的行號指示器,位元組碼直譯器工作時是通過改變這個計數器的值來選取下一條需要執行的位元組碼(包括分支、迴圈、跳轉、異常處理、執行緒恢復)

(2)java虛擬機器多執行緒是通過執行緒輪流切換分配處理器執行時間,為了多執行緒切換後能恢復到正確的額執行位置,每條執行緒都需要有一個獨立的程式計數器,各條執行緒之間計數器互不影響,獨立儲存,我們稱這類記憶體區域為“執行緒私有”的記憶體。

(3)如果執行緒正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;如果正在執行的是Native方法,這個計數器值則為空(Undefined)

解釋:JVM執行native方法,計數器為空(undefined),又怎麼繼續執行Java程式碼的問題?

問題:我們知道,程式計數器用來存放位元組碼指令地址;通過這個地址,虛擬機器就能知道執行到哪裡,以及怎麼往下執行,可呼叫native方法,值就變成空了,那麼機器不就直接崩潰了嗎?

解釋:參考C++理解是:當執行緒中呼叫native方法的時候,則重新啟動一個新的執行緒,那麼新的執行緒的計數器為空則不會影響當前執行緒的計數器,相互獨立。

問題:如果是新啟動的一個執行緒,那麼不會因為執行緒非同步問題,無法控制執行順序嗎?

解釋:當前執行緒應當會被阻塞,知道另外一個執行緒執行結束。例如:通過死迴圈來控制阻塞(當然死迴圈效率太低,這裡只是一個例子)

(4)此記憶體區域是唯一一個在Java虛擬機器規範中沒有規定任何OutOfMemoryError情況的區域

②java vm棧(java虛擬機器棧)

(1)棧的概念

“棧”-也是執行緒私有的一塊記憶體,生命週期和執行緒一樣。每個方法在執行的同時都會建立一個棧幀(StackFrame[1])用於儲存區域性變量表、運算元棧、動態連結、方法出口等資訊。每一個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程

區域性變量表,存放了可知的基本資料型別(boolean、byte、char、short、int、float、long、double),物件引用和returnAddress(指向了一條位元組碼指令的地址),區域性變量表所需的記憶體空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的區域性變數空間是完全確定的,在方法執行期間不會改變區域性變量表的大小

(2)棧的兩種異常

1、StackOverflowError

如果執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲StackOverflowError異常

解釋:每次方法呼叫都會有一個棧幀壓入虛擬機器棧,作業系統給JVM分配的記憶體是有限的,JVM分配給“虛擬機器棧”的記憶體是有限的。如果方法呼叫過多,導致虛擬機器棧滿了就會溢位。這裡棧深度就是指棧幀的數量。

棧溢位的一個例項

迴圈遞迴呼叫:(原理)每次呼叫一次方法就會建立一個棧幀壓入棧,達到棧的深度就會報棧異常

2、OutOfMemoryError

如果虛擬機器棧可以動態擴充套件(當前大部分的Java虛擬機器都可動態擴充套件,只不過Java虛擬機器規範中也允許固定長度的虛擬機器棧),如果擴充套件時無法申請到足夠的記憶體,就會丟擲OutOfMemoryError異常

③heap堆(Java7之前)

一個JVM例項只存在一個堆記憶體,被所有執行緒共享,堆記憶體的大小是可以調節的。java堆是垃圾回收器管理的主要區域,因此很多時候也被稱作“GC堆”。類載入器讀取了類檔案後,需要把類、方法、常變數放到堆記憶體中,儲存所有引用型別的真實資訊,以方便執行器執行。

堆記憶體邏輯上分為三部分:新生+養老+永久

(1)新生區

新生區是類的誕生、成長、消亡的區域,一個類在這裡產生,應用,最後被垃圾回收器收集,結束生命。新生區又分為兩部分:伊甸區(Edenspace)和倖存者區(Survivorpace),所有的類都是在伊甸區被new出來的。倖存區有兩個:0區(Survivor0space)和1區(Survivor1space)。當伊甸園的空間用完時,程式又需要建立物件,JVM的垃圾回收器將對伊甸園區進行垃圾回收(MinorGC),將伊甸園區中的不再被其他物件所引用的物件進行銷燬。然後將伊甸園中的剩餘物件移動到倖存0區.若倖存0區也滿了,再對該區進行垃圾回收,然後移動到1區。那如果1區也滿了呢?再移動到養老區。若養老區也滿了,那麼這個時候將產生MajorGC(FullGC),進行養老區的記憶體清理。若養老區執行了FullGC之後發現依然無法進行物件的儲存,就會產生OOM異常“OutOfMemoryError”。

如果出現java.lang.OutOfMemoryError:Javaheapspace異常,說明Java虛擬機器的堆記憶體不夠。原因有二:

(1)Java虛擬機器的堆記憶體設定不夠,可以通過引數-Xms、-Xmx來調整。

(2)程式碼中建立了大量大物件,並且長時間不能被垃圾收集器收集(存在被引用)。

 

(2)Java7的堆構成

(3)Java8的堆構成

JDK1.8之後將最初的永久代取消了,由元空間取代

(4)直接記憶體

直接記憶體(DirectMemory)並不是虛擬機器執行時資料區的一部分,也不是Java虛擬機器規範中定義的記憶體區域。但是這部分記憶體也被頻繁地使用,而且也可能導致OutOfMemoryError異常出現,所以我們放到這裡一起講解。在JDK1.4中新加入了NIO(NewInput/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,它可以使用Native函式庫直接分配堆外記憶體,然後通過一個儲存在Java堆中的DirectByteBuffer物件作為這塊記憶體的引用進行操作。這樣能在一些場景中顯著提高效能,因為避免了在Java堆和Native堆中來回複製資料。顯然,本機直接記憶體的分配不會受到Java堆大小的限制,但是,既然是記憶體,肯定還是會受到本機總記憶體(包括RAM以及SWAP區或者分頁檔案)大小以及處理器定址空間的限制。伺服器管理員在配置虛擬機器引數時,會根據實際記憶體設定-Xmx等引數資訊,但經常忽略直接記憶體,使得各個記憶體區域總和大於實體記憶體限制(包括物理的和作業系統級的限制),從而導致動態擴充套件時出現OutOfMemoryError異常。

④方法區

1:方法區是執行緒共享的,通常用來儲存裝載的類的元結構資訊。比如:執行時常量池+靜態變數+常量+類資訊+即時編譯器編譯後的程式碼等資料。

2:通常和永久區關聯在一起(Java7之前),但具體的跟JVM的實現和版本有關

詳細介紹:https://blog.csdn.net/SunshineLPL/article/details/78318709?locationNum=9&fps=1

3、方法區的常量池

Java中的常量池(字串常量池、class常量池和執行時常量池)

參考:https://blog.csdn.net/zm13007310400/article/details/77534349

⑤、類裝載器ClassLoader

(1)類裝載器ClassLoader

負責載入class檔案,class檔案在檔案開頭有特定的檔案標示,並且ClassLoader只負責class檔案的加

載,至於它是否可以執行,則由ExecutionEngine決定

 

(2)類裝載器ClassLoader2

•虛擬機器自帶的載入器

•啟動類載入器(Bootstrap)C++

•擴充套件類載入器(Extesion)Java

•應用程式類載入器(App)Java

也叫系統類載入器,載入當前應用的classpath的所有類

•使用者自定義載入器Java.lang.ClassLoader的子類,使用者可以定製類的載入方式

(3)類裝載器ClassLoader3

(4)Code案例

啟動類載入器:擴充套件類載入器:應用類載入器:

 

執行結果:

特別說明:擴充套件類載入器

把自己做的類打成jar包,放入到jre的目錄

 

擴充套件類資料夾下,java虛擬機器啟動的時候就會把擴充套件類加入的類載入機中,直接呼叫即可

•sun.misc.Launcher

它是一個java虛擬機器的入口應用

某個特定的類載入器在接到載入類的請求時,首先將載入任務委託給父類載入器,依次遞迴,如果父

類載入器可以完成類載入任務,就成功返回;只有父類載入器無法完成此載入任務時,才自己去加

載。

重要概念:雙親委託機制+沙箱機制(防止惡意程式碼對java的破壞)

解釋:雙親委託,載入類的時候先通過啟動類載入器和擴充套件類載入器載入,若果以上兩者雙親都沒有載入成功,再使用系統載入器,沙箱意思是啟動類載入器和擴充套件類載入器中已經定義的方法不會在系統載入器中進行使用者定義的同名方法。

⑥NativeInterface本地介面

Java語言本身不能對作業系統底層進行訪問和操作,但是可以通過JNI介面呼叫其他語言來實現對底層的訪問。

本地介面的作用是融合不同的程式語言為Java所用,它的初衷是融合C/C++程式,Java誕生的時候是C/C++橫行的時候,要想立足,必須有呼叫C/C++程式,於是就在記憶體中專門開闢了一塊區域處理標記為Native的程式碼,

它的具體做法是NativeMethodStack中登記Native方法,在ExecutionEngine執行時載入Nativelibraries。

目前該方法使用的越來越少了,除非是與硬體有關的應用,比如通過Java程式驅動印表機或者Java系統管理生產裝置,在企業級應用中已經比較少見。因為現在的異構領域間的通訊很發達,比如可以使用Socket通訊,也可以使用WebService等等,不多做介紹。

(1)NativeMethodStack

它的具體做法是NativeMethodStack中登記native方法,在ExecutionEngine執行時載入本地方法庫。

⑦pc暫存器

每個執行緒都有一個程式計數器,是執行緒私有的,就是一個指標,指向方法區中的方法位元組碼(用來儲存指向下一條指令的地址,也即將要執行的指令程式碼),由執行引擎讀取下一條指令,是一個非常小的記憶體空間,幾乎可以忽略不記。

通俗解釋:就是這份方法執行完了,要呼叫下一個方法,技術應該去找誰。

三、堆的調優

 

①堆記憶體調優簡介01

 

publicstaticvoidmain(String[]args){

longmaxMemory=Runtime.getRuntime().maxMemory();//返回Java虛擬機器試圖使用的最大記憶體量。

longtotalMemory=Runtime.getRuntime().totalMemory();//返回Java虛擬機器中的記憶體總量。

System.out.println("MAX_MEMORY="+maxMemory+"(位元組)、"+(maxMemory/(double)1024/1024)+"MB");

System.out.println("TOTAL_MEMORY="+totalMemory+"(位元組)、"+(totalMemory/(double)1024/1024)+"MB");

}

②模擬堆記憶體滿的情況

Stringstr="www.atguigu.com";

while(true)

{

str+=str+newRandom().nextInt(88888888)+newRandom().nextInt(999999999);

}

VM引數:-Xms8m-Xmx8m-XX:+PrintGCDetails

 

  1. 1、GC日誌列印資訊:

    -XX:+PrintGCTimeStamps輸出格式:

    289.556:[GC[PSYoungGen:314113K->15937K(300928K)]405513K->107901K(407680K),0.0178568secs][Times:user=0.06sys=0.00,real=0.01secs]

    293.271:[GC[PSYoungGen:300865K->6577K(310720K)]392829K->108873K(417472K),0.0176464secs][Times:user=0.06sys=0.00,real=0.01secs]

    詳解:

    293.271是從jvm啟動直到垃圾收集發生所經歷的時間,GC表示這是一次MinorGC(新生代垃圾收集);[PSYoungGen:300865K->6577K(310720K)]提供了新生代空間的資訊,PSYoungGen,表示新生代使用的是多執行緒垃圾收集器ParallelScavenge。300865K表示垃圾收集之前新生代佔用空間,6577K表示垃圾收集之後新生代的空間。新生代又細分為一個Eden區和兩個Survivor區,MinorGC之後Eden區為空,6577K就是Survivor佔用的空間。

    括號裡的310720K表示整個年輕代的大小。

    392829K->108873K(417472K),表示垃圾收集之前(392829K)與之後(108873K)Java堆的大小(總堆417472K,堆大小包括新生代和年老代)

    由新生代和Java堆佔用大小可以算出年老代佔用空間,如,Java堆大小417472K,新生代大小310720K那麼年老代佔用空間是417472K-310720K=106752k;垃圾收集之前老年代佔用的空間為392829K-300865K=91964k垃圾收集之後老年代佔用空間108873K-6577K=102296k.

    0.0176464secs表示垃圾收集過程所消耗的時間。

    [Times:user=0.06sys=0.00,real=0.01secs]提供cpu使用及時間消耗,user是使用者模式垃圾收集消耗的cpu時間,例項中垃圾收集器消耗了0.06秒使用者態cpu時間,sys是消耗系統態cpu時間,real是指垃圾收集器消耗的實際時間。

  2. 2、-XX:+PrintGCDetails輸出格式:

    293.289:[FullGC[PSYoungGen:6577K->0K(310720K)]

    [PSOldGen:102295K->102198K(134208K)]108873K->102198K(444928K)

    [PSPermGen:59082K->58479K(104192K)],0.3332354secs]

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

    說明:

    FullGC表示執行全域性垃圾回收

    [PSYoungGen:6577K->0K(310720K)]提供新生代空間資訊,解釋同上

    [PSOldGen:102295K->102198K(134208K)]提供了年老代空間資訊;

    108873K->102198K(444928K)整個堆空間資訊;

    [PSPermGen:59082K->58479K(104192K)]提供了持久代空間資訊;

  3.  


 

來源:https://jingyan.baidu.com/article/3ea51489c045d852e61bbaab.html

 

③堆記憶體分析

 

(1)安裝

 

 

(2)使用工具分析

-XX:+HeapDumpOnOutOfMemoryError

OOM時匯出堆到hprof檔案。

 

啟動引數:

-Xms1m-Xmx8m-XX:+HeapDumpOnOutOfMemoryError

 

-XX:+HeapDumpOnOutOfMemoryError

OOM時匯出堆到hprof檔案。

四、調優實戰

除了程式計數器外其它執行時區域都有發生OutOfMemoryError異常的可能,因為程式計數器不受虛擬機器記憶體管理。

①java堆的溢位

配置:-Xms20m-Xmx20m-XX:+HeapDumpOnOutOfMemoryError

出現辦法:大量的建立物件

②虛擬機器棧和本地方法棧溢位

配置:-Xss128k

出現辦法:在單執行緒下,無論是由於棧幀太大還是虛擬機器棧容量太小,當記憶體無法分配的時候,虛擬機器丟擲的都是StackOverflowError異常。每個執行緒分配到的棧容量越大,可以建立的執行緒數量自然就越少,建立執行緒時就越容易把剩下的記憶體耗盡。

③方法區和執行時常量池溢位

配置:-XX:PermSize=10M-XX:MaxPermSize=10M

JDK7將String常量池從Perm區移動到了JavaHeap區。在JDK1.6中,intern方法會把首次遇到的字串例項複製到永久代中,返回的也是永久代中的例項。但是在JDK1.7以後,String.intern()方法不會在複製例項,只是在常量池中記錄首次出現的例項引用。下面來看一些具體例子。

案例一:

 
  1. Stringstr1=newString("計算機")+newString("軟體");

  2. System.out.println(str1.intern()==str1);

  3.  

  4. Stringstr2=newString("ja")+newString("va");

  5. System.out.println(str2.intern()==str2);

輸出結果:

JDK1.6falsefalse
JDK1.7truefalse

分析:在JDK1.6中,intern方法會把首次遇到的字串例項複製到永久代中,返回的也是永久代中的例項,而由newString()建立的字串例項在Java堆中所以不是同一個引用,都返回false。但是在JDK1.7以後,String.intern()方法不會在複製例項,只是在常量池中記錄首次出現的例項引用,因此str1.intern()=str1,但是Java字串是Java中的關鍵字,早已建立,所以str2.intern()!=str2。當然如果用StringBuilder建立字串效果也是一樣的。

 
  1. Stringstr1=newStringBuilder("計算機").append("軟體").toString();

  2. System.out.println(str1.intern()==str1);

  3.  

  4. Stringstr2=newStringBuilder("ja").append("va").toString();

  5. System.out.println(str2.intern()==str2);

 

---------------------本文來自ZLL_CSDN2018的CSDN部落格,全文地址請點選:https://blog.csdn.net/smiling_Z/article/details/82686681?utm_source=copy

出現辦法:使用動態代理建立大量的類

④本機直接記憶體溢位

DirectMemory容量可通過-XX:MaxDirectMemorySize指定,如果不指定,則預設與java堆最大值(-Xmx指定)一樣。

配置:-Xmx20M

五、垃圾回收器與記憶體分配策略

①概述

Java記憶體執行時區域的各個部分,其中程式計數器、虛擬機器棧、本地方法棧3個區域隨執行緒而生,隨執行緒而滅;棧中的棧幀隨著方法的進入和退出而有條不紊地執行著出棧和入棧操作。每一個棧幀中分配多少記憶體基本上是在類結構確定下來時就已知的(儘管在執行期會由JIT編譯器進行一些優化,但在本章基於概念模型的討論中,大體上可以認為是編譯期可知的),因此這幾個區域的記憶體分配和回收都具備確定性,在這幾個區域內就不需要過多考慮回收的問題,因為方法結束或者執行緒結束時,記憶體自然就跟隨著回收了。而Java堆和方法區則不一樣,一個介面中的多個實現類需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體也可能不一樣,我們只有在程式處於執行期間時才能知道會建立哪些物件,這部分記憶體的分配和回收都是動態的,垃圾收集器所關注的是這部分記憶體,本章後續討論中的“記憶體”分配與回收也僅指這一部分記憶體。

②物件已死

是指不可能再被任何途徑使用的物件

(1)可達性分析演算法

在主流的商用程式語言(Java、C#,甚至包括前面提到的古老的Lisp)的主流實現中,都是稱通過可達性分析(ReachabilityAnalysis)來判定物件是否存活的。這個演算法的基本思路就是通過一系列的稱為“GCRoots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(ReferenceChain),當一個物件到GCRoots沒有任何引用鏈相連(用圖論的話來說,就是從GCRoots到這個物件不可達)時,則證明此物件是不可用的。如圖3-1所示,物件object5、object6、object7雖然互相有關聯,但是它們到GCRoots是不可達的,所以它們將會被判定為是可回收的物件。圖3-1可達性分析演算法判定物件是否可回收在Java語言中,為GCRoots的物件包括下面幾種:

虛擬機器棧(棧幀中的本地變量表)中引用的物件。

方法區中類靜態屬性引用的物件。方法區中常量引用的物件。

本地方法棧中JNI(即一般說的Native方法)引用的物件

 

(2)引用

Java對引用的概念進行了擴充,將引用分為強引用(StrongReference)、軟引用(SoftReference)、弱引用(WeakReference)、虛引用(PhantomReference)4種,這4種引用強度依次逐漸減弱。

強引用就是指在程式程式碼之中普遍存在的,類似“Objectobj=newObject()”這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的物件。
軟引用是用來描述一些還有用但並非必需的物件。對於軟引用關聯著的物件,在系統將
要發生記憶體溢位異常之前,將會把這些物件列進回收範圍之中進行第二次回收。如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常。在JDK1.2之後,提供SoftReference類來實現軟引用。
弱引用也是用來描述非必需物件的,但是它的強度比軟引用更弱一些,被弱引用關聯的
物件只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件。在JDK1.2之後,提供了WeakReference類來實現弱引用。
虛引用也稱為幽靈引用或者幻影引用,它是最弱的一種引用關係。一個物件是否有虛引
用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個物件例項。為一個物件設定虛引用關聯的唯一目的就是能在這個物件被收集器回收時收到一個系統通知。在JDK1.2之後,提供了PhantomReference類來實現虛引用

(3)物件的回收過程

即使在可達性分析演算法中不可達的物件,也並非是“非死不可”的,這時候它們暫時處
於“緩刑”階段,要真正宣告一個物件死亡,至少要經歷兩次標記過程:如果物件在進行可達性分析後發現沒有與GCRoots相連線的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此物件是否有必要執行finalize()方法。當物件沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機器呼叫過,虛擬機器將這兩種情況都視為“沒有必要執行”。如果這個物件被判定為有必要執行finalize()方法,那麼這個物件將會放置在一個叫做F-Queue的佇列之中,並在稍後由一個由虛擬機器自動建立的、低優先順序的Finalizer執行緒去執行它。這裡所謂的“執行”是指虛擬機器會觸發這個方法,但並不承諾會等待它執行結束,這樣做的原因是,如果一個物件在finalize()方法中執行緩慢,或者發生了死迴圈(更極端的情況),將很可能會導致F-Queue佇列中其他物件永久處於等待,甚至導致整個記憶體回收系統崩潰。finalize()方法是物件逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的物件進行第二次小規模的標記,如果物件要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個物件建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變數或者物件的成員變數,那在第二次標記時它將被移除出“即將回收”的集合;如果物件這時候還沒有逃脫,那基本上它就真的被回收了。從程式碼清單3-2中我們可以看到一個物件的finalize()被執行,但是它仍然可以存活。

③回收方法區

主要回收兩部分:廢棄常量和無用的類

常量池中的類(介面),方法,欄位的字面符號沒有被引用就會被回收

類回購比較苛刻:(1)該類的所有例項被回收

(2)載入該類的ClassLoader被回收

(3)該類對應的java.lang.Class物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法

④垃圾回收演算法

(1)標記清除演算法(參考上文)

不足:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後在程式執行過程中需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作

(2)複製收集演算法(針對於新生區)

為了解決效率問題,一種稱為“複製”(Copying)的收集演算法出現了,它將可用記憶體按容
量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著
的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。這樣使得每次都是
對整個半區進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要移動堆頂指
針,按順序分配記憶體即可,實現簡單,執行高效。只是這種演算法的代價是將記憶體縮小為了原
來的一半,未免太高了一點。複製演算法的執行過程如圖3-3所示。
圖3-3複製演算法示意圖

現在的商業虛擬機器都採用這種收集演算法來回收新生代,IBM公司的專門研究表明,新生代中的物件98%是“朝生夕死”的,所以並不需要按照1:1的比例來劃分記憶體空間,而是將記憶體分為一塊較大的Eden空間和兩塊較小Survivor空間,每次使用Eden和其中一塊Survivor[1]。當回收時,將Eden和Survivor中還存活著的物件一次性地複製到另外一塊Survivor空間上,最後清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機器預設EdenSurvivor的大小比例是8:1,也就是每次新生代中可用記憶體空間為整個新生代容量的90%(80%+10%),只有10%的記憶體會被“浪費”。當然,98%的物件可回收只是一般場景下的資料,我們沒有辦法保證每次回收都只有不多於10%的物件存當Surviv空間不夠用時,需要依賴其他記憶體(這裡指老年代)進行分配擔保(HandlePromotion)。記憶體的分配擔保就好比我們去銀行借款,如果我們信譽很好,在98%的情況下都能按時償還,於是銀行可能會預設我們下一次也能按時按量地償還貸款,只需要有一個擔保人能保證如果我不能還款時,可以從他的賬戶扣錢,那銀行就認為沒有風險了。記憶體的分配擔保也一樣,如果另外一塊Survivor空間沒有足夠空間存放上一次新生代收集下來的存活物件時,這些物件將直接通過分配擔保機制進入老年代。關於對新生代進行分配擔保的內容

(3)標記-整理演算法(針對於老年代)

複製收集演算法在物件存活率較高時就要進行較多的複製操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的記憶體中所有物件都100%存活的極端情況,所以在老年代一般不能直接選用這種演算法。根據老年代的特點,有人提出了另外一種“標記-整理”(Mark-Compact)演算法,標記過程仍然與“標記-清除”演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體,“標記-整理”演算法的示意圖如
圖3-4所示。

(4)對以上演算法在jvm的用法總結(分代收集演算法)

當前商業虛擬機器的垃圾收集都採用“分代收集”(GenerationalCollection)演算法,這種演算法並沒有什麼新的思想,只是根據物件存活週期的不同將記憶體劃分為幾塊。一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集演算法。在新生代中,每次垃圾收集時都發現有大批物件死去,只有少量存活,那就選用複製演算法,只需要付出少量存活物件的複製成本就可以完成收集。而老年代中因為物件存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”演算法來進行回收。

⑤hotspot的gc演算法實現

(1)JVMSafepoint安全點

http://www.bubuko.com/infodetail-2127087.html

⑥GC日誌解析

每一種收集器的日誌形式都是由它們自身的實現所決定的,換而言之,每個收集器的日誌格式都可以不一樣。但虛擬機器設計者為了方便使用者閱讀,將各個收集器的日誌都維持一定的共性,例如以下兩段典型的GC日誌:
33.125:[GC[DefNew:3324K->152K(3712K),0.0025925secs]3324K->152K(11904K),0.0031680secs]
100.667:[FullGC[Tenured:0K->210K(10240K),0.0149142secs]4603K->210K(19456K),[Perm:2999K->2999K(21248K)],0.0150007secs][Times:user=0.01sys=0.00,real=0.02secs]
最前面的數字“33.125:”和“100.667:”代表了GC發生的時間,這個數字的含義是從Java虛擬機器啟動以來經過的秒數。
GC日誌開頭的“[GC”和“[FullGC”說明了這次垃圾收集的停頓型別,而不是用來區分新生代GC還是老年代GC的。如果有“Full”,說明這次GC是發生了Stop-The-World的,例如下面這段新生代收集器ParNew的日誌也會出現“[FullGC”(這一般是因為出現了分配擔保失敗之類的問題,所以才導致STW)。如果是呼叫System.gc()方法所觸發的收集,那麼在這裡將顯示“[FullGC(System)”。
[FullGC283.736:[ParNew:261599K->261599K(261952K),0.0000288secs]
接下來的“[DefNew”、“[Tenured”、“[Perm”表示GC發生的區域,這裡顯示的區域名稱與使用的GC收集器是密切相關的,例如上面樣例所使用的Serial收集器中的新生代名為“DefaultNewGeneration”,所以顯示的是“[DefNew”。如果是ParNew收集器,新生代名稱就會變為“[ParNew”,意為“ParallelNewGeneration”。如果採用ParallelScavenge收集器,那它配套的新生代稱為“PSYoungGen”,老年代和永久代同理,名稱也是由收集器決定的。後面方括號內部的“3324K->152K(3712K)”含義是“GC前該記憶體區域已使用容量->GC後該記憶體區域已使用容量(該記憶體區域總容量)”。而在方括號之外的“3324K->152K(11904K)”表示“GC前Java堆已使用容量->GC後Java堆已使用容量(Java堆總容量)”。再往後,“0.0025925secs”表示該記憶體區域GC所佔用的時間,單位是秒。有的收集器會給出更具體的時間資料,如“[Times:user=0.01sys=0.00,real=0.02secs]”,這裡面的user、sys和real與Linux的time命令所輸出的時間含義一致,分別代表使用者態消耗的CPU時間、核心態消耗的CPU事件和操作從開始到結束所經過的牆鍾時間(WallClockTime)。CPU時間與牆鍾時間的區別是,牆鍾時間包括各種非運算的等待耗時,例如等待磁碟I/O、等待執行緒阻塞,而CPU時間不包括這些耗時,但當系統有多CPU或者多核的話,多執行緒操作會疊加這些CPU時間,所以讀者看到user或sys時間超過real時間是完全正常的。

⑦垃圾收集器引數總結

 

⑧MinorGC和FULLGC的不同

新生代GC(MinorGC):指發生在新生代的垃圾收集動作,因為Java物件大多都具備朝
生夕滅的特性,所以MinorGC非常頻繁,一般回收速度也比較快。
老年代GC(MajorGC/FullGC):指發生在老年代的GC,出現了MajorGC,經常會伴
隨至少一次的MinorGC(但非絕對的,在ParallelScavenge收集器的收集策略裡就有直接進行
MajorGC的策略選擇過程)。MajorGC的速度一般會比MinorGC慢10倍以上。

程式碼清單3-5新生代MinorGC
privatestaticfinalint_1MB=1024*1024;
/**
*VM引數:-verbose:gc-Xms20M-Xmx20M-Xmn10M-XX:+PrintGCDetails
-XX:SurvivorRatio=8
*/
publicstaticvoidtestAllocation(){
byte[]allocation1,allocation2,allocation3,allocation4;
allocation1=newbyte[2*_1MB];
allocation2=newbyte[2*_1MB];
allocation3=newbyte[2*_1MB];
allocation4=newbyte[4*_1MB];//出現一次MinorGC
}

執行結果:
[GC[DefNew:6651K->148K(9216K),0.0070106secs]6651K->6292K(19456K),
0.0070426secs][Times:user=0.00sys=0.00,real=0.00secs]
Heap
defnewgenerationtotal9216K,used4326K[0x029d0000,0x033d0000,0x033d0000)
edenspace8192K,51%used[0x029d0000,0x02de4828,0x031d0000)
fromspace1024K,14%used[0x032d0000,0x032f5370,0x033d0000)
tospace1024K,0%used[0x031d0000,0x031d0000,0x032d0000)
tenuredgenerationtotal10240K,used6144K[0x033d0000,0x03dd0000,0x03dd0000)
thespace10240K,60%used[0x033d0000,0x039d0030,0x039d0200,0x03dd0000)
compactingpermgentotal12288K,used2114K[0x03dd0000,0x049d0000,0x07dd0000)
thespace12288K,17%used[0x03dd0000,0x03fe0998,0x03fe0a00,0x049d0000)
Nosharedspacesconfigured.

⑨進入老年代的情況

(1)大物件直接進入老年代

導致問題:經常出現大物件容易
導致記憶體還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來“安置”它們虛擬機器提供了一個-XX:PretenureSizeThreshold引數,令大於這個設定值的物件直接在老年代分配。這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的記憶體複製(複習一下:新生代採用複製演算法收集記憶體)

執行程式碼清單3-6中的testPretenureSizeThreshold()方法後,我們看到Eden空間幾乎沒有被使用,而老年代的10MB空間被使用了40%,也就是4MB的allocation物件直接就分配在老年代中,這是因為PretenureSizeThreshold被設定為3MB(就是3145728,這個引數不能像-Xmx之類的引數一樣直接寫3MB),因此超過3MB的物件都會直接在老年代進行分配。注意PretenureSizeThreshold引數只對Serial和ParNew兩款收集器有效,ParallelScavenge收集器不認識這個引數,ParallelScavenge收集器一般並不需要設定。如果遇到必須使用此引數的場合,可以考慮ParNew加CMS的收集器組合。程式碼清單3-6大物件直接進入老年代
privatestaticfinalint_1MB=1024*1024;
/**
*VM引數:-verbose:gc-Xms20M-Xmx20M-Xmn10M-XX:+PrintGCDetails-XX:SurvivorRatio=8
*-XX:PretenureSizeThreshold=3145728
*/
publicstaticvoidtestPretenureSizeThreshold(){
byte[]allocation;
allocation=newbyte[4*_1MB];//直接分配在老年代中
}

執行結果:
Heap
defnewgenerationtotal9216K,used671K[0x029d0000,0x033d0000,0x033d0000)
edenspace8192K,8%used[0x029d0000,0x02a77e98,0x031d0000)
fromspace1024K,0%used[0x031d0000,0x031d0000,0x032d0000)
tospace1024K,0%used[0x032d0000,0x032d0000,0x033d0000)
tenuredgenerationtotal10240K,used4096K[0x033d0000,0x03dd0000,0x03dd0000)
thespace10240K,40%used[0x033d0000,0x037d0010,0x037d0200,0x03dd0000)
compactingpermgentotal12288K,used2107K[0x03dd0000,0x049d0000,0x07dd0000)
thespace12288K,17%used[0x03dd0000,0x03fdefd0,0x03fdf000,0x049d0000)
Nosharedspacesconfigured.

(2)長期存活的物件將進入老年代

進入老年代的物件按年齡計算,可通過設定引數-XX:MaxTenuringThreshold來設定年齡閾值

放在新生代,哪些物件應放在老年代中。為了做到這點,虛擬機器給每個物件定義了一個物件年齡(Age)計數器。如果物件在Eden出生並經過第一次MinorGC後仍然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並且物件年齡設為1。物件在Survivor區中每“熬過”一次MinorGC,年齡就增加1歲,當它的年齡增加到一定程度(預設為15歲),就將會被晉升到老年代中。物件晉升老年代的年齡閾值,可以通過引數-XX:MaxTenuringThreshold設定

(3)動態物件年齡判定

為了能更好地適應不同程式的記憶體狀況,虛擬機器並不是永遠地要求物件的年齡必須達到
了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有物件大小的總和大於Survivor空間的一半,無須等到MaxTenuringThreshold中要求的年齡。

執行程式碼清單3-8中的testTenuringThreshold2()方法,並設定-XX:
MaxTenuringThreshold=15,會發現執行結果中Survivor的空間佔用仍然為0%,而老年代比預期增加了6%,也就是說,allocation1、allocation2物件都直接進入了老年代,而沒有等到15歲的臨界年齡。因為這兩個物件加起來已經到達了512KB,並且它們是同年的,滿足同年物件達到Survivor空間的一半規則。我們只要註釋掉其中一個物件new操作,就會發現另外一個就不會晉升到老年代中去了

(4)空間分配擔保

在JDK6Update24之前的版本中執行測試

在發生MinorGC之前,虛擬機器會先檢查老年代最大可用的連續空間是否大於新生代所有
物件總空間,如果這個條件成立,那麼MinorGC可以確保是安全的。如果不成立,則虛擬機器會檢視HandlePromotionFailure設定值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代物件的平均大小,如果大於,將嘗試著進行一次MinorGC,儘管這次MinorGC是有風險的;如果小於,或者HandlePromotionFailure設定不允許冒險,那這時也要改為進行一次FullGC。

DK6Update24之後

規則變為只要老年代的連續空間大於新生代物件總大小或者歷次晉升的平均大小就會進行MinorGC,否則將進行FullGC

六、jdk命令列工具

①jps:虛擬機器程序狀況工具

JDK的很多小工具的名字都參考了UNIX命令的命名方式,jps(JVMProcessStatusTool)是其中的典型。除了名字像UNIX的ps命令之外,它的功能也和ps命令類似:可以列出正在執行的虛擬機器程序,並顯示虛擬機器執行主類(MainClass,main()函式所在的類)名稱以及這些程序的本地虛擬機器唯一ID(LocalVirtualMachineIdentifier,LVMID)。雖然功能比較單一,但它是使用頻率最高的JDK命令列工具,因為其他的JDK工具大多需要輸入它查詢到的LVMID來確定要監控的是哪一個虛擬機器程序。對於本地虛擬機器程序來說,LVMID與作業系統的程序ID(ProcessIdentifier,PID)是一致的,使用Windows的工作管理員或者UNIX的ps命令也可以查詢到虛擬機器程序的LVMID,但如果同時啟動了多個虛擬機器程序,無法根據程序名稱定位時,那就只能依賴jps命令顯示主類的功能才能區分了。

jps可以通過RMI協議查詢開啟了RMI服務的遠端虛擬機器程序狀態,hostid為RMI登錄檔中註冊的主機名。jps的其他常用選項見表4-2。

    

②jstat:虛擬機器統計資訊監視工具

jstat(JVMStatisticsMonitoringTool)是用於監視虛擬機器各種執行狀態資訊的命令列工具。它可以顯示本地或者遠端[1]虛擬機器程序中的類裝載、記憶體、垃圾集、JIT編譯等執行資料,在沒有GUI圖形介面,只提供了純文字控制檯環境的伺服器上,它將是執行期定位虛擬機器效能問題的首選工具

jstat命令格式為:
jstat[optionvmid[interval[s|ms][count]]]
對於命令格式中的VMID與LVMID需要特別說明一下:如果是本地虛擬機器程序,VMID與LVMID是一致的,如果是遠端虛擬機器程序,那VMID的格式應當是:
[protocol:][//]lvmid[@hostname[:port]/servername]
引數interval和count代表查詢間隔和次數,如果省略這兩個引數,說明只查詢一次。假設需要每250毫秒查詢一次程序2764垃圾收集狀況,一共查詢20次,那命令應當是:
jstat-gc276425020
選項option代表著使用者希望查詢的虛擬機器資訊,主要分為3類:類裝載、垃圾收集、執行期編譯狀況,具體選項及作用請參考表4-3中的描述。

程式碼清單4-1jstat執行樣例

D:\Develop\Java\jdk1.6.0_21\bin>jstat-gcutil2764
S0S1EOPYGCYGCTFGCFGCTGCT
0.000.006.2041.4247.20160.10530.4720.577

查詢結果表明:這臺伺服器的新生代Eden區(E,表示Eden)使用了6.2%的空間,兩個Survivor區(S0、S1,表示Survivor0、Survivor1)裡面都是空的,老年代(O,表示Old)和永久代(P,表示Permanent)則分別使用了41.42%和47.20%的空間。程式執行以來共發生MinorGC(YGC,表示YoungGC)16次,總耗時0.105秒,發生FullGC(FGC,表示FullGC)3次,FullGC總耗時(FGCT,表示FullGCTime)為0.472秒,所有GC總耗時(GCT,表示GCTime)為0.577秒。

③jinfo:java配置資訊工具

jinfo(ConfigurationInfoforJava)的作用是實時地檢視和調整虛擬機器各項引數

jinfo命令格式:
jinfo[option]pid
執行樣例:查詢CMSInitiatingOccupancyFraction引數值。
C:\>jinfo-flagCMSInitiatingOccupancyFraction1444
-XX:CMSInitiatingOccupancyFraction=85jinfo還可以使用-sysprops選項把虛擬機器程序的System.getProperties()的內容打印出來

例如:./jinfo-sysprops16414

③jmap:Java記憶體映像工具

jmap的作用並不僅僅是為了獲取dump檔案,它還可以查詢finalize執行佇列、Java堆和永久代的詳細資訊,如空間使用率、當前用的是哪種收集器等。

jmap命令格式:

jmap[option]vmid

option選項的合法值與具體含義見表4-4。

C:\Users\IcyFenix>jmap-dump:format=b,file=eclipse.bin3500
DumpingheaptoC:\Users\IcyFenix\eclipse.bin……
Heapdumpfilecreated

④jstack:Java堆疊跟蹤工具

jstack(StackTraceforJava)命令用於生成虛擬機器當前時刻的執行緒快照(一般稱為threaddump或者javacore檔案)。執行緒快照就是當前虛擬機器內每一條執行緒正在執行的方法堆疊的集合,生成執行緒快照的主要目的是定位執行緒出現長時間停頓的原因,如執行緒間死鎖、死迴圈、請求外部資源導致的長時間等待等都是導致執行緒長時間停頓的常見原因。執行緒出現停頓的時候通過jstack來檢視各個執行緒的呼叫堆疊,就可以知道沒有響應的執行緒到底在後臺做些什麼事情,或者等待著什麼資源。

jstack命令格式:
jstack[option]vmid
option選項的合法值與具體含義見表4-5。

使用jstack檢視執行緒堆疊(部分結果)
C:\Users\IcyFenix>jstack-l3500
2010-11-1923:11:26
FullthreaddumpJavaHotSpot(TM)64-BitServerVM(17.1-b03mixedmode):
"[ThreadPoolManager]-IdleThread"daemonprio=6tid=0x0000000039dd4000nid=0xf50inObject.wait()[0x000000003c96f000]
java.lang.Thread.State:WAITING(onobjectmonitor)
atjava.lang.Object.wait(NativeMethod)
-waitingon<0x0000000016bdcc60>(aorg.eclipse.equinox.internal.util.impl.tpt.threadpool.Executor)
atjava.lang.Object.wait(Object.java:485)
atorg.eclipse.equinox.internal.util.impl.tpt.threadpool.Executor.run(Executor.java:106)
-locked<0x0000000016bdcc60>(aorg.eclipse.equinox.internal.util.impl.tpt.threadpool.Executor)
Lockedownablesynchronizers:
-None

⑤visualVM除錯工具:過合一故障處理工具

(1)功能點

顯示虛擬機器程序以及程序的配置、環境資訊(jps、jinfo)。

監視應用程式的CPU、GC、堆、方法區以及執行緒的資訊(jstat、jstack)。

dump以及分析堆轉儲快照(jmap、jhat)。

方法級的程式執行效能分析,找出被呼叫最多、執行時間最長的方法。

離執行緒序快照:收集程式的執行時配置、執行緒dump、記憶體dump等資訊建立一個快照,

可以將快照發送開發者處進行Bug反饋。其他plugins的無限的可能性......

(2)位置

JDK_HOME/bin/visualvm/中

(3)使用

1、外掛安裝-點選“工具”→“外掛選單

2、生成、瀏覽堆轉儲快照

在“應用程式”視窗中右鍵單擊應用程式節點,然後選擇“堆Dump”。

在“應用程式”視窗中雙擊應用程式節點以開啟應用程式標籤,然後在“監視”標籤中單擊“堆Dump”。

如果需要把dump檔案儲存或傳送出去,要在heapdump節點上右鍵選擇“另存為”選單,否則當VisualVM關閉時,生成的dump檔案會被當做臨時檔案刪除掉。

要開啟一個已經存在的dump檔案,通過檔案選單中的“裝入”功能,選擇硬碟上的dump檔案即可。

3、分析程式效能(profiler)

先選擇“CPU”和“記憶體”按鈕中的一個,然後切換到應用程式中對程式進行操作,VisualVM會記錄到這段時間中應用程式執行過的法。如果是CPU分析,將會統計每個方法的執行次數、執行耗時;如果是記憶體分析,則會統計每個方法關聯的物件數以及這些物件所的空間。分析結束後,點選“停止”按鈕結束監控過程

注意:

在JDK1.5之後,在Client模式下的虛擬機器加入並且自動開啟了類共享——這是一個在多虛擬機器程序中共享rt.jar中類資料以提高載入速度和節省記憶體的優化,而根據相關Bug報告的反映VisualVM的Profiler功能可能會因為類共享而導致被監視的應用程式崩潰,所以讀者進行Profiling前,最好在被監視程式中使用-Xshare:off引數來關閉類共享優化。

4.BTrace動態日誌跟蹤

BTrace[3]是一個很“有趣”的VisualVM外掛,本身也是可以獨立執行的程式。它的作用是在不停止目標程式執行的前提下,通過HotSpot虛擬機器的HotSwap技術[4]動態加入原本並不存在的除錯程式碼。這項功能對實際生產中的程式很有意義:經常遇到程式出現問題,但排查錯誤的一些必要資訊,譬如方法引數、返回值等,在開發時並沒有列印到日誌之中,以至於不得不停掉服務,通過除錯增量來加入日誌程式碼以解決問題。當遇到生產環境服務無法隨便停止時,缺一兩句日誌導致排錯進行不下去是一件非常鬱悶的事情

在VisualVM中安裝了BTrace外掛後,在應用程式面板中右鍵點選要除錯的程式,會出現“TraceApplication......”選單,點選將進入BTrace面板。這個面板裡面看起來就像一個簡單的Java程式開發環境,裡面還有一小段Java程式碼,如圖4-16示。

應用例項:

BTrace的用法還有許多,列印呼叫堆疊、引數、返回值只是最基本的應用,在它的網站上有使用

BTrace進行效能監視、定位連線洩漏和記憶體洩漏、解決多執行緒競爭問題等例子,有

外掛中心地址:

http://Visualvmjava.net/pluginscenters.html。

官方主頁:http://kenai.com/projects/btrace/

HotSwap技術:程式碼熱替換技術,

HotSpot虛擬機器允許在不停止執行的情況下,更新已經載入的類的程式碼。

七、調優案例分析與實戰

JVM調優

①叢集間同步導致的記憶體溢位

由於資訊傳輸失敗需要重發的可能性,在確認所有註冊在GMS(GropMembershipservice)的節點都收到正確的資訊前,傳送的資訊必須儲存在記憶體中。當網路情況不能滿足傳輸要求時,重發資料在記憶體中不斷堆積,很快產生了記憶體溢位。

②堆外記憶體溢位錯誤

(1)從實踐經驗的角度出發,除了Java堆和永久代之外,我們注意到下面這些區域還會佔用較多的記憶體,這裡所有的記憶體總和受到作業系統程序最大記憶體的限制。

DirectMemory:可通過-XX:MaxDirectMemorySize調整大小,記憶體不足時丟擲OutOfMemoryError或者OutOfMemoryError:Directbuffermemory。

(2)執行緒堆疊:可通過-Xss調整大小,記憶體不足時丟擲StackOverflowError(縱向無法分配,即無法分配新的棧幀)或者OutOfMemoryError:unabletocreatenewnativethread(橫向無法分配,即無法建立新的執行緒)。

(3)Socket快取區:每個Socket連線都Receive和Send兩個快取區,分別佔大約37KB和25KB記憶體,連線多的話這塊記憶體佔用也比較可觀。如果無法分配,則可能會丟擲IOException:Toomanyopenfiles異常。

(4)JNI程式碼:如果程式碼中使用JNI呼叫本地庫,那本地庫使用的記憶體也不在堆中。

(5)虛擬機器和GC:虛擬機器、GC的程式碼執行也要消耗一定的記憶體。

③外部命令導致系統緩慢

例如:問題:這是一個來自網路的案例:一個數字校園應用系統,執行在一臺4個CPU的Solaris10作業系統上,中介軟體為GlassFish伺服器。系統在做大併發壓力測試的時候,發現請求響應時間比較慢,通過作業系統的mpstat工具發現CPU使用率很高,並且系統佔用絕大多數的CPU資源的程式並不是應用系統本身。這是個不正常的現象,通常情況下使用者應用的CPU佔用率應該佔主要地位,才能說明系統是正常工作的。通過Solaris10的Dtrace指令碼可以檢視當前情況下哪些系統呼叫花費了最多的CPU資源,Dtrace執行後發現最消耗CPU資源的竟然是“fork”系統呼叫。眾所周知,“fork”系統呼叫是Linux用來產生新程序的,在Java虛擬機器中,使用者編寫的Java程式碼最多隻有執行緒的概念,不應當有程序的產生。

答案:這是個非常異常的現象。通過本系統的開發人員,最終找到了答案:每個使用者請求的處理都需要執行一個外部shell指令碼來獲得系統的一些資訊。執行這個shell指令碼是通過Java的Runtime.getRuntime().exec()方法來呼叫的。這種呼叫方式可以達到目的,但是它在Java虛擬機器中是非常消耗資源的操作,即使外部命令本身能很快執行完畢,頻繁呼叫時建立程序的開銷也非常可觀。Java虛擬機器執行這個命令的過程是:首先克隆一個和當前虛擬機器擁有一樣環境變數的程序,再用這個新的程序去執行外部命令,最後再退出這個程序。如果頻繁執行這個操作,系統的消耗會很大,不僅是CPU,記憶體負擔也很重。使用者根據建議去掉這個Shell指令碼執行的語句,改為使用Java的API去獲取這些資訊後,系統很快恢復了正常。

④伺服器jvm程序崩潰

這是一個遠端斷開連線的異常,通過系統管理員瞭解到系統最近與一個OA門戶做了整合,在MIS系統工作流的待辦事項變化時,要通過Web服務通知OA門戶系統,把待辦事項的變化同步到OA門戶之中。通過SoapUI測試了一下同步待辦事項的幾個Web服務,發現呼叫後竟然需要長達3分鐘才能返回,並且返回結果都是連線中斷。由於MIS系統的使用者多,待辦事項變化很快,為了不被OA系統速度拖累,使用了非同步的方式呼叫Web服務,但由於兩邊服務速度的完全不對等,時間越長就累積了越多Web服務沒有呼叫完成,導致在等待的執行緒和Socket連線越來越多,最終在超過虛擬機器的承受能力後使得虛擬機器程序崩潰。解決方法:通知OA門戶方修復無法使用的整合介面,並將非同步呼叫改為生產者/消費者模式的訊息佇列實現後,系統恢復正常。

⑤不恰當的資料結構導致記憶體佔用過大

下面具體分析一下空間效率。在HashMap<Long,Long>結構中,只有Key和Value所存放的兩個長整型資料是有效資料,共16B(2×8B)。這兩個長整型資料包裝成java.lang.Long物件之後,就分別具有8B的MarkWord、8B的Klass指標,在加8B儲存資料的long值。在這兩個Long物件組成Map.Entry之後,又多了16B的物件頭,然後一個8B的next欄位和4B的int型的hash欄位,為了對齊,還必須新增4B的