從java到class追蹤程序的執行

java/android虛擬機


android開發使用語言便是java,而android虛擬機(Dalvik,art)和普通的hospot等java虛擬機很多東西也是相似的(android虛擬機命令是基於寄存器的),因此可以對比來看(dalvik命令基於棧,看起來會比較緊湊一些)


java或者說android虛擬機可以暫且當做是一個普通的進程,與一般的線程相對應,只不過其中會包含其他一般進程沒有的部分(class解釋器;JIT:Just In Time,即時編譯等)


java和虛擬機並不是綁定的關係,java規範和虛擬機規範是分開的,因此class文件並不只能由java文件來生成,事實上資深工作者可以直接編輯class文件來實現java語法不予許的功能

Dalvik VM並不是一個Java虛擬機,它沒有遵循Java虛擬機規範,不能直接執行Java的Class文件,使用的是寄存器架構而不是JVM中常見的棧架構。 —— [ 深入理解Java虛擬機 ][ 周志明 ]

即便如此,也可以通過java虛擬機來查看一般的android代碼執行情況,因爲dalvik執行的dex其實是class文件轉化而來,也是使用java語法編寫的應用程序.

一、Java,jdk

1、java說明

java文件是以java語法爲依據編譯的文件,因爲java中對安全限制較高,因此捨去了很多c++中指針等危險操作,對於訪問溢出等,在生成底層代碼時,會自動加上訪問檢測等操作,對於內存釋放等,也會自動處理(自動添加越界檢測等操作),因此java編寫很方便;不過在最後執行底層代碼時,也會因爲這些操作導致運行效率偏低.

2、jdk版本與功能

java編寫的依據便是jdk庫,同時在jdk有升級時,class文件規範,虛擬機自身也可能會有相應的更改,不同版本jdk可使用功能如下:

  1. 1995年5月23日;版本1.0;解釋執行,Java虛擬機,Applet,AWT
  2. 1996年1月23日;版本1.1;JDK 1.1版的技術代表有:JAR文件格式、JDBC、JavaBeans、RMI。Java語法也有了一定的發展,如內部類(Inner Class)和反射(Reflection)都是在這個時候出現的
  3. 1998年12月4日;JDK 1.2;java分爲SE,ME,EE;EJB,Java Plug-in,Java IDL,Swing;JIT(Just In Time),java中strictfp關鍵字,Collections集合等
  4. 2000年5月8日;JDK 1.3;2000年5月8日;添加數字計算,Timer API,JNDI平臺級服務等
  5. 2002年2月13日;JDK 1.4;正則表達式,異常鏈,NIO,日誌類,XML解釋器和XSLT轉換器等
  6. 2004年9月30日;JDK 1.5;自動裝箱、泛型、動態註解、枚舉、可變長參數、遍歷循環(foreach循環)等語法特性都是在JDK 1.5中加入的。在虛擬機和API層面上,這個版本改進了Java的內存模型(Java Memory Model,JMM)、提供了java.util.concurrent併發包等
  7. 2006年12月11日;JDK 1.6;提供動態語言支持(通過內置Mozilla JavaScript Rhino引擎實現)、提供編譯API和微型HTTP服務器API等。同時,這個版本對Java虛擬機內部做了大量改進,包括鎖與同步、垃圾收集、類加載等方面的算法都有相當多的改動。
  8. 2009年2月19日;JDK 1.7;JDK 1.7的主要改進包括:提供新的G1收集器(G1在發佈時依然處於Experimental狀態,直至2012年4月的Update 4中才正式“轉正”)、加強對非Java語言的調用支持(JSR-292,這項特性到目前爲止依然沒有完全實現定型)、升級類加載架構等。
  9. 2014年3月19日;JDK 1.8;Lambda表達式,Optional,Stream等

可以看到,隨着jdk版本提升,功能也是越來越強

二、瞭解java到class過程

編寫一個簡單的java文件,用於查看從java文件到最終字節碼的執行過程:

1、java文件

BaseEvent.java文件:

package com.knowledge.mnlin.frame.base;

/**
 * 功能----RxBus傳遞對象,用於基本的顯示toast等信息的傳遞
 * <p>
 * Created by MNLIN on 2017/9/23.
 */

public class BaseEvent {
    //operateCode 類型,EventBus傳遞過來時,判斷對應的類型
    public int operateCode;

    public Object data;

    public BaseEvent(int operateCode,Object data){
        this.operateCode=operateCode;
        this.data=data;
    }
}

代碼很簡單,只有構造方法,以及兩個成員變量.

2、class文件(經反編譯後顯示的內容)

BaseEvent.class文件:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.knowledge.mnlin.frame.base;

public class BaseEvent {
    public int operateCode;
    public Object data;

    public BaseEvent(int operateCode, Object data) {
        this.operateCode = operateCode;
        this.data = data;
    }
}

看起來經過編譯後,內容邏輯並沒有什麼變化,事實上,生成class文件時除了一些必要的處理(比如一些if不可能執行的代碼會被丟棄,case string會進行變化等),其他都不會有太大變化.可以看一些一些簡單的代碼的轉換:

①、if語句省略

一些if語句不可能到達的位置,代碼會被自動忽略,是不會進入class文件中的:

java文件:

if(true){
    Log.d("tag","true");
}else{
    Log.d("tag","true");
}

class文件:

Log.d("tag", "true");

可以看到,if語句中false部分因爲不可能執行,所以在編譯時就已經被去掉了,只剩下了恆爲true的代碼;

②、case語句

用於測試java文件case語句:

String str="a";
switch (str){
    case "a":{
        break;
    }
    case "b":{
        break;
    }
}

在生成class文件後反編譯,可以看到具體的邏輯:

String str = "a";
byte var5 = -1;
switch(str.hashCode()) {
case 97:
    if(str.equals("a")) {
        var5 = 0;
    }
    break;
case 98:
    if(str.equals("b")) {
        var5 = 1;
    }
}

switch(var5) {
case 0:
case 1:
default:
}

在java7之前,case也是不能使用string來判斷的;事實上語法自身也是不支持string來判斷的(實際是通過哈希碼和equal方法來判斷的),這裏相當於java的一個語法糖,將多餘的操作交給編輯器來完成,達到了簡化代碼的效果。

③、其他部分

除此之外,還有其他的一些內容,例如自動拆裝箱泛型擦除增強for循環變長參數枚舉類型等;

  • 自動拆裝箱:基本類型與對象之間編輯轉換,如Integer與int等
  • 泛型擦除:如List<String>與List<Integer>,在java中,被認爲是同一個類(事實上,在生成class時,已經記錄了泛型類型,因此java中通過反射纔可以獲取的到泛型參數)
  • 增強for循環:增強for循環自身是通過iterator接口來實現的,因此必須實現了iterator接口的對象纔可以使用增強for
  • 變長參數:在java1.5時,添加了變長參數功能,但實際上,只是一個數組而已。
  • 枚舉類型:枚舉類型在實際使用時,只是Enum類的子類,因此與一般類並沒有太大區別(java編譯後會自身生成[class name]$[enum name]類)

除此之外,還有常見的數組類型,其實是在虛擬機內部自動生成了類。當然class文件是看不到這部分內容的。

內部類的話,則分爲兩種:

I、一般內部類

一般的內部類其實邏輯時分成兩步:

  1. 創建對象;在java -> class時,會生成一個 [class name]$[\d]類,然後調用new創建對象;
  2. 使用生成的對象參與方法調用

java源碼:

view.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.e("tag","inner class");
    }
});

生成了額外的class文件:[class name]$[\d]

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.acchain.community.base;

import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;

class BaseEvent$1 implements OnClickListener {
    BaseEvent$1(BaseEvent this$0) {
        this.this$0 = this$0;
    }

    public void onClick(View v) {
        Log.e("tag", "inner class");
    }
}

可以看到,其實是編譯器自動生成了一個其他的class類。

II、lambda表示

java8以後,jdk支持lambda表達式,對於上面那段代碼,如果使用lambda簡寫的的話,是如下形式:

view.setOnClickListener(v -> Log.e("tag","lambda"));

這種寫法非常方便,但如果查看編譯後的class文件的話,可以發現並沒有額外的class文件生成,這是就需要查看未反編譯的class文件之間的差別

III、兩種方式對比

一般形式內部類class文件對應字節碼(截取其中一部分):

151: aload         5
153: new           #15                 // class com/acchain/community/base/BaseEvent$1
156: dup
157: aload_0
158: invokespecial #16                 // Method com/acchain/community/base/BaseEvent$1."<init>":(Lcom/acchain/community/base/BaseEvent;)V
161: invokevirtual #17                 // Method android/view/View.setOnClickListener:(Landroid/view/View$OnClickListen

lambda形式class文件對應字節碼(截取一部分):

166: invokedynamic #18,  0             // InvokeDynamic #0:onClick:()Landroid/view/View$OnClickListener;
171: invokevirtual #17                 // Method android/view/View.setOnClickListener:(Landroid/view/View$OnClickListen

兩者之間,字節碼是不一樣的,invokedynamic指令是在java1.7版本之後新加的內容,主要用於動態調用,具體功能可以參考書籍:《深入理解Java虛擬機》

好了,扯遠了,接下來還是以BaseEvent類來說明生成的字節碼

3、class文件(字節形式解析)

現在對BaseEvent.class文件,不進行反編譯,直接查看字節部分,這部分內容需要充分理解class文件的組成結構,現在直接貼上解析部分:

//根據java虛擬機規定,class文件格式採用了類似c語言結構體的僞結構,只包含兩種數據類型:無符號數,表
//無符號數爲基本數據類型,以u1,u2,u4,u8來分別表示1,2,4,8個字節,無符號數用來描述數字,索引,引用,數量,或暗中啊utf-8編碼的字符串值
//表 時有多個無符號數或者其他表作爲數據項構成的複合數據類型,所有表習慣性的以  _info  結尾;因此整個class文件可以看作一個 表(有時看來就像view佈局,裏面由view和group兩種構成,多個view放到一起看作是一個group)

//前四字節爲魔數值,表示是否爲可以被虛擬機接受的class文件,ascii碼含義爲:J~>:
cafe babe  

//第五第六字節爲次版本號(Minor Version),這裏表示爲0x0000(class文件默認格式爲大端存儲,高字節在低位,低字節在高位)
0000 

//七八字節爲主版本號(Major Version),這裏表示0x0034,即52,java版本號從45開始,jdk1.0對應45,jdk8.0對應52.0;jdk每個大版本更新都加1
0034 

//常量池容量(偏移地址:0x00000008)爲十六進制數0x0019,爲25,常量池座標從1開始,即索引1-24(0不不使用)共24個常量
//常量池存放兩大類常量:字面量Literal,符號引用Symbolic References
//字面量包括:文本字符串,聲明爲final的常量值等
//符號引用包括:類和接口的全限定名,字段的名稱和描述符,方法的名稱和描述符
0019 

//常量池容量已經確定,一次接下來會有24個常量(常量類型一共有14種,因此這24個常量類型不同的字節碼類不定)

//每一個常量爲一個表結構,每種常量類型表開始第一位爲u1,也就是一個字節,標誌位;這裏爲0x0a,即10,表示爲CONSTANT-Methodref_info:類中方法的符號引用;接下來查詢該表類型結構字段
/*
type  |  descriptor  |  remark
u1    |  tag         |  CONSTANT_Methodref(10)
u2    |  class_index |  constant_pool中的索引,CONSTANT_Class_info類型。記錄定義該方法的類。
u2    |  name_and_type_index  |  constant_pool中的索引,CONSTANT_NameAndType_info類型。指定類中扽方法名(name)和方法描述符(descriptor)。
*/
//因此接下來的1+2+2個字節都爲該類型表的數據
0a
//class_index字段爲0x0005,索引5,指向另一種表類型:CONSTANT_Class_info
0005
//name_and_type_index字段爲0x0013,索引19,指向另一種表類型:CONSTANT_NameAndType_info
0013

//按照常量池格式,可以依據解析剩餘23個表結構
09  
0004 0014 0900 0400 1507 0016 0700 1701
000b 6f70 6572 6174 6543 6f64 6501 0001
4901 0004 6461 7461 0100 124c 6a61 7661
2f6c 616e 672f 4f62 6a65 6374 3b01 0006
3c69 6e69 743e 0100 1628 494c 6a61 7661
2f6c 616e 672f 4f62 6a65 6374 3b29 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 2a4c 636f 6d2f 6b6e 6f77
6c65 6467 652f 6d6e 6c69 6e2f 6672 616d
652f 6261 7365 2f42 6173 6545 7665 6e74
3b01 000a 536f 7572 6365 4669 6c65 0100
0e42 6173 6545 7665 6e74 2e6a 6176 610c
000a 0018 0c00 0600 070c 0008 0009 0100
2863 6f6d 2f6b 6e6f 776c 6564 6765 2f6d
6e6c 696e 2f66 7261 6d65 2f62 6173 652f
4261 7365 4576 656e 7401 0010 6a61 7661
2f6c 616e 672f 4f62 6a65 6374 0100 0328
2956


//常量池結束後,緊跟兩個字節訪問標誌,0x0021表示爲public類型的類,並且屬於jdk1.0.2以後編譯的字節碼;注:jdk8時候,16個bit位都已經用完
0021

//this class字段,本類索引,指向常量池的第4個常量(表結構)

//super class字段,父類索引,指向常量池第5個常量(表結構)
0005 

//接口個數爲0,表示沒有實現任何接口
0000 

//若有實現的接口,則接下來應該是 2*接口個數 個字節的接口索引結合索引,因爲每個接口索引爲 u2 類型


//然後是u2結構的字段,表示fields_count,這裏爲0x0002表示有兩個field(類成員),因此該u2後會有兩個field_info表類型
0002 

//field_info表類型包含三個u2基本類型數據:access_flags,name_index,descriptor_index;
//第一個field_info,operateCode
//access_flags爲0x0001,表示爲public訪問權限
0001 
//name_index索引,引用常量池6,表示該字段名字
0006
//descriptor_index索引,表示該字段的類型描述,指向常量7(常量池中常量7是個utf-8的字符串,爲I,表示int類型);
0007
//在最後還有一個屬性表集合計數器用於存儲額外的信息(暫不考慮)
0000 

//第二個field_info,data
0001 0008 0009 0000

//方法表和成員表結構雷同
//有一個方法
0001 
//access_flags,public訪問權限
0001
//name_index,常量池中10
000a 
//descriptor_index索引,方法描述
000b 
//屬性表集合計數器,此時不爲0(上面的field_info中最後屬性表集合計數器是0,因此不做考慮);表示此方法的屬性表集合有一項屬性
0001 

//attribute_name_index,屬性值名稱索引;該字段表明屬性表集合的那項屬性指向常量池0x000c,也就是12索引,(12索引爲utf-8字符串:"Code",Code時虛擬機規範預定義的屬性,表示java代碼編譯成的字節碼指令);
000c 
//attribute_length;屬性值長度89
0000 0059
//max_stack;操作數棧深度最大值;
0002 
//max_locals;代表了局部變量表需要的存儲空間(slot爲虛擬機爲局部變量分配內存最小的使用單位);
0003
//code_length;java源程序編譯層的字節碼指令(的長度);
0000 000f 
//code;字節碼
2a //aload_0,將第1個reference(引用)類型的變量推送到棧頂(this值)
b7 0001 //invokespecial,將棧頂reference類型數據指向對象作爲方法接收者,調用此對象的構造器方法,dprivate方法等;具體調用的方法所對應常量池的地址爲:0x0001
2a
1b 
b5 0002 //賦值操作2
2a 
2c
b5 0003 //賦值操作,索引:常量池3
b1 //return;表示執行結束
//exception_table_length;
0000
//exception_table;exception_table_length爲0,因此這裏沒有異常信息 

    //有兩個Code的從屬 屬性
    0002

    //attribute_name_index;索引,指向字符串"LineNumberTable"
    000d
    //attributes_length;屬性值共18個字節
    0000 0012
    //line_number_table_length;一共4行
    0004
    //接下來爲四行的line對照表
    //第一行:字節碼的第0x0000行對應源碼的0x000f行
    0000 000f
    //第二到四行
    0004 0010
    0009 0011 
    000e 0012

    //同LineNumberTable屬性結構一樣,接下來是LocalVariableTable屬性結構
    //attribute_name_index;索引,指向字符串"LocalVariableTable"
    000e
    //attributes_length;
    0000 0020
    //local_variable_table_length
    0003
    //3個local_variable_info結構
    //第一個local_variable_info
    //start_pc,length,name_index,descriptor_index,index這五個字段定義了一個局部變量的聲明週期,類型,名稱等信息
    0000 000f 000f 0010 0000  
    //後兩個local_variable_info
    0000 000f 0006 0007 0001 
    0000 000f 0008 0009 0002 



//從此處開始,就不再是<init>方法的屬性了,而是整個類的屬性介紹;
//整個文件的屬性值個數
0001 

//這個文件只有以下一個屬性
//sourcefile屬性:attribute_name_index(u2),attribut_length(u4),sourcefile_index(u2)
//指向常量池17:"SourceFile"
0011 
//長度,2
0000 0002 
//文件的名字索引,指向常量池中18:"BaseEvent.java"
0012

這部分如果依靠人爲解析,其實會很麻煩,這裏盜用別人的圖,用來表示class文件的結構:

這裏寫圖片描述

class文件格式採用類似於c語言結構體的僞結構來存儲數據,只有兩種基本結構:無符號數,表
其中u1,u2,u4,u8這些表示佔用1、2、4、8個字節的無符號數,其他類型就是表結構;
具體的表結構可以參考百度(類型太多了);

4、class文件(javap解析過的顯示內容)

這裏通過javap可以看到方法具體的字節碼內容,通過javap生成基於棧的字節碼指令,因爲這種指令比較緊湊,也方便查看,javap查看的內容如下:

Classfile /D:/personal/github/FrameKnowledge/app/build/intermediates/classes/debug/com/knowledge/mnlin/frame/base/BaseEvent.class
  Last modified Dec 19, 2017; size 463 bytes
  MD5 checksum 25f902a85e0f04b251a24e7004d82012
  Compiled from "BaseEvent.java"
public class com.knowledge.mnlin.frame.base.BaseEvent
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#19         // java/lang/Object."<init>":()V
   #2 = Fieldref           #4.#20         // com/knowledge/mnlin/frame/base/BaseEvent.operateCode:I
   #3 = Fieldref           #4.#21         // com/knowledge/mnlin/frame/base/BaseEvent.data:Ljava/lang/Object;
   #4 = Class              #22            // com/knowledge/mnlin/frame/base/BaseEvent
   #5 = Class              #23            // java/lang/Object
   #6 = Utf8               operateCode
   #7 = Utf8               I
   #8 = Utf8               data
   #9 = Utf8               Ljava/lang/Object;
  #10 = Utf8               <init>
  #11 = Utf8               (ILjava/lang/Object;)V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               LocalVariableTable
  #15 = Utf8               this
  #16 = Utf8               Lcom/knowledge/mnlin/frame/base/BaseEvent;
  #17 = Utf8               SourceFile
  #18 = Utf8               BaseEvent.java
  #19 = NameAndType        #10:#24        // "<init>":()V
  #20 = NameAndType        #6:#7          // operateCode:I
  #21 = NameAndType        #8:#9          // data:Ljava/lang/Object;
  #22 = Utf8               com/knowledge/mnlin/frame/base/BaseEvent
  #23 = Utf8               java/lang/Object
  #24 = Utf8               ()V
{
  public int operateCode;
    descriptor: I
    flags: ACC_PUBLIC

  public java.lang.Object data;
    descriptor: Ljava/lang/Object;
    flags: ACC_PUBLIC

  public com.knowledge.mnlin.frame.base.BaseEvent(int, java.lang.Object);
    descriptor: (ILjava/lang/Object;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iload_1
         6: putfield      #2                  // Field operateCode:I
         9: aload_0
        10: aload_2
        11: putfield      #3                  // Field data:Ljava/lang/Object;
        14: return
      LineNumberTable:
        line 15: 0
        line 16: 4
        line 17: 9
        line 18: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  this   Lcom/knowledge/mnlin/frame/base/BaseEvent;
            0      15     1 operateCode   I
            0      15     2  data   Ljava/lang/Object;
}
SourceFile: "BaseEvent.java"
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8 -Duser.language=en -Duser.country=US

參考class文件結構可以很明顯的對比各個部分。
這裏只看構造函數中內容:

  1. 開始部分標明瞭方法訪問屬性,方法簽名(參數與返回值類型);
  2. 然後表明了該方法執行需要棧的深度爲2,局部變量表容量爲3,參數個數爲3
  3. 然後是方法中代碼對應的字節碼
  4. 接着是指令與源碼中行號的對應關係,這也是java在異常時可以拋出代碼行數的原因
  5. 最後就是局部變量表的內容,可以看到參數容量爲3,類型和對應的值也標註的很清楚

最核心的地方便是方法中代碼執行的邏輯部分,源碼很簡單,只是簡單的賦值操作:

this.operateCode=operateCode;
this.data=data;

對應的字節碼可以進行說明 :

//aload_0表示將局部變量表中0位置(BaseEvent)的值壓入棧中,此時棧高度爲1,
0: aload_0

// 調用父類Object的構造方法
1: invokespecial #1                  // Method java/lang/Object."<init>":()V

// aload_0表示將局部變量表中0位置(BaseEvent)的值壓入棧中,此時棧高度爲1
4: aload_0

// iload_1表示將局部變量表中1位置(int類型,operateCode)的值壓入棧中,此時棧高度爲2
5: iload_1

// 成員field賦值,此時棧高度變爲0
6: putfield      #2                  // Field operateCode:I

// aload_0表示將局部變量表中0位置(BaseEvent)的值壓入棧中,此時棧高度爲1
9: aload_0

// aload_2表示將局部變量表中2位置(Object)的值壓入棧中,此時棧高度爲2
10: aload_2

// 成員field賦值,此時棧高度變爲0
11: putfield      #3                  // Field data:Ljava/lang/Object;

//方法結束
14: return

可以看到,基於棧的指令不需要考慮寄存器等硬件設備,因此兼容性較高,但以爲需要頻繁的移動棧中數據以及賦值,代碼需要更多的執行時間。

三、擴展:從虛擬機看程序

class文件生成字節碼是無法直接運行的,因此需要在某個時刻翻譯爲機器語言,根據虛擬機的不同,這部分處理會有些差異。

對於最早期的java虛擬機來說,採用存解釋的方式執行,如此一來,虛擬機實現時就需要內置一個解釋器,在運行時將字節碼轉換爲機器碼,然後運行,這種方式很明顯效率會有些低,但勝在佔用內存較小。

安卓前期使用Dalvik虛擬機則內置了JIT(Just In Time,前面也有提及),虛擬機可以根據統計方法調用次數回邊次數(可簡單的理解爲方法內部循環體執行次數),將一些調用頻率較高的代碼(方法),編譯爲機器代碼,這樣可以顯著提高程序的執行效率

而在安卓4.4之後的ART(Android Running Time)則採用了AOT(Ahead Of Time)方式,在apk安裝時就將dex文件轉爲機器代碼,這樣雖然佔用的空間較大,安裝時間較長,但在每次使用時會非常快。

1、java內存結構概況

總的來說,虛擬機最後執行的只是機器代碼,而執行代碼時,肯定需要對局部變量,對象等進行存儲,用圖來簡略的說明虛擬機內的存儲結構:

這裏寫圖片描述

2、功能說明

  • 方法區是進程持有的,所有的線程都可以訪問。
  • 虛擬機棧也可以成爲線程棧,裏面存儲有方法調用鏈表
  • 本地方法棧時當初虛擬機設計時爲其他語言(如C語言)調用提供的接口
  • 程序計數器則記錄了當前線程中程序執行到了哪一步。

以java文件來說,每個類文件在加載後生成的class對象都保存在方法區中,如果類加載器相同,則全局保持單例;一些虛擬機中常量池也保存在方法區內。

堆中則保持所有線程通過new或其他方法創建的實例對象,爲所有線程共有。

至於方法中的局部變量,則只是在運行時保存與虛擬機棧中,通過上面的程序分析可以看出,如果虛擬機架構基於棧的話,那麼局部變量名稱都不會有記錄;事實上,虛擬機棧中的結構非常複雜,甚至可能有些實例對象創建後不保存於堆中,而是直接放入到虛擬機棧中進行使用。

3、虛擬機針對代碼的“優化”處理

在字節碼轉化爲機器碼的過程中,爲了提高運行效率,往往還會進行其他的一些代碼排序工作;這就會導致多線程環境下的同步問題;

一般來說,多線程環境下,如果有多個線程訪問修改同一數據,就需要加鎖,加鎖的本質其實是爲了保證多線程代碼執行時三個條件:原子性,可見性,有序性

  • 原子性:某些操作雖然代碼只有一行,卻需要虛擬機執行多條指令,如果不能保證原子性,則在多線程環境下數據訪問會出現錯誤。
  • 可見性:一個線程中對某個變量的修改對其他線程可見
  • 有序性:後面的代碼與前面的執行順序是無法確切肯定的(在前後代碼沒有關聯的情況下),例如:

    int i=0;
    int j=1;

    在真正由虛擬機執行時,是無法保證兩者執行的順序的,這就是指令重排序,雖然在單個線程內,這種順序不會影響最終的結果,但在多線程情況下,很可能會導致其他錯誤。

在這裏因爲考慮的是虛擬機對代碼執行的影響,因此不會深入說明如何保持線程同步;這裏只說明虛擬機對程序執行可能產生的影響;
由於亂序的可能,我們生成的字節碼指令就沒有那麼可靠了,因此在多數情況下,需要加鎖來保證程序的順利執行。

4、程序執行的“最小單位”

之前說多線程狀態下,一行代碼可能在執行一半時,其他線程就對其進行了修改,這裏簡單說明一下程序的原子性操作。
顧名思義,原子性操作便是指那些不可在分的操作,操作共八種:

  1. read : 從主內存中讀取
  2. load : 將read內容放入工作內存
  3. store :從工作內容中存儲
  4. write : 將store內容寫回主內存
  5. assign :賦值,將執行引擎中的值賦值給工作內存中變量
  6. use :使用工作內存中變量
  7. lock :鎖定
  8. unlock :解鎖

具體的操作規則可以參考其他書籍或百度,只要知道只有 如上的操作才能保證在處理過程中不會有其他數據影響。
而最終代碼的執行也時通過這些存儲使用操作來完成的。

工作內存和主內存是相對於線程進程而言的,每個線程都有自己的工作內存,所有的取值賦值操作等只能控制自己所在線程的工作內容中的數據,不能執行操縱主內存(同一進程主內存相同)中的內容;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章