本文目錄一覽:
- 1、Java 中的 常量共享池 什麼意思
- 2、運行一個java程序的步驟是怎樣的?請做詳細說明!
- 3、java中什麼是能夠在計算機CPU上執行的二進位代碼?
- 4、什麼是Java位元組碼?
- 5、JAVA常量池中存儲的常量是什麼
Java 中的 常量共享池 什麼意思
一直有個疑問,java中字元串池,String池,共享池是怎麼回事?
好像知道什麼意思,但具體又不太清楚,所以也一直沒太關注。
從網上看了些資料,解釋了下疑問。
在java的棧中,有共享池的概念,把一些常量會放到這個共享池中,包括字元串常量和基本類型常量。
共享的操作時在編譯時 由編譯器完成的,可以節省內存,並提高效率。
例如語句string str = “hello”, 首先在棧中創建字元串引用變數str, 再看看棧中有沒有「hello」,如果有就str直接指向它,沒有就創建「hello」並放在棧中,然後指向它。
對於int之類的基本類型的變數也差不多都是這樣的。
而對於 String str = new String(“hello”),則是創建新的對象,並放在堆內存中。是在runtime的時候分配內存的。
這樣做效率和節省內存方面不如String str = 「hello」, 但是更靈活,如果編譯時不知道要創建什麼樣的字元串,就
只能運行時創建了。
運行一個java程序的步驟是怎樣的?請做詳細說明!
Java程序從源文件創建到程序運行要經過兩大步驟:1、源文件由編譯器編譯成位元組碼(ByteCode)
2、位元組碼由java虛擬機解釋運行。因為java程序既要編譯同時也要經過JVM的解釋運行,所以說Java被稱為半解釋語言( “semi-interpreted” language)。
下面通過以下這個java程序,來說明java程序從編譯到最後運行的整個流程。代碼如下:
//MainApp.java
public class MainApp {
public static void main(String[] args) {
Animal animal = new Animal(“Puppy”);
animal.printName();
}
}
//Animal.java
public class Animal {
public String name;
public Animal(String name) {
this.name = name;
}
public void printName() {
System.out.println(“Animal [“+name+”]”);
}
}
第一步(編譯): 創建完源文件之後,程序會先被編譯為.class文件。Java編譯一個類時,如果這個類所依賴的類還沒有被編譯,編譯器就會先編譯這個被依賴的類,然後引用,否則直接引用,這個有點象make。如果java編譯器在指定目錄下找不到該類所其依賴的類的.class文件或者.java源文件的話,編譯器話報「cant find symbol」的錯誤。
編譯後的位元組碼文件格式主要分為兩部分:常量池和方法位元組碼。常量池記錄的是代碼出現過的所有token(類名,成員變數名等等)以及符號引用(方法引用,成員變數引用等等);方法位元組碼放的是類中各個方法的位元組碼。下面是MainApp.class通過反彙編的結果,我們可以清楚看到.class文件的結構:
第二步(運行):java類運行的過程大概可分為兩個過程:1、類的載入 2、類的執行。需要說明的是:JVM主要在程序第一次主動使用類的時候,才會去載入該類。也就是說,JVM並不是在一開始就把一個程序就所有的類都載入到內存中,而是到不得不用的時候才把它載入進來,而且只載入一次。
下面是程序運行的詳細步驟:
在編譯好java程序得到MainApp.class文件後,在命令行上敲java AppMain。系統就會啟動一個jvm進程,jvm進程從classpath路徑中找到一個名為AppMain.class的二進位文件,將MainApp的類信息載入到運行時數據區的方法區內,這個過程叫做MainApp類的載入。
然後JVM找到AppMain的主函數入口,開始執行main函數。
main函數的第一條命令是Animal animal = new Animal(“Puppy”);就是讓JVM創建一個Animal對象,但是這時候方法區中沒有Animal類的信息,所以JVM馬上載入Animal類,把Animal類的類型信息放到方法區中。
載入完Animal類之後,Java虛擬機做的第一件事情就是在堆區中為一個新的Animal實例分配內存, 然後調用構造函數初始化Animal實例,這個Animal實例持有著指向方法區的Animal類的類型信息(其中包含有方法表,java動態綁定的底層實現)的引用。
當使用animal.printName()的時候,JVM根據animal引用找到Animal對象,然後根據Animal對象持有的引用定位到方法區中Animal類的類型信息的方法表,獲得printName()函數的位元組碼的地址。
開始運行printName()函數。
特別說明:java類中所有public和protected的實例方法都採用動態綁定機制,所有私有方法、靜態方法、構造器及初始化方法clinit都是採用靜態綁定機制。而使用動態綁定機制的時候會用到方法表,靜態綁定時並不會用到。
java中什麼是能夠在計算機CPU上執行的二進位代碼?
java中的JVM是能夠在計算機CPU上執行的二進位代碼。
java的執行過程
Java代碼需要經過編譯和解釋兩個步驟,才在能在平台上運行。首先java語言的編譯器,幫java代碼編譯成class的位元組碼,之後通過java虛擬機(JVM)來解釋執行。
java代碼的編譯
java代碼是如何編譯的?
首先編譯的解釋:把用高級程序設計語言書寫的源程序,翻譯成等價的計算機彙編語言或機器語言書寫的目標程序的翻譯程序。編譯的具體過程,可以看看《編譯原理》相關的書籍。
其實java的編譯過程,和通常c/c++還是不同的。
java編譯後的位元組碼文件格式主要分為兩部分:常量池和方法位元組碼。常量池記錄的是代碼出現過的所有token(類名,成員變數名等等)以及符號引用(方法引用,成員變數引用等等);方法位元組碼放的是類中各個方法的位元組碼。
Java編譯器卻不將對變數和方法的引用編譯為數值引用,也不確定程序執行過程中的內存布局,而是將些符號引用信息保留在位元組碼中,由解釋器在運行過程中創立內存布局,然後再通過查表來確定一個方法所在的地址,這樣就有效地保證了java的可移植性和安全性。
c/c++的編譯,當C編譯器編譯生成一個對象的代碼時,該代碼是為在某一特定硬體平台運行而生成的。因此在編譯過程中,編譯程序通過查表將所有對符號的引用轉換為特定的內存偏移量,以保證程序運行。
java虛擬機(JVM)
簡單的可以這樣理解它的功能:就是將java編譯之後的位元組碼,解釋成cpu能夠執行的二進位代碼。
JVM是一個虛構出來的計算機,是通過在實際的計算機上模擬模擬各種計算機功能來實現的。JVM有自己完善的硬體架構,如處理器、堆棧、寄存器等,還具有相應的指令系統。JVM 的主要工作是解釋自己的指令集(即位元組碼)並映射到本地的 CPU 的指令集或 OS 的系統調用。Java語言是跨平台運行的,其實就是不同的操作系統,使用不同的JVM映射規則,讓其與操作系統無關,完成了跨平台性。JVM 對上層的 Java 源文件是不關心的,它關注的只是由源文件生成的類文件( class file)。類文件的組成包括 JVM 指令集,符號表以及一些補助信息。
java虛擬機工作的原理,可以自己找一下網上的資料。大家還需要思考的問題,jvm的內存、jvm的垃圾回收(GC)、Android的朋友還要區分(Dalvik 和標準 Java 虛擬機JVM)的區別。
總結:
java代碼編譯之後,可以直接運行在Windows或者其它裝有JVM虛擬機的系統下。而C或C++直接編譯成與機器和操作系統相關的代碼。所以C語言編譯的程序沒有跨平台性,就算沒有使用到操作系統相關的API,在不同的系統下也必須重新編譯才能運行。
什麼是Java位元組碼?
它是程序的一種低級表示,可以運行於Java虛擬機上。將程序抽象成位元組碼可以保證Java程序在各種設備上的運行
Java號稱是一門「一次編譯到處運行」的語言,從我們寫的java文件到通過編譯器編譯成java位元組碼文件(.class文件),這個過程是java編譯過程;而我們的java虛擬機執行的就是位元組碼文件。不論該位元組碼文件來自何方,由哪種編譯器編譯,甚至是手寫位元組碼文件,只要符合java虛擬機的規範,那麼它就能夠執行該位元組碼文件。
JAVA程序的運行
因為Java具有跨平台特性,為了實現這個特性Java執行在一台虛擬機上,這台虛擬機也就是JVM,Java通過JVM屏蔽了不同平台之間的差異,從而做到一次編譯到處執行。JVM位於Java編譯器和OS平台之間,Java編譯器只需面向JVM,生成JVM能理解的代碼,這個代碼即位元組碼,JVM再將位元組碼翻譯成真實機器所能理解的二進位機器碼。
位元組碼是怎麼產生的?
我們所編寫的程序都是.java格式,通常在執行的時候也許點擊一下eclipse的運行鍵就可以在控制台看到運行結果,但是也可以更酷一些,如果你裝了JDK,那就可以直接在以命令行的方式編譯運行你的.java文件,編譯後會形成.class文件,這個.class文件即位元組碼。
位元組碼怎麼解讀?
上圖是編譯好的位元組碼文件,即一堆16進位的位元組,如果使用IDE去打開,也許看到的是已經被反編譯的我們所熟悉的java代碼,但這才是純正的位元組碼
這裡只介紹位元組碼由哪些部分組成, 具體的意思自行百度或者看文尾的連接, 有較為詳細的講解
上圖即位元組碼文件的組成部分, Class文件的結構不像XML等描述語言那樣鬆散自由。由於它沒有任何分隔符號,
所以,以上數據項無論是順序還是數量都是被嚴格限定的。哪個位元組代表什麼含義,長度是多少,先後順序如何,都不允許改變, 如上圖左側即每一部分規定的長度
魔數(Magic Number)
魔數是用來區分文件類型的一種標誌,一般都是用文件的前幾個位元組來表示。
比如0XCAFE BABE表示的是class文件,那麼有人會問,文件類型可以通過文件名後綴來判斷啊?是的,但是文件名是可以修改的(包括後綴),那麼為了保證文件的安全性,將文件類型寫在文件內部來保證不被篡改。
至於為什麼是CAFE BABE估計大家也能猜到, 程序員與咖啡的不解之緣
版本號(Version)
版本號含主版本號和次版本號,都是各佔2個位元組。在此Demo種為0X0000 0033。其中前面的0000是次版本號,後面的0033是主版本號。通過進位轉換得到的是次版本號為0,主版本號為51。高版本的JDK能向下兼容以前版本的Class文件,但不能運行以後版本的Class文件,即使文件格式未發生任何變化. 這就是target參數的用處,可以在使用JDK 1.7編譯時指定-target 1.5
常量池(Constant Pool)
常量池是Class文件中的資源倉庫, 量池中主要存儲2大類常量:字面量和符號引用。字面量如文本字元串,java中聲明為final的常量值等等,而符號引用如類和介面的全局限定名,欄位的名稱和描述符,方法的名稱和描述符。常量池是一個表結構,在表的內容前有一個類型的計數器,表示常量池的長度
上面的表中描述了11中數據類型的結構,其實在jdk1.7之後又增加了3種(CONSTANT_MethodHandle_info,CONSTANT_MethodType_info以及CONSTANT_InvokeDynamic_info)。這樣算起來一共是14種
訪問標誌(Access_Flag)
訪問標誌信息包括該Class文件是類還是介面,是否被定義成public,是否是abstract,如果是類,是否被聲明成final。通過上面的源代碼,我們知道該文件是類並且是public。
0x 00 21:是0×0020和0×0001的並集。其中0×0020這個標誌值涉及到位元組碼指令
類索引(This Class Name)
類索引用於確定類的全限定名
0×00 03 表示引用第3個常量,同時第3個常量引用第19個常量,查找得」com/demo/Demo」。#3.#19
父類索引(Super Class Name)
0×00 04 同理:#4.#20(java/lang/Object)
介面索引(Interfaces)
通過上邊位元組碼圖可以看到,這個介面有2+n個位元組,前兩個位元組表示的是介面數量,後面跟著就是介面的表。我們這個類沒有任何介面,所以應該是0000。果不其然,查找位元組碼文件得到的就是0000。
欄位表集合(fields)
欄位表用於描述類和介面中聲明的變數。這裡的欄位包含了類級別變數以及實例變數,但是不包括方法內部聲明的局部變數。接下來就是2+n個欄位屬性。我們只有一個屬性a,所以應該是0001。查找文件果不其然是0001。
該區域含有欄位的訪問標誌, 訪問許可權, 欄位的名稱索引, 欄位的描述符索引, 屬性表
描述符的作用就是用來描述欄位的數據類型、方法的參數列表和返回值。而屬性表就是為欄位表和方法表提供額外信息的表結構。對於欄位來說,此處如果將欄位聲明為一個static final msg = “aaa”的常量,則欄位後就會跟著一個屬性表,其中存在一項名為ConstantValue,指向常量池中的一個常量,值為的”aaa”。
方法(methods)
包含訪問標誌表, 方法名索引 , 方法描述符索引, 屬性表數量,等
Attribute
0×0001 :同樣的,表示有1個Attributes了。
0x000f : #15(「SourceFile」)
0×0000 0002 attribute_length=2
0×0010 : sourcefile_index = #16(「Demo.java」)
SourceFile屬性用來記錄生成該Class文件的源碼文件名稱。
JAVA常量池中存儲的常量是什麼
我當初也存在這樣一個疑問,下面我把當初所搜集的一些資料以及自己的理解貼出來給你看看(比較多,需要耐心點看,呵呵):
理解Java常量池
JVM運行時數據區的內存模型由五部分組成:
【1】方法區
【2】堆
【3】JAVA棧
【4】PC寄存器
【5】本地方法棧
對於String s = “haha” ,它的虛擬機指令:
0: ldc #16; //String haha
2: astore_1
3: return
對於上面虛擬機指令,其各自的指令流程在《深入JAVA虛擬機》這樣描述到(結合上面實例):
ldc指令格式:ldc,index
ldc指令過程:
要執行ldc指令,JVM首先查找index所指定的常量池入口,在index指向的常量池入口,JVM將會查找CONSTANT_Integer_info,CONSTANT_Float_info和CONSTANT_String_info入口。如果還沒有這些入口,JVM會解析它們。而對於上面的hahaJVM會找到CONSTANT_String_info入口,同時,將把指向被拘留String對象(由解析該入口的進程產生)的引用壓入操作數棧。
astore_1指令格式:astore_1
astore_1指令過程:
要執行astore_1指令,JVM從操作數棧頂部彈出一個引用類型或者returnAddress類型值,然後將該值存入由索引1指定的局部變數中,即將引用類型或者returnAddress類型值存入局部變數1。
return 指令的過程:
從方法中返回,返回值為void。
談一下我個人理解:
從上面的ldc指令的執行過程可以得出:s的值是來自被拘留String對象(由解析該入口的進程產生)的引用,即可以理解為是從被拘留String對象的引用複製而來的,故我個人的理解是s的值是存在棧當中。上面是對於s值得分析,接著是對於”haha”值的分析,我們知道,對於String s = “haha” 其中”haha”值在JAVA程序編譯期就確定下來了的。簡單一點說,就是haha的值在程序編譯成class文件後,就在class文件中生成了(大家可以用UE編輯器或其它文本編輯工具在打開class文件後的位元組碼文件中看到這個haha值)。執行JAVA程序的過程中,第一步是class文件生成,然後被JVM裝載到內存執行。那麼JVM裝載這個class到內存中,其中的haha這個值,在內存中是怎麼為其開闢空間並存儲在哪個區域中呢?
說到這裡,我們不妨先來了解一下JVM常量池這個結構,《深入JAVA虛擬機》書中有這樣的描述:
常量池
虛擬機必須為每個被裝載的類型維護一個常量池。常量池就是該類型所用到常量的一個有序集和,包括直接常量(string,integer和floating point常量)和對其他類型,欄位和方法的符號引用。對於String常量,它的值是在常量池中的。而JVM中的常量池在內存當中是以表的形式存在的,對於String類型,有一張固定長度的CONSTANT_String_info表用來存儲文字字元串值,注意:該表只存儲文字字元串值,不存儲符號引用。說到這裡,對常量池中的字元串值的存儲位置應該有一個比較明了的理解了。
在介紹完JVM常量池的概念後,接著談開始提到的”haha”的值的內存分布的位置。對於haha的值,實際上是在class文件被JVM裝載到內存當中並被引擎在解析ldc指令並執行ldc指令之前,JVM就已經為haha這個字元串在常量池的CONSTANT_String_info表中分配了空間來存儲haha這個值。既然haha這個字元串常量存儲在常量池中,根據《深入JAVA虛擬機》書中描述:常量池是屬於類型信息的一部分,類型信息也就是每一個被轉載的類型,這個類型反映到JVM內存模型中是對應存在於JVM內存模型的方法區中,也就是這個類型信息中的常量池概念是存在於在方法區中,而方法區是在JVM內存模型中的堆中由JVM來分配的。所以,haha的值是應該是存在堆空間中的。
而對於String s = new String(“haha”) ,它的JVM指令:
0: new #16; //class String
3: dup
4: ldc #18; //String haha
6: invokespecial #20; //Method java/lang/String.””:(Ljava/lang/String;)V
9: astore_1
10: return
對於上面虛擬機指令,其各自的指令流程在《深入JAVA虛擬機》這樣描述到(結合上面實例):
new指令格式:new indexbyte1,indexbyte2
new指令過程:
要執行new指令,Jvm通過計算(indextype18)|indextype2生成一個指向常量池的無符號16位索引。然後JVM根據計算出的索引查找常量池入口。該索引所指向的常量池入口必須為CONSTANT_Class_info。如果該入口尚不存在,那麼JVM將解析這個常量池入口,該入口類型必須是類。JVM從堆中為新對象映像分配足夠大的空間,並將對象的實例變數設為默認值。最後JVM將指向新對象的引用objectref壓入操作數棧。
dup指令格式:dup
dup指令過程:
要執行dup指令,JVM複製了操作數棧頂部一個字長的內容,然後再將複製內容壓入棧。本指令能夠從操作數棧頂部複製任何單位字長的值。但絕對不要使用它來複制操作數棧頂部任何兩個字長(long型或double型)中的一個字長。上面例中,即複製引用objectref,這時在操作數棧存在2個引用。
ldc指令格式:ldc,index
ldc指令過程:
要執行ldc指令,JVM首先查找index所指定的常量池入口,在index指向的常量池入口,JVM將會查找CONSTANT_Integer_info,CONSTANT_Float_info和CONSTANT_String_info入口。如果還沒有這些入口,JVM會解析它們。而對於上面的haha,JVM會找到CONSTANT_String_info入口,同時,將把指向被拘留String對象(由解析該入口的進程產生)的引用壓入操作數棧。
invokespecial指令格式:invokespecial,indextype1,indextype2
invokespecial指令過程:對於該類而言,該指令是用來進行實例初始化方法的調用。鑒於該指令篇幅,具體可以查閱《深入JAVA虛擬機》中描述。上面例子中,即通過其中一個引用調用String類的構造器,初始化對象實例,讓另一個相同的引用指向這個被初始化的對象實例,然後前一個引用彈出操作數棧。
astore_1指令格式:astore_1
astore_1指令過程:
要執行astore_1指令,JVM從操作數棧頂部彈出一個引用類型或者returnAddress類型值,然後將該值存入由索引1指定的局部變數中,即將引用類型或者returnAddress類型值存入局部變數1。
return 指令的過程:
從方法中返回,返回值為void。
要執行astore_1指令,JVM從操作數棧頂部彈出一個引用類型或者returnAddress類型值,然後將該值存入由索引1指定的局部變數中,即將引用類型或者returnAddress類型值存入局部變數1。
通過上面6個指令,可以看出,String s = new String(“haha”);中的haha存儲在堆空間中,而s則是在操作數棧中。
上面是對s和haha值的內存情況的分析和理解;那對於String s = new String(“haha”);語句,到底創建了幾個對象呢?
我的理解:這裡”haha”本身就是常量池中的一個對象,而在運行時執行new String()時,將常量池中的對象複製一份放到堆中,並且把堆中的這個對象的引用交給s持有。所以這條語句就創建了2個String對象。
下面是一些String相關的常見問題:
String中的final用法和理解
final StringBuffer a = new StringBuffer(“111”);
final StringBuffer b = new StringBuffer(“222”);
a=b;//此句編譯不通過
final StringBuffer a = new StringBuffer(“111”);
a.append(“222”);//編譯通過
可見,final只對引用的”值”(即內存地址)有效,它迫使引用只能指向初始指向的那個對象,改變它的指向會導致編譯期錯誤。至於它所指向的對象的變化,final是不負責的。
String 常量池問題的幾個例子
下面是幾個常見例子的比較分析和理解:
[1]
String a = “a1”;
String b = “a” + 1;
System.out.println((a == b)); //result = true
String a = “atrue”;
String b = “a” + “true”;
System.out.println((a == b)); //result = true
String a = “a3.4”;
String b = “a” + 3.4;
System.out.println((a == b)); //result = true
分析:JVM對於字元串常量的”+”號連接,將程序編譯期,JVM就將常量字元串的”+”連接優化為連接後的值,拿”a” + 1來說,經編譯器優化後在class中就已經是a1。在編譯期其字元串常量的值就確定下來,故上面程序最終的結果都為true。
[2]
String a = “ab”;
String bb = “b”;
String b = “a” + bb;
System.out.println((a == b)); //result = false
分析:JVM對於字元串引用,由於在字元串的”+”連接中,有字元串引用存在,而引用的值在程序編譯期是無法確定的,即”a” + bb無法被編譯器優化,只有在程序運行期來動態分配並將連接後的新地址賦給b。所以上面程序的結果也就為false。
[3]
String a = “ab”;
final String bb = “b”;
String b = “a” + bb;
System.out.println((a == b)); //result = true
分析:和[3]中唯一不同的是bb字元串加了final修飾,對於final修飾的變數,它在編譯時被解析為常量值的一個本地拷貝存儲到自己的常量池中或嵌入到它的位元組碼流中。所以此時的”a” + bb和”a” + “b”效果是一樣的。故上面程序的結果為true。
[4]
String a = “ab”;
final String bb = getBB();
String b = “a” + bb;
System.out.println((a == b)); //result = false
private static String getBB() {
return “b”;
}
分析:JVM對於字元串引用bb,它的值在編譯期無法確定,只有在程序運行期調用方法後,將方法的返回值和”a”來動態連接並分配地址為b,故上面程序的結果為false。
通過上面4個例子可以得出得知:
String s = “a” + “b” + “c”;
就等價於String s = “abc”;
String a = “a”;
String b = “b”;
String c = “c”;
String s = a + b + c;
這個就不一樣了,最終結果等於:
StringBuffer temp = new StringBuffer();
temp.append(a).append(b).append(c);
String s = temp.toString();
由上面的分析結果,可就不難推斷出String 採用連接運算符(+)效率低下原因分析,形如這樣的代碼:
public class Test {
public static void main(String args[]) {
String s = null;
for(int i = 0; i 100; i++) {
s += “a”;
}
}
}
每做一次 + 就產生個StringBuilder對象,然後append後就扔掉。下次循環再到達時重新產生個StringBuilder對象,然後 append 字元串,如此循環直至結束。 如果我們直接採用 StringBuilder 對象進行 append 的話,我們可以節省 N – 1 次創建和銷毀對象的時間。所以對於在循環中要進行字元串連接的應用,一般都是用StringBuffer或StringBulider對象來進行append操作。
String對象的intern方法理解和分析:
public class Test4 {
private static String a = “ab”;
public static void main(String[] args){
String s1 = “a”;
String s2 = “b”;
String s = s1 + s2;
System.out.println(s == a);//false
System.out.println(s.intern() == a);//true
}
}
這裡用到Java裡面是一個常量池的問題。對於s1+s2操作,其實是在堆裡面重新創建了一個新的對象,s保存的是這個新對象在堆空間的的內容,所以s與a的值是不相等的。而當調用s.intern()方法,卻可以返回s在常量池中的地址值,因為a的值存儲在常量池中,故s.intern和a的值相等
原創文章,作者:YOLVE,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/130107.html