本文為joshua317原創文章,轉載請注明:轉載自joshua317博客 https://www.joshua317.com/article/189
Java內存分配與管理是Java的核心技術之一,不管學習任何一門語言,我們要知其然,知其所以然,本文主要分析下Java中類和對象在內存中的表現形式,方便我們對其有更深了解。一般Java在內存分配時會涉及到這幾個區域:棧區(stack)、堆區(heap)、方法區(Method Area)、常量池。我們先對下面幾個概念進行深刻了解后,再進行畫圖分析類和對象在內存中的變化及表現形式。
棧:存放基本類型的數據和對象的引用變量的數據,但對象本身不存放在棧中,而是存放在堆中(new 出來的對象)
堆:存放用new產生的對象數據,每個對象包含了一個與之對應的 class 類的信息。
方法區(又稱為靜態區):存放對象中用static定義的靜態成員
常量池:通常用來存放常量數據、靜態變量、類的加載信息等
一、棧區
在函數(方法)中定義的一些基本類型的變量或者對象的引用變量都在棧內存中分配。
當在一段代碼塊定義一個變量時,Java就在棧中為這個變量分配內存空間,當該變量退出該作用域后,Java會自動釋放掉為該變量所分配的內存空間,該內存空間可以立即被另作他用。棧中的數據大小和生命周期是可以確定的,當沒有引用指向數據時,這個數據就會消失。
每個方法(Method)執行時,都會創建一個方法棧區,用於存儲局部變量表、操作數棧、動態鏈接、方法出口信息等
棧中所存儲的變量和引用都是局部的(即:定義在方法體中的變量或者引用),局部變量和引用都在棧中(包括final的局部變量)
八種基本數據類型(byte、short、int、long、float、double、char、boolean)的局部變量(定義在方法體中的基本數據類型的變量)在棧中存儲的是它們對應的值
每個線程包含一個棧區,棧中只保存基本數據類型的變量和引用數據類型的變量,每個棧中的數據(基本數據類型和對象的引用)都是私有的,其它棧是無法進行訪問的。棧分為3個部分:基本類型變量區、執行環境上下文、操作指令區(存放操作指令)。
棧中還存儲局部的對象的引用(定義在方法體中的引用類型的變量),對象的引用並不是對象本身,而是對象在堆中的地址,換句話說,局部的對象的引用所指對象在堆中的地址在存儲在了棧中。當然,如果對象的引用沒有指向具體的對象,對象的引用則是null
棧的優勢是,存取速度比堆要快,僅次於寄存器,棧數據可以共享。但缺點是,存在棧中的數據大小與生存期必須是確定的,缺乏靈活性。
棧有一個很重要的特殊性,就是存在棧中的數據可以共享。
二、堆區
堆內存用來存放由new創建的對象和數組。在堆中分配的內存,由Java虛擬機的自動垃圾回收器來管理。
堆內存是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。Java堆(Java Heap)唯一目的就是存放對象實例。所有的對象實例及數組都要在**Java堆(Java Heap)**上分配內存空間。
在堆中產生了一個數組或對象后,在棧中定義一個特殊的變量,讓棧中這個變量的取值等於數組或對象在堆內存中的首地址,棧中的這個變量就成了數組或對象的引用變量。引用變量就相當於是為數組或對象起的一個名稱,以后就可以在程序中使用棧中的引用變量來訪問堆中的數組或對象。引用變量就相當於是為數組或者對象起的一個名稱。
引用變量是普通的變量,定義時在棧中分配,引用變量在程序運行到其作用域之外后被釋放。而數組和對象本身在堆中分配,即使程序運行到使用 new 產生數組或者對象的語句所在的代碼塊之外,數組和對象本身占據的內存不會被釋放,數組和對象在沒有引用變量指向它的時候,才變為垃圾,不能在被使用,但仍然占據內存空間不放,在隨后的一個不確定的時間被垃圾回收器收走(釋放掉),這也是Java比較占內存的原因。
實際上,棧中的變量指向堆內存中的變量,這就是Java中的指針!
Java的堆是一個運行時數據區,類的對象從中分配空間。對象一般通過new 來創建,例如new Date()
,它們不需要程序代碼來顯式的釋放。堆是由垃圾回收來負責的,堆的優勢是可以動態地分配內存大小,生存期也不必事先告訴編譯器,因為它是在運行時動態分配內存的,Java的垃圾收集器會自動收走這些不再使用的數據。但缺點是,由於要在運行時動態分配內存,存取速度較慢。
三、方法區
方法區跟堆一樣,又被稱為靜態區,通常存放常量數據。它存儲已被Java虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等,它跟堆一樣,被所有的線程共享。
3.1 存儲的類信息
對每個加載的類型(類class、接口interface、枚舉enum、注解annotation),JVM必須在方法區中存儲以下類型信息:
-
這個類型的完整有效名稱(全名=包名.類名)
-
這個類型直接父類的完整有效名稱( java.lang.Object除外,其他類型若沒有聲明父類,默認父類是Object)
-
這個類型的修飾符(public、abstract、final的某個子集)
-
這個類型直接接口的一個有序列表
除此之外還方法區(Method Area)存儲類信息還有
-
類型的常量池( constant pool)
-
域(Field)信息
-
方法(Method)信息
-
除了常量外的所有靜態(static)變量
3.2 存儲的常量
static final修飾的成員變量都存儲於 方法區(Method Area)中
3.3 存儲的靜態變量
-
靜態變量又稱為類變量,類中被static修飾的成員變量都是靜態變量(類變量)
-
靜態變量之所以又稱為類變量,是因為靜態變量和類關聯在一起,隨着類的加載而存在於方法區(而不是堆中)
-
八種基本數據類型(byte、short、int、long、float、double、char、boolean)的靜態變量會在方法區開辟空間,並將對應的值存儲在方法方法區,對於引用類型的靜態變量如果未用new關鍵字為引用類型的靜態變量分配對象(如:
static Object obj;
),那么對象的引用obj會存儲在方法區中,並為其指定默認值null
;若對於引用類型的靜態變量如果用new關鍵字為引用類型的靜態變量分配對象(如:static Cat cat = new Cat();
),那么對象的引用cat會存儲在方法區中,並且該對象在堆中的地址也會存儲在方法區中(注意此時靜態變量只存儲了對象的堆地址,而對象本身仍在堆內存中);當然這個過程還涉及到靜態變量初始化問題。
3.4 存儲的方法(Method)
程序運行時會加載類編譯生成的字節碼,這個過程中靜態變量(類變量)和靜態方法及普通方法對應的字節碼加載到方法區。
方法區中沒有實例變量,這是因為,類加載先於對應類對象的產生,而實例變量是和對象關聯在一起的,沒有對象就不存在實例變量,類加載時沒有對象,所以方法區中沒有實例變量。
靜態變量(類變量)和靜態方法及普通方法在方法區(Method Area)存儲方式是有區別的
四、常量池
常量池指的是在編譯期被確定,並被保存在已編譯的.class文件中的一些數據。
除了包含代碼中所定義的各種基本類型(如int、long等等)和對象型(如String及數組)的常量值(final)還包含一些以文本形式出現的符號引用,比如:類和接口的全限定名;字段的名稱和描述符;方法和名稱和描述符。
虛擬機必須為每個被裝載的類型維護一個常量池。常量池就是該類型所用到常量的一個有序集和,包括直接常量(string,integer和floating point常量)和對其他類型,字段和方法的符號引用。對於String常量,它的值是在常量池中的。而JVM中的常量池在內存當中是以表的形式存在的,對於String類型,有一張固定長度的CONSTANT_String_info表用來存儲文字字符串值,注意:該表只存儲文字字符串值,不存儲符號引用。說到這里,對常量池中的字符串值的存儲位置應該有一個比較明了的理解了。在程序執行的時候,常量池會儲存在方法區(Method Area),而不是堆中。
五、畫圖分析類實例化及操作時在內存中的變化及表現形式
上面這段代碼首先有個主程序的類Main,這個我們不過多說明。我們主要分析main函數體里面的這段代碼。
我們需要知道在Cat類中,定義了三個成員屬性:name、age、weight;定義了一個成員方法:say();
//實例化一個Cat對象
Cat cat = new Cat();
//給成員變量賦值
cat.name = "招財";
cat.age = 2;
cat.weight = 2.02;
//打印
System.out.println("小貓的名字:"+cat.name + " 小貓的年齡:"+cat.age);
//調用成員方法
cat.say();
在main() 函數里實例化對象 cat, 內存中在堆區內會給實例化對象 cat 分配一個內存地址,然后我們給對象 cat進行了賦值並且打印了一些信息,最后調用了成員方法 say() ,程序執行完畢。
1.在程序的執行過程中,首先Main類中的成員屬性和成員方法會加載到方法區
2.程序執行類Main的main() 方法時,main()函數方法體會進入棧區,這一過程叫做進棧(壓棧)。
3.程序執行到 Cat cat = new Cat();
時,首先會把Cat類的成員屬性和成員方法加載到方法區,此時方法的內存空間地址為1x000000
,同時在在堆內存開辟一塊內存空間74a14482
,用於存放 Cat 實例對象,並給成員屬性及成員方法分配對應的地址空間,比如下圖的0x000001~0x000004即為對象分配的堆內存地址,但此時成員屬性都是默認值,比如int類型默認值為0,String類型默認值為null,成員方法地址值為方法區對應成員方法體的內存地址值;然后在棧內存中會給變量cat分配一個棧地址34b23231
,用來存放Cat實例對象的引用地址的值74a14482
。
4.接下來對 cat 對象進行賦值
//給成員變量賦值
cat.name = "招財";
cat.age = 2;
cat.weight = 2.02;
先在棧區找到引用變量cat,然后根據地址值找到 new Cat() 對象的內存地址,並對里面的屬性進行賦值操作。由於成員屬性name的類型為String,為引用數據類型,所以此時會在常量池開辟一塊地址空間2x00000000
,存放招財
這個值,而age的類型為int,weight的類型為double,都為基本數據類型,所以值直接存放堆中。
5.當程序執行到 cat.say() ;
方法時,會先到棧區找到cat這個引用變量(這個變量存的是對象的引用地址),然后根據該地址值在堆內存中找到 new Cat() 對象里面的say()
方法進行調用,在調用say()
方法時,會在棧區開辟一塊空間進行運行。
6.在方法體void say()
被調用完成后,就會立刻馬上從棧內彈出(出站 ),最后,在main()函數完成后,main()函數也會出棧
本文為joshua317原創文章,轉載請注明:轉載自joshua317博客 https://www.joshua317.com/article/189