1. 程式人生 > >java new 關鍵字到底做了什麽?

java new 關鍵字到底做了什麽?

class 使用 共享 .com 分布 文章 情況下 instance ase

文章轉載自:http://m.blog.csdn.NET/article/details?id=52235915

一、關鍵字new概述

"new"可以說是Java開發者最常用的關鍵字,我們使用new創建對象,使用new並通過類加載器來實例化任何我們需要的東西,但你是否深入了解過new在編譯的瞬間都做了什麽?

在Java中使用new關鍵字創建對象變得很容易了,事實上,對這些事情你是不需要考慮的。需要訪問一個文件嗎?只需要創建一個新的File實例:new File(“jdbc.properties”),對於大多數Java開發人員而言,這就是他們需要知道的一切,是不是很簡單呢?!但當你使用了多個類加載器時,問題就不一樣了。

下面是對Oracle官網文章的翻譯:http://docs.oracle.com/javase/tutorial/java/javaOO/objectcreation.html

我們都知道,一個類為對象提供了藍圖,你從一個類創建一個對象。以下語句從createobjectdemo程序創建一個對象並將其賦值給一個引用變量:

Point originOne = new Point(23, 94);

Rectangle rectOne = new Rectangle(originOne, 100, 200);

Rectangle rectTwo = new Rectangle(50, 100);

第一行創建了一個 Point 類的對象,第二個和第三個線創建一個Rectangle 矩形類的對象。

這些陳述中的每一個都有三個部分(詳細討論):

聲明Declaration:粗體代碼是將變量名稱與對象類型關聯的變量聲明。

實例化Instantiating :new關鍵字是一個java運算符,它用來創建對象。

初始化Initialization:new運算符,隨後調用構造函數,初始化新創建的對象。

聲明一個變量來指向一個對象,即引用

在此之前,你知道,要聲明一個變量,你需要寫:

type name;

這將告訴編譯器你將使用name引用一個type類型的對象。用一個原始變量,這個聲明也保留了適當的內存量的變量。

你也可以在自己的行上聲明一個引用變量。例如:

Point originone;

如果你只是聲明一個像originone這樣的引用變量,其價值將待定,直到有一個對象真正被創造和分配給它。只是簡單地聲明一個引用變量而並沒有創建一個對象。對於這樣,你需要使用new運算符。在你的代碼中使用它之前,你必須指定一個對象給originone。否則,你會得到一個編譯器錯誤-----空指針異常。

處於這種狀態的變量,目前沒有引用任何的對象,可以說明如下(變量名,originone,一個引用沒指向任何對象)。

實例化一個類對象

new運算符實例化一個類對象,通過給這個對象分配內存並返回一個指向該內存的引用。new運算符也調用了對象的構造函數。

註意:“實例化一個類的對象”的意思就是“創建對象”。創建對象時,你正在創造一個類的“實例”,因而“實例化”一個類的對象。

new運算符需要一個單一的,後綴參數,需要調用構造函數。構造函數的名稱提供了需要實例化類的名稱。

new運算符返回它所創建的對象的引用。此引用通常被分配給一個合適的類型的變量,如:Point originone =new Point(23,94);

由new運算符返回的引用可以不需要被賦值給變量。它也可以直接使用在一個表達式中。例如: int height = new Rectangle().height;

初始化一個類對象

這是Point類的代碼

public class Point {

public int x = 0;

public int y = 0;

//constructor

public Point(int a, int b) {

x = a;

y = b;

}

}

這個類包含一個單一的構造函數。你可以識別一個構造函數,因為它的聲明使用與類具有相同的名稱,它沒有返回類型。在Point類構造函數的參數是兩個整數參數,如代碼聲明(int a,int b)。下面的語句提供了94和23作為這些參數的值:

Point originOne = new Point(23, 94); //結果可描述為下圖

技術分享圖片

這是Rectangle類,包含4個版本的構造方法

public class Rectangle {

public int width = 0;

public int height = 0;

public Point origin;

// four constructors

public Rectangle() {

origin = new Point(0, 0);

}

public Rectangle(Point p) {

origin = p;

}

public Rectangle(int w, int h) {

origin = new Point(0, 0);

width = w;

height = h;

}

public Rectangle(Point p, int w, int h) {

origin = p;

width = w;

height = h;

}

// a method for moving the rectangle

public void move(int x, int y) {

origin.x = x;

origin.y = y;

}

// a method for computing the area of the rectangle

public int getArea() {

return width * height;

}

}

每個構造函數都允許你為矩形的起始值、寬度和高度提供初始值,同時使用原始類型和引用類型。如果一個類有多個構造函數,它們必須有不同的簽名。java編譯器區分構造函數基於參數的數量和類型。當java編譯器遇到下面的代碼,它知道在矩形類,需要一點爭論,後面跟著兩個整數參數調用構造函數:

Rectangle rectOne = new Rectangle(originOne, 100, 200);

結果可描述為下圖:

技術分享圖片

總結:

1.Java關鍵字new是一個運算符。與+、-、*、/等運算符具有相同或類似的優先級。

2.創建一個Java對象需要三部:聲明引用變量、實例化、初始化對象實例。

3.實例化:就是“創建一個Java對象”-----分配內存並返回指向該內存的引用。

4.初始化:就是調用構造方法,對類的實例數據賦初值。

5.Java對象內存布局:包括對象頭和實例數據。如下圖:

技術分享圖片

對象頭:它主要包括對象自身的運行行元數據,比如哈希碼、GC分代年齡、鎖狀態標誌等;同時還包含一個類型指針,指向類元數據,表明該對象所屬的類型。

實例數據:它是對象真正存儲的有效信息,包括程序代碼中定義的各種類型的字段(包括從父類繼承下來的和本身擁有的字段)。

在hotSpot虛擬機中,對象在內存中的布局可以分成對象頭、實例數據、對齊填充三部分。對齊填充:它不是必要存在的,僅僅起著占位符的作用。

6.Object obj = new Object();

那“Object obj”這部分的語義將會反映到Java棧的本地變量表中,作為一個reference類型數據出現。而“new Object()”這部分的語義將會反映到Java堆中,形成一塊存儲了Object類型所有實例數據值(Instance Data,對象中各個實例字段的數據)的結構化內存,根據具體類型以及虛擬機實現的對象內存布局(Object Memory Layout)的不同,這塊內存的長度是不固定的。另外,在Java堆中還必須包含能查找到此對象類型數據(如對象類型、父類、實現的接口、方法等)的地址信息,這些類型數據則存儲在方法區中。

二、內存分配原理

內存分配,在哪分配?-------盡管Java對象的內存分配可以使用逃逸分析技術和棧外分配,但不可否認這僅僅是為了降低GC回收頻率以及提升GC回收效率的一種輔助手段,所以Java堆區仍然是分配/存儲對象實例的主要區域,這一點毋庸置疑。參閱http://blog.csdn.net/ljheee/article/details/52226368。

參考《Java虛擬機規範(第7版)》的描述,JVM包含三種引用類型,分別是類型 (class type),數組類型(array type)和接口類型(interface type),這些引用類型的值則分別 由類實例、數組實例以及實現了某個接口的派生類實例負責動態創建,那麽JVM中究 竟是如何為這些類型創建對應的對象實例呢?-------------如果是在Java語法層面上創建 一個對象,無非就是使用一個簡單的new關鍵字即可,但是在JVM中就沒有那麽簡 單了,其實牽扯到細節的實現相當復雜,而且過程繁多。簡單地說,當Java語法層面 使用new關鍵字創建一個Java對象時,JVM首先會檢查這個new指令的參數能否在常 量池中定位到一個類的符號引用,然後檢查與這個符號引用相對應的類是否已經成功經 歷加載、解析和初始化等步驟,當類完成裝載步驟之後,就已經完全確定出創建對象實 例時所需的內存空間大小,接下來JVM將會對其進行內存分配,以存儲所生成的對象 實例。如下圖所示:

技術分享圖片

為新對象分配內存是一件非常嚴謹和復雜的任務,JVM的設計者們不僅需要考慮內存如何分配、在哪分配等問題,並且由於內存分配算法與內存回收算法密切相關,所以還要考慮GC執行完內存回收後是否會在內存空間中產生內部碎片。如果內存空間以規整和有序的的方式分布,當為新對象分配內存時,只需要修改指針的偏移量將新對象分配在第一個空閑內存位置上,這種分配方式就叫做指針碰撞(Bump the Pointer),反之則只能使用空閑列表(Free List)執行內存分配。

基於分代的概念,Java堆區如果進一步細分的話,還可分為:新生代 ( Young )和老年代 ( Old );這也就是JVM采用的“分代思想”,簡單說,就是針對不同特征的java對象采用不同的策略實施存放和回收,所用分配機制和回收算法就不一樣。新生代 ( Young ) 又被劃分為三個區域:Eden、From Survivor、To Survivor。(《Java虛擬機精講》(高翔龍...))

分代收集算法:采用不同算法處理[存放和回收]Java瞬時對象和長久對象。大部分Java對象都是瞬時對象,朝生夕滅,存活很短暫,通常存放在Young新生代,采用復制算法對新生代進行垃圾回收。老年代對象的生命周期一般都比較長,極端情況下會和JVM生命周期保持一致;通常采用標記-壓縮算法對老年代進行垃圾回收。

這樣劃分的目的是為了使 JVM 能夠更好的管理堆內存中的對象,包括內存的分配以及回收。那麽Java堆區被細分成這麽多區域,對象實例究竟是存儲在堆區中的那一個區域下呢?在JVM運行數據區中,堆區和方法區是線程共享的數據區,任何線程都可以訪問到這兩個區域中的共享數據,由於對象實例的創建在JVM中非常頻繁,因此在並發環境下從堆中劃分內存空間是非線程安全的,所以務必需要保證數據操作的原子性。基於線程安全的考慮,如果一個類在分配內存之前成功完成的類加載,JVM會優先選擇在TLAB(Thread Local Allocation Buffer,本地線程分配緩存區)中為對象實例分配內存空間,TLAB在Java堆中是一塊線程私有數據區,它包含在Eden空間內,除了可以避免一系列的非線程安全問題外,同時還能提高內存分配的吞吐量,因此我們可以將這種內存分配方式稱之為快速分配策略。

當為對象成功分配好所需的內存空間(實例化)後,JVM接下來要做的任務就是-------初始化對象實例。JVM首先會對分配好的內存空間進行零值初始化,這一步操作確保了對象的實例字段在Java代碼中可以不用賦初值就能夠直接使用,程序能夠訪問到這些字段的數據類型所對應的零值。

對分配後的內存空間進行零值初始化後,JVM就會初始化對象頭和實例數據。最後將對象引入棧後,再更新PC寄存器中的字節碼指令地址。經過這一系列的操作步驟之後每一個Java對象實例才算是真正的創建成功。

總結:

1.在Java語法層面上創建一個對象,使用一個簡單的new關鍵字即可,但是在JVM中細節的實現相當復雜,而且過程繁多。

2.當Java語法層面使用new關鍵字創建一個Java對象時,JVM首先會檢查相對應的類是否已經成功經歷加載、解析和初始化等步驟;當類完成裝載步驟之後,就已經完全確定出創建對象實例時所需的內存空間大小,才能對其進行內存分配,以存儲所生成的對象實例。

3.實例化之後,進行初始化(初始化對象頭和實例數據)。

4.內存分配方式有:指針碰撞(Bump the Pointer)、快速分配策略、空閑列表(Free List)。

5.在並發環境下從堆中劃分內存空間是非線程安全的,new運算符具有-------數據操作的原子性;也就是說創建一個Java對象分配內存,要麽所有步驟都成功,返回對象的引用,要麽回歸到創建之前的內存狀態,返回為NULL。

6.通過new創建一個Java對象,如果成功則返回這個對象的引用,開發者不可直接操作對象實例,需要通過這個引用“牽引”。

看完這篇文章,相信你對Java關鍵字new及Java對象的完整創建過程有了更深的認識,就不會只停留在new一個對象就完了。(參閱《Java虛擬機精講》(高翔龍...))

java new 關鍵字到底做了什麽?