Java進階2 數組內存和對象的內存管理知識

Java進階2 數組內存和對象的內存管理知識 20131028java

前言:程序員

         在面試的時候,若是是Java的編程語言,也許你認爲沒有什麼能夠問的,只可以說明你對於Java瞭解的太淺了,幾乎就是兩個星期的節奏速成,沒有在底層掌握Java編程語言。那麼面試的時候,你就會發現不少的不會,因此在這個時候切記說你懂Java。面試

         還有有些人面試Java認爲就是面試SSH框架,其實我的理解方面,除了那種很小型的公司還有不懂技術的什麼什麼類型的企業,就會拿SSH器標準你。說一下本身的狀況:編程

         個人第一編程語言是C++,同時Java是本身的輔助,能夠算的上是本科生中學習Java最好的之一(謙虛點了),可是我本身真的對於SSH沒有掌握,由於爲了面試去學習SSh框架感受很不值,本身不喜歡爲了學習框架而去學習框架。對於Java中的框架,沒有1000也有2000的樣子,這麼多的框架怎麼學啊,因此當有須要的時候才能夠去學習。我本身掌握Spring的IOC機制,由於在暑假期間的時候確實須要,還有就是數據接口的框架,我本身掌握的是Mybatis框架技術,因此沒有去學習Hibernate框架。其實學習Java的關鍵不是說你會使用多少的框架,而是對於Java編程語言的真正意義上的掌握,而如今大多數人掌握的Java水平只是出於一種簡單的語法,根本不瞭解Java低層次的更深層次的知識,這樣面試的時候,你就會暴露出來,由於面試官問你的問題基本在教材中找不到答案,其實Java是一門輕鬆入門,可是若是想學懂得話,那就真的須要下點苦功夫了。數組

Chapter 1 Java數組內存分配緩存

1.Java是一種靜態編程語言,對應的Java數組也是靜態的即,數組被初始化以後,數組佔用的空間和數組的長度是不變的。數組初始化的方式有兩種:靜態初始化和動態初始化。app

         靜態初始化:程序員顯示的指定每個元素的初始值,有系統決定數組的長度;框架

         動態初始化:程序員指定數組的長度,由系統初始化數組的值,數組還可使用length訪問數組的長度。編程語言

         數組中的全部元素實質上都保存在內存的堆中,數組的名字保存在棧中。ide

         對於字符串數組的話,其實使用的是string pool 實現的,因此在堆中的內存中存放的知識字符串的地址。

2.數組必定要初始化嗎

         瞭解Java中數組的內存分配,其實java數組的名字是保存在棧中的,他自己不是數組對象,而是對數組對象的引用,只要讓數組的名字指向有效的數組對象便可使用數組變量。這裏的數組變量只是一個引用變量,相似C的指針,數組的初始化其實不是對數組變量執行初始化,而是在堆中建立數組對象,在堆中分配一塊連續的內存空間。

         int []arr = null;

    System.out.println(arr);這一段代碼是沒有任何問題的,由於訪問的是arr變量而不是arr的成員方法或者是屬性

    arr.length就會報錯,拋出NullPointerException,由於經過引用變量訪問一個還未引用的有效的對象的時候,就會出現這種異常。

public class TestMain {

    public static void main(String[] args) {     

       Person [] students;

       students = new Person[2];

       //students[0].printInfo(); // error NullPointerException

       //students[1].printInfo(); // error NullPointerException

       Person a = new Person(10,12.0);

       Person b = new Person(138,24.9);

       students[0] = a;

       students[1] = b;

       System.out.println("before change : ");

       students[0].printInfo();

       a.age = 100;

       a.height = 50.9;

       System.out.println("after changed : ");

       students[0].printInfo();

       /*

        * 實際上students[0] 和 a 執行的是同一個對象,當修改了a 的時候,對應的students[0]也會隨之修改

        * 數組內容一樣只是對於對象的一個引用,其中的指向的內容纔是實際的對象。

        */

    }

}

class Person{

    public int age;

    public double height;

    public Person(int a, double height){

       this.age = a; this.height = height;

    }

    public void printInfo(){

       System.out.println("age:" + this.age + ", height:" + this.height);

    }

}

Chapter 2 對象及其內存管理

         雖然Java是有JVM管理內存的,可是做爲程序員,也必須瞭解Java內存管理機制,咱們編寫源代碼不可以僅僅停留在代碼層面上,須要考慮每一行代碼對於系統的內存影響。Java的內存管理機制比較那一理解,因此可能會感受Java內存管理和實際開發距離比較遠。這是一種錯誤的理解,雖然JVM會關心程序的內存回收,可是並不意味着咱們程序員能夠隨意的使用系統的內存。

         Java內存管理分爲兩個方面:內存分配和內存回收。內存分配指的是建立Java對象是JVM爲該對象在對內存中分配內存空間;內存回收指的是當該Java對象失去引用的時候,變成垃圾,JVM的垃圾回收機制自動清理該對象,而且回收對象佔用的內存空間。

         JVM內存回收機制是由一條後臺線程維護的,並且該線程也是十分消耗資源的,若是咱們在程序中肆無忌憚的建立新對象,讓系統分配內存,那麼這些分配的內存都將有GVM的垃圾回收機制完成回收,這樣作的很差的地方是:

         不斷的分配內存空間是操做系統的內存空間減小,會下降程序的性能;同時大量已經分配內存的回收是的來及回收的負擔加劇,下降程序的性能。這一章主要介紹內存管理中的內存分配的知識。

2.1實例變量和類變量

成員變量和局部變量

         對於局部變量的話存在三種狀況:

         形參:在方法簽名中定義的局部變量,有放大調用者負責爲其賦值,隨着方法的結束而消亡。

         方法內的局部變量:在方法中定義的局部變量,必須在方法內部顯示的初始化,從初始化開始生效,而且隨着方法的結束失效;

         代碼塊的局部變量:在代碼塊中顯示的初始化,在代碼塊結束的時候,變量消亡。

         成員變量有兩種:靜態成員變量和普通的成員變量。靜態成員變量也就是說成員屬於該類而不是Class中的某一個對象。靜態變量的初始化,也就是類變量的初始化是在編譯的時候,隨着class的初始化而獲得初始化的,因此靜態變量會造編譯階段的時候就已經完成初始化,因此在普通的成員變量可使用它,不管是在靜態 變量以前仍是在靜態變量以後。可是對於靜態成員變量和靜態成員變量就會存在一個前後的問題:

public class ErrorDef{

         static int num1 = num2 + 3;

         static int num2 = 10;

}

可是對於下面的狀況是對的

public  class  RightDef{

         int num1 = num2 + 10;

         static int  num2 = 10;

}

public class TestMain {

    int num1 = num2 + 10;

    static int num2; //default 0

    public static void main(String[] args) {

       TestMain main  = new TestMain();

       System.out.println(main.num2); //0

       System.out.println(main.num1);// 10

    }

}

    在JVM中每個Class對應一個對象,每個Class能夠建立多個Java對象。因此靜態變量只會有一份。在某種意義上來所其實Class也是一個對象,因此的類都是Class的實例。每個類初始化以後,系統會爲該類建立一個對應的Class實例,程序能夠經過反射來得到某個類所對應的Class實例: Person.class ,或者是Class.forName(「Person」)便可。

 

    普通的成員變量的初始化時機:對於實例變量來講,他說與Java對象自己,每一次建立一個Java對象,都會須要爲實例變量分配內存空間,而且實例變量執行初始化。在程序中能夠在三個地方初始化成員變量:

    在聲明成員變量的時候初始化;非靜態的代碼塊兒中初始化;構造函數中初始化。前兩種方式比後一種方式更早的執行。前兩種的話,取決於器在程序中代碼的位置。僅限於Java編程語言。咱們整理一段Java代碼:

 

public class A {

    {

       a=2;// 建立A的對象的時候,會執行這一段代碼,沒建立一個對象都會調用這一段代碼,執行固然是在構造函數執行以前,並且能夠提早初始化值,可是不能夠右值,只能夠左值。

    }

    public int a; //只是一個引用

    {

       System.out.println("code block a = " +a  );

    }

    static {//靜態代碼塊,在加載類的時候執行,切只會執行一次

       System.out.println("static A ");

    }

    public A(){

       System.out.println("A.A()");

       System.out.println("in A.A() before change  a = " + a);

       a = 3;

    }

}

 

public class Base {

    public A objA;//不會調用構造函數 固然若是咱們在這裏顯示初始化的話,就會調用,在Base構造函數以前調用A的構造函數,執行一系列操做。

    static {

       System.out.println("Base static code");

    }

    {

       System.out.println("Base code ");

    }

    public Base(){

       System.out.println("Base.Base()");

       objA = new A();

    }

}

Main{

    Base b= new Base();

}

main start

Base static code

Base code

Base.Base()

static A

code block a = 2

A.A()

in A.A() before change  a = 2

 

在Java中,成員變量在聲明的時候初始化的底層實現:

double weight = 23.45;實際上是分爲兩部分實現的,當牀架Java對象的時候,根據該語句會爲其分配內存空間,可是沒有初始化值,weight = 23.45;這一句代碼會被提取出來到Java的構造器中執行,但不是構造函數。

對於Java編譯的知識咱們若是想要了解的更詳細的話,能夠將源代碼編譯以後生成class 而後使用 javap –c ClassName輸出,查看編譯的狀況。

         對於類的變量初始化時機:定義的時候直接初始化;或者使用靜態代碼塊初始化變量。兩種方式的執行順序按照其在代碼中聲明的順序執行。

下面看一段代碼:

public class Price {

    final static Price INSTANCE = new Price(2.9);

    static double initPrice = 20;

    public double currPrice;

    public Price(double discount){

       this.currPrice  = this.initPrice - discount;

    }

}

public class TestMain {

    public static void main(String[] args) throws ClassNotFoundException {

       System.out.println(Price.INSTANCE.currPrice);

       Price p = new Price(2.9);

       System.out.println(p.currPrice);

    }

}

//在第一次使用Price類的時候,靜態變量調用類的構造函數進行初始化,可是這個時候聲明的initPrice沒有進行初始化,默認是0,而不是20,因此在調用構造函數的時候會產生複數。

       String a = "yang";

       System.out.println(System.identityHashCode(a));

       String b = "yang";

       System.out.println(System.identityHashCode(b));

       String c = new String("yang");

       System.out.println(System.identityHashCode(c));

2.2繼承的執行順序

public class Base {

    static {

       System.out.println("Base static code");

    }

    {

       System.out.println("Base not static code ");

    }

    public Base(){

       System.out.println("Base.Base()");

    }

    public Base(int a){

       System.out.println("Base.Base(int )");

    }

}

public class Mid extends Base{

    static{

       System.out.println("Mid static code");

    }

    {

       System.out.println("Mid not static code");

    }

    Mid(){

       super();

       System.out.println("Mid.Mid()");

    }

    Mid(int a){

       super(a);

       System.out.println("Mid.Mid(int)");

    }

}

public class Sub extends Mid {

    static {

       System.out.println("Sub static code");

    }

    {

       System.out.println("Sub not static code");

    }

   

    Sub(){

       super(4);

       System.out.println("Sub.Sub() ");

    }

}

 

public static void main(String[] args) {

    Sub sub = new Sub();

}

Base static code

Mid static code

Sub static code

Base not static code

Base.Base(int )

Mid not static code

Mid.Mid(int)

Sub not static code

Sub.Sub()

執行順序的理解,其實首先執行的是父類的非靜態代碼區域,而後是父類的構造函數,可是super默認會執行默認的構造函數,當咱們不顯示的super執行父類的構造函數類型的時候,需有默認的構造函數,不然會直接報錯。其實在super就是指明執行哪個父類的構造函數。

         只要在程序中建立Java對象,系統老是調用最頂層的父類的初始化操做,包括初始化塊和構造函數,而後依次向下調用全部的類的初始化操做,最終執行的是本類的初始化操做,返回本類的實例,至於父類中調用哪個構造函數,分爲以下幾種狀況:1.子類的構造函數中使用super顯式的調用父類中的構造函數,系統會根據super的參數列表匹配父類的構造函數,這個是靜態綁定,也就是在編譯階段就已經肯定了。注意一點若是使用super的話,必須在構造函數中的第一句使用super指明父類的構造函數。2.子類的構造函數中執行體中的第一行代碼使用this關鍵字現實的調用該類中的重載的構造函數,系統圖會根據this調用裏傳入的實參列表來肯定該類中的另外一個構造器,執行該類的另外一個構造函數。3.既沒有super關鍵字,也沒有this關鍵字調用,系統將會在執行子類的構造器以前,隱式的調用默認的父類構造函數。

2.3 訪問子類對象中的實例變量

         子類中的方法能夠訪問父類中的實例變量,這是由於子類繼承父類就會得到父類的成員變量和方法;可是父類的方法不可以訪問子類的實例變量,由於父類不知道他被那個子類繼承,他的子類會增長那些變量。

下面分析一段代碼:父類 Base ,子類Sub extends Base , 在main中建立一個子類的對象。

public class Base {

    private int val = 2;

    public Base(){

       System.out.println("Base().val =" + this.val);

       System.out.println("Base.Base()");

       this.display();

       System.out.println(this.getClass());

    }

    public void display(){

       System.out.println("Base.val = " + val);

    }

    public Base(int a){

       System.out.println("Base.Base(int )");

    }

}

public class Sub extends Base {

    private int val= 22;

    Sub(){

       System.out.println("Sub.Sub() ");

       val = 222;

    }

    public void display(){

       System.out.println("Sub val="+val);

    }

}

public static void main(String[] args) throws ClassNotFoundException {

    Sub sub = new Sub();

}

輸出結果是:

Base().val =2首先調用父類的構造函數,其中使用this輸出的變量val是在子類中的聲明變量val初始化的2

Base.Base()//首先調用父類的構造函數

Sub val=0//這裏咱們就有點凌亂了,爲何是0,整理一下,首先是初始化類的構造函數,由於集成,因此首先是執行的父類的初始化,因此在上一條中咱們輸出的結果是在父類中初始化代碼塊的2,以後咱們在父類中使用this直接調用成員變量的話,那麼是調用的Base類的成員變量因此顯示的是2,可是咱們在後面使用的是調用函數,那麼就會有多態問題的出現,這個時候調用函數就是更具具體對象的類型去調用函數。因此這裏使用this.display()會根據類的多態調用的是子類中的函數。可是在這個時候,咱們子類對象知識分配了內存空間,而沒有初始化內存,因此這個時候沒有執行到子類的成員變量的初始化,可是咱們調用子類的成員變量固然是沒有初始化的值0.

class yang.main.Sub//咱們在程序父類中輸出類的值,會發現this指針其實是子類。

Sub.Sub()

         在這裏咱們在整理一個概念:Java對象在內存中的空間並非有構造代碼塊實現的內存分配,在構造代碼塊執行以前,其實對象在內存中的空間已經分配,構造代碼塊完成的是對內存區域的初始化工做。可是在分配內存空間的時候,沒有初始化,默認值都是0.,對於應用類型的變量則是NULL。

總結:當變量編譯時的類型和運行時類型是不一樣的,經過變量訪問它的而引用對象的實例變量的時候,該實例變量的值是由聲明該變量的類型決定的。可是經過該變量弟阿勇他引用對象的成員函數的時候,則會根據他實際的類型肯定的。

2.4父類實例的內存控制

public class Base {

    public int val = 2;

    public void display(){

       System.out.println("Base.val = " + val);

    }

}

public class Sub extends Base {

    public  int val= 22;

    public void display(){

       System.out.println("Sub val="+val);

    }

}

public static void main(String[] args) throws ClassNotFoundException {

    Base b = new Base();

    System.out.println(b.val); //2

    b.display(); // 2

   

    Sub sub = new Sub();

    System.out.println(sub.val);//22

    sub.display();//22

   

    Base btod = new Sub();

    System.out.println(btod.val);//2

    btod.display();//22

   

    Base btod2 = sub;

    System.out.println(btod2.val);//2

    btod2.display();//22

}

總結:無論聲明對象是哪種類型的,只要他們實際指向的是一個子類,那麼他調用方法就會將多態體現出來;可是若是調用的是成員變量,那麼變量的值老是和聲明這些對象的類型一致。

對於繼承的話,其實繼承了父類中的全部的函數和成員變量,可是由於在訪問權限上會作一些限制,其實子類在內存中僅僅有一個對象,可是對於父類中的內容是被隱藏掉。

Java程序中容許出現return this的語句可是不會容許出現 return super,由於在Java中不容許直接將super當成一個引用變量使用。

    若是在子類中定義了父類中已經定義的變量,這樣在Java是容許的,可是會在子類中隱藏掉,咱們可使用super關鍵字訪問

class  Parent{public String tag = 「yang」;}

class Derived extends Parent{ public String tag = 「teng」;}

Main:

Derived d = new Derived();

System.out.println(d.tag);// complie error

System.out.println(((Parent)d).tag); right  yang ;

2.4final 修飾符

2.4.1final 修飾變量

被final修飾的實例變量必須顯示的指定初始化值,並且只可以在三個位置指定初始化的值

    定義final實例變量的時候指定初始值;在非靜態代碼塊中爲final實例變量指定初始化值;在構造函數中初始化值。對於final實例變量JVM沒法默認初始化值,所以必須有程序員初始化。

對於final 靜態變量,就是是使用static聲明的變量的話,那麼只能在兩個地方進行初始化,一個是靜態代碼塊中,一個是聲明的地方。

2.4.2執行宏替換

    使用final聲明的變量在編譯階段的話會執行宏替換,相似C中的define

    再有就是Java會緩存全部的字符串常量,如執行String a = 「yang」; String b = 「yang」; a==b  is true ,由於Java的字符串緩衝池的做用,其實指向的是同一個對象的地址。字符串的話若是能夠在編譯階段 就能夠肯定的字符串,那麼就會直接進行編譯優化,進行替換,前提是在表達式中不存在變量,所有都是常量。

2.4.3final方法是不能夠被重寫的

class A{ final void funA(){}}

class B extends A {void funA(){} //error}

 

追夢的飛飛

於廣州中山大學圖書館 20131028

HomePage: http://yangtengfei.duapp.com

相關文章
相關標籤/搜索