JVM第一彈


JVM第一彈

基本概念

JVM是可運行java代碼的假想計算機,包括一套字節碼指令集,一組寄存器,一個棧,一個垃圾回收、堆和一個存儲方法域。JVM是運行在操作系統之上的,它與硬件沒有直接的交互。

運行過程

我們都知道Java代碼源文件,通過編譯器能夠產生相應的.Class字節碼文件,而字節碼文件又通過Java虛擬機中的解釋器,編譯成特定機器上的機器碼。

① Java源文件 ——> 編譯器 ——> 字節碼文件
② 字節碼文件 ——> JVM ——> 機器碼

每種平台的解釋器是不同的,但是虛擬機是相同的,這也就是java為什么能夠跨平台的原因了。當一個程序從開始運行,這時虛擬機就開始實例化了,多個程序 啟動就會存在多個虛擬機實例。
程序退出或者關閉,則虛擬機實例消亡,多個虛擬機實例之間數據不能共享。

類加載器

什么是類的加載?
類的加載是指將類的字節碼文件數據讀入到內存中,將其放在運行時數據區的方法區內,然后在堆區創建一個java.lang.Class對象,用來封裝類在方法區內的數據結構。
類的加載的最終產品是位於堆區內中的Class對象,Class對象封裝了類在方法區內的數據結構,並且向java程序員提供了訪問方法區內的數據結構的接口。

類加載器包括:

  1. 啟動類加載器(BootStrap)
    ——主要有C++進行實現的。用來加載jdk安裝目錄下的:jre/lib下的可執行jar包。
    也可以通過設置 -XbootClasspath來動態指定jar包位置。在java代碼中無法獲取到該對象。

String str = new String("HelloWorld");
System.out.println(str.getClass().getClassLoader());

//控制台打印null
  1. 擴展類加載器(ExtClassLoader)

    ——是java代碼實現的,用來加載java安裝目錄下 jre/lib/ext 目錄中的可執行jar包。

  2. 應用程序類加載器(AppClassLoader)

    ——是java代碼實現的,用來加載用戶編寫的代碼。我們新建一個類,獲取其類加載器就是AppClassLoader


public class MyClassLoaderTest {

    public static void main(String[] args) {
        String str = new String("HelloWorld");
        // 打印null
        System.out.println(str.getClass().getClassLoader());


        // 打印sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println(MyClassLoaderTest.class.getClassLoader());
        // 打印sun.misc.Launcher$ExtClassLoader@4554617c
        System.out.println(MyClassLoaderTest.class.getClassLoader().getParent());
        // 打印null
        System.out.println(MyClassLoaderTest.class.getClassLoader().getParent().getParent());

    }

}

由上述代碼可見: AppClassLoader extend ExtClassLoader extend BootstrapClassLoader

  1. 用戶自定義類加載器
    —— 用戶編寫類繼承自 java.lang.ClassLoader

為了防止用戶自定義類與jdk自帶的類沖突,jdk內有雙親委派機制和沙箱機制。

雙親委派機制

上述過程中,我們認識到了類加載器之間的繼承關系。當java在加載類的時候,由AppClassLoader委派其父類ExtClassLoader進行加載,ExtClassLoader會再次委派其父類BootStrapClassLoader進行加載,
如果BootStrapClassLoader找到該類那么加載該類返回該類的Class對象,但是,如果此時BootStrapClassLoader沒有找到該類,
那么就需要ExtClassLoader自身進行加載,如果ExtClassLoader找到該類那么加載該類返回該類的Class對象,
但是,如果ExtClassLoader也沒有找到該類,那么就要由AppClassLoader進行加載。
如果最后AppClassLoader也沒有找到該類,那么就會拋出 ClassNotFoundException

類加載器沒有向下尋找,沒有getChild只有getParent

如果你自己定義了一個與jdk自帶類名包名一致的類,那么java也不會去加載該類。

JVM結構

JVM內存區域主要分為

  • 線程私有區域
  1. 程序計數器
  2. 虛擬機棧
  3. 本地方法區
  • 線程共享區域
  1. Java堆
  2. 方法區
  • 直接內存

生命周期

  1. 線程私有數據區域生命周期與線程相同,依賴用戶線程的啟動/結束而創建/銷毀。
  2. 線程共享區域隨着虛擬機的啟動/關閉 而 創建/銷毀。

方法區和堆是所有線程共享的內存區域;而java棧、本地方法棧和程序計數器是運行時線程私有的內存區域。

  • 方法區
    主要存放靜態變量,常量,Class類模板(接口定義,構造函數),運行時常量池。

  • java堆(Heap),是Java虛擬機所管理的內存中最大的一塊。Java堆是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內存。又被稱作為運行時數據區。

  • 程序計數器(Program Counter Register),是一塊比較小的內存空間,它的作用可以看做是當前線程所執行的字節碼的行號指示器。每個線程都有一個私有的,可以理解為它是一個指針,指向方法字節碼地址,用來標記下一個要執行的方法字節碼地址。

  • JVM棧(JVM Stacks),與程序計數器一樣,Java虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命周期與線程相同,線程結束棧內存也就釋放了,對於棧來說不存在來及回收的問題。主要保存八大基本數據類型的變量、對象的引用變量以及實例方法。虛擬機棧描述的是Java方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀(Stack Frame)用於存儲局部變量表、操作棧、動態鏈接、方法出口等信息。每一個方法被調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。

  • 本地方法棧(Native Method Stacks),與c/c++交互的一塊區域,本地方法棧(Native Method Stacks)與虛擬機棧所發揮的作用是非常相似的,其區別不過是虛擬機棧為虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是為虛擬機使用到的Native方法服務。

垃圾回收器

  • Serial收集器,串行收集器是最古老,最穩定以及效率高的收集器,可能會產生較長的停頓,只使用一個線程去回收。
  • ParNew收集器,ParNew收集器其實就是Serial收集器的多線程版本。
  • Parallel收集器,Parallel Scavenge收集器類似ParNew收集器,Parallel收集器更關注系統的吞吐量。
  • Parallel Old 收集器,Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法
  • CMS收集器,CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。
  • G1收集器,G1 (Garbage-First)是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高概率滿足GC停頓時間要求的同時,還具備高吞吐量性能特征

調優命令

Sun JDK監控和故障處理命令有 jps jstat jmap jhat jstack jinfo

  • jps,JVM Process Status Tool,顯示指定系統內所有的HotSpot虛擬機進程。
  • jstat,JVM statistics Monitoring是用於監視虛擬機運行時狀態信息的命令,它可以顯示出虛擬機進程中的類裝載、內存、垃圾收集、JIT編譯等運行數據。
  • jmap,JVM Memory Map命令用於生成heap dump文件
  • jhat,JVM Heap Analysis Tool命令是與jmap搭配使用,用來分析jmap生成的dump,jhat內置了一個微型的HTTP/HTML服務器,生成dump的分析結果后,可以在瀏覽器中查看
  • jstack,用於生成java虛擬機當前時刻的線程快照。
  • jinfo,JVM Configuration info 這個命令作用是實時查看和調整虛擬機運行參數。

調優工具

常用調優工具分為兩類

  1. jdk自帶監控工具:jconsole和jvisualvm
  • jconsole,Java Monitoring and Management Console是從java5開始,在JDK中自帶的java監控和管理控制台,用於對JVM中內存,線程和類等的監控
  • jvisualvm,jdk自帶全能工具,可以分析內存快照、線程快照;監控內存變化、GC變化等。
  1. 第三方有:MAT(Memory Analyzer Tool)、GChisto。
  • MAT,Memory Analyzer Tool,一個基於Eclipse的內存分析工具,是一個快速、功能豐富的Java heap分析工具,它可以幫助我們查找內存泄漏和減少內存消耗
  • GChisto,一款專業分析gc日志的工具

你知道哪些JVM性能調優

  • 設定堆內存大小
    -Xmx:堆內存最大限制。

  • 設定新生代大小。
    新生代不宜太小,否則會有大量對象涌入老年代
    -XX:NewSize:新生代大小
    -XX:NewRatio 新生代和老生代占比
    -XX:SurvivorRatio:伊甸園空間和幸存者空間的占比

  • 設定垃圾回收器
    年輕代用 -XX:+UseParNewGC
    年老代用-XX:+UseConcMarkSweepGC

什么時候出現棧溢出

遞歸操作,程序沒有出口會一直進行壓棧操作

為什么會出現棧溢出

棧的深度不夠了

堆內存

邏輯上分為

  • 新生區
  • 養老區
  • 永久區

物理上分為

新生區 、 養老區、 永久區

又將新生區分為了三個區

  • 伊甸園區(80%)
  • 幸存者from區(10%)
  • 幸存者to區(10%)

新new的對象都放在伊甸園區,存活率2%,其他對象都被垃圾回收器回收
沒有被垃圾回收幸存下來的對象將會保存到幸存者區
當伊甸園區內存不足時,會進行輕量級(minor GC)垃圾回收,將幸存者from區和伊甸園區的還在用的對象移動到幸存者to區,
然后清空幸存者from區和伊甸園區,幸存者from區清空之后會交換from區和to區,保證to區始終是空的。注意from區向to區移動之前會判斷對象的年齡,
如果大於15,直接移動到養老區。年齡計數的原理:垃圾回收器回收一次,幸存活一次加一歲。
如果養老區的內存也不夠用了,就會觸動重量級GC(full GC)將養老區和新生區全量級回收垃圾對象。如果FullGC之后養老區的內存還是不夠用,那么會引發OOM。

如果程序一開始就new了一個比伊甸園區大的對象,伊甸園區沒有足夠的空間存放應該如何存放呢?此時會將對象存放到養老區,如果養老區也不夠存儲,那么會引發OOM。

對象分配規則

對象優先分配在Eden區,如果Eden區沒有足夠的空間時,虛擬機執行一次Minor GC。

大對象直接進入老年代(大對象是指需要大量連續內存空間的對象)。這樣做的目的是避免在Eden區和兩個Survivor區之間發生大量的內存拷貝(新生代采用復制算法收集內存)。

長期存活的對象進入老年代。虛擬機為每個對象定義了一個年齡計數器,如果對象經過了1次Minor GC那么對象會進入Survivor區,之后每經過一次Minor GC那么對象的年齡加1,知道達到閥值對象進入老年區。

動態判斷對象的年齡。如果Survivor區中相同年齡的所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象可以直接進入老年代。
空間分配擔保。每次進行Minor GC時,JVM會計算Survivor區移至老年區的對象的平均大小,如果這個值大於老年區的剩余值大小則進行一次Full GC,如果小於檢查HandlePromotionFailure設置,如果true則只進行Monitor GC,如果false則進行Full GC。

產生OOM的原因?

  • java設置的堆內存不夠,可以通過設置 -Xms -Xmx 來調整堆內存的大小
  • java內存中創建了大量的大對象,並且長時間不能被垃圾回收器回收

java8與元數據

在java8中,永久代已經移除了,被“元數據”(元空間)的區域所取代。元空間的本質和永久代類似,元空間與永久代的最大區別在於:
元空間並不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制。
類的源數據放入本定內存中,字符串和類的靜態變量放到java堆中,這樣可以加載多少類的元數據就不再由MaxPermSize控制,而由系統的實際可用空間來控制。

垃圾回收與算法

如果確定垃圾

  1. 引用計數法
    在java中,引用和對象是有關聯的。如果要操作對象則必須用引用進行。因此,一個簡單的方法就是通過引用計數來判斷一個對象是否可以回收。簡單來說,即一個對象如果沒有任何與之關聯的引用,即他們的引用計數都不為0,則說明對象不太可能再被用到,那么這個對象就是可回收對象。

  2. 可達性分析
    為了解決引用計數法的循環引用問題,java使用了可達性分析的方法。通過一系列的“GC roots”對象作為起點搜索,如果在“GC roots”和一個對象之間沒有可達路徑,則稱該對象是不可達的。

注意 不可達並不等價於可回收對象,不可達對象變為可回收對象至少要經過兩次標記過程。兩次標記后仍是可回收對象,則將面臨回收。

  1. 標記清楚算法

最基礎的垃圾回收算法,分為兩個階段:標記清楚。標記階段是標記出來所有要回收的對象,清楚階段回收被標記的對象所占的空間。

該算法的缺點:
內存碎片化嚴重,垃圾清理完成后,造成很多內存空間不連續。后續可能發生大對象不能找到可利用的問題。

MajorGC使用該算法

  1. 復制算法
    為了解決標記清楚算法內存碎片化的缺陷而提出的算法。按照內存容量將內存划分為等大小的兩塊。每次只使用其中一塊,當這一塊內存滿后將尚存活的對象復制到另一塊上去,把已使用的內存清理掉。

MinorGC使用該算法

缺點:
這種算法雖然實現簡單,內存效率高,不易產生碎片,但是最大的問題是可以用內存被壓縮到了原本的一半。且存活對象增多的話,copying算法的效率也大大降低。

  1. 標記整理算法
    結合以上兩個算法,為了避免缺陷而提出。標記階段和標記清楚算法相同,標記后不是清理對象,而是將存活對象移向內存的一端。然后清楚端邊界的對象.

  1. 分代收集算法
    分代收集算法是目前大部分JVM所采用的方法,其核心思想是根據對象村花的不同生命周期將內存划分為不同的域,一般情況下將GC堆划分為老生代和新生代。老生代的特點是每次垃圾回收只有少量對象需要被回收,新生代的特點是每次垃圾回收是都有大量垃圾需要被回收,因此可以根據不同區域采用不同的算法。

6.1. 新生代與復制算法

目前大部分的JVM的GC對於新生代都采取了copying方法,因為新生代中每次垃圾回收都要回收大部分對象,
即要復制的操作比較少,但通常並不是按照1:1來划分新生代。一般將新生代划分為一塊較大的Eden空間和兩個比較小的Surviror空間(FromSpace,ToSpace),每次使用Eden空間和其中的一塊Surivor空間,當進行回收時,將該兩塊空間中還存活的對象復制到另外一塊Survivor空間中。

6.2 老年代與標記復制算法
而老年代因為每次只回收少量的對象,因此采用Mark-Compact算法。

  1. JAVA虛擬機提到過的處於方法區的永生帶,它用來存儲class類,常量、方法描述等。對永生代的回收主要包括廢棄常量和無用的類

  2. 對象的內存分配主要在新生代的EdenSpace和SurvivorSpace的FormSpace(Survivor目前存放對象的那一塊),少數情況會直接分配到老生代。

  3. 當新生代的EdenSpace和FromSpace空間不足時就會發生一次GC,進行GC后,EdenSpace和FromSpace區的存活對象會被移動到ToSpace,然后將EdenSpace和FromSpace進行清理。

  4. 如果ToSpace無法足夠存儲某個對象,則將這個對象存儲到老生代。

  5. 進行GC后,使用的便是EdenSpace和ToSpace了,如此反復循環。

  6. 當對象在Survivor區躲過一次GC后,其年齡就會+1。默認情況下年齡達到15的對象就會移動到老生代中

Java中的四種引用

強引用

在Java中最常見的就是強引用,把一個對象賦值給一個引用變量,這個引用變量就是一個強引用。
當一個對象被強引用變量引用時,它處於可達狀態,它是不可能被垃圾回收機制回收的,即使該對象以后永遠都不會被用到,JVM也不會回收。因此強引用是造成Java內存泄漏主要原因之一。

軟引用

軟引用需要使用SoftReference類來實現,對於只有軟引用的對象來說,當系統內存足夠時他不會被回收,當系統內存足夠用時,它不會被回收,當系統內存不足時它會被回收。軟引用通常用在對內存敏感的程序中。

弱引用

弱引用需要用WeakReference類來實現,它比軟引用的生存期更短,對於只有弱引用的對象來說,只要垃圾回收機制一運行,不管JVM的內存空間足夠,總會回收該對象占用的內存。

虛引用

虛引用需要PhantomReference類來實現,它不能單獨使用,必須和引用隊列聯合使用。虛引用的主要作用是跟蹤對象被垃圾回收的狀態。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM