JVM包含兩個子系統和兩個組件,兩個子系統爲Class loader(類裝載)、Execution engine(執行引擎);兩個組件爲Runtime data area(運行時數據區)、Native Interface(本地接口)。java
做用 :首先經過編譯器把 Java 代碼轉換成字節碼,類加載器(ClassLoader)再把字節碼加載到內存中,將其放在運行時數據區(Runtime data area)的方法區內,而字節碼文件只是 JVM 的一套指令集規範,並不能直接交給底層操做系統去執行,所以須要特定的命令解析器執行引擎(Execution Engine),將字節碼翻譯成底層系統指令,再交由 CPU 去執行,而這個過程當中須要調用其餘語言的本地庫接口(Native Interface)來實現整個程序的功能。程序員
下面是Java程序運行機制詳細說明算法
Java程序運行機制步驟編程
從上圖能夠看,java文件經過編譯器變成了.class文件,接下來類加載器又將這些.class文件加載到JVM中。
其實能夠一句話來解釋:類的加載指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,而後在堆區建立一個 java.lang.Class對象,用來封裝類在方法區內的數據結構。數組
Java 虛擬機在執行 Java 程序的過程當中會把它所管理的內存區域劃分爲若干個不一樣的數據區域。這些區域都有各自的用途,以及建立和銷燬的時間,有些區域隨着虛擬機進程的啓動而存在,有些區域則是依賴線程的啓動和結束而創建和銷燬。Java 虛擬機所管理的內存被劃分爲以下幾個區域:安全
不一樣虛擬機的運行時數據區可能略微有所不一樣,但都會聽從 Java 虛擬機規範, Java 虛擬機規範規定的區域分爲如下 5 個部分:服務器
淺拷貝(shallowCopy)只是增長了一個指針指向已存在的內存地址,數據結構
深拷貝(deepCopy)是增長了一個指針而且申請了一個新的內存,使這個增長的指針指向這個新的內存,多線程
使用深拷貝的狀況下,釋放內存的時候不會由於出現淺拷貝時釋放同一個內存的錯誤。併發
淺複製:僅僅是指向被複制的內存地址,若是原地址發生改變,那麼淺複製出來的對象也會相應的改變。
深複製:在計算機中開闢一塊新的內存地址用於存放複製的對象。
物理地址
堆的物理地址分配對對象是不連續的。所以性能慢些。在GC的時候也要考慮到不連續的分配,因此有各類算法。好比,標記-消除,複製,標記-壓縮,分代(即新生代使用複製算法,老年代使用標記——壓縮)
棧使用的是數據結構中的棧,先進後出的原則,物理地址分配是連續的。因此性能快。
內存分別
堆由於是不連續的,因此分配的內存是在運行期
確認的,所以大小不固定。通常堆大小遠遠大於棧。
棧是連續的,因此分配的內存大小要在編譯期
就確認,大小是固定的。
存放的內容
堆存放的是對象的實例和數組。所以該區更關注的是數據的存儲
棧存放:局部變量,操做數棧,返回結果。該區更關注的是程序方法的執行。
PS:
程序的可見度
堆對於整個應用程序都是共享、可見的。
棧只對於線程是可見的。因此也是線程私有。他的生命週期和線程相同。
隊列和棧都是被用來預存儲數據的。
說到對象的建立,首先讓咱們看看 Java
中提供的幾種對象建立方式:
Header | 解釋 |
---|---|
使用new關鍵字 | 調用了構造函數 |
使用Class的newInstance方法 | 調用了構造函數 |
使用Constructor類的newInstance方法 | 調用了構造函數 |
使用clone方法 | 沒有調用構造函數 |
使用反序列化 | 沒有調用構造函數 |
下面是對象建立的主要流程:
虛擬機遇到一條new指令時,先檢查常量池是否已經加載相應的類,若是沒有,必須先執行相應的類加載。類加載經過後,接下來分配內存。若Java堆中內存是絕對規整的,使用「指針碰撞「方式分配內存;若是不是規整的,就從空閒列表中分配,叫作」空閒列表「方式。劃份內存時還須要考慮一個問題-併發,也有兩種方式: CAS同步處理,或者本地線程分配緩衝(Thread Local Allocation Buffer, TLAB)。而後內存空間初始化操做,接着是作一些必要的對象設置(元信息、哈希碼…),最後執行<init>
方法。
類加載完成後,接着會在Java堆中劃分一塊內存分配給對象。內存分配根據Java堆是否規整,有兩種方式:
選擇哪一種分配方式是由 Java 堆是否規整來決定的,而 Java 堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。
對象的建立在虛擬機中是一個很是頻繁的行爲,哪怕只是修改一個指針所指向的位置,在併發狀況下也是不安全的,可能出現正在給對象 A 分配內存,指針還沒來得及修改,對象 B 又同時使用了原來的指針來分配內存的狀況。解決這個問題有兩種方案:
Java
程序須要經過 JVM
棧上的引用訪問堆中的具體對象。對象的訪問方式取決於 JVM
虛擬機的實現。目前主流的訪問方式有 句柄 和 直接指針 兩種方式。
指針: 指向對象,表明一個對象在內存中的起始地址。
句柄: 能夠理解爲指向指針的指針,維護着對象的指針。句柄不直接指向對象,而是指向對象的指針(句柄不發生變化,指向固定內存地址),再由對象的指針指向對象的真實內存地址。
Java
堆中劃分出一塊內存來做爲句柄池,引用中存儲對象的句柄地址,而句柄中包含了對象實例數據與對象類型數據各自的具體地址信息,具體構造以下圖所示:
優點:引用中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是很是廣泛的行爲)時只會改變句柄中的實例數據指針,而引用自己不須要修改。
若是使用直接指針訪問,引用 中存儲的直接就是對象地址,那麼Java
堆對象內部的佈局中就必須考慮如何放置訪問類型數據的相關信息。
優點:速度更快,節省了一次指針定位的時間開銷。因爲對象的訪問在Java
中很是頻繁,所以這類開銷聚沙成塔後也是很是可觀的執行成本。HotSpot 中採用的就是這種方式。
內存泄漏是指再也不被使用的對象或者變量一直被佔據在內存中。理論上來講,Java是有GC垃圾回收機制的,也就是說,再也不被使用的對象,會被GC自動回收掉,自動從內存中清除。
可是,即便這樣,Java也仍是存在着內存泄漏的狀況,java致使內存泄露的緣由很明確:長生命週期的對象持有短生命週期對象的引用就極可能發生內存泄露,儘管短生命週期對象已經再也不須要,可是由於長生命週期對象持有它的引用而致使不能被回收,這就是java中內存泄露的發生場景。
在java中,程序員是不須要顯示的去釋放一個對象的內存的,而是由虛擬機自行執行。在JVM中,有一個垃圾回收線程,它是低優先級的,在正常狀況下是不會執行的,只有在虛擬機空閒或者當前堆內存不足時,纔會觸發執行,掃面那些沒有被任何引用的對象,並將它們添加到要回收的集合中,進行回收。
GC 是垃圾收集的意思(Gabage Collection),內存處理是編程人員容易出現問題的地方,忘記或者錯誤的內存
回收會致使程序或系統的不穩定甚至崩潰,Java 提供的 GC 功能能夠自動監測對象是否超過做用域從而達到自動
回收內存的目的,Java 語言沒有提供釋放已分配內存的顯示操做方法。
java語言最顯著的特色就是引入了垃圾回收機制,它使java程序員在編寫程序時再也不考慮內存管理的問題。
因爲有這個垃圾回收機制,java中的對象再也不有「做用域」的概念,只有引用的對象纔有「做用域」。
垃圾回收機制有效的防止了內存泄露,能夠有效的使用可以使用的內存。
垃圾回收器一般做爲一個單獨的低級別的線程運行,在不可預知的狀況下對內存堆中已經死亡的或很長時間沒有用過的對象進行清除和回收。
程序員不能實時的對某個對象或全部對象調用垃圾回收器進行垃圾回收。
垃圾回收有分代複製垃圾回收、標記垃圾回收、增量垃圾回收。
對於GC來講,當程序員建立對象時,GC就開始監控這個對象的地址、大小以及使用狀況。
一般,GC採用有向圖的方式記錄和管理堆(heap)中的全部對象。經過這種方式肯定哪些對象是"可達的",哪些對象是"不可達的"。當GC肯定一些對象爲"不可達"時,GC就有責任回收這些內存空間。
能夠。程序員能夠手動執行System.gc(),通知GC運行,可是Java語言規範並不保證GC必定會執行。
垃圾收集器在作垃圾回收的時候,首先須要斷定的就是哪些內存是須要被回收的,哪些對象是「存活」的,是不能夠被回收的;哪些對象已經「死掉」了,須要被回收。
通常有兩種方法來判斷:
當對象對當前使用這個對象的應用程序變得不可觸及的時候,這個對象就能夠被回收了。
垃圾回收不會發生在永久代,若是永久代滿了或者是超過了臨界值,會觸發徹底垃圾回收(Full GC)。若是你仔細查看垃圾收集器的輸出信息,就會發現永久代也是被回收的。這就是爲何正確的永久代大小對避免Full GC是很是重要的緣由。
垃圾回收不會發生在永久代,若是永久代滿了或者是超過了臨界值,會觸發徹底垃圾回收(Full GC)。若是你仔細查看垃圾收集器的輸出信息,就會發現永久代也是被回收的。這就是爲何正確的永久代大小對避免Full GC是很是重要的緣由。請參考下Java8:從永久代到元數據區
(譯者注:Java8中已經移除了永久代,新加了一個叫作元數據區的native內存區)
標記無用對象,而後進行清除回收。
標記-清除算法(Mark-Sweep)是一種常見的基礎垃圾收集算法,它將垃圾收集分爲兩個階段:
標記-清除算法之因此是基礎的,是由於後面講到的垃圾收集算法都是在此算法的基礎上進行改進的。
優勢:實現簡單,不須要對象進行移動。
缺點:標記、清除過程效率低,產生大量不連續的內存碎片,提升了垃圾回收的頻率。
標記-清除算法的執行的過程以下圖所示
爲了解決標記-清除算法的效率不高的問題,產生了複製算法。它把內存空間劃爲兩個相等的區域,每次只使用其中一個區域。垃圾收集時,遍歷當前使用的區域,把存活對象複製到另一個區域中,最後將當前使用的區域的可回收的對象進行回收。
優勢:按順序分配內存便可,實現簡單、運行高效,不用考慮內存碎片。
缺點:可用的內存大小縮小爲原來的一半,對象存活率高時會頻繁進行復制。
複製算法的執行過程以下圖所示
在新生代中可使用複製算法,可是在老年代就不能選擇複製算法了,由於老年代的對象存活率會較高,這樣會有較多的複製操做,致使效率變低。標記-清除算法能夠應用在老年代中,可是它效率不高,在內存回收後容易產生大量內存碎片。所以就出現了一種標記-整理算法(Mark-Compact)算法,與標記-整理算法不一樣的是,在標記可回收的對象後將全部存活的對象壓縮到內存的一端,使他們緊湊的排列在一塊兒,而後對端邊界之外的內存進行回收。回收後,已用和未用的內存都各自一邊。
優勢:解決了標記-清理算法存在的內存碎片問題。
缺點:仍須要進行局部對象移動,必定程度上下降了效率。
標記-整理算法的執行過程以下圖所示
當前商業虛擬機都採用分代收集的垃圾收集算法。分代收集算法,顧名思義是根據對象的存活週期將內存劃分爲幾塊。通常包括年輕代、老年代 和 永久代,如圖所示:
若是說垃圾收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。下圖展現了7種做用於不一樣分代的收集器,其中用於回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,還有用於回收整個Java堆的G1收集器。不一樣收集器之間的連線表示它們能夠搭配使用。
CMS 是英文 Concurrent Mark-Sweep 的簡稱,是以犧牲吞吐量爲代價來得到最短回收停頓時間的垃圾回收器。對於要求服務器響應速度的應用上,這種垃圾回收器很是適合。在啓動 JVM 的參數加上「-XX:+UseConcMarkSweepGC」來指定使用 CMS 垃圾回收器。
CMS 使用的是標記-清除的算法實現的,因此在 gc 的時候回產生大量的內存碎片,當剩餘內存不能知足程序運行要求時,系統將會出現 Concurrent Mode Failure,臨時 CMS 會採用 Serial Old 回收器進行垃圾清除,此時的性能將會被下降。
新生代垃圾回收器通常採用的是複製算法,複製算法的優勢是效率高,缺點是內存利用率低;老年代回收器通常採用的是標記-整理的算法進行垃圾回收。
分代回收器有兩個分區:老生代和新生代,新生代默認的空間佔比總空間的 1/3,老生代的默認佔比是 2/3。
新生代使用的是複製算法,新生代裏有 3 個分區:Eden、To Survivor、From Survivor,它們的默認佔比是 8:1:1,它的執行流程以下:
每次在 From Survivor 到 To Survivor 移動時都存活的對象,年齡就 +1,當年齡到達 15(默認配置是 15)時,升級爲老生代。大對象也會直接進入老生代。
老生代當空間佔用到達某個值以後就會觸發全局垃圾收回,通常使用標記整理的執行算法。以上這些循環往復就構成了整個分代垃圾回收的總體執行流程。
所謂自動內存管理,最終要解決的也就是內存分配和內存回收兩個問題。前面咱們介紹了內存回收,這裏咱們再來聊聊內存分配。
對象的內存分配一般是在 Java 堆上分配(隨着虛擬機優化技術的誕生,某些場景下也會在棧上分配,後面會詳細介紹),對象主要分配在新生代的 Eden 區,若是啓動了本地線程緩衝,將按照線程優先在 TLAB 上分配。少數狀況下也會直接在老年代上分配。總的來講分配規則不是百分百固定的,其細節取決於哪種垃圾收集器組合以及虛擬機相關參數有關,可是虛擬機對於內存的分配仍是會遵循如下幾種「普世」規則:
多數狀況,對象都在新生代 Eden 區分配。當 Eden 區分配沒有足夠的空間進行分配時,虛擬機將會發起一次 Minor GC。若是本次 GC 後仍是沒有足夠的空間,則將啓用分配擔保機制在老年代中分配內存。
這裏咱們提到 Minor GC,若是你仔細觀察過 GC 平常,一般咱們還能從日誌中發現 Major GC/Full GC。
所謂大對象是指須要大量連續內存空間的對象,頻繁出現大對象是致命的,會致使在內存還有很多空間的狀況下提早觸發 GC 以獲取足夠的連續空間來安置新對象。
前面咱們介紹過新生代使用的是標記-清除算法來處理垃圾回收的,若是大對象直接在新生代分配就會致使 Eden 區和兩個 Survivor 區之間發生大量的內存複製。所以對於大對象都會直接在老年代進行分配。
虛擬機採用分代收集的思想來管理內存,那麼內存回收時就必須判斷哪些對象應該放在新生代,哪些對象應該放在老年代。所以虛擬機給每一個對象定義了一個對象年齡的計數器,若是對象在 Eden 區出生,而且可以被 Survivor 容納,將被移動到 Survivor 空間中,這時設置對象年齡爲 1。對象在 Survivor 區中每「熬過」一次 Minor GC 年齡就加 1,當年齡達到必定程度(默認 15) 就會被晉升到老年代。
虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗,解析和初始化,最終造成能夠被虛擬機直接使用的java類型。
Java中的全部類,都須要由類加載器裝載到JVM中才能運行。類加載器自己也是一個類,而它的工做就是把class文件從硬盤讀取到內存中。在寫程序的時候,咱們幾乎不須要關心類的加載,由於這些都是隱式裝載的,除非咱們有特殊的用法,像是反射,就須要顯式的加載所須要的類。
類裝載方式,有兩種 :
1.隱式裝載, 程序在運行過程當中當碰到經過new 等方式生成對象時,隱式調用類裝載器加載對應的類到jvm中,
2.顯式裝載, 經過class.forname()等方法,顯式加載須要的類
Java類的加載是動態的,它並不會一次性將全部類所有加載後再運行,而是保證程序運行的基礎類(像是基類)徹底加載到jvm中,至於其餘類,則在須要的時候才加載。這固然就是爲了節省內存開銷。
實現經過類的權限定名獲取該類的二進制字節流的代碼塊叫作類加載器。
主要有一下四種類加載器:
類裝載分爲如下 5 個步驟:
在介紹雙親委派模型以前先說下類加載器。對於任意一個類,都須要由加載它的類加載器和這個類自己一同確立在 JVM 中的惟一性,每個類加載器,都有一個獨立的類名稱空間。類加載器就是根據指定全限定名稱將 class 文件加載到 JVM 內存,而後再轉化爲 class 對象。
類加載器分類:
雙親委派模型:若是一個類加載器收到了類加載的請求,它首先不會本身去加載這個類,而是把這個請求委派給父類加載器去完成,每一層的類加載器都是如此,這樣全部的加載請求都會被傳送到頂層的啓動類加載器中,只有當父加載沒法完成加載請求(它的搜索範圍中沒找到所需的類)時,子加載器纔會嘗試去加載類。
當一個類收到了類加載請求時,不會本身先去加載這個類,而是將其委派給父類,由父類去加載,若是此時父類不能加載,反饋給子類,由子類去完成類的加載。
JDK 自帶了不少監控工具,都位於 JDK 的 bin 目錄下,其中最經常使用的是 jconsole 和 jvisualvm 這兩款視圖監控工具。
做者:ThinkWon
來源: https://thinkwon.blog.csdn.ne...