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