Java學習篇【三、進制、數據類型與內存分析】

先來看一些聲明例子:

int a, b, c;             // 聲明三個int型整數:a、 b、c
int d = 3, e = 4, f = 5; // 聲明三個整數並賦予初值
byte z = 22;             // 聲明並初始化 z
String s = "bboyhan";    // 聲明並初始化字符串 s
double pi = 3.14159;     // 聲明瞭雙精度浮點型變量 pi
char x = 'x';            // 聲明變量 x 的值是字符 'x'。

1、字節與進制

我們都知道,在計算機裏邊,所有的一切都是以二進制01的形式進行表示。在計算機中,表示數據的最小單位,叫位(bit),也叫比特位。byte,叫做字節。

1byte(也可以表示爲1B)=8bit,1KB=1024B。字節換算表如下:

換算公式
1KB(Kilobyte,千字節)=1024B= 10^3 B
1MB(Megabyte,兆字節,百萬字節,簡稱“兆”)=1024KB= 10^6 B
1GB(Gigabyte,吉字節,十億字節,又稱“千兆”)=1024MB= 10^9 B
1TB(Terabyte,萬億字節,太字節)=1024GB= 10^12 B
1PB(Petabyte,千萬億字節,拍字節)=1024TB= 10^15 B
1EB(Exabyte,百億億字節,艾字節)=1024PB= 10^18 B
1ZB(Zettabyte,十萬億億字節,澤字節)= 1024EB= 10^21 B
1YB(Yottabyte,一億億億字節,堯字節)= 1024ZB= 10^24 B
1BB(Brontobyte,一千億億億字節)= 1024YB= 10^27 B
1NB(NonaByte,一百萬億億億字節) = 1024BB = 10^30 B
1DB(DoggaByte,十億億億億字節) = 1024 NB = 10^33 B

由於數據在計算機中的表示,最終以二進制的形式存在,所以有時候使用二進制,可以更直觀地解決問題。但你會發現二進制數太長了。比如int 類型佔用4個字節,32位。

比如100,用int類型的二進制數表達將是:0000 0000 0000 0000 0110 0100

這樣的表現形式從常人的理解來看,不易讀懂也不直觀。因此,C、C++、以及java中沒有提供在代碼直接寫二進制數的方法。在Java中提供了八進制和十六進制的表現形式。

1、八進制表示

# 表示方法:在進制數前面加一個零(0)。

# 10進制的100(即 1*10^2+0*10^1+0*10^0 = 100)
int a = 100;

# 8進制的100(即 1*8^2+4*8^1+4*8^0 = 100)
int a = 0144;

2、十六進制表示

# 表示方法:在進制數前面加一個0x。

# 10進制的100(即 1*10^2+0*10^1+0*10^0 = 100)
int a = 100;

# 16進制的100(即 6*16^1+4*16^0 = 100)
int a = 0x64;

2、數據類型

我們在上一篇中瞭解了“變量”的概念,現在就來深入理解一下。變量就是申請內存來存儲值。也就是說,當創建變量的時候,需要在內存中申請空間。內存管理系統根據變量的類型爲變量分配存儲空間,分配的空間只能用來儲存該類型數據。

因此,通過定義不同類型的變量,可以在內存中儲存整數、小數或者字符。Java 分爲兩大數據類型:內置數據類型(也叫基本類型)、引用數據類型

  • 基本類型: 簡單數據類型是不能簡化的、內置的數據類型、由編程語言本身定義,它表示了真實的數字、字符和整數。

  • 引用數據類型: Java語言本身不支持C++中的結構(struct)或聯合(union)數據類型,它的複合數據類型一般都是通過類或接口進行構造,類提供了捆綁數據和方法的方式,同時可以針對程序外部進行信息隱藏。

2.1 內置數據類型

Java語言提供了八種基本類型。六種數字類型(四個整數型,兩個浮點型),一種字符類型,還有一種布爾型。

類別 類型 說明 字節數 取值返回 默認值
整數型 byte Java中最小的數據類型,在內存中佔8位(bit) 1 -128~127 0
short 短整型,在內存中佔16位 2 -32768~32717 0
int 整型,用於存儲整數,在內在中佔32位 4 -2147483648~2147483647 0
long 長整型,在內存中佔64位 8 -263~263-1 0L
浮點型 float 單精度浮點型,在內存中佔32位,即4個字節,用於存儲帶小數點的數字(與double的區別在於float類型有效小數點只有6~7位) 4 3.4e-45~1.4e38 0.0f
double 雙精度浮點型,用於存儲帶有小數點的數字,在內存中佔64位 8 4.9e-324~1.8e308 0.0d
字符型 char 字符型,用於存儲單個字符,佔16位,Unicode碼,用單引號賦值 2 0~65535 `\u0000`
布爾型 boolean 布爾類型,佔1個字節,用於判斷真或假 1 僅有兩個值,即true、false false

注: 實際上,JAVA中還存在另外一種基本類型void,它也有對應的包裝類 java.lang.Void,不過我們無法直接對它們進行操作。

2.2 引用數據類型

在Java中,引用類型的變量非常類似於C/C++的指針。引用類型指向一個對象,指向對象的變量是引用變量。對象、數組都是引用數據類型。 所有引用類型的默認值都是null。

User a = new User(“bboyhan”),即a爲引用變量,new User("bboyhan")爲創建的一個對象實例,a變量指向這個對象。

2.3 數據類型與內存的關係

Java的數據類型定義之後進行內存分配,有1個前提條件,即確定變量的類型。確定了變量的類型,即確定了數據需分配內存空間的大小,數據在內存的存儲方式。

  • 基本數據類型:所有的內置數據類型不存在“引用”的概念,基本數據類型都是直接存儲在內存中的內存棧上的,數據本身的值就是存儲在棧空間裏面

  • 引用數據類型:引用類型繼承於Object類(也是引用類型)都是按照Java裏面存儲對象的內存模型來進行數據存儲的,使用Java內存堆和內存棧來進行這種類型的數據存儲。簡單地講,“引用”是存儲在有序的內存棧上的,而對象本身的值存儲在內存堆上的。由此可見,不管何種引用類型的變量,他們引用的都是對象。

3、內存分析(堆、棧、方法區)

Java的程序運行,得益於JVM(Java虛擬機)。因此,在談到Java的內存問題,其實指的是JVM的內存問題。首先,我們先來理解一下Java程序的執行過程:

java程序執行過程

首先,Java源代碼文件(.java後綴)會被Java編譯器編譯爲字節碼文件(.class後綴),然後由JVM中的類加載器加載各個類的字節碼文件,加載完畢之後,交由JVM執行引擎執行。在整個程序執行過程中,JVM會用一段空間來存儲程序執行期間需要用到的數據和相關信息,這段空間一般被稱作爲Runtime Data Area(運行時數據區),也就是我們常說的JVM內存。因此,在Java中我們常常說到的內存管理就是針對這段空間進行管理(如何分配和回收內存空間)。

運行時數據區 大致可分爲:

  1. 堆(Heap)
  2. 棧(Stack),也叫Java虛擬機棧
  3. 方法區(Method Area)
  4. 程序計數器(Program Counter Register)
  5. 本地方法棧(Native Method Stack)

運行時數據區

3.1 堆(Heap)

在HotSpot JVM 實現中Heap內存被“分代”管理。分割爲:

  1. Heap Memory 堆內存

堆內存是我們程序運行時可以申請的內存空間,用於存儲程序運行時的數據信息。

  1. Non Heap Memory 非堆內存

除了堆內存區域用來存放存活(living)的數據,JVM 還需要尤其是類描述、元數據等更多信息。所以這些信息統一被存放在命名爲Permanent generation(永久/常駐代)的區域。非堆內存其實就是JVM 留給自己用的,所以方法區、JVM 內部處理或優化所需的內存(如JIT編譯後的代碼緩存)、每個類結構(如運行時常數池、字段和方法數據)以及方法和構造方法的代碼等都在非堆內存中。並且,非堆內存由JVM 管理,我們無法在程序中使用。

Heap 是應用程序在運行期請求操作系統分配給自己的向高地址擴展的數據結構,是不連續的內存區域。由於從操作系統/JVM 管理的內存分配,所以在分配和銷燬時都要佔用時間,因此用堆的效率比較低。但是堆的優點在於:編譯器不必知道要從堆裏分配多少存儲空間,也不必知道存儲的數據要在堆裏停留多長的時間。因此,用堆保存數據時會得到更大的靈活性。事實上,面向對象的多態性,堆內存分配是必不可少的,因爲多態變量所需的存儲空間只有在運行時創建了對象之後才能確定。所以堆內存最大的特點就是:堆允許程序在運行時動態地申請某個大小的內存空間。

在JVM 中,堆(Heap)是可供各條線程共享的運行時內存區域,也是供所有類實例和數組對象分配內存的區域。Java堆在虛擬機啓動的時候就被創建,它存儲了被自動內存管理系統(Automatic Storage Management System,也即是常說的“Garbage Collector(垃圾收集器)”)所管理的各種對象,這些受管理的對象無需,也無法顯式地被銷燬。Java堆的容量可以是固定大小的,也可以隨着程序執行的需求動態擴展,並在不需要過多空間時自動收縮。Java堆所使用的內存亦不需要保證是連續的。

JVM 實現應當提供給程序員或者最終用戶調節Java堆初始容量的手段,對於可以動態擴展和收縮Java堆來說,則應當提供調節其最大、最小容量的手段。在Java 中,要求創建一個對象時,只需用new 關鍵字及相關的代碼即可。執行這些代碼時,JVM 會在堆內存中自動進行數據存儲空間的分配。

3.2 棧(Stack)

Java虛擬機棧(Java Virtual Machine Stacks),該區域屬於線程私有,它的生命週期也與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀,棧它是用於支持續虛擬機進行方法調用和方法執行的數據結構。對於執行引擎來講,活動線程中,只有棧頂的棧幀是有效的,稱爲當前棧幀,這個棧幀所關聯的方法稱爲當前方法,執行引擎所運行的所有字節碼指令都只針對當前棧幀進行操作。棧幀用於存儲局部變量表、操作數棧、動態鏈接、方法返回地址和一些額外的附加信息。在編譯程序代碼時,棧幀中需要多大的局部變量表、多深的操作數棧都已經完全確定了,並且寫入了方法表的Code屬性之中。因此,一個棧幀需要分配多少內存,不會受到程序運行期變量數據的影響,而僅僅取決於具體的虛擬機實現。

在Java虛擬機規範中,對這個區域規定了兩種異常情況:

  1. 如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常。
  2. 如果虛擬機在動態擴展棧時無法申請到足夠的內存空間,則拋出OutOfMemoryError異常。

這兩種情況存在着一些互相重疊的地方:當棧空間無法繼續分配時,到底是內存太小,還是已使用的棧空間太大,其本質上只是對同一件事情的兩種描述而已。在單線程的操作中,無論是由於棧幀太大,還是虛擬機棧空間太小,當棧空間無法分配時,虛擬機拋出的都是StackOverflowError異常,而不會得到OutOfMemoryError異常。而在多線程環境下,則會拋出OutOfMemoryError異常。

java程序執行過程

JVM爲每個新創建的線程都分配一個堆棧.也就是說,對於一個Java程序來說,它的運行就是通過對堆棧的操作來完成的。JVM對堆棧只進行兩種操作:以幀爲單位的壓棧和出棧操作(FILO,先進後出,這裏不理解的朋友後續我會在數據結構篇中細講)。

  1. 局部變量表(Local Variable Table),用來存儲方法中的局部變量(包括在方法中聲明的非靜態變量以及函數形參)。對於基本數據類型的變量,則直接存儲它的值,對於引用類型的變量,則存的是指向對象的引用。局部變量表的大小在編譯器就可以確定其大小了,因此在程序執行期間局部變量表的大小是不會改變的。
  2. 操作數棧(Operand Stack),數據運算的地方,可理解爲java虛擬機棧中的一個用於計算的臨時數據存儲區。存儲的數據與局部變量表一致含int、long、float、double、reference、returnType,操作數棧中byte、short、char壓棧前(bipush)會被轉爲int。大多數指令都在操作數棧彈棧運算,然後將結果壓棧。
  3. 動態鏈接(Dynamic Linking),指向運行時常量池的引用,因爲在方法執行的過程中有可能需要用到類中的常量,所以必須要有一個引用指向運行時常量。
  4. 方法返回地址(Return Address),當一個方法執行後,返回到方法被調用的位置,確保程序繼續執行。
  5. 其它附加信息

補充:

  1. 局部變量表,用於存放方法參數和方法內部定義的局部變量。在編譯期由Code屬性中的max_locals確定局部變量表的大小。 局部變量表的容量以變量槽(Variable Slot)爲最小單位。虛擬機規範中並沒有明確指明一個Slot應占用的內存空間大小,只是很有導向性地說到每個Slot都應該能存放一個boolean、byte、char、short、int、float、reference或returnAddress類型的數據,這8種數據類型,都可以使用32位或更小的物理內存來存放,但這種描述與明確指出“每個Slot佔用32位長度的內存空間”是有一些差別的,它允許Slot的長度可以隨着處理器、操作系統或虛擬機的不同而發生變化。只要保證即使在64位虛擬機中使用了64位的物理內存空間去實現一個Slot,虛擬機仍要使用對齊和補白的手段讓Slot在外觀上看起來與32位虛擬機中的一致。
  2. 關於操作數棧想更深入瞭解的朋友,可以網上查閱相關的操作指令結合反編譯class文件進行解讀,反編譯指令:javap -c xxx.class
  3. 在概念模型中,兩個棧幀作爲虛擬機棧的元素,是完全相互獨立的。但在大多虛擬機的實現裏都會做一些優化處理,令兩個棧幀出現一部分重疊。讓下面棧幀的部分操作數棧與上面棧幀的部分局部變量表重疊在一起,這樣在進行方法調用時就可以共用一部分數據,無須進行額外的參數複製傳遞
  4. 當一個方法開始執行後,只有兩種方式可以退出,一種是遇到方法返回的字節碼指令;一種是遇見異常,並且這個異常沒有在方法體內得到處理。無論採用何種退出方式,在方法退出之後,都需要返回到方法被調用的位置,程序才能繼續執行,方法返回時可能需要在棧幀中保存一些信息,用來幫助恢復它的上層方法的執行狀態。一般來說,方法正常退出時,調用者的PC計數器的值可以作爲返回地址,棧幀中很可能會保存這個計數器值。而方法異常退出時,返回地址是要通過異常處理器表來確定的,棧幀中一般不會保存這部分信息。

3.3 本地方法棧

本地方法棧與Java棧的作用和原理非常相似。區別只不過是Java棧是爲執行Java方法服務的,而本地方法棧則是爲執行本地方法(Native Method)服務的。在JVM規範中,並沒有對本地方發展的具體實現方法以及數據結構作強制規定,虛擬機可以自由實現它。

3.4 方法區

方法區在JVM中也是一個非常重要的區域,它與堆一樣,是被線程共享的區域。在方法區中,存儲了每個類的信息(包括類的名稱、方法信息、字段信息)、靜態變量、常量以及編譯器編譯後的代碼等。

方法區的組成 說明
類型信息 類型的完整有效名(包含直接父類),類型的修飾符,這個類型直接接口的一個有序列表等。
常量池(Constant Pool) jvm爲每個已加載的類型都維護一個常量池。常量池就是這個類型用到的常量的一個有序集合,包括實際的常量(string,integer, 和floating point常量)和對類型,域和方法的符號引用。池中的數據項象數組項一樣,是通過索引訪問的。因爲常量池存儲了一個類型所使用到的所有類型,域和方法的符號引用,所以它在java程序的動態鏈接中起了核心的作用。
域(Field)信息 jvm必須在方法區中保存類型的所有域的相關信息以及域的聲明順序,域的相關信息包括:域名、域類型、域修飾符(public, private, protected,static,final,volatile, transient的某個子集)
方法(Method)信息 同域信息一樣包括聲明順序,包括:方法名,方法的返回類型(或 void),方法參數的數量和類型(有序的) ,異常表,方法的修飾符(public, private, protected, static, final, synchronized, native, abstract的一個子集)除了abstract和native方法外,其他還有保存方法的字節碼(bytecodes)操作數棧和方法棧幀的局部變量區的大小
類變量 類變量被類的所有實例共享,即使沒有類實例時你也可以訪問它。這些變量只與類相關,所以在方法區中,它們成爲類數據在邏輯上的一部分。在jvm使用一個類之前,它必須在方法區中爲每個non-final類變量分配空間。
對類加載器的引用 如果一個類型是由用戶類加載器加載的,那麼jvm會將這個類加載器的一個引用作爲類型信息的一部分保存在方法區中。
除了常量外的所有靜態(static)變量
其它 以上例子並不全,感興趣的朋友可以繼續深入研究,歡迎指正、探討交流

補充:

  1. 在java源代碼中,完整有效名由類的所屬包名稱加一個".",再加上類名組成。例如,類Object的所屬包爲java.lang,那它的完整名稱爲java.lang.Object,但在類文件裏,所有的"."都被斜槓“/”代替,就成爲java/lang/Object。完整有效名在方法區中的表示根據不同的實現而不同。
  2. 常量池(Constant Pool) ,是每一個類或接口的常量池的運行時表示形式,在類和接口被加載到JVM後,對應的運行時常量池就被創建出來。當然並非Class文件常量池中的內容才能進入運行時常量池,在運行期間也可將新的常量放入運行時常量池中,比如String的intern方法。

3.5 程序計數器

程序計數器(Program Counter Register),也有稱作爲PC寄存器,是一個記錄着當前線程所執行的字節碼的行號指示器。程序計數器是指CPU中的寄存器,它保存的是程序當前執行的指令的地址(也可以說保存下一條指令的所在存儲單元的地址),當CPU需要執行指令時,需要從程序計數器中得到當前需要執行的指令所在存儲單元的地址,然後根據得到的地址獲取到指令,在得到指令之後,程序計數器便自動加1或者根據轉移指針得到下一條指令的地址,如此循環,直至執行完所有的指令。

在JVM中,多線程是通過線程輪流切換來獲得CPU執行時間的,因此,在任一具體時刻,一個CPU的內核只會執行一條線程中的指令,因此,爲了能夠使得每個線程都在線程切換後能夠恢復在切換之前的程序執行位置,每個線程都需要有自己獨立的程序計數器,並且不能互相被幹擾,否則就會影響到程序的正常執行次序。因此,可以說程序計數器是每個線程所私有的。

補充:

  1. 在JVM規範中規定,如果線程執行的是非native方法,則程序計數器中保存的是當前需要執行的指令的地址;
  2. 執行native本地方法時,程序計數器的值爲空(Undefined)。因爲native方法是java通過JNI直接調用本地C/C++庫,可以近似的認爲native方法相當於C/C++暴露給java的一個接口,java通過調用這個接口從而調用到C/C++方法。由於該方法是通過C/C++而不是java進行實現。那麼自然無法產生相應的字節碼,並且C/C++執行時的內存分配是由自己語言決定的,而不是由JVM決定的。
  3. 如果線程執行的是native方法,則程序計數器中的值是undefined。由於程序計數器中存儲的數據所佔空間的大小不會隨程序的執行而發生改變,因此,對於程序計數器是不會發生內存溢出現象(OutOfMemory)的。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章