深刻理解 JVM 的內存區域

深刻理解運行時數據區

代碼示例:html

1. JVM 向操做系統申請內存java

  JVM 第一步就是經過配置參數或者默認配置參數向操做系統申請內存空間,根據內存大小找到具體的內存分配表,而後把內存段的起始地址和終止地址分配給 JVM,接下來 JVM 就進行內部分配算法

2. JVM 得到內存空間後,會根據配置參數分配堆、棧以及方法區的內存大小編程

  -Xms30m -Xmx30m -Xss1m -XX:MaxMetaspaceSize=30m數組

3. 類加載(類加載的細節後續章節會講):緩存

  這裏主要是把 class 放入方法區、還有 class 中的靜態變量和常量也要放入方法區安全

4. 執行方法及建立對象oracle

  啓動 main 線程,執行 main 方法,開始執行第一行代碼。此時堆內存中會建立一個 對象,對象引用  就存放在棧中框架

  後續代碼中遇到 new 關鍵字,會再建立一個對象,對象引用  就存放在棧中ide

 

 

總結一下 JVM 運行內存的總體流程

 

  JVM 在操做系統上啓動,申請內存,先進行運行時數據區的初始化,而後把類加載到方法區,最後執行方法。

 

  方法的執行和退出過程在內存上的體現上就是虛擬機棧中棧幀的入棧和出棧。

 

  同時在方法的執行過程當中建立的對象通常狀況下都是放在堆中,最後堆中的對象也是須要進行垃圾回收清理的。

從底層深刻理解運行時數據區

堆空間分代劃分

  堆被劃分爲新生代和老年代(Tenured),新生代又被進一步劃分爲 Eden  Survivor ,最後 Survivor  From Survivor  To Survivor 組成

  (後續對象分配和垃圾回收會細講這塊)

GC 概念

  GC- Garbage Collection 垃圾回收,在 JVM 中是自動化的垃圾回收機制,咱們通常不用去關注,在 JVM  GC 的重要區域是堆空間

咱們也能夠經過一些額外方式主動發起它,好比 System.gc(),主動發起。(項目中切記不要使用)

JHSDB 工具

 

  JHSDB 是一款基於服務性代理實現的進程外調試工具。服務性代理是 HotSpot 虛擬機中一組用於映射 Java 虛擬機運行信息的,主要基於 Java語言實現的API 集合

JDK1.8的開啓方式

  開啓 HSDB 工具:

  Jdk1.8 啓動 JHSDB 的時候必須將 sawindbg.dll(通常會在 JDK 的目錄下)複製到對應目錄的 jre (注意在 win 上安裝了 JDK1.8 後每每同級目錄下有一個jre 的目錄)

 

 

 

 

 

 

JDK1.9及之後的開啓方式

  進入 JDK  bin 目錄下,咱們能夠在命令行中使用 jhsdb hsdb 來啓動它

 

代碼改造

 

  VM 參數加入

 

  -XX:+UseConcMarkSweepGC

-XX:-UseCompressedOops

 

 

 

JHSDB 中查看對象

 

實例代碼啓動

由於 JVM 啓動有一個進程,須要藉助一個命令 jps查找到對應程序的進程

在 JHSDB 工具中 attach 上去

 

 

 

JHSDB中查看對象

查看堆參數

 

 

 

上圖中能夠看到實際 JVM 啓動過程當中堆中參數的對照,能夠看到,在不啓動內存壓縮的狀況下。堆空間裏面的分代劃分都是連續的。

再來查看對象

能夠看到 JVM 中全部的對象,都是基於 class 的對象

 

 

 

全路徑名搜索

 

 

 

雙擊出現這個 Teacher 類的對象,兩個,就是 T1  T2 對象

 

 

 

 

 

 

 

最後再對比一下堆中分代劃分能夠得出爲何 T1  Eden,T2 在老年代

 

 

 

JHSDB 中查看棧

 

 

 

 

 

 

從上圖中能夠驗證棧內存,同時也能夠驗證到虛擬機棧和本地方法棧在 Hotspot 中是合二爲一的實現了

 

 

 

當咱們經過 Java 運行以上代碼時,JVM 的整個處理過程以下:

  1. JVM 向操做系統申請內存,JVM 第一步就是經過配置參數或者默認配置參數向操做系統申請內存空間。

  2. JVM 得到內存空間後,會根據配置參數分配堆、棧以及方法區的內存大小。

  3. 完成上一個步驟後,JVM 首先會執行構造器,編譯器會在.java 文件被編譯成.class 文件時,收集全部類的初始化代碼,包括靜態變量賦值語句、

靜態代碼塊、靜態方法,靜態變量和常量放入方法區

  4. 執行方法。啓動 main 線程,執行 main 方法,開始執行第一行代碼。此時堆內存中會建立一個 Teacher 對象,對象引用 student 就存放在棧中。

執行其餘方法時,具體的操做:棧幀執行對內存區域的影響。棧幀執行對內存區域的影響

 

 

 

從底層深刻理解運行時數據區總結

深刻辨析堆和棧

 

功能

Ø 以棧幀的方式存儲方法調用的過程,並存儲方法調用過程當中基本數據類型的變量(int、short、long、byte、float、double、boolean、char 等)以

及對象的引用變量,其內存分配在棧上,變量出了做用域就會自動釋放;

Ø 而堆內存用來存儲 Java中的對象。不管是成員變量,局部變量,仍是類變量,它們指向的對象都存儲在堆內存中;

 

 線程獨享仍是共享

Ø 棧內存歸屬於單個線程,每一個線程都會有一個棧內存,其存儲的變量只能在其所屬線程中可見,即棧內存能夠理解成線程的私有內存。

Ø 堆內存中的對象對全部線程可見。堆內存中的對象能夠被全部線程訪問。

 

 空間大小

棧的內存要遠遠小於堆內存

虛擬機內存優化技術

棧的優化技術——棧幀之間數據的共享

  在通常的模型中,兩個不一樣的棧幀的內存區域是獨立的,可是大部分的 JVM 在實現中會進行一些優化,使得兩個棧幀出現一部分重疊。(主要體如今方法中有參數傳遞的狀況),讓下面棧幀的操做數棧和上面棧幀的部分局部變量重疊在一塊兒,這樣作不但節約了一部分空間,更加劇要的是在進行方法調用時就能夠直接公用一部分數據,無需進行額外的參數複製傳遞了

 

 

 

使用 JHSDB 工具查看棧空間同樣能夠看到

 

 

 

內存溢出

棧溢出

參數:-Xss1m,具體默認值須要查看官網:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html#BABHDABI

 

 

 

HotSpot 版本中棧的大小是固定的,是不支持拓展的。

java.lang.StackOverflowError通常的方法調用是很難出現的,若是出現了可能會是無限遞歸。

虛擬機棧帶給咱們的啓示:方法的執行由於要打包成棧楨,因此天生要比實現一樣功能的循環慢,因此樹的遍歷算法中:遞歸和非遞歸(循環來實現)都有存在的意義。遞歸代碼簡潔,非遞歸代碼複雜可是速度較快。

OutOfMemoryError:不斷創建線程,JVM 申請棧內存,機器沒有足夠的內存。(通常演示不出,演示出來機器也死了)

同時要注意棧區的空間 JVM 沒有辦法去限制的由於 JVM 在運行過程當中會有線程不斷的運行沒辦法限制因此只限制單個虛擬機棧的大小

 

堆溢出

內存溢出:申請內存空間,超出最大堆內存空間。

若是是內存溢出,則經過 調大 -Xms,-Xmx參數。

若是不是內存泄漏,就是說內存中的對象倒是都是必須存活的,那麼久應該檢查 JVM的堆參數設置,與機器的內存對比,看是否還有能夠調整的空間,再從代碼上檢查是否存在某些對象生命週期過長、持有狀態時間過長、存儲結構設計不合理等狀況,儘可能減小程序運行時的內存消耗。

 

方法區溢出

1運行時常量池溢出

2方法區中保存的 Class對象沒有被及時回收掉或者 Class信息佔用的內存超過了咱們配置。

注意 Class 要被回收條件比較苛刻(僅僅是能夠不表明必然由於還有一些參數能夠進行控制):

一、該類全部的實例都已經被回收,也就是堆中不存在該類的任何實例。

二、加載該類的 ClassLoader 已經被回收

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

 

 

cglib是一個強大的,高性能,高質量的 Code生成類庫,它能夠在運行期擴展 Java類與實現 Java接口。

CGLIB包的底層是經過使用一個小而快的字節碼處理框架 ASM,來轉換字節碼並生成新的類。除了 CGLIB包,腳本語言例如 Groovy和 BeanShell,

也是使用 ASM來生成 java的字節碼。固然不鼓勵直接使用 ASM,由於它要求你必須對 JVM內部結構包括 class文件的格式和指令集都很熟悉。

 

本機直接內存溢出

直接內存的容量能夠經過 MaxDirectMemorySize 來設置(默認與堆內存最大值同樣),因此也會出現 OOM 異常

由直接內存致使的內存溢出,一個比較明顯的特徵是在 HeapDump 文件中不會看見有什麼明顯的異常狀況,若是發生了 OOM,同時 Dump 文件很小,能夠考慮重點排查下直接內存方面的緣由。

 

常量池

Class 常量池(靜態常量池)

在 class 文件中除了有類的版本、字段、方法和接口等描述信息外,還有一項信息是常量池 (Constant Pool Table),用於存放編譯期間生成的各類字面量和符號引用

 

 

字面量給基本類型變量賦值的方式就叫作字面量或者字面值。

好比:String a=「b」,這裏「b」就是字符串字面量,一樣類推還有整數字面值、浮點類型字面量、字符字面量。

 

符號引用 符號引用以一組符號來描述所引用的目標。符號引用能夠是任何形式的字面量,JAVA 在編譯的時候一個每一個 java 類都會被編譯成一個 class文件,但在編譯的時候虛擬機並不知道所引用類的地址(實際地址),就用符號引用來代替,而在類的解析階段(後續 JVM 類加載會具體講到)就是爲了把這個符號引用轉化成爲真正的地址的階段

一個 java類(假設爲 People 類)被編譯成一個 class 文件時,若是 People 類引用了 Tool類,可是在編譯時 People 類並不知道引用類的實際內存地址,因

此只能使用符號引用(org.simple.Tool)來代替。而在類裝載器裝載 People 類時,此時能夠經過虛擬機獲取 Tool 類的實際內存地址,所以即可以既將符號org.simple.Tool 替換爲 Tool類的實際內存地址。

 

運行時常量池

  運行時常量池(Runtime Constant Pool)是每個類或接口的常量池(Constant_Pool)的運行時表示形式,它包括了若干種不一樣的常量:從編譯期可知的數值字面量到必須運行期解析後才能得到的方法或字段引用。(這個是虛擬機規範中的描述,很生澀)

  運行時常量池是在類加載完成以後, Class 常量池中的符號引用值轉存到運行時常量池中,類在解析以後,將符號引用替換成直接引用。

  運行時常量池在 JDK1.7 版本以後,就移到堆內存中了,這裏指的是物理空間,而邏輯上仍是屬於方法區(方法區是邏輯分區)。

  在 JDK1.8 中,使用元空間代替永久代來實現方法區,可是方法區並無改變,所謂"Your father will always be your father"。變更的只是方法區中內容的物理存放位置,可是運行時常量池和字符串常量池被移動到了堆中。可是不論它們物理上如何存放,邏輯上仍是屬於方法區的。

 

字符串常量池

  字符串常量池這個概念是最有爭議的,King 老師翻閱了虛擬機規範等不少正式文檔,發現沒有這個概念的官方定義,因此與運行時常量池的關係不去擡槓,咱們從它的做用和 JVM 設計它用於解決什麼問題的點來分析它

  以 JDK1.8 爲例,字符串常量池是存放在堆中,而且與 java.lang.String 類有很大關係。設計這塊內存區域的緣由在於:String 對象做爲 Java 語言中重要的數據類型,是內存中佔據空間最大的一個對象。高效地使用字符串,能夠提高系統的總體性能。

 

  因此要完全弄懂,咱們的重心其實在於深刻理解 String。

 

String

String 類分析JDK1.8

String 對象是對 char 數組進行了封裝實現的對象,主要有 2 個成員變量:char 數組,hash 

 

 

 

 

 

String 對象的不可變性

 

  瞭解了 String 對象的實現後,你有沒有發如今實現代碼中 String 類被 final 關鍵字修飾了,並且變量 char 數組也被 final 修飾了

 

  咱們知道類被 final 修飾表明該類不可繼承,而 char[]被 final+private 修飾,表明了 String 對象不可被更改。Java 實現的這個特性叫做 String 對象的不可變性,即 String 對象一旦建立成功,就不能再對它進行改變。

Java 這樣作的好處在哪裏呢?

第一,保證 String 對象的安全性。假設 String 對象是可變的,那麼 String 對象將可能被惡意修改

第二,保證 hash 屬性值不會頻繁變動,確保了惟一性,使得相似 HashMap 容器才能實現相應的 key-value 緩存功能

第三,能夠實現字符串常量池。在 Java ,一般有兩種建立字符串對象的方式,一種是經過字符串常量的方式建立,如 String str=「abc」;另外一種是字

符串變量經過 new 形式的建立,如 String str = new String(「abc」)。

String 的建立方式及內存分配的方式

 

1String str=abc」;

 

當代碼中使用這種方式建立字符串對象時,JVM 首先會檢查該對象是否在字符串常量池中,若是在,就返回該對象引用,不然新的字符串將在常量池中被建立。這種方式能夠減小同一個值的字符串對象的重複建立,節約內存。(str 只是一個引用)

 

 

2String str = new String(abc)

首先在編譯類文件時,"abc"常量字符串將會放入到常量結構中,在類加載時,「abc"將會在常量池中建立;其次,在調用 new ,JVM 命令將會調用 String的構造函數,同時引用常量池中的"abc」字符串,在堆內存中建立一個 String 對象;最後,str 將引用 String 對象

 

 

三、使用 new,對象會建立在堆中,同時賦值的話,會在常量池中建立一個字符串對象,複製到堆中。

具體的複製過程是先將常量池中的字符串壓入棧中,在使用 String 的構造方法是,會拿到棧中的字符串做爲構方法的參數。

這個構造函數是一個 char 數組的賦值過程,而不是 new 出來的,因此是引用了常量池中的字符串對象。存在引用關係。

public class Location {

  private String city;

  private String region;

}

 

 

 

 

 

4String str2= "ab"+ "cd"+ "ef";

編程過程當中,字符串的拼接很常見。前面我講過 String 對象是不可變的,若是咱們使用 String 對象相加,拼接咱們想要的字符串,是否是就會產生多個對象呢    例如如下代碼:

分析代碼可知:首先會生成 ab 對象,再生成 abcd 對象,最後生成 abcdef 對象,從理論上來講,這段代碼是低效的。

編譯器自動優化了這行代碼,編譯後的代碼,你會發現編譯器自動優化了這行代碼,以下

String str= "abcdef";

大循環使用

 

 

intern

String  intern 方法,若是常量池中有相同值,就會重複使用該對象,返回對象引用

 

 

一、new Sting() 會在堆內存中建立一個 a的 String對象,king"將會在常量池中建立

二、在調用 intern方法以後,會去常量池中查找是否有等於該字符串對象的引用,有就返回引用。

三、調用 new Sting() 會在堆內存中建立一個 b的 String 對象

四、在調用 intern 方法以後,會去常量池中查找是否有等於該字符串對象的引用,有就返回引用。

因此 a  b 引用的是同一個對象

相關文章
相關標籤/搜索