深入理解JVM-Class文件結(jié)構(gòu)和類加載

先談談JVM

這篇文章主要是講class文件和類加載機制,但是整個過程都和jvm密切相關(guān),所以先從jvm說起。

JVM.jpg

Java之所以跨平臺【我們把處理器和操作系統(tǒng)的整體稱為平臺】,得益于java編譯后生成的存儲字節(jié)碼的文件,即class文件以及虛擬機的實現(xiàn)。

JVM

JVM(Java Virtual Machine) 就是我們平時一直說的java虛擬機,是一種軟件實現(xiàn),是整個java實現(xiàn)跨平臺的核心部分,可以運行class格式的類文件,jvm屏蔽了平臺,使得java程序只需要在各自平臺上的虛擬機上運行,可以實現(xiàn)同一個class文件跨平臺運行。

JVM、JDK、JRE的關(guān)系

JRE: Java Runtime Environment   --java運行環(huán)境,  不包含開發(fā)工具(編譯器,調(diào)試器),包含jvm
JDK:Java Development Kit  --java開發(fā)工具包 包含jre

JRE: 包含了java虛擬機,java基礎(chǔ)類庫。是使用java語言編寫的程序運行所需要的軟件環(huán)境,是提供給想運行java程序的用戶使用的。
JDK:編寫java程序所需的開發(fā)工具包,供開發(fā)使用。JDK包含了JRE,同時還包含了編譯java源碼的編譯器javac,還包含了很多java程序調(diào)試和分析的工具:jconsole,jvisualvm等工具軟件,還包含了java程序編寫所需的文檔和demo例子程序。

運行java程序,安裝JRE就可以了。

編寫java程序,安裝JDK即可。

三者關(guān)系是jdk包含jre,jre包含jvm,如果要用圖片描述,如圖所示。(其中jre除了包含jvm,還包含了類似rt.jar這樣的基礎(chǔ)類庫和其它軟件環(huán)境,jdk除了包含jre還包含上述的其它工具等)

關(guān)系圖.png

JVM實例和jvm執(zhí)行引擎實例

  • jvm實例 對應一個獨立運行的java程序,進程級別當一個java程序啟動時,一個jvm實例誕生,當該程序關(guān)閉時,jvm實例隨之消亡。如果一個計算機運行多個java程序,每個java程序都運行它自己的jvm實例。
  • jvm執(zhí)行引擎實例則是對應所屬程序的線程,線程級別

jvm生命周期

  • 啟動:當啟動一個java程序時,一個jvm實例就誕生了,任何一個擁有main方法的class都可以作為jvm實例運行的起點。
  • 運行:main()函數(shù)作為程序初始線程起點,其它線程由該線程啟動,包括守護線程(daemon)和non-daemon(普通線程)。守護線程是JVM自己使用的線程比如GC線程就是個守護線程,只要這個jvm實例還有普通線程執(zhí)行,就不會停止,但是可以用exit()強制終止程序。
  • 消亡:所有非守護線程退出時,JVM實例結(jié)束生命,若安全管理器允許,程序也可以使用java.lang.Runtime類或者System.exit(0)來退出。實際上exit也是用到Runtime類來退出,Runtime是個神奇的類,它還可以用于啟動和關(guān)閉非java進程。
//查看java.lang.System類的部分源碼    
public static void exit(int status) {       
    Runtime.getRuntime().exit(status);   
}

再談談Class文件

各種不同平臺的虛擬機和平臺都統(tǒng)一使用一種存儲格式-字節(jié)碼,是和平臺無關(guān)的基石

大致過程為
java編譯器將java源程序生成與平臺無關(guān)的字節(jié)碼文件(即class文件),然后jvm對字節(jié)碼文件解釋執(zhí)行,不同系統(tǒng)的jvm解釋器將其解釋執(zhí)行為各自的機器編碼。
可以看出:java依賴于jvm,jvm給java提供了運行環(huán)境,但解釋器與平臺相關(guān),不同系統(tǒng)有不同的jvm,因此可以說java是跨平臺的【雖說跨平臺,但也要考慮jdk版本的差異性】,但jvm不是跨平臺的。

圖片來源

平臺無關(guān)性.png

虛擬機不關(guān)心class的來源是什么語言,java,scala,JRuby這些基于jvm的語言等通過各自的編譯器都可以生成class文件。jvm和java準確來說沒啥關(guān)系,準確來說應該是和class文件有關(guān)系,但是java生成class需要jvm。

一些概念

字節(jié)碼(Byte-code)

不同平臺的虛擬機與所有平臺統(tǒng)一使用的程序存儲格式。是一種包含執(zhí)行程序,由一序列 op 代碼/數(shù)據(jù)對組成的二進制文件,是一種中間碼。字節(jié)是電腦里的數(shù)據(jù)量單位。

class文件

class文件是一組以8位字節(jié)為基礎(chǔ)單位的二進制流,各個數(shù)據(jù)項目嚴格按照順序緊湊排在class文件,中間無分隔符,這使得class文件里幾乎都是程序運行的必要數(shù)據(jù)。【當遇到需占用8位以上字節(jié)的數(shù)據(jù)時,則會按高位在前的方式分割成多個8位字節(jié)存儲】

class文件是一組以8位字節(jié)為基礎(chǔ)單位的二進制流。

這邊以前一直有個問題困擾著我,class文件是不是二進制文件。因為在看深入理解jvm虛擬機的時候看到這么一句話,但是用16進制查看工具查看class文件的時候又出現(xiàn)包括有CAFEBABE等內(nèi)容。而且如果class文件如果是二進制,那么計算機直接就能運行了,為什么還需要jvm呢。

計算機最后只認0和1(即二進制),即使是復雜的程序最終到cpu執(zhí)行層面還是一串串0和1的指令。
【class文件其實是特殊的二進制文件,用UltraEdit打開確實是0和1,用hex這樣的16進制工具打開為16進制】Class文件中包含了Java虛擬機指令集和符號表以及若干其他輔助信息。

class文件并不是機器語言而是二進制文件
機器語言指的是硬件能直接運行的二進制指令代碼

魔數(shù)與class文件

每個Class文件的頭4個字節(jié)稱為魔數(shù)(Magic Number),它唯一作用就是用來確定文件是否能被虛擬機接受。

很多文件存儲標準中都用魔數(shù)進行身份標識,如圖片gif,jpeg都在文件頭部中存儲著魔數(shù)。使用魔數(shù)而不是用擴展名來進行識別主要是基于安全考慮,因為擴展名我們可以隨意通過重命名等方式改動。

有趣的是class文件的魔數(shù)很貼切java的圖標含義-CAFFBABE,即咖啡寶貝。之所以說是4個字節(jié),因為這是用16進制打開的,比如說CA實際上是一個字節(jié)。

緊跟在魔數(shù)4個字節(jié)后的是版本號,這里第五個字節(jié)和第六個字節(jié)是次版本號,第七和第八字節(jié)是主版本號。

按十進制的話,jdk1.7是51,jdk1.8是52,這里我使用jdk1.8編譯生成的某個class文件用Hex Fiend打開,這邊是1.8說明可以被1.8以上版本的虛擬機運行,但是不能被以下的運行。

下面編譯一個基礎(chǔ)的java類

魔數(shù)和版本號

圖上主版本號為第7,8字節(jié),就是00 34,之所以兩位代表一個字節(jié)是因為在16進制中

1、1字節(jié) = 8位(8個二進制位) 1Byte = 8bit;
2、一個十六進制 = 4個二進制位
3、1字節(jié) = 2個十六進制

class文件采用一種偽結(jié)構(gòu)存儲數(shù)據(jù),這種結(jié)構(gòu)只有兩種數(shù)據(jù)類型。1.無符號數(shù) 2.表

存儲類型 含義 舉例[類型-名稱-數(shù)目]
無符號數(shù) 基本數(shù)據(jù)類型,以u1,u2,u4,u8一定字節(jié)數(shù)目的無符號數(shù), u4-magic-1 u2-methods_count-1
由多個無符號數(shù)或者其它表作為數(shù)據(jù)項構(gòu)成的復合數(shù)據(jù)類型,以_info結(jié)尾 method_info-methods-methods_count

無論是無符號數(shù)還是表。當需要描述同一類型但不明確數(shù)量時,經(jīng)常使用前置的容量計數(shù)器加上若干個連續(xù)的數(shù)據(jù)項形式【如列表中methods_info類型】

jvm常量池

常量池跟在主次版本后,跟著是常量池入口【由于常量的數(shù)量不固定,所以先放個u2類型的數(shù)據(jù)代表常量池容量計數(shù)器。】

常量池中每一項常量都是一個表,jdk1.7有14種結(jié)構(gòu)不同的表結(jié)構(gòu),這14個表有個共同特點,就是表開始的第一位都是一個u1類型的標志位,JVM根據(jù)這個標志位[tag]來確定某個常量池項表示什么類型的字面量,比如tag為1就是指CONSTANT_utf8_info

常量池.png

常量池類型表【可以看到為】

類 型 標 志 描 述
CONSTANT_utf8_info 1 UTF-8編碼的字符串
CONSTANT_Integer_info 3 整形字面量
CONSTANT_Float_info 4 浮點型字面量
CONSTANT_Long_info 5 長整型字面量
CONSTANT_Double_info 6 雙精度浮點型字面量
CONSTANT_Class_info 7 類或接口的符號引用
CONSTANT_String_info 8 字符串類型字面量
CONSTANT_Fieldref_info 9 字段的符號引用
CONSTANT_Methodref_info 10 類中方法的符號引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符號引用
CONSTANT_NameAndType_info 12 字段或方法的符號引用
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_MothodType_info 16 標志方法類型
CONSTANT_InvokeDynamic_info 18 表示一個動態(tài)方法調(diào)用點

該圖片出處

常量池各個表結(jié)構(gòu)

這里舉個例子,這里編寫個非常簡單的java類

package jvm;
public class Pool {
    private String b = "常量池";
}

編譯成class過后,用16進制工具打開,文本大概是這樣的

cafe babe 0000 0034 0016 0a00 0500 1108
0012 0900 0400 1307 0014 0700 1501 0001
6201 0012 4c6a 6176 612f 6c61 6e67 2f53
7472 696e 673b 0100 063c 696e 6974 3e01
0003 2829 5601 0004 436f 6465 0100 0f4c
696e 654e 756d 6265 7254 6162 6c65 0100
124c 6f63 616c 5661 7269 6162 6c65 5461
626c 6501 0004 7468 6973 0100 0a4c 6a76
6d2f 506f 6f6c 3b01 000a 536f 7572 6365
4669 6c65 0100 0950 6f6f 6c2e 6a61 7661
0c00 0800 0901 0009 e5b8 b8e9 878f e6b1
a00c 0006 0007 0100 086a 766d 2f50 6f6f
6c01 0010 6a61 7661 2f6c 616e 672f 4f62
6a65 6374 0021 0004 0005 0000 0001 0002
0006 0007 0000 0001 0001 0008 0009 0001
000a 0000 0039 0002 0001 0000 000b 2ab7
0001 2a12 02b5 0003 b100 0000 0200 0b00
0000 0a00 0200 0000 0300 0400 0400 0c00
0000 0c00 0100 0000 0b00 0d00 0e00 0000
0100 0f00 0000 0200 10
這邊u4類型的魔數(shù)為cafebabe,u2類型的次版本號為0034,16進制的34轉(zhuǎn)為10進制為52,說明是jdk1.8編譯生成的class文件,
次版本號的后面跟著就是常量池的東西了
首先入口是u2類型的0016說明有22-1即21個常量
常量池對應16進制 tag 對應結(jié)構(gòu) 除了tag其它說明【對照表格】 順序
0a 0005 0011 0a[ CONSTANT_Methodref_info] {tag:u1 index:u2 index:u2} 第一個u2類型的描述指向CONSTANT_Class_info的index[5],第二個 u2類型描述指向CONSTANT_NameAndType_info的index[此處11轉(zhuǎn)10進制為17]組成 1
08 0012 08[ CONSTANT_String_info] {tag:u1 index:u2} 這里的u2描述指向字符串字面量索引(18),說明這個字面量的位置在第18個常量項中 2
09 0004 0013 09[CONSTANT_Fieldref_info] {tag:u1 index:u2 index:u2} u2:指向CONSTANT_Class_info的索引項[4] u2:指向CONSTANT_NameAndType_info的索引項[19] 3
07 0014 07[CONSTANT_Class_info] {tag:u1 index:u2} u2:指向類的全限定名的索引[20] 4
07 0015 07[CONSTANT_Class_info] {tag:u1 index:u2} u2:指向類的全限定名的索引[21] 5
01 0001 62 01[CONSTANT_utf8_info] {tag:u1 length:u2 bytes:u1} 長度為1的utf-8編碼字符串 這里b的utf-8編碼就是62(這里兩個為一個字節(jié)) 6
01 0012 4c6a 6176 612f 6c61 6e67 2f53 7472 696e 673b 01[CONSTANT_utf8_info] {tag:u1 length:u2 bytes:u1} 長度為length(18)的utf-8編碼字符串,就是從4c6a到673b共有18個字節(jié),而4c6a 6176 612f 6c61 6e67 2f53 7472 696e 673b的utf-8編碼剛好為Ljava/lang/String;具體可到http://hexutf8.com/驗證 7
01 0006 3c69 6e69 743e 0100 0328 2956 01[CONSTANT_utf8_info] {tag:u1 length:u2 bytes:u1} 長度為6, 3c69 6e69 743e 即<init> 8
01 0003 282956 01[CONSTANT_utf8_info] {tag:u1 length:u2 bytes:u1} length:3 282956 即()V 9
01 0004 436f 6465 01[CONSTANT_utf8_info] {tag:u1 length:u2 bytes:u1} length:4 436f 6465 即Code 10
...依次類推 ... ... ...
01 0008 6a 766d 2f50 6f6f 01[CONSTANT_utf8_info] {tag:u1 length:u2 bytes:u1} length:8 6a 766d 2f50 6f6f 即jvm/Pool 20
01 0010 6a61 7661 2f6c 616e 672f 4f626a65 6374 01[CONSTANT_utf8_info] {tag:u1 length:u2 bytes:u1} length:16 6a61 7661 2f6c 616e 672f 4f626a65 6374 即java/lang/Object 21

常量池作為占用class文件空間最大的數(shù)據(jù)項目之一【數(shù)目不固定,可以很大】,明明就寫了幾行代碼卻包含這么多各種信息,如果一一看16進制文件再比對含義會十分繁瑣。

好在oracle提供了javap -verbose的命令可以幫助我們輸出常量表【還是以上面的Pool.class為例】

輸出結(jié)果如下


注意點

1.常量池可以理解為class文件中的資源倉庫,有很多種類型,主要存放兩大常量
①.字面量 
字面量就是通俗理解的java常量,如文本字符串,final修飾的常量值等
②.符號引用
符號引用屬于編譯原理的概念,主要包含以下三種
Ⅰ類和接口的全限定名
Ⅱ字段的名稱和描述符
Ⅲ方法的名稱和描述符
2.不同與其它計數(shù)器,常量池容量計數(shù)器它是以1開頭,之所以這樣是因為要滿足需要表達"不引用任何一個常量池項目"這樣的特定情況,這種情況用0表示。

值的一提的是,class文件中的方法、字段都需要引用CONSTANT_Utf8_info型常量來描述名稱,所以受它最大長度的限制【u2型能表示的最大長度為65535bytes即64kb(注意這里指的是長度的大小)】,因此超過64KB的變量或方法名,將無法編譯【正常也沒人寫那么長 = =】。

運行時常量池、字符串常量池的區(qū)別

運行時常量池: 就是上述javap -v后后描述的的常量池,包括有字面量引用和符號引用。

運行時常量池存在方法區(qū)中,相較于class常量池,運行時常量池更具有動態(tài)性。class文件中除了有類的版本字段方法接口等描述信息外,還有一項信息是常量池用于存放編譯器生成的字面量和符號引用

  • 字面量:1.文本字符串 2.八種基本類型的值 3.被聲明為final的常量等 【字面量可以使用string.intern()動態(tài)添加】
  • 符號引用:1.類和方法的全限定名 2.字段的名稱和描述符 3.方法的名稱和描述符
引用類型.png
字符串常量池
在目JDK1.7的HotSpot中,已經(jīng)把原本放在永久帶的字符串常量池移除,之后的字符串常量池應該是放在堆中。

舉個很常見的例子

 public static void main(String[] args) {
   String s1 = "A";
   String s2 = new String("A");
   System.out.println(s1 == s2);// false
 }

訪問標志

常量池后面緊跟的是兩個字節(jié)的訪問標志【u2】。它的作用是正如其名,用于識別一些類或者接口的訪問信息,比如這個Class是類還是接口【class文件也有可能代表的是接口】,是否聲明為public,是否為abstract類型,如果是類是否被聲明為final。

訪問標志占兩字節(jié),共16位。

在<<深入理解jvm>>一書中是用一系列標志值來表示,參考了訪問標志、類索引、父類索引、接口索引集合一文,感覺用2進制的方式更好理解點,比如說ACC_ABSTRCT【是否為abstract類型】用0x0400表示,統(tǒng)一都是轉(zhuǎn)換為16進制的方式,可以更細化點用2進制的方式表示。

0x0400 = 0000 0100 0000 0000

訪問標志實際上就是一系列組合,因為有16位所以共有16個標志可以使用,但是目前就定義了8個,不知道jdk9和10是否也是。這8個如圖所示。

訪問標志含義.png
訪問標志.png

還是以上述class文件為例,常量池結(jié)束后,0021即為訪問標志,轉(zhuǎn)為二進制后為 0000 0000 0010 0001

為0000 0000 0010 0000【ACC_SUPER】與上0000 0000 0000 0001【ACC_PUBLIC】的組合,

通過javap -verbose命令也可以看到訪問標志

類索引、父類索引、接口索引集合

類索引:一個java類可能有多個類,這樣編譯出來的class文件不只是一個,但每個class文件只表示一個類,類索引就是用來確定這個類叫什么名字【全限定名】。
父類索引:java不支持多繼承,就是除了Object,每個類都會有且只有一個父類【如果沒有繼承除了Object的類,就默認繼承Object】,父類索引的作用就是描述類繼承自哪個類。
接口索引集合:與其他兩項不同,接口索引是集合,因為一個類可以實現(xiàn)多個接口,接口索引集合分兩部分,入口是索引數(shù),用一個u2類型表示索引的容量,如果類沒有實現(xiàn)任何接口,則該計數(shù)器值為0,后面的索引表不占任何字節(jié)。如果有實現(xiàn)n個接口,后面就用n個索引分別描述各自的接口索引位置。

還是以剛才的class文件為例

類索引【u2】、父類索引【u2】、接口索引集合【u2容量+對應索引】

0004 0005 0000

這邊類索引【0004】對應常量池中的第四個,而第四個的CONSTANT_Class_info又指向第20個,即jvm/Pool,說明它的全限定名就是這個。

父類索引【0005】對應常量池中的第五個,第五個指向21,即它的全限定名為java/lang/Object。該類沒有繼承其它類,因此默認繼承Object這個祖宗。

因為剛才那個類沒有實現(xiàn)任何接口,所以此處為0000后續(xù)也都不是接口索引集合的范疇內(nèi)

這邊再舉一個有實現(xiàn)接口的例子

簡要寫一個類和一個接口

package jvm;
public class Aimpl implements A {
}
package jvm;
public interface A {
}

Aimpl.class文件二進制如下

cafe babe 0000 0034 0012 0a00 0300 0e07
000f 0700 1007 0011 0100 063c 696e 6974
3e01 0003 2829 5601 0004 436f 6465 0100
0f4c 696e 654e 756d 6265 7254 6162 6c65
0100 124c 6f63 616c 5661 7269 6162 6c65
5461 626c 6501 0004 7468 6973 0100 0b4c
6a76 6d2f 4169 6d70 6c3b 0100 0a53 6f75
7263 6546 696c 6501 000a 4169 6d70 6c2e
6a61 7661 0c00 0500 0601 0009 6a76 6d2f
4169 6d70 6c01 0010 6a61 7661 2f6c 616e
672f 4f62 6a65 6374 0100 056a 766d 2f41
0021 0002 0003 0001 0004 0000 0001 0001
0005 0006 0001 0007 0000 002f 0001 0001
0000 0005 2ab7 0001 b100 0000 0200 0800
0000 0600 0100 0000 0300 0900 0000 0c00
0100 0000 0500 0a00 0b00 0000 0100 0c00
0000 0200 0d

根據(jù)剛才的經(jīng)驗,先javap -verbose Aimpl找到常量池最后一個,是 jvm/A,在轉(zhuǎn)hex編碼后為

0021開始是訪問標志,0000 0000 0010 0001說明為ACC_PUBLIC, ACC_SUPER的組合。后面則是這幾個索引和集合信息。

類索引【u2】、父類索引【u2】、接口索引集合【u2容量+對應索引】
分別對應0002 0003 0001(1個) 0004(接口位于常量池第四個)
這里不一一查看16進制編碼,對應javap -verbose Aimp做驗證。類為jvm/Aimpl,父類為java/lang/Object,接口數(shù)目為1,接口全限定名為jvm/A
Constant pool:
   #1 = Methodref          #3.#14         // java/lang/Object."<init>":()V
   #2 = Class              #15            // jvm/Aimpl
   #3 = Class              #16            // java/lang/Object
   #4 = Class              #17            // jvm/A
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               LocalVariableTable
  #10 = Utf8               this
  #11 = Utf8               Ljvm/Aimpl;
  #12 = Utf8               SourceFile
  #13 = Utf8               Aimpl.java
  #14 = NameAndType        #5:#6          // "<init>":()V
  #15 = Utf8               jvm/Aimpl
  #16 = Utf8               java/lang/Object
  #17 = Utf8               jvm/A

字段表集合

字段表用于描述接口或類中聲明的變量、字段,包括類級變量及實例級變量,不包含局部變量【方法內(nèi)的變量】

在java中,我們可以用【public、private、protected】描述字段的作用域,可以用static描述是不是類變量,final描述其可變性,volatile描述可見性【是否強制從主存讀寫】,transient是否可以被序列化,這些都可以用是否來描述,jvm用訪問標識的方式來確認這些信息。

字段表集合是由若干個字段表【field_info】組成的集合,jdk編譯成class文件后將先把字段個數(shù)計算好并設(shè)到字段計數(shù)器中,在把相應字段信息依次設(shè)到字段表集合【類似數(shù)組】的結(jié)構(gòu)中。

JVM規(guī)范了field_info表,來描述字段,結(jié)構(gòu)如圖所示


field_info

字段訪問標志

如同類,平時我們描述字段的時候也有訪問標志,但是因為是描述字段用的又有些不同。字段訪問標志共有9種,具體如圖所示


字段訪問標志

用16位的方式各自位置為1說明被標志位對應的修飾符修飾。

//測試簡單java類
package jvm;
public class Field {
    public String a = "a";
    private volatile String b = "b";
}

編譯后
cafe babe 0000 0034 0019 0a00 0700 1408
0008 0900 0600 1508 000a 0900 0600 1607
0017 0700 1801 0001 6101 0012 4c6a 6176
612f 6c61 6e67 2f53 7472 696e 673b 0100
0162 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 124c 6f63
616c 5661 7269 6162 6c65 5461 626c 6501
0004 7468 6973 0100 0b4c 6a76 6d2f 4649
656c 643b 0100 0a53 6f75 7263 6546 696c
6501 000a 4649 656c 642e 6a61 7661 0c00
0b00 0c0c 0008 0009 0c00 0a00 0901 0009
6a76 6d2f 4649 656c 6401 0010 6a61 7661
2f6c 616e 672f 4f62 6a65 6374 0021 0006
0007 0000 0002 0001 0008 0009 0000 0042
000a 0009 0000 0001 0001 000b 000c 0001
000d 0000 0043 0002 0001 0000 0011 2ab7
0001 2a12 02b5 0003 2a12 04b5 0005 b100
0000 0200 0e00 0000 0e00 0300 0000 0300
0400 0400 0a00 0500 0f00 0000 0c00 0100
0000 1100 1000 1100 0000 0100 1200 0000
0200 13
javap -verbose Field后
Constant pool:
   #1 = Methodref          #7.#20         // java/lang/Object."<init>":()V
   #2 = String             #8             // a
   #3 = Fieldref           #6.#21         // jvm/FIeld.a:Ljava/lang/String;
   #4 = String             #10            // b
   #5 = Fieldref           #6.#22         // jvm/FIeld.b:Ljava/lang/String;
   #6 = Class              #23            // jvm/FIeld
   #7 = Class              #24            // java/lang/Object
   #8 = Utf8               a
   #9 = Utf8               Ljava/lang/String;
  #10 = Utf8               b
  #11 = Utf8               <init>
  #12 = Utf8               ()V
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               LocalVariableTable
  #16 = Utf8               this
  #17 = Utf8               Ljvm/FIeld;
  #18 = Utf8               SourceFile
  #19 = Utf8               FIeld.java
  #20 = NameAndType        #11:#12        // "<init>":()V
  #21 = NameAndType        #8:#9          // a:Ljava/lang/String;
  #22 = NameAndType        #10:#9         // b:Ljava/lang/String;
  #23 = Utf8               jvm/FIeld
  #24 = Utf8               java/lang/Object
  
根據(jù)經(jīng)驗,字段為0002,有兩個,且field_info結(jié)構(gòu)為訪問標志+名稱索引+描述索引+屬性計數(shù)器 都為u2類型
①0001 0008 0009 0000 表示ACC_PUBLIC,a,Ljava/lang/String; 0
②0042 000a 0009 0000 訪問標志0000000001000010即ACC_VOLATILE和ACC_PRIVATE,b,Ljava/lang/String;0
在字段表中,變量修飾符使用標志位表示,字段數(shù)據(jù)類型和字段名稱則引用常量池中常量表

方法表集合

類似字段表集合,方法表集合是由若干個方法表(method_info)組成的集合,然后有2個字節(jié)的方法計數(shù)器,后面依次是每個method_info的信息。

如圖所示,方法表集合是由訪問標志(access_flags)、名稱索引(name_index)、描述索引(descriptor_index)、屬性表集合(attribute_info)組成。

訪問標志

方法表.png

類似類、字段的訪問標志,方法表集合也有訪問標志,不過不同的是volatile、transient不能修飾方法所以沒有這兩項,與之相對的是多了synchronized、native、stricfp可以修飾方法的關(guān)鍵字。

名稱索引

指向常量池

舉個簡單的例子


一個類中代碼量最多的往往也就是方法,方法的訪問標志、名稱索引、描述符索引都描述很清楚,可是方法內(nèi)部的大量代碼在哪,實際上是放在方法表集合中一個名為"Code"的屬性當中,這部分歸屬在屬性表集合中。

屬性表集合

在Class文件、字段表、方法表都可以攜帶自己的屬性表集合,用于描述某些場景專有的信息。

屬性表集合相對其他class文件,對順序要求不再那么高。

屬性表占著非常大的一部分且定義了眾多屬性【jdk7中有21項,21項簡要介紹,更高的版本就不懂了】

這邊重要學習了兩種屬性

Code屬性

code屬性比較復雜,它是經(jīng)過編譯器編譯成字節(jié)碼指令之后的數(shù)據(jù)。就是說java程序中的方法體經(jīng)過javac編譯器處理后,最終變成字節(jié)碼存儲在Code屬性內(nèi)。

并非所有方法表都有這個屬性,接口和抽象類就沒有【沒有方法體】。

Code屬性表結(jié)構(gòu)

類型 名稱 數(shù)量 描述
u2 attribute_name_index 1 指向CONSTANT_UTF_8常量索引。固定為Code,表示屬性名稱
u4 attribute_length 1
u2 max_stack 1 操作數(shù)棧深度最大值
u2 max_locals 1 局部變量表所需存儲空間,單位為Slot,對于byte,short等長度不超過32位的數(shù)據(jù)用1Slot,double和long這樣的用兩Slot
u4 code_length 1 存儲java源程序編譯后字節(jié)碼指令長度【雖然是u4類型,但虛擬機規(guī)范要求它用兩個字節(jié),超過java編譯器則會拒絕編譯】
u1 code code_length java源程序編譯后字節(jié)碼,根據(jù)長度知道范圍,單指令u1字節(jié),最多可以存256種,當前jvm規(guī)范提供約200種編碼表示對應指令含義,需對應虛擬機字節(jié)碼指令表
u2 exception_table_length 1 異常表長度
exception_info exception_table exception_length 異常
u2 attributes_count 1 屬性長度
attribute_info attributes attributes_count 屬性表【包含LineNumberTable、LocalVariableTable、SourceFile、ConstantValue、Deprected等等屬性】
Code屬性是Class文件中最重要的一個屬性,在Class文件中,Code屬性用于描述代碼,所有的其它數(shù)據(jù)項目都用來描述元數(shù)據(jù),了解code屬性對了解字節(jié)碼執(zhí)行引擎來說是必要基礎(chǔ)。

ConstantValue屬性

之所以學習這個,是因為后面類加載機制有聯(lián)系到這個屬性

這個屬性的作用是通知虛擬機為靜態(tài)變量賦值,只要被static修飾的變量才有這個屬性,【有該屬性的字段必須有ACC_STATIC訪問標志,反過來不一定】。

對于 "int x = 123" 和 "static int x =123"這類代碼在日常編寫中很常見,但虛擬機對這兩種變量賦值的時刻卻不同。
對于非static變量[實例變量],是在實例構(gòu)造器<init>進行
對于類變量,有兩種方式選擇
①在類構(gòu)造器<clinit>方法中賦值
②使用ConstantValue屬性初始化
目前Sun javac編譯器是這么做的【具體咋做不知道 = =】,如果同時使用final和static修飾一個變量[這種修飾就相當于個常量],并且是String或基本類型,就使用②,如果沒有被final修飾或不是基本類型和String,就選擇①在<clinit>方法中初始化

<clinit>和<init>是什么

<clinit> 類構(gòu)造器

<init> 實例構(gòu)造器

簡單來說

java在編譯后會在字節(jié)碼文件生成clinit方法,稱為類構(gòu)造器,會將靜態(tài)變量的初始化收斂【放在】clinit方法里執(zhí)行,<clinit> 方法在類加載過程中執(zhí)行

java在編譯后會在字節(jié)碼文件生成init方法,稱為實例構(gòu)造器

Class文件整體結(jié)構(gòu)

結(jié)合所學習的和個人理解畫了class文件的大致結(jié)構(gòu)圖。

各個字段.png

類加載機制

1.加載

類加載第一步,

1.通過一個類的全限定名獲取定義此類的二進制流

2.將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)。

3.在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問接口。

這個的Class對象不一定從硬盤上的class文件獲取,也有可能從【jar、war】中獲取,也可以在程序運行時通過動態(tài)代理的方式生成,也可以從jsp文件轉(zhuǎn)成的class文件中獲取。

【談一談jsp:我們知道jsp中可以編寫java代碼,而且jsp請求時,會先轉(zhuǎn)換成servlet【以前一直以為是項目啟動的時候編譯jsp,其實是訪問jsp時,并且判斷是否為第一次】,然后編譯生成lass文件。jsp也是種servlet,也有jspInit()、jspDestroy()、jspService()等方法在不同階段調(diào)用,也有自己的生命周期,這邊參考 https://www.cnblogs.com/labing/p/5869745.html】

在win下用weblogic啟動稍微測試了下

發(fā)現(xiàn)路徑放在weblogic的domain下的模塊區(qū)域,由此推測jsp是web中間件幫忙編譯的

jsp
weblogic中的jsp

2.驗證

該環(huán)節(jié)主要用于確保加載進來的字節(jié)流是否符合jvm規(guī)范,不危害jvm安全。如果驗證錯誤。將拋出諸如java.lang.VerifyError或其子類異常【常見于jar包沖突導致類加載時驗證失敗】。

java本身是安全的語言,因為java會拒絕編譯一些"危險"的操作[比如使用純粹的java代碼無法做到訪問數(shù)組邊界外的數(shù)據(jù),跳轉(zhuǎn)到不存在的對象],如果這么做編譯器將拒絕編譯或報相應的錯,但是class文件不一定要由java源碼過來,java代碼無法做的事可以通過class文件來表達,虛擬機如果不驗證檢查輸入的字節(jié)流,對其完全信任,很有可能會因為載入有害的字節(jié)流造成不良影響,所以驗證也是保護虛擬機的一項重要工作。

根據(jù)java虛擬機規(guī)范,驗證按順序有4個階段

驗證類型 目的 說明【舉例】 其它注意點
1.文件格式驗證 確保輸入的字節(jié)流可以正確解析并存儲到方法區(qū)中,通過這個階段驗證后,后續(xù)三個階段都是基于方法區(qū)進行驗證的 (1)是否是class文件【魔數(shù)以 CAFEBABE開頭】,試過javap執(zhí)行非class文件,報了找不到類的錯(2)版本號在當前jvm里是否支持,否則將報類似java.lang.UnsupportedClassVersionError: org/apache/lucene/store/Directory : Unsupported major.minor version 51.0的錯 (3)常量池的常量中是否有不被支持的常量類型 (4) Class文件各個部分和本身是否有被刪除或附加其它信息 [5]CONSTANT_Utf8_info型常量中是否有不符合UTF8編碼的數(shù)據(jù)。。。 第一階段遠不止這些,文件驗證主要目的還是保證輸入的字節(jié)流能夠正確解析存儲在方法區(qū)內(nèi)。
2.元數(shù)據(jù)驗證 對類的元數(shù)據(jù)語義校驗,保證符合Java語言規(guī)范要求 (1)某個類是否有父類【除了Object,所有類都應當有父類(默認繼承Object)】(2)某個類是否繼承了不允許被繼承的類【一般指被final修飾的類】(3)如果類是抽象類,是否實現(xiàn)它父類的方法或者它實現(xiàn)的接口的方法 驗證java語言規(guī)范,保證不存在不符合java語言規(guī)范的元數(shù)據(jù)信息
3.字節(jié)碼驗證 驗證程序語義是否符合邏輯,保證字節(jié)碼流可以被jvm安全執(zhí)行。 (1)保證跳轉(zhuǎn)指令不會跳到方法體以外的字節(jié)碼指令上 (2)保證方法體中的類型轉(zhuǎn)換有效,例如可以把子類對象類型賦給父類是安全的【向上轉(zhuǎn)型,但是相反過來就不安全了,向上轉(zhuǎn)型會使子類覆蓋的方法缺失,但是它還是安全的,而向下轉(zhuǎn)型則是不安全的,比如說人是動物,但動物不一定是人。】,甚至把對象賦值給跟它毫無關(guān)系的數(shù)據(jù)類型,這是危險的( 3 )保證任意時刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼都能配合工作,例如不會出現(xiàn)類似在操作棧放一個int的數(shù)據(jù),使用時按long類型來加載到本地變量表中。 最為復雜的一個階段,如果一個類方法體的字節(jié)碼沒有通過字節(jié)碼驗證,那肯定是有問題的,如果通過了也不一定是安全的
符號引用驗證 判斷引用是否符合規(guī)定 (1)符號引用中通過字符串描述的全限定名是否能夠找到對應的類。(2)在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段。(3)符號引用中的類、字段、方法的訪問性(private、protected、public、default)是否可被當前類訪問。 符號引用主要是確保解析動作能正常執(zhí)行,如果沒通過符號驗證將拋出IncompatibleClassChangeError的子類,類似NoSuchMethodError之類的錯。

值得一提的是,雖然對于jvm的類加載機制來說,驗證是非常重要【可以有效防止惡意代碼攻擊】但不是一定必要的階段【對運行期不影響,而且從性能來講,驗證階段的工作量在類加載的過程中,占比較大的工作量】,所以對于很有把握,反復驗證過代碼【包括自己寫的和第三方包】,可以設(shè)置-Xverify:none參數(shù)來關(guān)閉類驗證,縮短類加載時間。

3.準備

準備階段主要作用

1.在方法區(qū)內(nèi)為類變量【被static修飾的變量,不包括實例變量】分配內(nèi)存

2.設(shè)置其初始值【通常情況下是數(shù)據(jù)類型的初始值,比如int是0,boolean是false,除去基本類型的reference為null】

這邊剛開始還不大懂初始值設(shè)置是什么個情況,看了網(wǎng)上資料是這么說的

準備階段中靜態(tài)變量[static]和靜態(tài)常量[static final]有何區(qū)別

普通原始類型靜態(tài)變量和引用類型(即使是常量),

//比如說某個要加載的類有這么一段代碼
public class ABC {
    public int i = 0; 
    public static int a = 1; //靜態(tài)變量 變量a在準備階段過后初始值是0而不是1,因為這時候還未執(zhí)行java方法,把a賦值為1的putstatic指令是程序編譯后,存放在類構(gòu)造器<clinit>之中,所以賦值的動作在初始化階段之后才進行
    public static final int b = 2; //靜態(tài)常量 有final和static的情況,編譯時javac會為b生成ConstantValue屬性,在準備階段就會根據(jù)這個屬性賦值
    public static final Integer c = Integer.valueOf(3);//靜態(tài)引用類型常量 無ConstantValue
}
#編譯后查看jvm指令集 可以看到b變量有ConstantValue屬性,并且對于變量b無賦值等指令,經(jīng)javac編譯成class文件的時候就已賦值
javap -p ABC.class

經(jīng)過putstatic、invokestatic指令過后,才進行賦值,這個指令是在類加載初始化階段執(zhí)行,而不是準備階段調(diào)用,而原始類型常量不需要此步驟。

總結(jié)

putstatic指令是程序被編譯后,存放于類構(gòu)造器<client>方法之中,在編譯階段生成ConstantValue屬性,在準備階段虛擬機會根據(jù)ConstantValue屬性賦值。

4.解析

解析階段是虛擬機常量池內(nèi)的符號引用替換為直接引用的過程。

引用類型 描述
符號引用 一組來描述所引用的目標對象,這里的符號可以是各種形式的字面量或者說是字符串,用于無歧義的定位到目標。
直接引用 直接引用可以是(1)直接定位到目標的指針 ( 2 )偏移量【 指向?qū)嵗兞浚瑢嵗椒ǖ闹苯右檬峭ㄟ^偏移量,通過這個偏移量虛擬機可以直接在該類的內(nèi)存區(qū)域中找到方法字節(jié)碼的起始位置】 ( 3 )可以直接定位到目標的句柄。

符號引用和直接引用的區(qū)別

符號引用一般是具有某個含義的字面量或者說是字符串,符號引用的字面量形式明確定義在Java虛擬機規(guī)范的Class文件格式中,在編譯期間并提前訂好命名規(guī)范用于給jvm做識別。符號引用目的是確保解析動作能正常執(zhí)行。如果沒通過,將報IncompatibleClassChangeError子類的錯,如java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等錯誤。

直接引用是直接和內(nèi)存做掛鉤的,同一符號在不同系統(tǒng)、版本的jvm編譯出來的直接引用不一定相同。

簡單來說,就是符號引用和虛擬機內(nèi)存布局無關(guān),引用的目標不一定加載到內(nèi)存中,而有了直接引用,那引用的目標必定已經(jīng)被加載入內(nèi)存中了。符號引用是字面量,會被jvm識別進而轉(zhuǎn)為可以直接使用的"直接引用",比如說一個類【org.simple.People】要引用另外一個類【舉個例子org.simple.Food, 實際是以二進制形式的完全限定名放在class文件中】,編譯時并不知道實際地址,所以先用某個符號表示,接著再讓jvm翻譯成自己能直接引用的內(nèi)存地址。

5.初始化

虛擬機規(guī)范嚴格規(guī)定有5中情況必須立即對類進行"初始化",

初始化階段是類加載最后一步,到了初始化階段才正在開始執(zhí)行java代碼【或者說是字節(jié)碼】,初始化階段是執(zhí)行性類構(gòu)造器<clinit>()方法的過程。

關(guān)于<clinit>()

1.<clinit>()方法是由編譯器自動收集類中的變量賦值動作和靜態(tài)語句塊合并產(chǎn)生的,編譯器收集順序是源文件出現(xiàn)順序決定的,后面定義的變量,在前面靜態(tài)塊中可以賦值但不能訪問。
2.<clinit>()方法和類的構(gòu)造函數(shù)【或者說實例構(gòu)造器<init>】不同,不需要顯示調(diào)用父類構(gòu)造器,虛擬機會自動保證子類的<clinit>()方法執(zhí)行前先執(zhí)行完父類的<clinit>()方法,所以虛擬機中第一個被執(zhí)行<clinit>()方法方法的類肯定是Object。
3.由于父類的<clinit>()方法比子類先執(zhí)行,這就意味著父類的static塊執(zhí)行順序優(yōu)于子類。
4.<clinit>()對于類和接口不是必須的,如果沒有靜態(tài)語句塊,也沒有對變量賦值操作,可以不生成這個方法。
5.接口不能使用靜態(tài)語句塊,但可以賦值,雖然不是必須的但接口也可以生成<clinit>(),但跟類不同,執(zhí)行接口的<clinit>()不需要先執(zhí)行父接口的<clinit>()方法,只有父接口定義的變量使用時才會初始化父接口。
6.<clinit>()方法在多線程中會被正確的做加鎖同步操作,如果多線程去初始化一個類,只有一個類去執(zhí)行這個<clinit>(),其它線程都需要等待知道執(zhí)行<clinit>()完畢。

1聽起來很繞,如圖所示

demo

類加載器

虛擬機團隊吧類加載階段中"通過一個類的全限定名來獲取類的二進制字節(jié)流"這個動作放在虛擬機外部實現(xiàn)。至于如果決定去獲取需要的類【可能有和jdk自帶類一樣全限定名的類】,實現(xiàn)這部分的代碼就是類加載器。

三種類加載器【按順序】

1. 啟動類加載器 Bootstrap CLassloder 
2. 擴展類加載器 Extention ClassLoader 
3. 應用程序類加載器 AppClassLoader

關(guān)于類加載器網(wǎng)上文章很多,這篇寫的不錯,一看你就懂,超詳細java中的ClassLoader詳解

對于java虛擬機,有兩種不同類加載器,啟動類加載器和其它加載器

啟動類加載器在HotSpot虛擬機中用c++實現(xiàn),是虛擬機的一部分 ,第一個類加載器是BootStrap,比較特殊,不需要被加載,而是嵌套在java虛擬機內(nèi)核內(nèi),jvm啟動時bootstarp已經(jīng)啟動,用c++寫的二進制代碼(非字節(jié)碼),它可以去加載別的類。

除了啟動類其它類加載器全由java實現(xiàn),全部繼承自java.lang.ClassLoader,獨立于虛擬機外部

啟動類加載器:它的作用是將JAVA_HOME/lib目錄下的類加載到內(nèi)存中。需要注意的是由于啟動類加載器涉及到虛擬機本地的實現(xiàn)細節(jié),開發(fā)人員將無法直接獲取到啟動類加載器的引用,所以不允許直接通過引用進行操作。

標準擴展擴展類加載器:由Sun的ExtClassLoader實現(xiàn)的,它的作用是將JAVA_HOME/lib/ext目錄下或由系統(tǒng)變量 java.ext.dir指定位置中的類加載到內(nèi)存中,它可以由開發(fā)人員直接使用。

應用程序類加載器:它是由Sun的AppClassLoader實現(xiàn)的,它的作用是將classpath路徑下指定的類加載到內(nèi)存中。它也可以由開發(fā)人員使用。

自定義類加載器:自定義的類加載器繼承自ClassLoader,并覆蓋findClass方法,它的作用是將特殊用途的類加載到內(nèi)存中。

一旦一個類被加載了,將不會再次被加載。

雙親委派機制

工作流程

如果一個類加載器受到類加載的請求,它不會自己先去加載這個類,而是把請求委派給父加載器執(zhí)去完成,依次往上遞歸直到傳到最頂層,只有當最頂層的加載器反饋無法完成這個加載的請求,子加載器才會嘗試自己去加載。

比如說java.lang.Object,它存在rt.jar中,無論哪一個類加載器要加載這個類最終都是委派給最頂端的啟動類加載器進行加載。因此Object類在程序的各種類加載器環(huán)境下都是同一個類。

作用

這個機制的作用是用于確定java虛擬機要加載一個類時用哪個類加載器加載,有個重要的作用是防止內(nèi)存中出現(xiàn)多份同樣的字節(jié)碼。

比如說兩個類分別為A類和B類,兩個類都有用到System類,如果都自己加載這個類而不是委托上層類加載器加載,將導致內(nèi)存中出現(xiàn)兩份System字節(jié)碼。如果使用委托,會遞歸到父類最后由Bootstrap加載,找不到的情況下才向下,如果Bootstrap發(fā)現(xiàn)已經(jīng)加載過了就直接返回內(nèi)存中的System而不需要重新加載。

一個類要用哪個類加載器去加載

如果類A引用了類B,虛擬機將使用加載A的類加載器加載B,如果這里的B類是rt.jar下的一些關(guān)鍵基礎(chǔ)類【比如Object】,加載A的類加載器會向上委托去加載,這個類Bootstrap能處理,就交給它處理【假設(shè)處理不了Object才會向下請求讓兒子加載。事實是Object就是由Bootstrap加載器加載】

委托是從下向上,然后具體查找過程卻是自上至下。

雙親委派機制.png

注意點

父加載器不是父類

getParent()不在URLClassLoader而在ClassLoader中 返回的是ClassLoder(抽象類)對象

boostrap classloader由c/c++編寫 本身也是虛擬機的一部分 無法在java中獲取它的引用

geParent()實際返回個ClassLoader對象的parent,parent的賦值在ClassLoader對象的構(gòu)造方法中

ClassLoader由getSystemClassLoader()生成。

關(guān)于類加載器的疑問

1.是否自己寫個類叫java.lang.System

答案:通常不可以,但可以采取另類方法達到這個需求。
解釋:為了不讓我們寫System類,類加載采用委托機制,這樣可以保證爸爸們優(yōu)先,爸爸們能找到的類,兒子就沒有機會加載。而System類是Bootstrap加載器加載的,就算自己重寫,也總是使用Java系統(tǒng)提供的System,自己寫的System類根本沒有機會得到加載。

但是,我們可以自己定義一個類加載器來達到這個目的,為了避免雙親委托機制,這個類加載器也必須是特殊的。由于系統(tǒng)自帶的三個類加載器都加載特定目錄下的類,如果我們自己的類加載器放在一個特殊的目錄,將會出現(xiàn)類正常編譯但無法被加載運行【即使自定義的類加載器,也是會拋出java.lang.SecurityException異常】

2.如何判斷一個類是不是相同的類

初始java的時候,以為類名一樣就是同樣的類名就是相同的類【太年輕】,后來發(fā)現(xiàn)全限定名一樣的類就是相同的類,并且一直都這么認為,實際上比較兩個類是否“相等”,只有在兩個類由同一個類加載器加載的前提下才有意義,否則即使兩個類來源于同一個class文件,被同一個虛擬機加載,只要加載它們兩的類加載器不同,就不是兩個相同的類。

總結(jié)

class文件是java虛擬機執(zhí)行引擎的數(shù)據(jù)入口,里面的結(jié)構(gòu)順序是固定的,各自有不同的含義,個人感覺就像是一輛車有多個部件組成,最后再由引擎(jvm)做驅(qū)動運行。

class文件是java技術(shù)體系基礎(chǔ)構(gòu)成之一,不依賴特定平臺【只要對應平臺有合適版本的jvm】,是java實現(xiàn)跨平臺的重要支柱,了解class文件的結(jié)構(gòu)以及類加載過程對進一步了解jvm有重要的意義,不然寫了代碼但不知道運行的過程,總感覺會缺了點什么,知其然而不知其所以然。

寫博客主要也是以做筆記加深記憶為主,希望對這塊內(nèi)容有更深刻的理解,不斷進步。由于本人水平不高,文章有不對的地方,請批評指正。

參考資料

<<深入理解jvm虛擬機>>

從一個class文件深入理解Java字節(jié)碼結(jié)構(gòu)

JDK、JRE的關(guān)系

JVM-String常量池與運行時常量池

《Java虛擬機原理圖解》 1.2.2、Class文件中的常量池詳解(上)

JVM(三):類加載機制(類加載過程和類加載器

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內(nèi)容