面試之Java虛擬機專題

說下對象的建立方法?對象的內存佈局?對象的訪問定位?java

四種不一樣的方法建立對象程序員

一、用new語句建立對象,這是最經常使用的建立對象的方式;數據庫

二、調用對象的clone方法。編程

  MyObject obj =new MyObject();數組

  MyObject objs= obj.clone();安全

使用clone方法克隆一個對象的步驟:函數

  1)被克隆的類要實現Cloneable接口;工具

  2)被克隆的類要重寫clone方法;佈局

 1 class Obj implements Cloneable{ 2     private Date birth = new Date(); 3     public Date getBirth(){ 4        return birth; 5 } 6     public void setBirth(){ 7         this.birth=birth; 8 } 9     public void changeDate(){ 10         this.birth.setMonth(4); 11 } 12     public Object clone(){ 13         Obj o = null;//o指向了複製後的新對象
14         try{ 15             o=(Obj)super.clone();//實現淺複製
16         }catch(CloneNotSupportedException e){ 17 e.printStackTrace(); 18 } 19     //實現深複製
20     o.birth = (Date)this.getBirth().clone(); 21     return o; 22 } 23 } 24 public class TestRef{ 25     public static void main(String[] args){ 26         Obj a = new Obj(); 27         Obj b = (Obj)a.clone(); 28 b.changeDate(); 29         System.out.println("a="+a.getBirth()); 30         System.out.println("b="+b.getBirth()); 31 } 32 } 33 //程序運行結果:
34 //a=Sun Jul 13 23:58:56 CST 2013
35 //b=Mon May 13 23:58:56 CST 2013

  那麼在編程時,如何選擇使用哪一種複製方式呢?首先,檢查類有無非基本類型(即對象)的數據成員。若沒有,則返回super.clone()便可;如有,確保類中包含的全部非基本類型的成員變量都實現了深複製。性能

    Object o = super.clone();//先執行淺複製

    對每個對象attr執行如下語句:

    o.attr = this.getAttr().clone();

    最後返回。

  須要注意的是,clone方法的保護機制在Oject中clone()是被聲明爲protected的,以User類爲例,經過聲明爲protected,就能夠保證只有User類裏面才能「克隆」User對象。

引伸:淺複製和深複製有什麼區別?

  淺複製:被複制的對象的全部變量都含有與原來對象相同的值,而全部其餘對象的引用仍然指向原來的對象。換而言之,淺複製僅僅複製所考慮的對象,而不復制他引用的對象。

  深複製:被複制對象的全部變量都含有與原來對象相同的值,除去那些引用其餘對象的變量。那些引用其餘對象的變量將指向被複制的新對象,而再也不是原有的那些被引用的對象。換而言之,深複製把複製的對象所引用的對象都複製了一遍。

擴展:

原型模式主要用於對象的複製,實現一個接口(實現Cloneable接口),重寫一個方法(重寫Object類中的clone方法),即完成了原型模式。

原型模式中的拷貝分爲「淺拷貝」和「深拷貝」:

  淺拷貝:對值類型的成員變量進行值的複製,對引用類型的成員變量只複製引用,不復制引用的對象。

  深拷貝:對值類型的成員變量進行值的複製,對引用類型的成員變量也進行引用對象的複製。

  (Object類中clone方法只會拷貝對象中的基本數據類型的值,對於數據中、容器對象、引用對象等都不會拷貝,這就是淺拷貝。若是要實現深拷貝,必須將原型模式中的數組、容器對象、引用對象等另行拷貝。)

原型模式的優勢:

  1)若是建立新的對象比較複雜時,能夠利用原型模式簡化對象的建立過程。

  2)使用原型模式建立對象比直接new一個對象在性能上要好得多,由於Object類的clone方法是一個本地方法,它直接操做內存中的二進制流,特別是複製大對象時,性能的差異很是明顯。

原型模式的適用場景:

  由於以上優勢,因此在須要重複地建立類似對象時能夠考慮使用原型模式。好比須要在一個循環體內建立對象,假如對象建立過程比較複雜或者循環次數不少的話,使用原型模式不但能夠簡化建立過程,並且可使系統的總體性能提升不少。

三、運行反射手段,使用Class.forName()

  MyObject object =(MyObject)Class.forName("subin.rnd.MyObject").newInstance();

四、運用反序列化手段,調用java.io.ObjectInputStream對象的readObject()方法。

什麼是內存泄漏和內存溢出?

內存泄漏:指一個再也不被程序使用的對象或變量還在內存中佔有存儲空間。

 兩種狀況:

  1)在堆中申請的空間沒有被釋放;

  2)對象已再也不被使用但還仍然在內存中保留着;

內存泄露的典型例子是一個沒有重寫hashCode和equals方法的Key類在HashMap中保存的狀況,最後會生成不少重複的對象。全部的內存泄漏最後都會拋出OutOfMemoryError異常。

形成內存泄漏的緣由:

  1)靜態集合類

  2)各類鏈接,例如數據庫鏈接等

  3)監聽器

  4)變量不合理的做用域

內存泄露的解決方案:

  1)避免在循環中建立對象;

  2)儘早釋放無用對象的引用;

  3)儘可能少用靜態變量,由於靜態變量存放在永久代(方法區),永久代基本不參與垃圾回收;

  4)使用字符串處理,避免使用String,應大量使用StringBuffer,每個String對象都得獨立佔用內存一塊區域;

在實際場景中,你怎麼查找內存泄漏?

  可使用 Jconsole。

內存溢出:指程序運行過程當中沒法申請到足夠的內存而致使的內存的一種錯誤。

內存溢出的幾種狀況(OOM異常):

OutOfMemoryError異常:

  除了程序計數器外,虛擬機內存的其餘幾個運行時區域都有發生OutOfMemoryError(OOM)異常的可能。

  一、虛擬機棧和本地方法棧溢出

    若是線程請求的棧深度大與虛擬機所容許的最大深度,將拋出StackOverflowError異常。

    若是虛擬機在擴展棧時沒法申請到足夠的空間,則拋出OutOfMemoryError異常。

  二、堆溢出

    通常的異常信息:java.lang.OutOfMemoryError:Java heap spaces。

  解決方案:

    出現這種異常,通常手段是先經過內存影像分析工具(如Eclipse Memory Analyzer)對dump出來的堆轉存快照進行分析,重點是確認內存中的對象是不是必要的,先分清是由於內存泄漏(Memory Leak)仍是內存溢出(Memory Overflow)。

    若是是內存泄露,可進一步經過工具查看泄露對象到GC Roots的引用鏈。因而就能找到泄露對象是經過怎樣的路徑與GC Roots相關聯並致使垃圾收集器沒法自動回收。

    若是不存在泄露,那就應該檢查虛擬機的參數(-Xmx與-Xms)的設置是否適當。

  三、方法區溢出

    異常信息:java.lang.OutOfMemoryError:PermGen space。

  四、運行時常量池溢出

    異常信息:java.lang.OutOfMemoryError:PermGen space。

    若是要向運行時常量池中添加內容,最簡單的作法就是使用String.intern()這個Native方法。該方法的做用是:若是池中已經包含一個等於此String的字符串,則返回表明池中這個字符串的String對象;不然,將此String對象包含的字符串添加到常量池中,而且返回此String對象的引用。因爲常量池分配在方法區內,咱們能夠經過-XX:PermSize和-XX:MaxPermSize限制方法區的大小,從而間接限制其中常量池的容量。

  致使內存溢出的緣由:

    1)內存中加載的數據量過大,如一次從數據庫取出過多數據;

    2)集合類中有對對象的引用,使用完後未清空,使得JVM不能回收;

    3)代碼中存在死循環或循環產生過多重複的對象實體;

    4)啓動參數內存值設定的大小。

  內存溢出的解決方法:

    第一步,修改JVM啓動參數,直接增長內存。(-Xms,-Xmx參數必定不要忘記加。通常要將-Xms和-Xmx選項設置爲相同,以免在每次GC後調整堆的大小;建議堆的最大值設置爲可用的內存的最大值的80%)。

    第二步,檢查錯誤日誌,查看「OutOfMemory」錯誤前是否有其餘異常或錯誤。

    第三步,對代碼進行走查和分析,找出可能發生內存溢出的位置。

    第四步,使用內存查看工具動態查看內存使用狀況(Jconsole)。

如何減小GC出現的次數?(Java內存管理)

1)對象不用時最好顯式置爲NULL

  通常而言,爲NULL的對象都會被做爲垃圾處理,因此將不一樣的對象顯式地設爲NULL,有利於GC收集器斷定垃圾,從而提升了GC的效率。

2)儘可能少用System.gc()

  此函數建議JVM進行主GC,雖然只是建議而非必定,但不少狀況下它會觸發主GC,從而增長主GC的頻率,也即增長了間歇性停頓的次數。

3)儘可能少用靜態變量

  靜態變量屬於全局變量,不會被GC回收,他們會一直佔用內存。

4)儘可能使用StringBuffer,而不用String來累加字符串。

  因爲String是固定長度的字符串,累加String對象時,並不是在一個String對象中擴增,而是從新建立新的String對象,如str5=str1+str2+str3+str4,這條語句執行過程當中會產生多個垃圾對象,由於對次做「+」操做時都必須產生新String對象,但這些過渡對象對系統來講沒有實際意義的,只會增長更多的垃圾。避免這種狀況能夠改用StringBuffer來累加字符串,因StringBuffer是可變長的,它在原有基礎上進行擴增,不會產生中間對象。

5)分散對象建立或刪除的時間

  集中在短期內大量建立新對象,特別是大對象,會致使忽然須要大量內存,JVM在面臨這種狀況時,只能進行主GC,已回收內存或整合內存碎片,從而增長主GC的頻率。

  集中刪除對象,道理也是同樣的。它使得忽然出現了大量的垃圾對象,空閒空間必然減小,從而大大增長了下一次建立新對象時強制主GC的機會。

6)儘可能少用finalize函數。由於它會加大GC的工做量,所以儘可能少用finalize方式回收資源。

7)若是須要使用常常用到的照片,可使用軟引用類型,它能夠儘量將圖片保存在內存中,供程序調用,而不引發OutOfMemory。

8)能用基本類型如int,long,就不用Integer,Long對象

  基本類型變量佔用的內存資源比相應包裝類對象佔用的少得多,若是沒有必要,最好使用基本變量。

9)增大-Xmx的值。

數組多大放在JVM老年代?永久帶對象如何GC?若是想不被GC怎麼辦?若是想在GC中生存一次怎麼辦?

  虛擬機提供了一個-XX:PretenureSizeThreshold參數(一般是3MB),令大於這個設置值的對象直接在老年代分配。這樣作的目的是避免在Eden區及兩個Survivor區之間發生大量的內存複製。

  垃圾回收不會發生在永久代,若是永久代滿了或者是超過了臨界值,會觸發徹底垃圾回收(FullGC)。若是仔細查看垃圾回收器的輸出信息,就會發現永久代也是被回收的。這就是爲何正確的永久代大小對避免FullGC是很是重要的。

  讓對象實現finalize()方法,一次對象的自我拯救。

JVM常見的啓動參數有哪些?

  -Xms:設置堆的最小值。

  -Xmx:設置堆的最大值。

  -Xmn:設置新生代的大小。

  -Xss:設置每一個線程的棧大小。

  -XX:NewSize:設置新生代的初始值

  -XX:MaxNewSize:設置新生代的最大值

  -XX:PermSize:設置永久代的初始值

  -XX:MaxPermSize:設置永久代的最大值

  -XX:SurvivorRatio:年輕代中Eden區與Survivor區的大小比值

  -XX:PretenureSizeThreshold:零大於這個設置值的對象直接在老年代分配。

說下幾種經常使用的內存調試工具:jps、jump、jhat、jstack、jconsole、jstat

  Java內存泄露的問題調查方法:jmap,jstack的使用等等。

  jps:查看虛擬機進程的情況,如進程ID。

  jmap:用於生成堆轉儲快照文件(某一時刻的)。

  jhat:對於生成的堆轉儲快照文件進行分析。

  jstack:用來生成線程快照(某一時刻的)。生成線程快照的主要目的是定位線程長時停頓的緣由(如死鎖,死循環,等待I/O等),經過查看各個線程的調用堆棧,就能夠知道沒有響應的線程在後臺作了什麼或者等待什麼資源。

  jstat:虛擬機統計信息監視工具。如顯示垃圾收集的狀況,內存使用得狀況。

  Jconosole:主要是內存監控和線程監控。內存監控:能夠顯示內存的使用狀況。線程監控:遇到線程停頓時,可使用這個功能。

描述Java類加載器的工做原理及其組織結構

  Java類加載器的做用就是在運行時加載類。

  Java類加載器基於三個機制:委託性、可見性和單一性

  一、委託機制是指雙親委派模型。當一個類加載和初始化的時候,類僅在有須要加載的時候被加載。假設你有一個應用須要的類叫做abc.class,首先加載這個類的請求由Application類加載器委託給它的父類加載器Extension類加載器,而後再委託給Bootstrap類加載器。Bootstrap類加載器會先看看rt.jar中有沒有這個類,由於並無這個類,因此這個請求又回到Extension類加載器,它會查看jre/lib/ext目錄下有沒有這個類,若是這個類被Extension類加載器找到了,那麼它將被加載,而Application類加載器不會加載這個類;而若是這個類沒有被Extension類加載器找到,那麼再由Application類加載器從classpath中尋找,若是沒找到,就會拋出異常。

  雙親委派模型機制的優勢就是可以提升軟件系統的安全性。由於在此機制下,用戶自定義的類加載器不可能加載本應該由父類加載器加載的可靠類,從而防止不可靠的惡意代碼代替由父類加載器加載的可靠代碼。如java.lang.Object類老是由根類加載器加載的,其餘任何用戶自定義的類加載器都不可能加載含有惡意代碼的java.lang.Object類。

  二、可見性原理是子類的加載器能夠看見全部的父類加載器加載的類,而父類加載器看不到子類加載器加載的類。

  三、單一性原理是指僅加載一個類一次,這是由委託機制確保子類加載器不會再次加載父類加載器加載過的類。

Java的類加載器有三個,對應Java的三種類:

  Bootstrap Loader  //負責加載系統類(指的是內置類,像String)

  ExtClassLoader  //負責加載擴展類(就是繼承類和實現類)

  AppClassLoader  //負責加載應用類(程序員自定義的類)

    Java提供了顯式加載類的API:Class.forName(classname)。

相關文章
相關標籤/搜索