1. 程式人生 > >Java記憶體模型及效能優化及Java垃圾回收

Java記憶體模型及效能優化及Java垃圾回收

一、JVM記憶體模型

  1. 首先介紹下Java程式具體執行的過程:
  • Java原始碼檔案(.java字尾)會被Java編譯器編譯為位元組碼檔案(.class字尾);
  • 由JVM中的類載入器載入各個類的位元組碼檔案,載入完畢之後,交由JVM執行引擎執行;
  • 在整個程式執行過程中,JVM會用一段空間來儲存程式執行期間需要用到的資料和相關資訊,這段空間一般被稱作為Runtime Data Area(執行時資料區),也就是我們常說的JVM記憶體;
  • 因此,在Java中我們常常說到的記憶體管理就是針對這段空間進行管理(如何分配和回收記憶體空間)。

  2.  JVM的記憶體劃分和各區域職責:

  

  • 程式計數器:程式計數器是指CPU中的暫存器,它儲存的是程式當前執行的指令的地址
    (也可以說儲存下一條指令的所在儲存單元的地址),當CPU需要執行指令時,需要從程式計數器中得到當前需要執行的指令所在儲存單元的地址,然後根據得到的地址獲取到指令,在得到指令之後,程式計數器便自動加1或者根據轉移指標得到下一條指令的地址,如此迴圈,直至執行完所有的指令;(注:JVM中的程式計數器並不像組合語言中的程式計數器一樣是物理概念上的CPU暫存器,但是邏輯作用上是等同的,在JVM中多執行緒是通過執行緒輪流切換來獲得CPU執行時間的,在任一具體時刻,一個CPU的核心只會執行一條執行緒中的指令,為了能夠使得每個執行緒都線上程切換後能夠恢復在切換之前的程式執行位置,每個執行緒都需要有自己獨立的程式計數器,並且不能互相被幹擾,否則就會影響到程式的正常執行次序。因此,可以這麼說,程式計數器是每個執行緒所私有的)
  • Java棧:Java棧是Java方法執行的記憶體模型,Java棧中存放的是一個個的棧幀,每個棧幀(包括:區域性變量表、運算元棧、執行時常量池(在下文中提到的方法區內)的引用、方法返回地址和一些額外的附加資訊)對應一個被呼叫的方法,當執行緒執行一個方法時,就會隨之建立一個對應的棧幀,並將建立的棧幀壓棧。當方法執行完畢之後,便會將棧幀出棧;(注:由於每個執行緒正在執行的方法可能不同,因此每個執行緒都會有一個自己的Java棧,互不干擾)
  • 本地方法棧:Java棧是為執行Java方法服務的,而本地方法棧則是為執行本地方法(Native Method)服務的;
  • 堆:Java中的堆是用來儲存物件本身的以及陣列;
  • 方法區:它與堆一樣,是被執行緒共享的區域,儲存了每個類的資訊(包括類的名稱、方法資訊、欄位資訊)、靜態變數、常量以及編譯器編譯後的程式碼等。(注:在方法區中有一個非常重要的部分就是執行時常量池,它是每一個類或介面的常量池的執行時表示形式,在類和介面被載入到JVM後,對應的執行時常量池就被創建出來。當然並非Class檔案常量池中的內容才能進入執行時常量池,在執行期間也可將新的常量放入執行時常量池中,比如String的intern方法。)

二、垃圾回收機制

  文章結尾處:

三、針對分代垃圾回收調整部分引數

      

  JVM記憶體的系統級的調優主要的目的是減少Minor GC的頻率和Full GC的次數,過多的Minor GC和Full GC是會佔用很多的系統資源,影響系統的吞吐量。

1.  年輕代分三個區,一個Eden區,兩個Survivor區(from和to區),可以通過-XXSurvivorRatio調整比例

    • 作用:預設-XX:SurvivorRatio=8,表示Survivor區與Eden區的大小比值是1:1:8,在MinorGC過程,如果survivor空間不夠大,不能夠儲存所有的從eden空間和from suvivor空間複製過來活動物件,溢位的物件會被複制到old代,溢位遷移到old代,會導致old代的空間快速增長

2.  大部分物件在先在Eden區中申請記憶體。

    • 作用:可以通過設定-XX:PreTenureSizeThreShold大小,令大於這個值的物件直接儲存到年老代,避免在Eden區與Survivor區之間頻繁地通過複製演算法回收記憶體

3.  當Eden區滿時,無法為新的物件分配記憶體時,會進行Minor GC對其回收無用物件佔用的記憶體,如果還有存活物件,則將存活的物件複製到Survivor From區(兩個中Survivor對稱);然後從Eden區存活下來的物件,就會被複制到From,當這個From區滿時,此區的存活物件將被複制到To區,接下來Eden區存活下來的物件就會被複制到To區,經歷一定的次數Minor GC後還存活的物件,將被複制“年老區(Tenured)”。

    • 作用:Minor預設15次,可通過-MaxTenuringThreshold引數調整年輕代回收次數防止物件過早進入年老代,降低年老代溢位的可能性

4.  年輕代和年老代的預設比例為1:2,即年輕代佔堆記憶體的1/3,年老代佔2/3,可調整-XX:NewRatio的大小設定年輕和年老的比例。

    • 作用:預設-XX:NewRatio=2,即young:tenured=1:2,適當調整年輕代大小,可以一定層度上較少Full GC出現的概率

四、其餘效能調優常用引數設定

  1. -Xms and -Xmx (or: -XX:InitialHeapSize and -XX:MaxHeapSize):指定JVM的初始和最大堆記憶體大小,兩值可以設定相同,以避免每次垃圾回收完成後JVM重新分配記憶體。
  2. -Xmn:設定年輕代大小。整個堆大小=年輕代大小 + 年老代大小 + 持久代大小。所以增大年輕代後,將會減小年老代大小。此值對系統性能影響較大,Sun官方推薦配置為整個堆的3/8。
  3. -Xss:設定每個執行緒的堆疊大小。JDK5.0以後每個執行緒堆疊大小為1M。在相同實體記憶體下,減小這個值能生成更多的執行緒。但是作業系統對一個程序內的執行緒數還是有限制的,不能無限生成,經驗值在3000~5000左右。
  4. -XX:+HeapDumpOnOutOfMemoryError and -XX:HeapDumpPath:讓JVM在發生記憶體溢位時自動的生成堆記憶體快照(堆記憶體快照檔案有可能很龐大,推薦將堆記憶體快照生成路徑指定到一個擁有足夠磁碟空間的地方。)
  5. -XX:OnOutOfMemoryError:當記憶體溢發生時,我們甚至可以可以執行一些指令,比如發個E-mail通知管理員或者執行一些清理工作($ java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -XX:OnOutOfMemoryError ="sh ~/cleanup.sh" MyApp)
  6. -XX:PermSize and -XX:MaxPermSize:設定永久代大小的初始值和最大值(預設:最小值為實體記憶體的1/64,最大值為實體記憶體的1/16,永久代在堆記憶體中是一塊獨立的區域,這裡設定的永久代大小並不會被包括在使用引數-XX:MaxHeapSize 設定的堆記憶體大小中)
  7. -XX:PretenureSizeThreshold :令大於這個設定值的物件直接在老年代分配。這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的記憶體複製

與C/C++相比,java語言不需要程式設計師直接控制記憶體回收,java程式的記憶體分配和回收都是由JRE在後臺自動進行,JRE會負責回收那些不再使用的記憶體,這種機制被稱為垃圾回收機制(Garbage Collection,GC):

一、主要負責兩件事情:

1.發現無用的物件;

2.回收被無用物件佔用的記憶體空間,使之再次被程式使用(一般是在CPU空閒或者記憶體不足時)

注:事實上,除了釋放沒用物件佔用的記憶體空間外,垃圾回收也可以清除記憶體紀錄碎片(由於建立物件和垃圾回收器釋放丟棄物件所佔的記憶體空間)

二、特點

1.垃圾回收機制的工作目標是回收無用物件的記憶體空間,這些記憶體空間都是jvm堆記憶體(執行時資料區,用以儲存類的例項,即物件)裡的記憶體空間,不包含其它物力資源,比如資料庫連線、磁碟I/O等;

2.Java語言沒有顯式的提供分配記憶體和刪除記憶體的方法,一些開發人員將引用物件設定為null或者呼叫System.gc()或者Runtime.getRuntime.gc()來釋放記憶體(後兩種方法僅是建議,慎重使用)

3.垃圾回收不可預知,不同的jvm採用不同的垃圾回收機制和演算法,有可能定時發生,有可能CPU空閒時發生,也有可能記憶體耗盡時發生(下面說下最為熟知的分代垃圾回收)

1.年輕代(Young Generation):

所有新生成的物件首先都是放在年輕代的。年輕代的目標就是儘可能快速的收集掉那些生命週期短的物件。年輕代分三個區。一個Eden區,兩個Survivor區(一般而言)。大部分物件在Eden區中生成。當Eden區滿時,還存活的物件將被複制到Survivor區(兩個中的一個)(YGC,年輕代垃圾回收),當這個Survivor區滿時,此區的存活物件將被複制到另外一個Survivor區,當這個Survivor去也滿了的時候,從第一個Survivor區複製過來的並且此時還存活的物件,將被複制“年老區(Tenured)”。需要注意,Survivor的兩個區是對稱的,沒先後關係,所以同一個區中可能同時存在從Eden複製過來 物件,和從前一個Survivor複製過來的物件,而複製到年老區的只有從第一個Survivor去過來的物件。而且,Survivor區總有一個是空的。同時,根據程式需要,Survivor區是可以配置為多個的(多於兩個),這樣可以增加物件在年輕代中的存在時間,減少被放到年老代的可能。

2.年老代(Old Generation):

在年輕代中經歷了N次垃圾回收後仍然存活的物件,就會被放到年老代中。因此,可以認為年老代中存放的都是一些生命週期較長的物件。

3.持久代(Permanent Generation):

用於存放靜態檔案,如今Java類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者呼叫一些class,例如Hibernate等,在這種時候需要設定一個比較大的持久代空間來存放這些執行過程中新增的類。持久代大小通過-XX:MaxPermSize=<N>進行設定。

4.什麼情況下觸發垃圾回收

由於物件進行了分代處理,因此垃圾回收區域、時間也不一樣。GC有兩種型別:Minor GC和Full GC。

Minor GC

一般情況下,當新物件生成,並且在Eden申請空間失敗時,就會觸發Minor GC,對Eden區域進行GC,清除非存活物件,並且把尚且存活的物件移動到Survivor區。然後整理Survivor的兩個區。這種方式的GC是對年輕代的Eden區進行,不會影響到年老代。因為大部分物件都是從Eden區開始的,同時Eden區不會分配的很大,所以Eden區的GC會頻繁進行。因而,一般在這裡需要使用速度快、效率高的演算法,使Eden去能儘快空閒出來。

Full GC

對整個堆進行整理,包括Young、Tenured和Perm。Full GC因為需要對整個對進行回收,所以比Full GC要慢,因此應該儘可能減少Full GC的次數。在對JVM調優的過程中,很大一部分工作就是對於Full GC的調節。有如下原因可能導致Full GC:

 年老代(Tenured)被寫滿

持久代(Perm)被寫滿

System.gc()被顯示呼叫

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