jvm學(xué)習(xí)

本系列文章主要是對(duì)學(xué)習(xí)《深入理解java虛擬機(jī)》的記錄,以加深自己的理解,也方便自己后續(xù)復(fù)習(xí)回顧

前言

之前學(xué)習(xí)java,只是會(huì)用常用的語(yǔ)法、框架,但在開(kāi)發(fā)過(guò)程中,總會(huì)遇到一些奇怪的現(xiàn)象和疑惑的地方。然后覺(jué)得必須深入理解java相關(guān)的實(shí)現(xiàn)。
到現(xiàn)在已經(jīng)前前后后看了《深入理解java虛擬機(jī)》大概有四、五遍。前兩遍基本上第五章以后就不怎么看得下去了,后面幾遍才慢慢得能把整本書(shū)看完,部分重點(diǎn)的章節(jié)看了更多遍。現(xiàn)在就希望把學(xué)習(xí)理解到的jvm相關(guān)的知識(shí)記錄一下,也希望自己在記錄的過(guò)程中,能夠認(rèn)識(shí)理解的更深。

運(yùn)行時(shí)數(shù)據(jù)區(qū)

  • 程序計(jì)數(shù)器
    線程獨(dú)有的內(nèi)存區(qū)域。
    感覺(jué)和CPU里的程序計(jì)數(shù)器的意義一樣。cpu中的程序計(jì)數(shù)器通過(guò)計(jì)數(shù)來(lái)指定cpu要執(zhí)行的指令。
    jvm執(zhí)行的是字節(jié)碼,虛擬機(jī)的當(dāng)前線程根據(jù)計(jì)數(shù)來(lái)指定要執(zhí)行的字節(jié)碼。

根據(jù)虛擬機(jī)概念模型,字節(jié)碼解釋器通過(guò)改變計(jì)數(shù)器的值來(lái)選取下一條需要執(zhí)行的字節(jié)碼。

此內(nèi)存區(qū)域?yàn)槲ㄒ灰粋€(gè)虛擬機(jī)規(guī)范中沒(méi)有規(guī)定任何OutOfMemoryError的區(qū)域。

  • 虛擬機(jī)棧
    線程獨(dú)有。生命周期和當(dāng)前線程相同。
    每個(gè)方法在執(zhí)行時(shí),都會(huì)創(chuàng)建一個(gè)棧幀用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。
    每個(gè)方法被調(diào)用執(zhí)行完成的過(guò)程,對(duì)應(yīng)著棧幀在虛擬機(jī)棧入棧、出棧的過(guò)程。

  • 本地方法棧
    和虛擬機(jī)棧類似,只不過(guò)本地方法棧存儲(chǔ)的是當(dāng)前線程調(diào)用的本地方法相關(guān)的信息。


  • 所有線程共享。
    幾乎所有對(duì)象實(shí)例都在這里分配內(nèi)存。GC主要是回收這里的內(nèi)存。

  • 方法區(qū)
    所有線程共享。
    用于存儲(chǔ)虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等。

  • 運(yùn)行時(shí)常量池
    屬于方法區(qū)的一部分。
    編譯生成的class文件不僅包含對(duì)類相關(guān)的基本信息的描述,還有常量池用于描述編譯期生成的各種字面量和符號(hào)引用。當(dāng)類被加載時(shí),常量池中的信息被存在方法區(qū)里。

  • 直接內(nèi)存
    并不屬于jvm管理的內(nèi)存區(qū)域。nio分配內(nèi)存就是直接調(diào)用本地方法直接在堆外分配內(nèi)存。
    如果直接內(nèi)存申請(qǐng)的大小加上jvm分配的內(nèi)存大于機(jī)器的總內(nèi)存,就會(huì)OOM。

字面量: 字符串,一些數(shù)字類型值和final 修飾的常量等。
符號(hào)引用: 類和接口的全限定名、字段的名稱和描述符、方法的名稱和描述符。

類在內(nèi)存的布局

hotspot的實(shí)現(xiàn),對(duì)象在內(nèi)存中分3塊存儲(chǔ):對(duì)象頭(Header),實(shí)例數(shù)據(jù)(Instance data),對(duì)齊填充(padding)。
對(duì)象頭分兩部分?jǐn)?shù)據(jù):一部分為32bit/64bit的Mark Word,用來(lái)存儲(chǔ)對(duì)象的運(yùn)行時(shí)數(shù)據(jù),包括hash code,gc分代年齡,鎖狀態(tài)標(biāo)識(shí),線程持有的鎖,偏向線程id等。另一部分為類型指針,用來(lái)確定當(dāng)前對(duì)象為哪一個(gè)類的實(shí)例。
實(shí)例數(shù)據(jù)部分存儲(chǔ)本對(duì)象及其繼承下來(lái)的相關(guān)父類中的屬性字段的值。
填充部分,對(duì)象內(nèi)存塊的大小必須是8字節(jié)的整數(shù)倍,如果不夠進(jìn)行填充。

垃圾回收

jvm回收的是哪些區(qū)域的內(nèi)存?
和虛擬機(jī)相關(guān)的虛擬機(jī)棧,程序技術(shù)器,本地方法棧中的內(nèi)存在改線程停止后,所占的內(nèi)存就會(huì)被釋放。
而方法區(qū),堆內(nèi)存占用只有在運(yùn)行時(shí)知道,并且隨著 程序的運(yùn)行 占用內(nèi)存也會(huì)隨著變化。方法區(qū)中有類加載后存儲(chǔ)的類信息描述,這一塊內(nèi)存可以被回收的不多。堆中的對(duì)象是主要可以回收的區(qū)域。
GC需要解決哪些問(wèn)題?

  1. 有哪些對(duì)象是應(yīng)該被回收的?
  2. 怎么對(duì)內(nèi)存進(jìn)行回收?

1. 有哪些對(duì)象可以被回收?
只有那些永遠(yuǎn)不會(huì)被引用的對(duì)象才可以被回收。
判斷對(duì)象死亡的方法:

  • 引用計(jì)數(shù)
    原理:對(duì)象維護(hù) 一個(gè)引用計(jì)數(shù)變量。有被引用則計(jì)數(shù)加1。如果引用計(jì)數(shù)為0代表當(dāng)前對(duì)象可以被回收。
    這種方法是多數(shù)人認(rèn)為jvm實(shí)現(xiàn)的方法(起碼我大學(xué)期間是這么認(rèn)為),但是因?yàn)檫@種方法在判定循環(huán)引用(即對(duì)象a引用對(duì)象b,同時(shí)對(duì)象b引用對(duì)象a,除了它們彼此引用再?zèng)]有別的引用關(guān)系)的實(shí)現(xiàn)邏輯上比價(jià)麻煩,所以實(shí)際jvm很少有使用。
  • 可達(dá)性分析
    原理: 從一系列GC ROOT對(duì)象開(kāi)始向下搜索,搜索經(jīng)過(guò)的對(duì)象添加到對(duì)應(yīng)的引用鏈.如果一個(gè)對(duì)象沒(méi)在任何引用鏈中,則可以被回收。
    (如下圖,解決了循環(huán)引用的問(wèn)題)

    GC ROOT的選取
  • 虛擬機(jī)棧中引用的對(duì)象
  • 方法區(qū)中靜態(tài)屬性引用的對(duì)象
  • 方法區(qū)中常量引用的對(duì)象
  • 本地方法棧中引用的對(duì)象

2 垃圾收集算法

  • 標(biāo)記-清除
    先標(biāo)記出哪些對(duì)象可以回收,然后再清除可回收對(duì)象
    回收前內(nèi)存狀態(tài):


    回收后內(nèi)存狀態(tài):

    特點(diǎn)
    標(biāo)記,清除效率都不高
    回收后很多垃圾碎片

  • 復(fù)制
    將內(nèi)存分為兩塊,回收時(shí)將存活的對(duì)象完全復(fù)制到另一塊內(nèi)存中,然后將之前的內(nèi)存清空。
    回收前內(nèi)存狀態(tài):


    回收后內(nèi)存狀態(tài):

    特點(diǎn)
    實(shí)現(xiàn)效率高
    需要更多的額外內(nèi)存

  • 標(biāo)記-整理
    先標(biāo)記哪些對(duì)象可以回收,然后將可回收對(duì)象移動(dòng)到內(nèi)存的一端
    回收前內(nèi)存狀態(tài):


    回收后內(nèi)存狀態(tài):

3 分代收集
實(shí)際各種jvm都是用分代收集算法來(lái)進(jìn)行垃圾回收。
jvm把堆內(nèi)存分為新生代(對(duì)象存活率低,每次垃圾回收此區(qū)域大部分對(duì)象都被回收)和老年代(對(duì)象存活率高,每次垃圾回收此區(qū)域很少對(duì)象被回收),jvm根據(jù)對(duì)象特點(diǎn)在相應(yīng)區(qū)域分配和回收對(duì)象內(nèi)存。
新生代內(nèi)存分為:EDEN區(qū)和2個(gè)SURVIVOR區(qū),EDEN/SURVIVOR=8/1,采用復(fù)制算法進(jìn)行垃圾回收。

下面舉實(shí)例來(lái)說(shuō)明內(nèi)存分配與回收的過(guò)程
內(nèi)存配置說(shuō)明:
EDEN:8M, SURVIVOR:1M
老年代:40M

a) 新建對(duì)象a,b,c,需要內(nèi)存1m,2m,3m
b) jvm在EDEN區(qū)給a,b,c分配內(nèi)存,運(yùn)行一段時(shí)間后,對(duì)象b,c不可達(dá),處于可回收狀態(tài)
c) 新建對(duì)象d,需要內(nèi)存4m
d) 此時(shí)EDEN區(qū)只剩4m內(nèi)存不足以為對(duì)象d分配內(nèi)存,觸發(fā)minor gc
e) EDEN存活的對(duì)象為a,把對(duì)象## <p id="runningData">運(yùn)行時(shí)數(shù)據(jù)區(qū)</p>a復(fù)制到SURVIVOR_a,把EDEN區(qū)域清空
f) 把對(duì)象d分配到EDEN區(qū),此時(shí)EDEN占用4m,SURVIIVOR_a占1m
g) 新建對(duì)象e,需要對(duì)象5m
接下來(lái)分兩種情況
1 如果PretenureSizeThreshold<5m
對(duì)象e被直接分配到老年代

2 如果PretenureSizeThreshold>5m
a) EDEN區(qū)剩余內(nèi)存不足以分配,觸發(fā)minor gc
b) 把EDEN和SURVIVOR_a區(qū)中的存活對(duì)象復(fù)制到SURVIVOR_b,然后將EDEN和SURVIVOR_a清空
c) SURVIVOR_b不足以存放復(fù)制來(lái)的對(duì)象,直接把對(duì)象d移到老年代
d) 把對(duì)象e分配在EDEN

說(shuō)明
1 如果對(duì)象在SURVIVOR中經(jīng)過(guò)多次(默認(rèn)配置為15次)minor gc,沒(méi)有被回收,該對(duì)象會(huì)被移到老年代。對(duì)象年紀(jì)(經(jīng)歷過(guò)的gc次數(shù))信息在對(duì)象頭中存儲(chǔ)
2 如果老年代中的內(nèi)存不足以分配會(huì)觸發(fā)full gc,如果full gc后內(nèi)存仍不足,會(huì)OOM
3 一般來(lái)說(shuō),minor gc的頻率更高,時(shí)間更短。full gc的頻率更低,花費(fèi)時(shí)間更長(zhǎng)。

類文件結(jié)構(gòu)

現(xiàn)在基于jvm平臺(tái)的語(yǔ)言不僅有java,還有g(shù)roovy,scala和google最近一直在推的kotlin等。
所有這些語(yǔ)言的語(yǔ)法和所用的編譯器可能都不同,但只要它們編譯生成的class文件(字節(jié)碼)符合規(guī)范,就能在虛擬機(jī)上運(yùn)行。

class文件是一組以8位為單位的2進(jìn)制數(shù)據(jù)流。
class文件中有兩種數(shù)據(jù)類型:無(wú)符號(hào)數(shù)和表。class文件的數(shù)據(jù)項(xiàng)如下:


常量項(xiàng)的結(jié)構(gòu)如下:

舉例說(shuō)明
以最簡(jiǎn)單的Hello World代碼為例,分析編譯生成的class文件,來(lái)學(xué)習(xí)class的文件結(jié)構(gòu)。

Hello.java文件如下:

public class Hello{
    public static void main(String[] args){
        System.out.println("Hello World!");
    }
}

Hello.class文件如下:


class 文件分析

  1. magic code (u4)
    文件最頭4個(gè)字節(jié)為magic code:CAFE BABE。
    用來(lái)標(biāo)識(shí)此文件為可以被虛擬機(jī)接收的class文件。
  2. version
    接下來(lái)4字節(jié)為版本號(hào):0000(副版本) 0034(主版本)。
    代表class版本號(hào)為:52.0,對(duì)應(yīng)jdk1.8。
  3. 常量池?cái)?shù)量
    接下來(lái)2字節(jié)為001d(29)。
    代表常量池有28項(xiàng)常量。第0項(xiàng)常量預(yù)留,用來(lái)表達(dá)不指向任何常量的含義。
  4. 常量解析
    接下來(lái)字段為28個(gè)常量的定義。
  1. 第1個(gè)常量
    第一個(gè)字節(jié)為0A,代表為Method_ref info。
    根據(jù)上圖常量結(jié)構(gòu),得知method_ref 表中,接下來(lái)兩個(gè)U2分別指向兩個(gè)常量索引0006(const_pool的第6個(gè)常量)和 000F(const_pool的第15個(gè)常量),分別代表指向聲明方法的類描述符和指向名稱及類型的描述符。
    ** 結(jié)合下面javap 生成的文件 ,我們可以找到#6,#15,然后依次找到最終含義 **
    2)第2個(gè)常量
    第一個(gè)字節(jié) 為09,代表為Field_ref info。
    ...............后續(xù)常量解析和上面同理。

用javap 命令可以對(duì)class文件進(jìn)行分析

javap Hello.class 
Compiled from "Hello.java"
public class Hello {
  public Hello();
  public static void main(java.lang.String[]);
}

Classfile /home/fll/code/javaTest/Hello.class
  Last modified 2017-5-31; size 416 bytes
  MD5 checksum 7c04c33532f23f7d4aca1d0ec468a57f
  Compiled from "Hello.java"
public class Hello
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #18            // Hello World!
   #4 = Methodref          #19.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #21            // Hello
   #6 = Class              #22            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               Hello.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = Class              #23            // java/lang/System
  #17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;
  #18 = Utf8               Hello World!
  #19 = Class              #26            // java/io/PrintStream
  #20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V
  #21 = Utf8               Hello
  #22 = Utf8               java/lang/Object
  #23 = Utf8               java/lang/System
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (Ljava/lang/String;)V
{
  public Hello();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World!
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 4: 0
        line 5: 8
}
SourceFile: "Hello.java"

類加載機(jī)制

虛擬機(jī)把描述類的數(shù)據(jù)從class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn),解析和初始化,并最終形成可以被虛擬機(jī)直接使用的java類型

類的生命周期

類初始化時(shí)機(jī)

  1. 遇到new,getstatic,putstatic或invokestatic指令時(shí)。(使用new 實(shí)例化對(duì)象,讀取或設(shè)置類的靜態(tài)字段_** 被final 修飾定義時(shí)賦初值除外**,調(diào)用類的靜態(tài)方法)
  2. 通過(guò)反射調(diào)用一個(gè)未初始化的類
  3. 初始化一個(gè)類時(shí),要先初始化其父類
  4. 虛擬機(jī)啟動(dòng)時(shí),會(huì)初始化Main主類

類加載全過(guò)程
包括加載、驗(yàn)證、準(zhǔn)備、解析和初始化整個(gè)過(guò)程。

  • 加載
    1. 通過(guò)類的全限定名來(lái)加載類的二進(jìn)制字節(jié)流(不限定來(lái)源,可以是jar,war,反射生成,只要結(jié)構(gòu)符合類文件結(jié)構(gòu))
    2. 將這個(gè)類代表的靜態(tài)結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)的運(yùn)行時(shí)結(jié)構(gòu)
    3. 在內(nèi)存中生成代表這個(gè)類的Class對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的入口。
  • 驗(yàn)證
    確保加載的字節(jié)流符合當(dāng)前虛擬機(jī)的要求,不會(huì)危及運(yùn)行安全。
  • 準(zhǔn)備
    為類變量在內(nèi)存方法區(qū)中分配內(nèi)存并設(shè)置初始值。(這里的初始值并不是指程序制定的默認(rèn)值,而是指數(shù)據(jù)類型的零值)
private static int a =1;//此階段后a會(huì)被設(shè)置初值為0,后續(xù)初始化后才會(huì)被賦值為1
  • 解析
    將常量池內(nèi)的符號(hào)引用替換為直接引用
  • 初始化
    執(zhí)行類構(gòu)造器<clinit>()方法
    <clinit>()由編譯器收集類變量賦值語(yǔ)句和靜態(tài)語(yǔ)句塊,合并而成。收集順序和語(yǔ)句在源代碼文件中的出現(xiàn)順序一致。
    靜態(tài)語(yǔ)句塊可以放在變量定義前,但語(yǔ)句內(nèi)對(duì)變量的操作只能有賦值
/**
*可以正確執(zhí)行,輸出i值為2
**/
public class CliTest {
    static {
        i = 4;
    }
    private static int i =2;

    public static void print(){
        System.out.println(i);
    }

    public static void main(String[] args) {
        print();
    }
}
/**
*不能正確執(zhí)行,報(bào)非法向前引用
**/
public class CliTest {
  static {
      i = 4;
      i++;
  }
  private static int i =2;

  public static void print(){
      System.out.println(i);
  }

  public static void main(String[] args) {
      print();
  }
}

虛擬機(jī)會(huì)保證一個(gè)類的<clinit>()方法在多線程環(huán)境下被正確執(zhí)行。
多線程下,一個(gè)線程進(jìn)入執(zhí)行<clinit>()方法,其它線程會(huì)阻塞、等待。(但靜態(tài)語(yǔ)句塊只會(huì)被執(zhí)行一次,即使阻塞解除,其它線程也不會(huì)再執(zhí)行靜態(tài)語(yǔ)句塊)

類加載器

通過(guò)類全限定名加載類二進(jìn)制字節(jié)流的動(dòng)作是放在java虛擬機(jī)外實(shí)現(xiàn)得。我們可以通過(guò)java程序?qū)崿F(xiàn)自己的類加載器。
類的唯一性,由加載這個(gè)類的加載器和類本身確定

雙親委派模型

類加載器種類:

  • 啟動(dòng)類加載器(Bootstrap ClassLoader) :屬于java虛擬機(jī)的一部分。負(fù)責(zé)加載存放在<java_home>\lib 或 -Xbootclasspath指定路徑下的符合條件(僅通過(guò)文件名識(shí)別,如rt.jar)的類文件。
    ** 啟動(dòng)類加載器無(wú)法被程序直接引用。如果自定義類加載器,需要把加載請(qǐng)求委托給啟動(dòng)類加載器,直接用null代替即可 **
  • 擴(kuò)展類加載器(Extension ClassLoader):由ExtClassLoader實(shí)現(xiàn)。負(fù)責(zé)加載存放在<java_home>\lib\ext 或 java.ext.dirs指定的目錄下的類文件。開(kāi)發(fā)者可以直接使用。
  • 應(yīng)用程序類加載器(Application ClassLoader):由AppClassLoader實(shí)現(xiàn)。用來(lái)加載用戶類路徑上的類文件。如果用戶沒(méi)有自定義類加載器默認(rèn)用的就是這個(gè)類加載器。

雙親委派模型


除了啟動(dòng)類加載器都有自己的父類加載器。當(dāng)一個(gè)類加載器收到類加載請(qǐng)求時(shí),首先自己不會(huì)加載該類,而是把請(qǐng)求委派給自己的父類加載器。父加載器也會(huì)將請(qǐng)求委派給它的父類加載器,直到最終委派到啟動(dòng)類加載器。只有當(dāng)父加載器反饋?zhàn)约簾o(wú)法加載該類時(shí),子類才會(huì)嘗試去加載。


內(nèi)存模型

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容