標籤:工作 setname 驗證 建議 就會 imm bsp 並且 end
1.java是如何管理記憶體的
java的記憶體管理就是對象的分配和釋放問題。(其中包括兩部分)
分配:記憶體的分配是由程式完成的,程式員需要通過關鍵字new為每個對象申請記憶體空間(基本類型除外),所有的對象都在堆(Heap)中分配空間。
釋放:對象的釋放是由記憶體回收機制決定和執行的,這樣做確實簡化了程式員的工作。但同時,它也加重了JVM的工作。因為,GC為了能夠正確釋放對象,GC必須監控每一個對象的運行狀態,包括對象的申請、引用、被引用、賦值等,GC都需要進行監控。
2.什麼叫java的記憶體泄露
在java中,記憶體流失就是存在一些被分配的對象,這些對象有下面兩個特點,首先,這些對象是可達的,即在有向圖中,存在通路可以與其相連(也就是說仍存在該記憶體對象的引用);其次,這些對象是無用的,即程式以後不會再使用這些對象。如果對象滿足這兩個條件,這些對象就可以判定為Java中的記憶體流失,這些對象不會被GC所回收,然而它卻佔用記憶體。
3.JVM的記憶體地區組成
java把記憶體分兩種:一種是棧記憶體,另一種是堆記憶體
(1)在函數中定義的基本類型變數和對象的引用變數都在函數的棧記憶體中分配;
(2)堆記憶體用來存放由new建立的對象和數組以及對象的執行個體變數。在函數(代碼塊)中定義一個變數時,java就在棧中為這個變數分配記憶體空間,當超過變數的範圍後,java會自動釋放掉為該變數所分配的記憶體空間;在堆中分配的記憶體由java虛擬機器的自動記憶體回收行程來管理
堆和棧的優缺點
堆的優勢是可以動態分配記憶體大小,生存期也不必事先告訴編譯器,因為它是在運行時動態分配記憶體的。
缺點就是要在運行時動態分配記憶體,存取速度較慢;棧的優勢是,存取速度比堆要快,僅次於直接位於CPU中的寄存器。
另外,棧資料可以共用。但缺點是,存在棧中的資料大小與生存期必須是確定的,缺乏靈活性。
4.java中資料在記憶體中是如何儲存的
a)基礎資料型別 (Elementary Data Type)
java的基礎資料型別 (Elementary Data Type)共有8種,即int,short,long,byte,float,double,boolean,char(注意,並沒有String的基本類型 )。這種類型的定義是通過諸如int a = 3;long b = 255L;的形式來定義的。如int a = 3;這裡的a是一個指向int類型的引用,指向3這個字面值。這些字面值的資料,由於大小可知,生存期可知(這些字面值定義在某個程式塊裡面,程式塊退出後,欄位值就消失了),出於追求速度的原因,就存在於棧中。
另外,棧有一個很重要的特殊性,就是存在棧中的資料可以共用。比如:
我們同時定義:
int a=3;
int b=3;
編譯器先處理int a = 3;首先它會在棧中建立一個變數為a的引用,然後尋找有沒有字面值為3的地址,沒找到,就開闢一個存放3這個字面值的地址,然後將a指向3的地址。接著處理int b = 3;在建立完b這個引用變數後,由於在棧中已經有3這個字面值,便將b直接指向3的地址。這樣,就出現了a與b同時均指向3的情況。
定義完a與b的值後,再令a = 4;那麼,b不會等於4,還是等於3。在編譯器內部,遇到時,它就會重新搜尋棧中是否有4的字面值,如果沒有,重新開闢地址存放4的值;如果已經有了,則直接將a指向這個地址。因此a值的改變不會影響到b的值。
b)對象
在java中,建立一個對象包括對象的聲明和執行個體化兩步,下面用一個例題來說明對象的記憶體模型。假設有類Rectangle定義如下:
public class Rectangle { double width; double height; public Rectangle( double w, double h){
w = width; h = height; }}
(1)聲明對象時的記憶體模型
用Rectangle rect;聲明一個對象rect時,將在棧記憶體為對象的引用變數rect分配記憶體空間,但Rectangle的值為空白,稱rect是一個Null 物件。Null 物件不能使用,因為它還沒有引用任何”實體”。
(2)對象執行個體化時的記憶體模型
當執行rect=new Rectangle(3,5);時,會做兩件事:在堆記憶體中為類的成員變數width,height分配記憶體,並將其初始化為各資料類型的預設值;接著進行顯式初始化(類定義時的初始化值);最後調用構造方法,為成員變數賦值。返回堆記憶體中對象的引用(相當於首地址)給引用變數rect,以後就可以通過rect來引用堆記憶體中的對象了。
c)建立多個不同的對象執行個體
一個類通過使用new運算子可以建立多個不同的對象執行個體,這些對象執行個體將在堆中被分配不同的記憶體空間,改變其中一個對象的狀態不會影響其他對象的狀態。例如:
Rectangle r1= new Rectangle(3,5);Rectangle r2= new Rectangle(4,6);
此時,將在堆記憶體中分別為兩個對象的成員變數 width 、 height 分配記憶體空間,兩個對象在堆記憶體中佔據的空間是互不相同的。如果有:
Rectangle r1=new Rectangle(3,5);Rectangle r2=r1;
則在堆記憶體中只建立了一個對象執行個體,在棧記憶體中建立了兩個對象引用,兩個對象引用同時指向一個對象執行個體。
d)封裝類
基本類型都有對應的封裝類:如int對應Integer類,double對應Double類等,基本類型的定義都是直接在棧中,如果用封裝類來建立對象,就和普通對象一樣了。例如:int i=0;i直接儲存在棧中。Integer i(i此時是對象)= new Integer(5);這樣,i對象資料存放區在堆中,i的引用儲存在棧中,通過棧中的引用來操作對象。
e)String
String是一個特殊的封裝類資料。可以用以下兩種方式建立:String str = new String(“abc”);String str = “abc”;
第一種建立方式,和普通對象的的建立過程一樣;
第二種建立方式,java內部將此語句轉化為以下幾個步驟:
(1)先定義一個名為str的對String類的對象引用變數:String str;
(2)在棧中尋找有沒有存放值為”abc”的地址,如果沒有,則開闢一個存放字面值為”abc”
地址,接著建立一個新的String類的對象o,並將o的字串值指向這個地址,而且在棧
這個地址旁邊記下這個引用的對象o。如果已經有了值為”abc”的地址,則尋找對象o,並
回o的地址。
(3)將str指向對象o的地址。
值得注意的是,一般String類中字串值都是直接存值的。但像String str = “abc”;這種
合下,其字串值卻是儲存了一個指向存在棧中資料的引用。
為了更好地說明這個問題,我們可以通過以下的幾個代碼進行驗證。
String str1="abc";String str2="abc";System.out.println(s1==s2);//true
注意,這裡並不用 str1.equals(str2);的方式,因為這將比較兩個字串的值是否相等。==號,根據JDK的說明,只有在兩個引用都指向了同一個對象時才返回真值。而我們在這裡要看的是,str1與str2是否都指向了同一個對象。
我們再接著看以下的代碼。
String str1= new String("abc");String str2= "abc";System.out.println(str1==str2);//false
建立了兩個引用。建立了兩個對象。兩個引用分別指向不同的兩個對象。
以上兩段代碼說明,只要是用new()來建立對象的,都會在堆中建立,而且其字串是單獨存值的,即使與棧中的資料相同,也不會與棧中的資料共用。
f)數組
當定義一個數組,int x[];或int[] x;時,在棧記憶體中建立一個數組引用,通過該引用(即數組名)來引用數組。x=new int[3];將在堆記憶體中分配3個儲存 int型資料的空間,堆記憶體的首地址放到棧記憶體中,每個數組元素被初始化為0。
g)靜態變數
用static的修飾的變數和方法,實際上是指定了這些變數和方法在記憶體中的”固定位置”-static storage,可以理解為所有執行個體對象共有的記憶體空間。static變數有點類似於C中的全域變數的概念;靜態表示的是記憶體的共用,就是它的每一個執行個體都指向同一個記憶體位址。把static拿來,就是告訴JVM它是靜態,它的引用(含間接引用)都是指向同一個位置,在那個地方,你把它改了,它就不會變成原樣,你把它清理了,它就不會回來了。
那靜態變數與方法是在什麼時候初始化的呢?對於兩種不同的類屬性,static屬性與instance屬性,初始化的時機是不同的。instance屬性在建立執行個體的時候初始化,static屬性在類載入,也就是第一次用到這個類的時候初始化,對於後來的執行個體的建立,不再次進行初始化。
我們常可看到類似以下的例子來說明這個問題:
class Student{static int numberOfStudents =0;Student(){numberOfStudents ++;}}
每一次建立一個新的Student執行個體時,成員numberOfStudents都會不斷的遞增,並且所有的Student執行個體都訪問同一個numberOfStudents變數,實際上intnumberOfStudents變數在記憶體中只儲存在一個位置上。
5.java的記憶體管理執行個體
Java程式的多個部分(方法,變數,對象)駐留在記憶體中以下兩個位置:即堆和棧,現在我們只關心三類事物:執行個體變數,局部變數和對象:
執行個體變數和對象駐留在堆上
局部變數駐留在棧上
讓我們查看一個 java 程式,看看他的各部分如何建立並且映射到棧和堆中:
public class Dog {Collar c;String name;//1.main()方法位於棧上public static void main(String[] args) {//2.在棧上建立引用變數d,但Dog對象尚未存在Dog d;//3.建立新的Dog對象,並將其賦予d引用變數d = new Dog();//4.將引用變數的一個副本傳遞給go()方法d.go(d);}//5.將go()方法置於棧上,並將dog參數作為局部變數void go(Dog dog){//6.在堆上建立新的Collar對象,並將其賦予Dog的執行個體變數c = new Collar();}//7.將setName()添加到棧上,並將dogName參數作為其局部變數void setName(String dogName){//8.name的執行個體對象也引用String對象name =dogName;}//9.程式執行完成後,setName()將會完成並從棧中清除,此時,局部變數dogName也會消失,儘管它所引用的String仍在堆上}
6. 記憶體回收機制
問題一:什麼叫記憶體回收機制?
記憶體回收是一種動態儲存裝置管理技術,它自動地釋放不再被程式引用的對象,按照特定的垃圾收集演算法來實現資源自動回收的功能。當一個對象不再被引用的時候,記憶體回收它佔領的空間,以便空間被後來的新對象使用,以免造成記憶體泄露。
問題二:java的記憶體回收有什麼特點?
jAVA語言不允許程式員直接控制記憶體空間的使用。記憶體空間的分配和回收都是由JRE負責在後台自動進行的,尤其是無用記憶體空間的回收操作(garbagecollection,也稱記憶體回收),只能由運行環境提供的一個超級線程進行監測和控制。
問題三:記憶體回收行程什麼時候會運行?
一般是在CPU空閑或空間不足時自動進行記憶體回收,而程式員無法精確控制記憶體回收的時機和順序等。、
問題四:什麼樣的對象符合記憶體回收條件?
當沒有任何獲得線程能訪問一個對象時,該對象就符合記憶體回收條件。
問題五:記憶體回收行程是怎樣工作的?
記憶體回收行程如發現一個對象不能被任何活線程訪問時,他將認為該對象符合刪除條件,就將其加入回收隊列,但不是立即銷毀對象,何時銷毀並釋放記憶體是無法預知的。記憶體回收不能強制執行,然而java提供了一些方法(如:System.gc()方法),允許你請求JVM執行記憶體回收,而不是要求,虛擬機器會盡其所能滿足請求,但是不能保證JVM從記憶體中刪除所有不用的對象。
問題六:一個java程式能夠耗盡記憶體嗎?
可以。垃圾收集系統嘗試在對象不被使用時把他們從記憶體中刪除。然而,如果保持太多活的對象,系統則可能會耗盡記憶體。記憶體回收行程不能保證有足夠的記憶體,只能保證可用記憶體儘可能的得到高效的管理。
問題七:如何顯示的使對象符合記憶體回收條件?
(1)Null 參考:當對象沒有對他可到達引用時,他就符合記憶體回收的條件。也就是說如果沒有對他的引用,刪除對象的引用就可以達到目的,因此我們可以把引用變數設定為null,來符合記憶體回收的條件。
StringBuffer sb = new StringBuffer("hello");System.out.println(sb);sb= null;
(2)重新為引用變數賦值:可以通過設定引用變數引用另一個對象來解除該引用變數與一個對象間的參考關聯性。
StringBuffer sb1 = new StringBuffer(“hello”);
StringBuffer sb2 = new StringBuffer(“goodbye”);
System.out.println(sb1);
sb1=sb2;//此時”hello”符合回收條件
(3)方法內建立的對象:所建立的局部變數僅在該方法的作用期間記憶體在。一旦該方法返回,在這個方法內建立的對象就符合垃圾收集條件。有一種明顯的例外情況,就是方法的返回對象。
public static void main(String[] args) {Date d = getDate();System.out.println("d="+d);}private static Date getDate() {Date d2 = new Date();StringBuffer now = new StringBuffer(d2.toString());System.out.println(now);return d2;}
(4)隔離引用:這種情況中,被回收的對象仍具有引用,這種情況稱作隔離島。若存在這兩個執行個體,他們互相引用,並且這兩個對象的所有其他引用都刪除,其他任何線程無法訪問這兩個對象中的任意一個。也可以符合記憶體回收條件。
public class Island {Island i;public static void main(String[] args) {Island i2 = new Island();Island i3 = new Island();Island i4 = new Island();i2. i =i3;i3. i =i4;i4. i =i2;i2= null;i3= null;i4= null;}}
問題八:垃圾收集前進行清理——finalize()方法
java提供了一種機制,使你能夠在對象剛要被記憶體回收之前運行一些代碼。這段代碼位於名為finalize()的方法內,所有類從Object類繼承這個方法。由於不能保證記憶體回收行程會刪除某個對象。因此放在finalize()中的代碼無法保證運行。因此建議不要重寫finalize();
7.final問題
final使得被修飾的變數”不變”,但是由於對象型變數的本質是”引用”,使得”不變”也有了兩種含義:引用本身的不變和引用指向的對象不變。
引用本身的不變:
final StringBuffer a= new StringBuffer("immutable");final StringBuffer b= new StringBuffer("not immutable");a=b;//編譯期錯誤final StringBuffer a=new StringBuffer("immutable");final StringBuffer b=new StringBuffer("not immutable");
引用指向的對象不變:
final StringBuffer a= new StringBuffer("immutable");a.append("broken!");//編譯通過final StringBuffer a=new StringBuffer("immutable");a.append("broken!");//編譯通過
可見,final只對引用的”值”(也即它所指向的那個對象的記憶體位址)有效,它迫使引用只能指向初始指向的那個對象,改變它的指向會導致編譯期錯誤。至於它所指向的對象的變化,final是不負責的。這很類似==操作符:==操作符只負責引用的”值”相等,至於這個地址所指向的對象內容是否相等,==操作符是不管的。在舉一個例子:
public class Name {private String firstname;private String lastname;public String getFirstname() {return firstname;}public void setFirstname(String firstname) {this.firstname = firstname;}public String getLastname() {return lastname;}public void setLastname(String lastname) {this.lastname = lastname;}} public class Name {private String firstname;private String lastname;public String getFirstname() {return firstname;}public void setFirstname(String firstname) {this.firstname = firstname;}public String getLastname() {return lastname;}public void setLastname(String lastname) {this.lastname = lastname;}}
編寫測試方法:
public static void main(String[] args) {final Name name = new Name();name.setFirstname("JIM");name.setLastname("Green");System.out.println(name.getFirstname()+ " " +name.getLastname());}public static void main(String[] args) {final Name name = new Name();name.setFirstname("JIM");name.setLastname("Green");System.out.println(name.getFirstname()+" "+name.getLastname());}
理解final問題有很重要的含義。許多程式漏洞都基於此—-final只能保證引用永遠指向固定對象,不能保證那個對象的狀態不變。在多線程的操作中,一個對象會被多個線程共用或修改,一個線程對對象無意識的修改可能會導致另一個使用此對象的線程崩潰。一個錯誤的解決方案就是在此對象建立的時候把它聲明為final,意圖使得它”永遠不變”。其實那是徒勞的.final還有一個值得注意的地方,
先看以下樣本程式:
class Something {final int i ;public void doSomething() {System. out .println( "i = " + i );}}class Something {final int i;public void doSomething() {System.out.println("i = " + i);}}
對於類變數,java虛擬機器會自動進行初始化。如果給出了初始值,則初始化為該初始值。如果沒有給出,則把它初始化為該類型變數的預設初始值。但是對於用final修飾的類變數,虛擬機器不會為其賦予初值,必須在constructor(構造器)結束之前被賦予一個明確的值。可以修改為”final int i = 0;”。
8.如何把程式寫得更健壯
(1)儘早釋放無用對象的引用。
好的辦法是使用臨時變數的時候,讓引用變數在退出活動域後,自動化佈建為null,暗示垃圾收集器來收集該對象,防止發生記憶體泄露。對於仍然有指標指向的執行個體,jvm就不會回收該資源,因為記憶體回收會將值為null的對象作為垃圾,提高GC回收機制效率;
(2)定義字串應該盡量使用String str=”hello”;的形式,避免使用String str = new String(“hello”);的形式。因為要使用內容相同的字串,不必每次都new一個String。例如我們要在構造器中對一個名叫s的String引用變數進行初始化,把它設定為初始值,應當這樣做:
public class Demo {private String s;public Demo() {s = "Initial Value";}} public class Demo {private String s;...public Demo {s = "Initial Value";}...}而非s = new String("Initial Value");s = new String("Initial Value");
後者每次都會調用構造器,產生新對象,效能低下且記憶體開銷大,並且沒有意義,因為String對象不可改變,所以對於內容相同的字串,只要一個String對象來表示就可以了。也就說,多次調用上面的構造器建立多個對象,他們的String類型屬性s都指向同一個對象。
(3)我們的程式裡不可避免大量使用字串處理,避免使用String,應大量使用StringBuffer,因為String被設計成不可變(immutable)類,所以它的所有對象都是不可變對象,請看下列代碼;
String s = "Hello";s = s + " world!";String s = "Hello";s = s + " world!";
在這段代碼中,s原先指向一個String對象,內容是”Hello”,然後我們對s進行了+操作,那麼s所指向的那個對象是否發生了改變呢?答案是沒有。這時,s不指向原來那個對象了,而指向了另一個String對象,內容為”Hello world!”,原來那個對象還存在於記憶體之中,只是s這個引用變數不再指向它了。
通過上面的說明,我們很容易匯出另一個結論,如果經常對字串進行各種各樣的修改,或者說,不可預見的修改,那麼使用String來代表字串的話會引起很大的記憶體開銷。因為String對象建立之後不能再改變,所以對於每一個不同的字串,都需要一個String對象來表示。這時,應該考慮使用StringBuffer類,它允許修改,而不是每個不同的字串都要產生一個新的對象。並且,這兩種類的對象轉換十分容易。
(4)盡量少用靜態變數,因為靜態變數是全域的,GC不會回收的;
(5)盡量避免在類的建構函式裡建立、初始化大量的對象,防止在調用其自身類的構造器時造成不必要的記憶體資源浪費,尤其是大對象,JVM會突然需要大量記憶體,這時必然會觸發GC最佳化系統記憶體環境;顯示的聲明數組空間,而且申請數量還極大。
以下是初始化不同類型的對象需要消耗的時間:
運算操作 |
樣本 |
標準化時間 |
本地賦值 |
i = n |
1.0 |
執行個體賦值 |
this.i = n |
1.2 |
方法調用 |
Funct() |
5.9 |
建立對象 |
New Object() |
980 |
建立數組 |
New int[10] |
3100 |
從表中可以看出,建立一個對象需要980個單位的時間,是本地賦值時間的980倍,是方法調用時間的166倍,而建立一個數組所花費的時間就更多了。
(6)盡量在合適的情境下使用對象池技術以提高系統效能,縮減縮減開銷,但是要注意對象池的尺寸不宜過大,及時清除無效對象釋放記憶體資源,綜合考慮應用運行環境的記憶體資源限制,避免過高估計運行環境所提供記憶體資源的數量。
(7)大集合對象擁有大資料量的業務對象的時候,可以考慮分塊進行處理,然後解決一塊釋放一塊的策略。
(8)不要在經常調用的方法中建立對象,尤其是忌諱在迴圈中建立對象。可以適當的使用hashtable,vector建立一組對象容器,然後從容器中去取那些對象,而不用每次new之後又丟棄。
(9)一般都是發生在開啟大型檔案或跟資料庫一次拿了太多的資料,造成Out Of Memory Error的狀況,這時就大概要計算一下資料量的最大值是多少,並且設定所需最小及最大的記憶體空間值。
(10)盡量少用finalize函數,因為finalize()會加大GC的工作量,而GC相當於耗費系統的計算能力。
(11)不要過濫使用雜湊表,有一定開發經驗的開發人員經常會使用hash表(hash表在JDK中的一個實現就是HashMap)來緩衝一些資料,從而提高系統的運行速度。比如使用HashMap緩衝一些物料資訊、人員資訊等基礎資料,這在提高系統速度的同時也加大了系統的記憶體佔用,特別是當緩衝的資料比較多的時候。其實我們可以使用作業系統中的緩衝的概念來解決這個問題,也就是給被緩衝的分配一個一定大小的緩衝容器,按照一定的演算法淘汰不需要繼續緩衝的對象,這樣一方面會因為進行了對象緩衝而提高了系統的運行效率,同時由於緩衝容器不是無限制擴大,從而也減少了系統的記憶體佔用。現在有很多開源的緩衝實現項目,比如ehcache、oscache等,這些項目都實現了FIFO 、MRU等常見的緩衝演算法。
java中的各種資料類型在記憶體中儲存的方式