1 - JVM隨筆分類(java虛擬機的內存區域分配(一個不斷記錄和推翻以及再記錄的一個過程))

java虛擬機的內存區域分配html

 
在JVM運行時,類加載器ClassLoader在加載到類的字節碼後,交由jvm的執行引擎處理,
執行過程當中須要空間來存儲數據 (相似於Cpu及主存),此時的這段空間的分配和釋放過程是
此處須要關心和理解的,暫能夠稱爲運行時的數據的內存區的分配,
 
首先運行時的數據區包括,程序計數器,以及Stack(虛擬機 ),以及虛擬機堆,方法區,本地方法棧,
雖然運行時區域分配只要包含上述的描述組件,但實際運行中,程序計數器外,應該再加一個寄存器,
目前先描述上面5個,寄存器後面一併寫入,
 
程序計數器:
java中的多線程是經過線程輪流切換並分配處理器執行時間來實現的,再任何一個肯定的時刻,一個處理器只會處理一條線程中的指令
,所以,爲了線程切換後能恢復到正確的執行位置, 每條線程都須要有一個獨立的程序計數器,各條線程之間的計數器互不影響,
獨立存儲,咱們稱這一類內存區域爲「線程私有」的內存區域,而程序計數器則是一塊較小的內存,它的做用即是記錄當前線程
所執行的字節碼的行號指示器,因此也能夠稱做爲「線程私有的內存區域的一種」,除了程序計數器爲線程私有的內存區域外,
虛擬機中的「棧」也是能夠稱做爲「線程私有的」內存區域的一種。
除此以外,程序計數器(ProgramCounter)也被稱做爲PC寄存器,是在線程啓動的時候會建立該PC寄存器,即程序計數器,用於記錄,
當前正在執行的JVM指令的地址,用於線程切換後能夠執行到正確的位置,那麼除了PC寄存器外,在JVM中還有最經常使用的另外三個寄存器,
分別是,optop操做數棧頂指針,frame當前執行環境指針,vars指向當前執行環境中第一個局部變量的指針,全部的寄存器均爲32位,
除了PC使用與記錄程序的執行位置外, optop,frame,vars則是用於記錄指向Java棧區的指針,
(注:PC寄存器內存區域是JVM虛擬機中惟一一個沒有規定任何OutOfMemoryError狀況的區域)
 
Java虛擬機棧
Java棧的內存區域很小,默認狀況下JVM設置棧的內存爲1M,於程序計數器同樣,「棧」也是線程私有的內存區域,每一個棧中的數據,
都是線程私有的,其它棧不能訪問,它的生命週期和線程相同,「棧」描述的是 Java 方法執行時 的內存模型
每一個方法被執行時都會同時建立一個「棧幀 用於存儲局部變量表,操做棧, 動態連接,方法出口等信息,每個方法被調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中的入棧和出棧的過程。
 
局部變量表 存放了 編譯期可知的各類基本數據類型,(byte,short,char,int,float,long,double,boolean),對象引用(
reference類型,對象引用地址),和returnAddress類型,(指向了一條字節碼指令的地址),其中64位長度的long和double會佔用2個局部變量空間外,
其他數據類型均佔用1個(對象引用和returnAddress也屬於佔用1個空間的數據類型),
局部變量表所需的內存空間是在 編譯器間 完成分配的,當線程執行一個方法時,該方法在棧幀中所需分配多大的內存空間是徹底肯定的,
在方法的運行期間是不會改變局部變量表的大小的,
「棧」是存放線程調用方法時,存儲局部變量表,操做,方法出口等於方法執行相關的信息, Java棧每次所建立
內存的大小是由Xss來調節的,方法層次太多會撐爆這個內存區域,若不夠時將會拋出StackOverflowError的異常信息
 
通常狀況下Xss設置的大小是設置當前線程棧的空間大小,若線程棧的空間大小設置的過大,
則會致使linux服務器的內存可建立線程數將會較少,由於「棧」是一個線程的私有區域,每個方法被調用直至完成的過程,
就對應這一個線程棧的入棧和出棧的過程,若是一個線程在訪問一個方法時的棧大小建立過大,假設Xss爲10M,那麼每個
線程在訪問方法時,線程棧則都會建立10M的棧內存空間,若是此時服務器剩餘的內存空間爲100M,則此時最大可建立線程數
則爲10個,這顯然是不符合整個項目的應用的,除非擴大服務器的內存空間(線程棧的建立是使用的服務器剩餘的內存空間進行建立的)
或者縮小每個線程所使用的建立棧的內存大小,因此通常內存棧的空間大小Xss設置爲256K通常便可,若是一個方法的層次太多,撐爆
了256K的線程內存區域,則會拋出異常便可,但通常較小的內存棧空間,則意味着在相同的服務器內存環境中,能夠建立更多的線程數據。
 
上面也已經提到「棧幀」是用來存儲局部變量表,操做數堆棧,動態連接,方法出口等信息的, 總體來講,虛擬機棧中能夠分爲三個部分,
局部變量,執行環境,和操做數堆棧(即上述提到的操做棧),其中執行變量部分包含了當前方法調用所使用的全部局部變量,vars寄存器指向這一點,執行環境
用於維護堆棧自己的操做,frame(幀)寄存器,指向它,操做數堆棧經過字節碼指令用做工做空間,在此到處置字節碼指令的參數,並找到
字節碼指令的結果,操做數堆棧的頂部則由optop寄存器指向,
執行環境一般夾在局部變量表和操做數堆棧之間, 當前正在執行的方法的操做數堆棧始終是最頂層的堆棧部分,所以optop寄存器指向整個
Java堆棧的頂部、
通常狀況下當一個方法嵌套另外的方法同時執行時,
首先理解下上面所提到的關於棧的概念,能夠知道,每個方法被 線程 執行的時候都會建立一個棧幀,用於存儲局部變量表,操做數堆棧,和執行環境(動態連接,方法出口等)信息
,每個方法被線程執行的時候,都會建立一個棧幀,而棧幀的內存大小是由Xss來設置的,好比200KB,則表示每個線程在執行的過程中都具有了一個200KB的棧內存空間分配的大小,
每個方法在被執行的時候都會建立一個棧幀,若是是當前方法的嵌套層次太多時,則在當前方法中調用其餘方法時,則也會建立所嵌套方法的堆棧,可是當前線程所執行的方法體的整個的
方法的嵌套層次所建立的堆棧不能超過Xss所設置的值,若是方法所執行的層次太多,則可能會致使棧溢出。( 線程在執行一個方法時,建立的是一個棧的內存空間大小,隨着棧的深度愈來愈大,
每一次所執行的嵌套的方法都相似於建立一個幀,即當前棧上面的一個棧幀,也包含了一個, 臥槽,我知道了!!!看下面
棧和棧幀是不同的!!使用Xss參數來設置棧的大小,但每一個方法在被執行的時候,都會建立一個棧幀!!用於存儲局部變量表等等的信息,這和上面所寫到的是同樣的!,
可是棧是後入先出的一個數據結構,而棧幀則是棧上面的一個實體!,相似於這樣的一個效果,
 
全部的棧幀組成一塊兒造成棧!!,而棧幀所作的操做則如同上面所提到的,記錄當前方法的局部變量表,方法的出入口,執行環境等信息,每個方法在被嵌套執行的時候,都會建立一個棧幀,方法的嵌套層次越多,
則建立的當前棧的棧幀越多,而隨着棧幀建立的深度越大,一旦超出了所設置棧的空間大小,則會出現所對應的StackOverflowError棧內存溢出的異常。!這樣的理解彷佛是很對,而且很正確的,!,
這也是爲何會說,當前方法所執行的方法深度過深時,會出現棧溢出異常的問題所在!。
因此:有一句話是,每個方法從調用開始到執行完成的過程,則就對應着一個棧幀在虛擬機棧裏面從入棧到出棧的過程,(請詳細知道和明白棧和棧幀的不一樣,再好好體驗一下這句話,!是很正確的!)
那麼則再好好回顧一下上面所提到的針對棧的寄存器,分別是optop,frame,vars寄存器,
 
StackOverflowError深度:若是使用jvm默認設置(JVM默認設置線程建立棧的大小爲1M),棧的深度大多數狀況下可達到1000~2000,足以在平常開發中使用。注意避免代碼中存在超過1000的方法嵌套。每一個方法嵌套對應一個棧幀。
 
 
本地方法棧
本地方法棧於虛擬機棧所發揮的做用是很是類似的,其區別則是虛擬機棧執行Java(也就是字節碼)服務,而本地方法棧則是
爲虛擬機使用到的Native方法服務,保存native方法進入區域的地址,本地方法棧一樣會拋出StackOverflowError於OutofMemory
Error異常。
 
Java堆
Java堆(Heap)是虛擬機中所管理的內存最大的一塊,由於Java堆是全部線程共享的一塊內存區域,在虛擬機啓動時建立,此內存區域
做用則是存放對象實例,全部的對象實例以及數組都要在堆上分配,Java堆也是垃圾收集器管理的主要區域,從 內存回收的角度來看,
如今JVM的收集器基本都是採用 分代收集算法,因此Java堆還能夠細分爲,新生代和老年代,以及Eden區,From Survivor 和 To Survivor
區域等,以及從 內存分配的角度來看,Java堆中還可能劃分出多個線程私有的分配緩衝區(ThreadLocal,AppocationBuffer,TLAP)等
內存區域空間,可是不管如何劃分,都是在方便JVM收集器GC收集的過程當中,或者線程分配的方式來分配的堆內存區域,不管如何分配,
都與存放內容無關,不管任何區域,存放的都是對象實例,
相比於棧來講,堆內存最不一樣的地方在於編輯器是沒法知道要從堆內存中分配多少存儲空間,也沒法得知存儲的數據要在堆中存儲多長時間,
所以,用堆保存的數據會具有更大的靈活性,在程序的執行過程當中,會在堆裏自動進行對象和數據的存儲,堆所佔用
內存的大小由-Xmx和Xms指令來調節,
 
隨意使用一個Main方法運行一個無限循環的new Object()的程序,使用
-verbos:gc -Xms10M -Xmx10M -XX:+PrintGCDetails -XX:SurvivorRatio=9
 -XX+HeapDumpOnOutOfMemoryError,便能很快報錯OOM(內存溢出異常),並能自動生成DUMP文件,
由於只給堆內存分配了10M的空間而已,:(此處引述該段話的意義在於,提供了很好的內存溢出的測試思路,
能夠經過配置較低的堆內存以及棧等內存,來調試對JVM的深刻理解的一種方式,經過這種方式,能夠測試DUMP的
打印路徑,DUMP文件的分析等等,以及能夠測試JVM的GC收集算法等啦(分代收集等等啦),沒必要每次都在生產或真實環境中進行數據的測試了。)
 
方法區
method方法區又稱做 靜態區,它用於存儲已被 虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據,
注:(方法區存儲的是在編譯期間已經被虛擬機加載的一些內存信息,在編譯期間被加載的信息通常則爲靜態數據,常量,類的基本信息等,而非編譯時JVM加載的數據,如非靜態變量等,則是在實例建立後存儲在堆中)
如:存放全部的類信息,靜態變量,靜態方法,常量和 成員方法
方法區於Java堆同樣,也是各個線程共享的一個內存區域,在HotSport虛擬機上,不少人願意把方法區稱爲「永久代」
實際上二者並不等價,僅僅是由於HotSport虛擬機把GC的分代手機擴展到了方法區,或者說使用永久代來實現方法區而已,
但對於其它按照Java虛擬機規範實現的虛擬機(BEA,IMB J9)等是不存在永久代的概念的,
相比於Java堆而言,方法區的內存回收主要是針對常量池的回收和對類型的卸載,但該區域的內存回收機制相比於堆的GC內存回收機制
來講「成績」難以讓人滿意,尤爲是類型的卸載等,回收條件較爲苟刻,但該部分區域的回收是存在必要的,當方法區沒法知足內存分配
需求時,則將拋出OutOfMemoryError異常,
方法區的大小由-XX:PermSize和-XX:MaxPermSize來調節,類太多可能會撐爆方法區,靜態變量或常量也有可能撐爆方法區。
 
運行時常量池
該區域屬於方法區,除了方法區中記錄類的版本,字段,方法,接口等信息外,還有一項則是常量池,能夠將常量池理解爲 方法區的 資源從庫, 主要用於存放編譯期生成的各類字面量,
和符號引用(字面量即常量,符號引用即 類和接口的全限定名, 字段的名稱和描述符, 方法的名稱和描述符, Java中八種基本類型的包裝類的大部分都實現了常量池技術 )
http://chenzehe.iteye.com/blog/1727062 Byte,Short,Integer,Long,Character這5種整型的包裝類也只是在對應值在-128到127時纔可以使用到對象池
如類和接口的常量,編譯器生成的各類字面量也是放置到常量池中,java中常量變量在
編譯器時則便會進行自動運算後賦值, 幾張圖輕鬆理解String.intern() - CSDN博客,除此以外,javac在編譯器還存在代碼的標註檢查,註解處理器,
自動裝箱,條件編譯,解析於填充符號表等,具體可詳細看下javac編譯期間所作的具體操做,當一個類中的成員變量(static 變量)和成員方法(static 方法)被引用的時候,JVM
也是經過常量池中的引用來查找成員變量和成員方法在內存中的實際地址,而後返回引用地址。
 
回顧方法區和常量池:方法區主要包括 類的基本信息,其中包括( 類的訪問標誌,這個class是不是類仍是接口,是否認義public類型,是否認義abstrace類型,是否聲明瞭final等,
以及記錄當前該 class類的索引關係,父類索引和接口索引接口的關係; 字段表集合,記錄當前接口或類中聲明的變量,包括類級別變量和實例級變量,字段的做用域,是不是實例變量(static)或類變量,
可變性,併發可見性(volatile)等, 方法表集合,包括訪問標誌,名稱索引,等於字段表的描述是一致的,父類方法沒有在子類中從新,則方法集合中不會出現來自父類的方法信息,) https://blog.csdn.net/u011116672/article/details/49865023
常量,靜態變量,以及編譯器編譯後代碼等,其中,常量池屬於方法區,但常量池中記錄的通常爲,常量數據,和符號引用數據等,
符號引用具體能夠參看:  https://www.cnblogs.com/shinubi/articles/6116993.html
 
 
假設此時,在一個方法中,Object obj = new Object(),這樣一個代碼出如今方法體中,也會涉及到內存的分配的操做,
此時,在Object obj,則被分配至棧內存空間中的局部變量表中,做爲一個引用類型出現,reference類型,而此時所執行的方法中的其他的
變量信息則也是同時記錄在此時的方法棧的局部變量表中,而此時的 new Object()則會被分配至 堆內存中,造成一塊存儲了Object類型全部
實例數據值(對象中各個實例字段的數據(即類的非靜態變量,存儲在堆中,做用於整個類中))的結構化內存,
,其中棧內存的局部變量表中的reference類型將記錄 該實例對象在堆內存的具體地址,除此以外,堆中的Object()實例,
也同時必須可以找到此對象的類型數據(對象類型,父類,實現的接口,方法)等基本的class類的信息,這些類的基本毫無疑問則記錄在
方法區中,其中 new Obejct()中的成員變量,成員方法(靜態方法)等信息則是記錄在方法區中的常量池中,實例方法等信息則仍是記錄在方法區中,
並不在常量池中記錄,
 
關於堆中的空間分配:
 
JVM中GC垃圾收集器經常使用的-->幾種垃圾收集算法
  1. 引用計數算法,對於互相引用且沒有被其餘引用的對象沒法處理收集,JVM實際上也並未採用
  2. 根搜索算法(JVM其他幾個算法的實現基礎),設立若干的根對象,當任何一個 根對象到某一個對象的均不可達時,則認爲該對象是能夠回收的,根對象又叫作(GC ROOTS),
JAVA中扮演GC ROOTS根對象的主要包括如下四種對象,分別是: 1. 虛擬機棧中的引用對象(即虛擬機棧幀中的局部變量表中所引用的對象),2. 方法區中的類靜態屬性引用的對象,3. 方法區的常量引用對象,4. 本地方法棧的JNI引用對象,    以上四中對象扮演GC ROOTS的角色,只要是某一個對象到任何一個根對象皆不可到達時,則表示該對象爲可回收對象
            根搜索算法解決了能夠判斷哪些對象是能夠被回收的,哪些對象是不能夠回收的問題,可是在JVM垃圾回收的過程當中,還須要解決的另外兩個問題則是,何時能夠回收這些內存垃圾,以及如何回收這些垃圾內存,在根搜索算法的基礎上, 現代虛擬機中垃圾搜索的實現當中 主要存在的則是以下三種 分別是標記-清除算法,複製算法,標記-整理算法,還有一個則是 分代收集算法,前三種算法均是擴展於根搜索算法,對於分代搜索算法,也會在下面作詳細的相關介紹。
  1. 標記清除算法,堆的有效內存空間快被耗盡後,遍歷全部的GC ROOTS,將全部GC ROOTS可達的對象標記爲存活對象,而後清除全部GC ROOTS不可達的對象,(缺點:須要遍歷全部的堆對象,判斷是否和GC ROOTS 可達,效率低下,且在標記清除算法執行過程當中,須要中止程序應用程序,第二:標記清除所清理出的內存是不連續的內存空間,由於被清除的對象是出如今內存的各個角落的,因此致使內存空間佈局很亂,連續空間較爲難找,則在從新分配數組對象時,尋找不到一個連續的內存空間,將會出現莫名的問題(數組的內存空間是連續的內存集合))
  2. 複製算法(又能夠叫作,標記/複製/清除算法),複製算法與標記清除算法不一樣的是,複製算法將會把內存劃分爲兩個不一樣的區間,分別是活動內存區間,和空閒內存區間,在任意的時間點,只會有一個內存區間被使用,當活動內存區間的有效內存被消耗完的時候,JVM則暫停程序運行,將活動區間中的存活對象所有複製到 空閒空間中,且嚴格按照內存地址進行依次排列,於此同時,GC線程將更新後的存活對象指向新的內存地址,且清空活動內存中所剩餘的垃圾對象。 能夠看出的是,儘管複製算法也是用了GC ROOTS的遍歷方式獲得存活對象,而後所有複製到空間內存空間中,但複製算法彌補了 標記/清除算法中,內存混亂的問題,但缺點是:須要浪費一半的內存,作空閒內存,這是不可忽略的特色。
  3. 標記整理算法(又能夠叫作,標記/整理/清除算法),於標記/清除算法不一樣的是,標記整理算法,1. 也是先遍歷全部的GC ROOTS,將後將存活對象進行標記,2. 移動所對應的存活對象,且嚴格按照內存的空間地址進行移動,而後將末端的內存地址進行回收, 相比於標記清除算法,解決了內存分散的特色,相比於複製算法,則消除了內存減半的代價,可是其惟一的缺點即是:標記整理的執行效率不高,既要標記又要整理對應的存活對象的引用地址,相對來講,總體的執行效率要低於複製算法。
 
上述三種GC的實現算法共同點和不一樣點分別是:(總結:):
  1. 共同點:都須要先暫停應用的執行,(由於都存在標記階段,經過遍歷GC ROOTS來判斷獲得存活的對象,即經過根搜索算法進行標記獲得可回收對象,)
  2. 執行效率上: 複製算法>標記/整理算法>標記/清除算法(此處的效率只是簡單的對比時間複雜度,實際狀況不必定如此)。
  3. 內存整齊度上:複製算法=標記整理算法>標記清除算法
  4. 內存利用率上:標記整理算法=標記清除算法>複製算法
能夠看到標記清除算法,算是一個比較落後的算法,可是上述的算法在執行過程當中,都存在或多或少的優缺點,因此在特定的場合或者說是內存結構中,使用特定的算法,都將會特別有效果,根據不一樣的內存狀況來選擇對一個的垃圾清理算法,是較爲合適的一種行爲;
 
問?爲何上述的三種算法在執行過程中,都須要暫停應用程序的執行,?
答:首先上述三種算法都存在對象的標記行爲,以此來判斷出那些是可回收對象,那些是活躍對象,即根據GC ROOTS是否可達進行搜索標記,即上述所提到的根搜索算法,那麼在經過根搜索算法,標記的過程中,假設此時A對象被標記爲了可回收對象,那麼因爲應用程序是在可執行狀態下,此時又建立了B對象,且B對象引用A對象,是可達的,可是因爲B對象的建立和引用是在標記以後,此時則會出現了A對象進行了垃圾回收,而B對象則經過,整理也好,複製也好,被保留了下來,那麼此時程序再次經過B對象獲取A對象時,則會出現A對象爲null的狀況,那麼這必然是在程序的過程中不容許出現的狀況,因此在涉及到 標記 再最終有一個過程是清除過程的這樣的算法中,必然是先執行對應的 垃圾清理算法,而後再算法執行過程後,通知喚醒對應的應用程序中線程,而後繼續執行相關的應用程序的任務。(GC 的線程在執行的過程當中,必然是和應用程序的線程相互配合,才能達到垃圾清理後,也不影響程序的正常運行的效果,好比此處的暫停應用程序的線程執行,先優先執行GC的垃圾回收的線程)
 
分代收集算法:
 
JVM中GC垃圾收集器-->經常使用的幾種收集器即各收集器的使用區別;
 

版權聲明java


做者:Arnold zhaolinux

出處:博客園Arnold的技術博客--https://www.cnblogs.com/zh94/web

您的支持是對博主最大的鼓勵,感謝您的認真閱讀。算法

本文版權歸做者全部,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,不然保留任何追究法律責任的權利。數組

相關文章
相關標籤/搜索