征服面試官20道的Java高頻經典面試題

前言

 

疫情確診的人數每天都在增加,離去的人也在增多,這個世界上有很多事我們無能爲力也無從選擇,日升日落,白晝黑夜,我們能看見白晝中的光芒,我們也能看見黑暗裏的流氓。暮色四合,齷齪八開。鮮花還是塑料花,香或臭,當潮水散去,現在即歷史,而歷史通常是由後人說的。

 

所以還是上次跟鄉親們說的,我們不要傳播未經證實或者不該傳播的消息,輿論的力量是我們無法估計的,有些也是我們無法承擔的,所以鄉親們也要重視起來,點到即止。無法控制別人,但可以做好自己,幫不了別人,但可以不禍害別人。

 

我不是個喜歡蹭熱度的人,上面那段話鄉親們看看就好,現階段最重要的就是老老實實待在家裏,不聚集,也儘量不出門,自己和家人都要做好安全防護,老百姓經歷了太多風風雨雨,相信這次一定也會安然無恙的渡過此劫。那待在家裏的這段時間,如果能遠程辦公的,就做好公司交代的事,無法辦公的鄉親們也不要停止學習,因爲疫情過去之後,一定會有巨大的變動或者機會來臨,而到那時,你準備好了嗎?

 

這幾天一直在想,碼之初能做點什麼?最終決定在這個期間就推出一個面試系列,都是經過我精心整理的,希望能給鄉親們一點幫助。下面進入正題。

 

 

高頻面試題

 

1、說說對象的四中引用?

1)強引用只要引用存在,垃圾回收器永遠不會回收。

 Object obj = new Object();
User user=new User();

可直接通過obj取得對應的對象 如 obj.equels(new Object()); 而這樣 obj 對象對後面 new Object 的一個強 引用,只有當 obj 這個引用被釋放之後,對象纔會被釋放掉,這也是我們經常所用到的編碼形式。 

 

2)軟引用 非必須引用,內存溢出之前進行回收,可以通過以下代碼實現

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj); 
obj = null;
sf.get();//有時候會返回null

這時候sf是對obj的一個軟引用,通過sf.get()方法可以取到這個對象,當然,當這個對象被標記爲需要回收的對象 時,則返回null; 軟引用主要用戶實現類似緩存的功能,在內存足夠的情況下直接通過軟引用取值,無需從繁忙的 真實來源查詢數據,提升速度;當內存不足時,自動刪除這部分緩存數據,從真正的來源查詢這些數據。

 

 

3)弱引用 第二次垃圾回收時回收,可以通過如下代碼實現

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj); 
obj = null;
wf.get();//有時候會返回null 
wf.isEnQueued();//返回是否被垃圾回收器標記爲即將回收的垃圾

弱引用是在第二次垃圾回收時回收,短時間內通過弱引用取對應的數據,可以取到,當執行過第二次垃圾回收時, 將返回null。弱引用主要用於監控對象是否已經被垃圾回收器標記爲即將回收的垃圾,可以通過弱引用的isEnQueued 方法返回對象是否被垃圾回收器標記。

ThreadLocal 中有使用到弱引用:

public class ThreadLocal<T> {
 static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
value = v; }
}
//....
}
//.....
}

 

4)虛引用垃圾回收時回收,無法通過引用取到對象值,可以通過如下代碼實現:

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj); 
obj=null;
pf.get();//永遠返回null
pf.isEnQueued();//返回是否從內存中已經刪除

虛引用是每次垃圾回收的時候都會被回收,通過虛引用的get方法永遠獲取到的數據爲null,因此也被成爲幽靈引 用。虛引用主要用於檢測對象是否已經從內存中刪除。

 

2、HashSet是如何保證不重複的?

向 HashSet 中 add ()元素時,判斷元素是否存在的依據,不僅要比較hash值,同時還要結合 equles 方法比較。 

HashSet 中的 add ()方法會使用 HashMap 的 add ()方法。以下是 HashSet 部分源碼: 

private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;
public HashSet() {
        map = new HashMap<>();
}
public boolean add(E e) {
}

 

HashMap 的 key 是唯一的,由上面的代碼可以看出 HashSet 添加進去的值就是作爲 HashMap 的key。所以不會 重複( HashMap 比較key是否相等是先比較 hashcode 在比較 equals )。

 

 

3、HashMap是線程安全的嗎,爲什麼不是線程安全的(最好畫圖說明多線程 環境下不安全)?

不是線程安全的; 

如果有兩個線程A和B,都進行插入數據,剛好這兩條不同的數據經過哈希計算後得到的哈希碼是一樣的,且該位 置還沒有其他的數據。所以這兩個線程都會進入我在上面標記爲1的代碼中。假設一種情況,線程A通過if判斷,該 位置沒有哈希衝突,進入了if語句,還沒有進行數據插入,這時候 CPU 就把資源讓給了線程B,線程A停在了if語句 裏面,線程B判斷該位置沒有哈希衝突(線程A的數據還沒插入),也進入了if語句,線程B執行完後,輪到線程A執 行,現在線程A直接在該位置插入而不用再判斷。這時候,你會發現線程A把線程B插入的數據給覆蓋了。發生了線 程不安全情況。本來在 HashMap 中,發生哈希衝突是可以用鏈表法或者紅黑樹來解決的,但是在多線程中,可能 就直接給覆蓋了。 

上面所說的是一個圖來解釋可能更加直觀。如下面所示,兩個線程在同一個位置添加數據,後面添加的數據就覆蓋住了前面添加的。

如果上述插入是插入到鏈表上,如兩個線程都在遍歷到最後一個節點,都要在最後添加一個數據,那麼後面添加數據的線程就會把前面添加的數據給覆蓋住。則

在擴容的時候也可能會導致數據不一致,因爲擴容是從一個數組拷貝到另外一個數組。

 

 

5、HashMap的擴容過程

當向容器添加元素的時候,會判斷當前容器的元素個數,如果大於等於閾值(知道這個閾字怎麼念嗎?不念 fa 值,念 yu 值四聲)---即當前數組的長度乘以加載因子的值的時候,就要自動擴容啦。

擴容( resize )就是重新計算容量,向 HashMap 對象裏不停的添加元素,而 HashMap 對象內部的數組無法裝載更 多的元素時,對象就需要擴大數組的長度,以便能裝入更多的元素。當然 Java 裏的數組是無法自動擴容的,方法 是使用一個新的數組代替已有的容量小的數組,就像我們用一個小桶裝水,如果想裝更多的水,就得換大水桶。

 HashMap hashMap=new HashMap(cap);
  • cap =3, hashMap 的容量爲4;
  • cap =4, hashMap 的容量爲4;
  • cap=5, 的容量爲8;
  • cap =9, hashMap 的容量爲16;
  • 如果 cap 是2的n次方,則容量爲 cap ,否則爲大於 cap 的第一個2的n次方的數。

6、Arrays.sort 和 Collections.sort 實現原理和區別?

Collection和Collections區別

java.util.Collection 是一個集合接口。它提供了對集合對象進行基本操作的通用接口方法。

java.util.Collections 是針對集合類的一個幫助類,他提供一系列靜態方法實現對各種集合的搜索、排序、 線程安全等操作。然後還有混排(Shuffling)、反轉(Reverse)、替換所有的元素(fill)、拷貝(copy)、返 回Collections中最小元素(min)、返回Collections中最大元素(max)、返回指定源列表中最後一次出現指定目 標列表的起始位置( lastIndexOfSubList )、返回指定源列表中第一次出現指定目標列表的起始位置

( IndexOfSubList )、根據指定的距離循環移動指定列表中的元素(Rotate);

 

事實上Collections.sort方法底層就是調用的array.sort方法,

public static void sort(Object[] a) {
        if (LegacyMergeSort.userRequested)
            legacyMergeSort(a);
        else
            ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
}
//void java.util.ComparableTimSort.sort()
static void sort(Object[] a, int lo, int hi, Object[] work, int workBase, int workLen)
{
        assert a != null && lo >= 0 && lo <= hi && hi <= a.length;
        int nRemaining  = hi - lo;
        if (nRemaining < 2)
return;  // Arrays of size 0 and 1 are always sorted
   // If array is small, do a "mini-TimSort" with no merges
        if (nRemaining < MIN_MERGE) {
            int initRunLen = countRunAndMakeAscending(a, lo, hi);
            binarySort(a, lo, hi, lo + initRunLen);
            return;
  }
}

legacyMergeSort (a):歸併排序 ComparableTimSort.sort() : Timsort 排序

Timsort 排序是結合了合併排序(merge sort)和插入排序(insertion sort)而得出的排序算法。

 

Timsort的核心過程:

 

TimSort 算法爲了減少對升序部分的回溯和對降序部分的性能倒退,將輸入按其升序和降序特點進行了分 區。排序的輸入的單位不是一個個單獨的數字,而是一個個的塊-分區。其中每一個分區叫一個run。針對這 些 run 序列,每次拿一個 run 出來按規則進行合併。每次合併會將兩個 run合併成一個 run。合併的結果保 存到棧中。合併直到消耗掉所有的 run,這時將棧上剩餘的 run合併到只剩一個 run 爲止。這時這個僅剩的run 便是排好序的結果。

 

綜上述過程,Timsort算法的過程包括:

 

  1. 如何數組長度小於某個值,直接用二分插入排序算法。
  2. 找到各個run,併入棧。
  3. 按規則合併run。

 

 

 

7、Cloneable接口實現原理?

Cloneable接口是Java開發中常用的一個接口, 它的作用是使一個類的實例能夠將自身拷貝到另一個新的實例中, 注意,這裏所說的“拷貝”拷的是對象實例,而不是類的定義,進一步說,拷貝的是一個類的實例中各字段的值。 

在開發過程中,拷貝實例是常見的一種操作,如果一個類中的字段較多,而我們又採用在客戶端中逐字段複製的方 法進行拷貝操作的話,將不可避免的造成客戶端代碼繁雜冗長,而且也無法對類中的私有成員進行復制,而如果讓需要 具備拷貝功能的類實現Cloneable接口,並重寫clone()方法,就可以通過調用clone()方法的方式簡潔地實現實例 拷貝功

深拷貝(深複製)和淺拷貝(淺複製)是兩個比較通用的概念,尤其在C++語言中,若不弄懂,則會在delete的時候出問 題,但是我們在這幸好用的是Java。雖然Java自動管理對象的回收,但對於深拷貝(深複製)和淺拷貝(淺複製),我們 還是要給予足夠的重視,因爲有時這兩個概念往往會給我們帶來不小的困惑。 

淺拷貝是指拷貝對象時僅僅拷貝對象本身(包括對象中的基本變量),而不拷貝對象包含的引用指向的對象。深拷 貝不僅拷貝對象本身,而且拷貝對象包含的引用指向的所有對象。舉例來說更加清楚:對象 A1 中包含對 B1 的引 用, B1 中包含對 C1 的引用。淺拷貝 A1 得到 A2 , A2 中依然包含對 B1 的引用, B1 中依然包含對 C1 的引 用。深拷貝則是對淺拷貝的遞歸,深拷貝 A1 得到 A2 , A2 中包含對 B2 ( B1 的 copy )的引用, B2 中包含 對 C2 ( C1 的 copy )的引用。 

若不對clone()方法進行改寫,則調用此方法得到的對象即爲淺拷貝 。

 

 

8、簡單講一下異常分類以及處理機制?

 

Java標準庫內建了一些通用的異常,這些類以Throwable爲頂層父類。Throwable又派生出Error類和Exception類。

錯誤:Error類以及他的子類的實例,代表了JVM本身的錯誤。錯誤不能被程序員通過代碼處理,Error很少出現。因此,程序員應該關注Exception爲父類的分支下的各種異常類。

異常:Exception以及他的子類,代表程序運行時發送的各種不期望發生的事件。可以被Java異常處理機制使用, 是異常處理的核心。

總體上我們根據 Javac 對異常的處理要求,將異常類分爲二類。

 

  • 非檢查異常( unckecked exception ): Error 和 RuntimeException 以及他們的子類。javac 在編譯時, 不會提示和發現這樣的異常,不要求在程序處理這些異常。所以如果願意,我們可以編寫代碼處理(使用catch...finally )這樣的異常,也可以不處理。對於這些異常,我們應該修正代碼,而不是去通過異常處理器處 理 。這樣的異常發生的原因多半是代碼寫的有問題。如除0錯誤 ArithmeticException ,錯誤的強制類型轉換錯 誤 ClassCastException ,數組索引越界 ArrayIndexOutOfBoundsException ,使用了空對象NullPointerException 等等。
  • 檢查異常( checked exception ):除了 Error 和 RuntimeException 的其它異常。javac 強制要求程序員 爲這樣的異常做預備處理工作(使用 try...catch...finally 或者 throws )。在方法中要麼用 try-catch 語句捕 獲它並處理,要麼用throws子句聲明拋出它,否則編譯不會通過。這樣的異常一般是由程序的運行環境導致的。因 爲程序可能被運行在各種未知的環境下,而程序員無法干預用戶如何使用他編寫的程序,於是程序員就應該爲這樣 的異常時刻準備着。如 SQLException , ClassNotFoundException 等。

 

需要明確的是:檢查和非檢查是對於 javac 來說的,這樣就很好理解和區分了。

 

 

 

9、wait和sleep的區別?

 

源碼如下:

public class Thread implements Runnable {
public static native void sleep(long millis) throws InterruptedException;
     public static void sleep(long millis, int nanos) throws InterruptedException {
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException(
        "nanosecond timeout value out of range");
        }
        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++; }
        sleep(millis);
    }
//...
}
public class Object {
     public final native void wait(long timeout) throws InterruptedException;
     public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }
        if (nanos > 0) {
timeout++; }
        wait(timeout);
    }
//...
}

1、 sleep 來自 Thread 類,和 wait 來自 Object 類。

2、最主要是sleep方法沒有釋放鎖,而wait方法釋放了 鎖,使得其他線程可以使用同步控制塊或者方法。
3、wait,notify和 notifyAll 只能在同步控制方法或者同步控制塊裏面使用,而 sleep 可以在任何地方使用(使 用範圍)

4、 sleep 必須捕獲異常,而 wait , notify 和 notifyAll 不需要捕獲異常

  • sleep 方法屬於 Thread 類中方法,表示讓一個線程進入睡眠狀態,等待一定的時間之後,自動醒來進入到可 運行狀態,不會馬上進入運行狀態,因爲線程調度機制恢復線程的運行也需要時間,一個線程對象調用了 sleep方法之後,並不會釋放他所持有的所有對象鎖,所以也就不會影響其他進程對象的運行。但在 sleep 的過程中過 程中有可能被其他對象調用它的 interrupt() ,產生 InterruptedException 異常,如果你的程序不捕獲這個異 常,線程就會異常終止,進入 TERMINATED 狀態,如果你的程序捕獲了這個異常,那麼程序就會繼續執行catch語 句塊(可能還有 finally 語句塊)以及以後的代碼。注意 sleep() 方法是一個靜態方法,也就是說他只對當前對象有效,通過 t.sleep() 讓t對象進入 sleep ,這樣 的做法是錯誤的,它只會是使當前線程被 sleep 而不是 t 線程。
  • wait 屬於 Object 的成員方法,一旦一個對象調用了wait方法,必須要採用 notify() 和 notifyAll() 方法 喚醒該進程;如果線程擁有某個或某些對象的同步鎖,那麼在調用了 wait() 後,這個線程就會釋放它持有的所有 同步資源,而不限於這個被調用了 wait() 方法的對象。wait() 方法也同樣會在 wait 的過程中有可能被其他對 象調用 interrupt() 方法而產生 。

 

 

 

10、數組在內存中如何分配?

 

對於 Java 數組的初始化,有以下兩種方式,這也是面試中經常考到的經典題目。

 

靜態初始化:初始化時由程序員顯式指定每個數組元素的初始值,由系統決定數組長度,如:

 //只是指定初始值,並沒有指定數組的長度,但是系統爲自動決定該數組的長度爲4 
 String[] computers = {"Dell", "Lenovo", "Apple", "Acer"}; //1 
//只是指定初始值,並沒有指定數組的長度,但是系統爲自動決定該數組的長度爲3 
 String[] names = new String[]{"多啦A夢", "大雄", "靜香"}; //2

 

動態初始化:初始化時由程序員顯示的指定數組的長度,由系統爲數據每個元素分配

初始值,如:

 //只是指定了數組的長度,並沒有顯示的爲數組指定初始值,但是系統會默認給數組數組元素分配初始值爲null 
 String[] cars = new String[4]; //3

 

因爲 Java 數組變量是引用類型的變量,所以上述幾行初始化語句執行後,三個數組在內存中的分配情況如下圖所 示:

 

由上圖可知,靜態初始化方式,程序員雖然沒有指定數組長度,但是系統已經自動幫我們給分配了,而動態初始化 方式,程序員雖然沒有顯示的指定初始化值,但是因爲 Java 數組是引用類型的變量,所以系統也爲每個元素分配 了初始化值 null ,當然不同類型的初始化值也是不一樣的,假設是基本類型int類型,那麼爲系統分配的初始化值 也是對應的默認值0。

 

11、說說Java反射機制?

Java 反射機制是在運行狀態中,對於任意一個類,都能夠獲得這個類的所有屬性和方法,對於任意一個對象都能夠 調用它的任意一個屬性和方法。這種在運行時動態的獲取信息以及動態調用對象的方法的功能稱爲 Java 的反射機 制。 

Class 類與 java.lang.reflect 類庫一起對反射的概念進行了支持,該類庫包含了 Field,Method,Constructor 類 (每 個類都實現了 Member 接口)。這些類型的對象時由 JVM 在運行時創建的,用以表示未知類裏對應的成員。 

這樣你就可以使用 Constructor 創建新的對象,用 get() 和 set() 方法讀取和修改與 Field 對象關聯的字段,用 invoke() 方法調用與 Method 對象關聯的方法。另外,還可以調用 getFields() getMethods() 和 getConstructors() 等很便利的方法,以返回表示字段,方法,以及構造器的對象的數組。這樣匿名對象的信息 就能在運行時被完全確定下來,而在編譯時不需要知道任何事情。 

import java.lang.reflect.Constructor;
public class ReflectTest {
    public static void main(String[] args) throws Exception {
      Class clazz = null;
      clazz = Class.forName("com.jas.reflect.Fruit");
      Constructor<Fruit> constructor1 = clazz.getConstructor();
      Constructor<Fruit> constructor2 = clazz.getConstructor(String.class);
      Fruit fruit1 = constructor1.newInstance();
      Fruit fruit2 = constructor2.newInstance("Apple");
  } 
}
class Fruit{
    public Fruit(){
      System.out.println("無參構造器 Run..........."); }
      public Fruit(String type){
      System.out.println("有參構造器 Run..........." + type);
  } 
}
運行結果: 無參構造器 Run........... 有參構造器 Run...........Apple

 

12、Java獲取反射的三種方法?

 

  1. 通過new對象實現反射機制。
  2. 通過路徑實現反射機制。
  3. 通過類名實現反射機制。

 

代碼示例:

public class Student {
    private int id;
    String name;
    protected boolean sex;
    public float score;
}
public class Get {
  //獲取反射機制三種方式
  public static void main(String[] args) throws ClassNotFoundException {
    //方式一(通過建立對象)
    Student stu = new Student();
    Class classobj1 = stu.getClass(); 
    System.out.println(classobj1.getName()); 
    //方式二(所在通過路徑-相對路徑)
    Class classobj2 = Class.forName("fanshe.Student"); 
    System.out.println(classobj2.getName()); 
    //方式三(通過類名)
    Class classobj3 = Student.class; 
    System.out.println(classobj3.getName());
  }
}

 

13、String 和 StringBuilder、StringBuffer 的區別?

Java 平臺提供了兩種類型的字符串:String 和StringBuffer/StringBuilder,它們可以儲存和操作字符串。其中 String 是隻 讀字符串,也就意味着 String 引用的字符串內容是不能被改變的。而StringBuffer/StringBuilder 類表示的字符串對象可以直接進行修改。StringBuilder 是 Java 5 中引入的,它和 StringBuffer 的方法完全相同,區 別在於它是在單線程環境下使用的,因爲它的所有方面都沒有被 synchronized修飾,因此它的效率也比 StringBuffer 要高。

14、描述一下 JVM 加載 class 文件的原理機制?

JVM 中類的裝載是由類加載器(ClassLoader)和它的子類來實現的,Java中的類加載器是一個重要的 Java 運行時系統組件,它負責在運行時查找和裝入類文件中的類。

由於 Java 的跨平臺性,經過編譯的 Java 源程序並不是一個可執行程序,而是 一個或多個類文件。當 Java 程序需要使用某個類時,JVM 會確保這個類已經被 加載、連接(驗證、準備和解析)和初始化。類的加載是指把類的.class 文件 中的數據讀入到內存中,通常是創建一個字節數組讀入.class 文件,然後產生 與所加載類對應的 Class 對象。加載完成後,Class 對象還不完整,所以此時 的類還不可用。當類被加載後就進入連接階段,這一階段包括驗證、準備(爲靜態變量分配內存並設置默認的初始值)和解析(將符號引用替換爲直接引 用)三個步驟。最後 JVM 對類進行初始化,包括:1)如果類存在直接的父類並 且這個類還沒有被初始化,那麼就先初始化父類;2)如果類中存在初始化語 句,就依次執行這些初始化語句。

類的加載是由類加載器完成的,類加載器包括:根加載器(BootStrap)、擴展 加載器(Extension)、系統加載器(System)和用戶自定義類加載器 (java.lang.ClassLoader 的子類)。從 Java 2(JDK 1.2)開始,類加載過程 採取了父親委託機制(PDM)。PDM 更好的保證了 Java 平臺的安全性,在該機 制中,JVM 自帶的 Bootstrap 是根加載器,其他的加載器都有且僅有一個父類 加載器。類的加載首先請求父類加載器加載,父類加載器無能爲力時才由其子 類加載器自行加載。JVM 不會向 Java 程序提供對 Bootstrap 的引用。下面是關 於幾個類加載器的說明:

  • Bootstrap:一般用本地代碼實現,負責加載 JVM 基礎核心類庫 (rt.jar); 
  • Extension:從 java.ext.dirs 系統屬性所指定的目錄中加載類 庫,它的父加載器是 Bootstrap; 
  • System:又叫應用類加載器,其父類是 Extension。它是應用最 廣泛的類加載器。它從環境變量 classpath 或者系統屬性 java.class.path 所指定的目錄中記載類,是用戶自定義加載器 的默認父加載器。 

 

15、闡述 ArrayList、Vector、LinkedList 的存儲性能和特性。

ArrayList 和 Vector 都是使用數組方式存儲數據,此數組元素數大於實際 存儲的數據以便增加和插入元素,它們都允許直接按序號索引元素,但是插入 元素要涉及數組元素移動等內存操作,所以索引數據快而插入數據慢,Vector中的方法由於添加了 synchronized 修飾,因此 Vector 是線程安全的容器,但 性能上較 ArrayList 差,因此已經是 Java 中的遺留容器。

LinkedList 使用雙 向鏈表實現存儲(將內存中零散的內存單元通過附加的引用關聯起來,形成一 個可以按序號索引的線性結構,這種鏈式存儲方式與數組的連續存儲方式相 比,內存的利用率更高),按序號索引數據需要進行前向或後向遍歷,但是插 入數據時只需要記錄本項的前後項即可,所以插入速度較快。

Vector 屬於遺留 容器(Java 早期的版本中提供的容器,除此之外,Hashtable、Dictionary、BitSet、Stack、Properties 都是遺留容器),已經不推薦使用,但是由於ArrayList 和 LinkedListed 都是非線程安全的,如果遇到多個線程操作同一個 容器的場景,則可以通過工具類 Collections 中的 synchronizedList 方法將其 轉換成線程安全的容器後再使用(這是對裝潢模式的應用,將已有對象傳入另 一個類的構造器中創建新的對象來增強實現)。

 

16、 內存模型以及分區,需要詳細到每個區放什麼?

JVM 分爲堆區和棧區,還有方法區,初始化的對象放在堆裏面,引用放在棧裏面,class 類信息常量池(static 常量和 static 變量)等放在方法區new:

  •   方法區:主要是存儲類信息,常量池(static 常量和 static 變量),編譯後的代碼(字 節碼)等數據。
  •   堆:初始化的對象,成員變量 (那種非 static 的變量),所有的對象實例和數組都要 在堆上分配。
  •   棧:棧的結構是棧幀組成的,調用一個方法就壓入一幀,幀上面存儲局部變量表,操 作數棧,方法出口等信息,局部變量表存放的是 8 大基礎類型加上一個應用類型,所 以還是一個指向地址的指針。
  •   本地方法棧:主要爲 Native 方法服務。  程序計數器:記錄當前線程執行的行號。

 

 

17、java 中垃圾收集的方法有哪些?

1. 標記-清除:這是垃圾收集算法中最基礎的,根據名字就可以知道,它的思想就是標記哪些要被 回收的對象,然後統一回收。這種方法很簡單,但是會有兩個主要問題:1.效率不 高,標記和清除的效率都很低;2.會產生大量不連續的內存碎片,導致以後程序在 分配較大的對象時,由於沒有充足的連續內存而提前觸發一次 GC 動作。

2. 複製算法:爲了解決效率問題,複製算法將可用內存按容量劃分爲相等的兩部分,然後每次只 使用其中的一塊,當一塊內存用完時,就將還存活的對象複製到第二塊內存上,然 後一次性清楚完第一塊內存,再將第二塊上的對象複製到第一塊。但是這種方式,內存的代價太高,每次基本上都要浪費一般的內存。於是將該算法進行了改進,內存區域不再是按照 1:1 去劃分,而是將內存劃分爲8:1:1 三部分,較大那份內存交 Eden 區,其餘是兩塊較小的內存區叫 Survior 區。每次都會優先使用 Eden 區,若 Eden 區滿,就將對象複製到第二塊內存區上,然 後清除 Eden 區,如果此時存活的對象太多,以至於 Survivor 不夠時,會將這些對 象通過分配擔保機制複製到老年代中。(java 堆又分爲新生代和老年代)

3. 標記-整理該算法主要是爲了解決標記-清除,產生大量內存碎片的問題;當對象存活率較高 時,也解決了複製算法的效率問題。它的不同之處就是在清除對象的時候現將可回 收對象移動到一端,然後清除掉端邊界以外的對象,這樣就不會產生內存碎片了。

4. 分代收集現在的虛擬機垃圾收集大多采用這種方式,它根據對象的生存週期,將堆分爲新生 代和老年代。在新生代中,由於對象生存期短,每次回收都會有大量對象死去,那 麼這時就採用複製算法。老年代裏的對象存活率較高,沒有額外的空間進行分配擔 保,所以可以使用標記-整理 或者 標記-清除。

 

 

18、簡述 java 類加載機制?

虛擬機把描述類的數據從 Class 文件加載到內存,並對數據進行校驗,解析和初始化,最 終形成可以被虛擬機直接使用的 java 類型。

19、類加載器雙親委派模型機制?

當一個類收到了類加載請求時,不會自己先去加載這個類,而是將其委派給父類,由父類去加載,如果此時父類不能加載,反饋給子類,由子類去完成類的加載。

 

20、什麼是類加載器,類加載器有哪些?

實現通過類的權限定名獲取該類的二進制字節流的代碼塊叫做類加載器。主要有一下四種類加載器:

  • 啓動類加載器(Bootstrap ClassLoader)用來加載 java 核心類庫,無法被 java 程序直接 引用。
  • 擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫。Java 虛擬機的 實現會提供一個擴展庫目錄。該類加載器在此目錄裏面查找並加載 Java 類。
  • 系統類加載器(system class loader):它根據 Java 應用的類路徑(CLASSPATH) 來加載 Java 類。一般來說,Java 應用的類都是由它來完成加載的。可以通過ClassLoader.getSystemClassLoader()來獲取它。
  • 用戶自定義類加載器,通過繼承 java.lang.ClassLoader 類的方式實現。

 

21、簡述 java 內存分配與回收策率以及 Minor GC 和Major GC

1. 對象優先在堆的 Eden 區分配。

2. 大對象直接進入老年代.

3. 長期存活的對象將直接進入老年代。

當 Eden 區沒有足夠的空間進行分配時,虛擬機會執行一次 Minor GC.Minor Gc 通 常發生在新生代的 Eden 區,在這個區的對象生存期短,往往發生 Gc 的頻率較高, 回收速度比較快;Full Gc/Major GC 發生在老年代,一般情況下,觸發老年代 GC的時候不會觸發 Minor GC,但是通過配置,可以在 Full GC 之前進行一次 Minor GC 這樣可以加快老年代的回收速度。

 

 

分享不易,如果感興趣的話,可以關注我的公衆號“碼之初”或者“ma_zhichu”,閱讀更多精品技術文章。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章