java知識 之 對象及其內存管理

java中的內存管理分爲兩個方面:

  • 內存分配:指創建java對象時JVM爲該對象在堆空間中所分配的內存空間。

  • 內存回收:指java 對象失去引用,變成垃圾時,JVM的垃圾回收機制自動清理該對象,並回收該對象所佔用的內存。

雖然JVM 內置了垃圾回收機制,但仍可能導致內存泄露、資源泄露等,所以我們不能肆無忌憚的創建對象。此外,垃圾回收機制是由一個後臺線程完成,也是很消耗性能的。

1.實例變量和類變量

java程序中的變量,大體可以分爲成員變量局部變量。其中局部變量可分爲如下三類:

  • 形參:在方法名中定義的變量,有方法調用者負責爲其賦值,隨着方法的結束而消亡。
  • 方法內局部變量:在方法內定義的變量,必須在方法內對其進行初始化。它從初始化完成後開始生效,隨着方法結束而消亡。
  • 代碼塊內局部變量:在代碼塊內定義的變量,必須在代碼塊內對其顯示初始化。從初始化完成後生效,隨着代碼塊的結束而消亡。

局部變量的作用時間很短暫,他們被存在棧內存中。
類體內定義的變量爲成員變量。如果使用static修飾,則爲靜態變量或者類變量,否則成爲非靜態變量或者實例變量。

static:
他的作用是將實例成員編程類成員。只能修飾在類裏定義的成員部分,包括變量、方法、內部內(枚舉與接口)、初始化塊。不能用於修飾外部類、局部變量、局部內部類。

使用static修飾的成員變量是類類型,屬於類本身,沒有修飾的屬於實例變量,屬於該類的實例。在同一個JVM中,每個類可以創建多個java對象。同一個JVM中每個類只對應一個Class對象,機類變量只佔一塊內存空間,但是實例變量,每次創建便會分配一塊內存空間。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Person
{
String name;
int age;
static int eyeNum;
public void info()
{
System.out.println("我的名字是:" + name
+ ", 我的年齡是:" + age);
}
}
public class FieldTest
{
public static void main(String[] args)
{
// 類變量屬於該類本身,只要該類初始化完成,
// 程序即可使用類變量。
Person.eyeNum = 2; //①
// 通過Person類訪問eyeNum類變量
System.out.println("Person的eyeNum屬性:"
+ Person.eyeNum);
// 創建第一個Person對象
Person p = new Person();
p.name = "豬八戒";
p.age = 300;
// 通過p訪問Person類的eyeNum類變量
System.out.println("通過p變量訪問eyeNum類變量:"
+ p.eyeNum); //②
p.info();
// 創建第二個Person對象
Person p2 = new Person();
p2.name = "孫悟空";
p2.age = 500;
p2.info();
// 通過p2修改Person類的eyeNum類變量
p2.eyeNum = 3; //③
// 分別通過p、p2和Person訪問Person類的eyeNum類變量
System.out.println("通過p變量訪問eyeNum類變量:"
+ p.eyeNum);
System.out.println("通過p2變量訪問eyeNum類變量:"
+ p2.eyeNum);
System.out.println("通過Person類訪問eyeNum類變量:"
+ Person.eyeNum);
}
}

上述代碼中的內存分配如下:

當Person類初始化完成,類變量也隨之初始化完成,不管再創建多少個Person對象,系統都不再爲 eyeNum 分配內存,但會爲 name 和age 分配內存並初始化。當eyeNum值改變後,通過每個Person對象訪問eyeNum的值都隨之改變。

a.實例變量的初始化

對於實例變量,它屬於java對象本身,每次程序創建java對象時都會爲其分配內存空間,並初始化。
實例變量初始化地方:

  • 定義實例化變量時;
  • 非靜態初始化塊中;
  • 構造器中。

其中前兩種比第三種更早執行,而前兩種的執行順序與他們在程序中的排列順序相同。它們三種作用完全類似,經過編譯後都會提取到構造器中執行,且位於所有語句之前,定義變量賦值和初始化塊賦值的順序與他們在源代碼中一致。

可以使用 javap命令查看java編譯器的機制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
用法: javap <options> <classes>
其中, 可能的選項包括:
-help --help -? 輸出此用法消息
-version 版本信息
-v -verbose 輸出附加信息
-l 輸出行號和本地變量表
-public 僅顯示公共類和成員
-protected 顯示受保護的/公共類和成員
-package 顯示程序包/受保護的/公共類
和成員 (默認)
-p -private 顯示所有類和成員
-c 對代碼進行反彙編
-s 輸出內部類型簽名
-sysinfo 顯示正在處理的類的
系統信息 (路徑, 大小, 日期, MD5 散列)
-constants 顯示最終常量
-classpath <path> 指定查找用戶類文件的位置
-cp <path> 指定查找用戶類文件的位置
-bootclasspath <path> 覆蓋引導類文件的位置

b.類變量的初始化

類變量屬於java 類本身,每次運行時纔會初始化。
類變量的初始化地方:

  • 定義類變量時初始化;
  • 靜態代碼塊中初始化

如下代碼,表面上看輸出的是:17.2,17.2;但是實際上輸出的是:-2.8,17.2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Price
{
// 類成員是Price實例
final static Price INSTANCE = new Price(2.8);
// 在定義一個類變量。
static double initPrice = 20;
// 定義該Price的currentPrice實例變量
double currentPrice;
public Price(double discount)
{
// 根據靜態變量計算實例變量
currentPrice = initPrice - discount;
}
}
public class PriceTest
{
public static void main(String[] args)
{
// 通過Price的INSTANCE訪問currentPrice實例變量
System.out.println(Price.INSTANCE.currentPrice);//輸出:-2.8
// 顯式創建Price實例
Price p = new Price(2.8);
// 通過先是創建的Price實例訪問currentPrice實例變量
System.out.println(p.currentPrice); //輸出:17.2
}
}

第一次使用Price 時,程序對其進行初始化,可分爲兩個階段:
(1)系統爲類變量分配內存空間;
(2)按初始化代碼順序對變量進行初始化。

這裏的運行結果爲:-2.8,17.2
說明:初始化第一階段,系統先爲 INSTANCE,initPrice兩個類變量分配內存空間,他們的默認值爲null和0.0,接着第二階段依次爲他們賦值。對 INSTANCE 賦值時要調用 Price(2.8),創建Price實例,爲currentPrice賦值,此時,還未對 initPrice 賦值,就是用他的默認值0,則 currentPrice 值爲-2.8,接着程序再次將 initPrice 賦值爲20,但對於 currentPrice 實例變量已經不起作用了。

以下爲在ide中的debug結果截圖:

2.父類構造器

java中,創建對象時,首先會依次調用每個父類的非靜態初始化塊、構造器(總是先從Object開始),然後再使用本類的非靜態初始化塊和構造器進行初始化。在調用父類時可以用super進行顯示調用,也可以隱式調用

在子類調用父類構造器時,有以下幾種場景:

  • 子類構造器第一行代碼是用super()進行顯示調用父類構造器,則根據super傳入的參數調用相應的構造器;
  • 子類構造器第一行代碼是用this()進行顯示調用本類中重載的構造器,則根據傳入this的參數調用相應的構造器;
  • 之類構造器中沒有this和super,則在執行子類構造器前,隱式調用父類無參構造器。

注:super和this都是顯示調用構造器,只能在構造器中使用,且必須在第一行,只能使用它們其中之一,最多隻能調用一次。

一般情況下,子類對象可以訪問父類的實例變量,但父類不能訪問子類的,因爲父類不知道它會被哪個子類繼承,子類又會添加怎樣的方法。但在極端的情況下,父類可以訪問子類變量的情況,如下實例代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package cn.imtianx.p02;
class Base {
private int i = 2;
public Base() {
this.display();//this:運行時是Driver類型,編譯時是Base 類型,這裏是Driver對象
}
public void display() {
System.out.println(i);
}
}
// 繼承Base的Derived子類
class Derived extends Base {
private int i = 22;
public Derived() {
i = 222;
}
public void display() {
System.out.println(i);
}
}
public class Test {
public static void main(String[] args) {
// 創建Derived的構造器創建實例
new Derived();
}
}

上面的代碼執行後,輸出的並不是2、22或者222,而是0。在調用Derived 的構造器前會隱式調用Base的無參構造器,初始化 i= 2,此時如果輸出this.i則爲2,它訪問的是Base 類中的實例變量,但是當調用this.display()時,表現的爲Driver對象的行爲,對於driver對象,它的變量i還未賦初始值,僅僅是爲其開闢了內存空間,其值爲0。

在java 中,構造器負責實例變量的初始化(即,賦初始值),在執行構造器前,該對象內存空間已經被分配了,他們在內存中存的事其類型所對應的默認值。

在上面的代碼中,出現了變量的編譯時類型與運行時類型不同。通過該變量訪問他所引用的對象的實例變量時,該實例變量的值由申明該變量的類型決定的,當通過該變量調用它所引用的實例對象的實例方法時,該方法將由它實際所引用的對象來決定

當子類重寫父類方法時,也會出現父類調用之類方法的情形,如下具體代碼,通過上面的則很容易理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Animal
{
private String desc;
public Animal()
{
this.desc = getDesc();
}
public String getDesc()
{
return "Animal";
}
public String toString()
{
return desc;
}
}
public class Wolf extends Animal
{
private String name;
private double weight;
public Wolf(String name , double weight)
{
this.name = name;
this.weight = weight;
}
// 重寫父類的getDesc()方法
@Override
public String getDesc()
{
return "Wolf[name=" + name + " , weight="
+ weight + "]"; //輸出:Wolf[name=null , weight=0.0]
}
public static void main(String[] args)
{
System.out.println(new Wolf("灰太狼" , 32.3));
}
}

3.父子實例的內存控制

java中的繼承,在處理成員變量和方法時是不同的。如果之類重寫了父類的方法,則完全覆蓋父類的方法,並將其其移到子類中,但如果是完全同名的實例變量,則不會覆蓋,不會從父類中移到子類中。所以,對於一個引用類型的變量,如果訪問他所引用對象的實例變量時,該實例變量的值取決於申明該變量的類型,而調用方法時,則取決於它實際引用對象的類型。

在繼承中,內存中子類實例保存有父類的變量的實例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base {
int count = 2;
}
class Mid extends Base {
int count = 20;
}
public class Sub extends Mid {
int count = 200;
public static void main(String[] args) {
// 創建一個Sub對象
Sub s = new Sub();
// 將Sub對象向上轉型後賦爲Mid、Base類型的變量
Mid s2m = s;
Base s2b = s;
// 分別通過3個變量來訪問count實例變量
System.out.println(s.count); //輸出:200
System.out.println(s2m.count); //輸出:20
System.out.println(s2b.count); //輸出:2
}
}

內存中的示意圖:

在內存中只有一個Sub對象,並沒有Mid和Base對象,但存在3個count的實例變量。

子類中會隱藏父類的變量可以通過super來獲取,對於類變量,也可以通過super來訪問。

4.final 修飾符

final 的修飾範圍:

  • 修飾變量,被賦初始值後不可重新賦值;
  • 修飾方法 ,不能被重寫;
  • 修飾類,不能派生出子類。

對於final 類型的變量,初始化可以在:定義時、非靜態代碼塊和構造器中;對於final 類型的類變量,初始化可以在:定義時和靜態代碼塊中。

當final類型的變量定義時就指定初始值,那麼該該變量本質上是一個“宏變量”,編譯器會把用到該變量的地方直接用其值替換。

如果在內部內中使用局部變量,必須將其指定爲final類型的。普通的變量作用域就是該方法,隨着方法的執行結束,局部變量也隨之消失,但內部類可能產生隱式的“閉包”,使局部變量脫離它所在的方法繼續存在。內部內可能擴大局部變量的作用域,如果內部內中訪問的局部變量沒有適用final修飾,則可以隨意修改它的值,這樣將會引起混亂,所以編譯器要求被內部訪問的局部變量必須使用final 修飾。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章