Interview - JAVA & JVM

# 設計模式
https://www.cnblogs.com/malihe/p/6891920.html


# Java 單例模式的懶漢和餓漢

單例模式中的懶漢和餓漢的區別在於類初始化時是否有初始化實例對象。

懶漢:在getInstance中判空和執行對象初始化,需要加鎖

餓漢:在static域進行對象的初始化,也就是在類初始化時執行了對象的初始化。getInstance時不需要加鎖,直接獲取

https://www.cnblogs.com/aspirant/p/6878555.html


# Java 動態代理

https://www.cnblogs.com/gonjan-blog/p/6685611.html


# Java 內存模型

Java內存模型規定了所有的變量(不包括虛擬機棧中局部變量表)都存儲在主內存中。每條線程中還有自己的工作內存,線程的工作內存中保存了被該線程所使用到的變量(這些變量是從主內存中拷貝而來)。線程對變量的所有操作(讀取,賦值)都必須在工作內存中進行。不同線程之間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要通過主內存來完成。(JMM:Java Memory Model)
在這裏插入圖片描述


# Java 同步機制

volatile、synchronized 和 final

volatile:輕量級鎖。使用volatile修飾的共享變量可以保證可見性,JMM會把該線程工作內存中的變量刷新到主內存;同時,寫操作會導致其他線程中的緩存無效。(volatile 能保證單個共享變量的讀和寫具有原子性, i++ 這種操作無法使用volatile保證,應該使用Atomic進行原子操作。)

volatile的原理:利用Lock前綴指令(將當前處理器的緩存行的數據寫回到系統內存,這個寫回內存的操作會使其他CPU緩存了該內存的地址的數據無效),和 緩存一致性協議(在多處理器下,爲零保證各個處理器的緩存是一致的,每個處理器都會通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期了。當處理器發現自己緩存行對應的地址被修改,就會將當前處理器的緩存行設置爲無效狀態。當處理器對這個數據進行讀寫的時候,會重新把數據從內存中讀取到處理器緩存中。)。

volatile另一個特性:禁止指令重排序優化(重排序是指編譯器和處理器爲了優化程序性能而對指令序列進行排序的一種手段),volatile修飾共享變量,會在指令序列中插入 內存屏障 來禁止特定類型的處理器重排序:
1). 當第二個操作是volatile寫時,無論第一個操作是什麼,都不能進行重排序
2). 當第一個操作是volatile讀時,無論第二個操作是什麼,都不能進行重排序
3). 當第一個操作是volatile寫,第二個操作是volatile讀時,不能進行重排序

synchronized:重量級鎖,與concurrent包中提供的Lock效果一致。鎖對象時使用的是monitorenter和monitorexit,鎖方法時使用的是ACC_SYNCHRONIZED

https://blog.csdn.net/xlgen157387/article/details/78327228

final:final 域 只能被顯式的賦值一次,但並不代表final域不能被多次初始化,如:

public class FinalTest{
   final int i;
   static FinalTest obj;

   public FinalTest(){
   i  =1;
   /**
   *這裏會使正在被構造的對象逸出,如果和上一句做了重排序,那麼其他線程就可以通過obj訪問到還爲被初始化的final域。
   **/
   obj = this; 
  }
}

# 生產者消費者模型

BlockingQueue:指定大小的隊列,提供put和take方法。當隊列爲空時,take被阻塞;當隊列滿時,put被阻塞。

wait & notify:如果隊列已空,則consume方法wait;如果隊列已滿,則produce方法wait。consume和produce方法結束時notifyAll。

Lock & Condition:與wait和notify實現相似。

http://www.importnew.com/27063.html


# 三個特性:原子性、可見性與有序性

原子性:volatile提供讀寫原子性,AtomicInteger / AtomicLong / AtomicReference提供對象操作原子性,synchronized / Lock 提供段操作原子性。

可見性:當一個線程修改了共享變量時,另一個線程可以讀取到這個修改後的值 (Volatile,final,synchronized(當線程獲取鎖時會從主內存中獲取共享變量的最新值,釋放鎖的時候會將共享變量同步到主內存中))

有序性:synchronized 保證只有一個線程持有鎖,因此具有有序性


# ArrayList 和 LinkedList 的區別

1.ArrayList是實現了基於動態數組的數據結構,LinkedList基於鏈表的數據結構。
2.對於隨機訪問get和set,ArrayList覺得優於LinkedList,因爲LinkedList要移動指針。
3.對於新增和刪除操作add和remove,LinedList比較佔優勢,因爲ArrayList要移動數據。


# ArrayList,LinkedList 循環刪除問題

foreach方法刪除或者在iterator中使用list.remove()方法都會報ConcurrentModificationException的異常,原因是list.remove()不會通知iterator,iterator通過檢查modCount和expectModCount來確定是否被修改,如果已經被修改,則拋出異常。

使用iterator.remove()可以避免該問題,原因是Iterator在remove時同步了modCount和expectModCount。


# ArrayList 和 Vector 的異同

同:兩者都是採用遞增數組來存儲數據

異:Vector 支持同步,是線程安全的,ArrayList 線程非安全。因此 ArrayList 的效率要高於Vector。雖然和ArrayList創建的Iterator是同一接口,但是,因爲Vector是同步的,當一個Iterator被創建而且正在被使用,另一個線程改變了Vector的狀態(例如,添加或刪除了一些元素),這時調用Iterator的方法時將拋出ConcurrentModificationException,因此必須捕獲該異常。


# Hash算法,Hash衝突的解決

散列函數: 一個好的散列函數的值應該儘可能平均分佈
1). 直接尋址法:取keyword或keyword的某個線性函數值爲散列地址。即H(key)=key或H(key) = a•key + b,當中a和b爲常數(這樣的散列函數叫做自身函數)
2). 數字分析法:分析一組數據,比方一組員工的出生年月日,這時我們發現出生年月日的前幾位數字大體同樣,這種話,出現衝突的機率就會非常大,可是我們發現年月日的後幾位表示月份和詳細日期的數字區別非常大,假設用後面的數字來構成散列地址,則衝突的機率會明顯減少。因此數字分析法就是找出數字的規律,儘可能利用這些數據來構造衝突機率較低的散列地址。
3). 平方取中法:取keyword平方後的中間幾位作爲散列地址。
4). 摺疊法:將keyword切割成位數同樣的幾部分,最後一部分位數能夠不同,然後取這幾部分的疊加和(去除進位)作爲散列地址。
5). 隨機數法:選擇一隨機函數,取keyword的隨機值作爲散列地址,通經常使用於keyword長度不同的場合。
6). 除留餘數法:取keyword被某個不大於散列表表長m的數p除後所得的餘數爲散列地址。即 H(key) = key MOD p, p<=m。不僅能夠對keyword直接取模,也可在摺疊、平方取中等運算之後取模。對p的選擇非常重要,一般取素數或m,若p選的不好,easy產生同義詞。

負載因子 / 裝填因子(α= 填入表中的元素個數 / 散列表的長度):比方我們存儲70個元素,但我們可能爲這70個元素申請了100個元素的空間。70/100=0.7,這個數字稱爲負載因子。負載因子的大小,太大不一定就好,並且浪費空間嚴重,負載因子和散列函數是聯動的。

散列表衝突解決:
(1)線性探查法:衝突後,線性向前試探,找到近期的一個空位置。缺點是會出現堆積現象。存取時,可能不是同義詞的詞也位於探查序列,影響效率。
(2)雙散列函數法:在位置d衝突後,再次使用還有一個散列函數產生一個與散列表桶容量m互質的數c,依次試探(d+n*c)%m,使探查序列跳躍式分佈。
(3)拉鍊法:數組每個節點都存儲一個鏈表,正常情況下,鏈表只有頭結點,當衝突時,將值插入到該節點的next節點

著名的Hash算法:
MD4,MD5,SHA-1,SHA-256


# hashCode 和 equals 方法的區別

hashCode() 和 equals() 都用於比較兩個對象是否相等,由於存在hash衝突,因此不同對象hashCode() 函數返回的值可能相同;而重寫的 equals 方法由於比較複雜,因此更加耗時,所以compare時先比較hashCode,再比較equals。

equals的兩個對象他們的hashCode一定是相等的;hashcode相等的兩個對象不一定equal。


# HashTable 和 HashMap

HashTable已經被廢棄。

HashTable:繼承自Dictionary;hash數組大小選用素數和奇數(初始值11),散列算法((hash & 0x7FFFFFFF) % tab.length)使用簡單的取模hash,結果更加均勻;使用拉鍊法來解決Hash衝突;支持線程同步,使用 synchronized 對方法進行加鎖。

HashTable:繼承自AbstractMap;hash數組的大小選用2的整數次冪(初始值16),散列算法直接使用二級制位運算,hash 衝突加重,但效率更高;使用拉鍊法,紅黑樹來解決Hash衝突(JDK1.8);不支持線程同步,效率更高。

hashSeed:在散列算法時可以解決hash衝突,由於jdk1.8引入了紅黑樹,因此被去除。


# HashMap,LinkedHashMap,TreeMap

HashMap:不支持排序,使用Iterator遍歷順序不固定,使用散列表來實現,查找和更改的速度快

LinkedHashMap:繼承自HashMap,同時維護了一個雙向鏈表來緩存插入的順序,使用Iterator遍歷的順序與插入的順序一致。accessOrder 爲true時,每次訪問(put / get)一個節點,該節點都會往後移動一次,因此右側節點最常被訪問,左側節點最少被訪問。

TreeMap:繼承自SortedMap,默認升序排列,使用紅黑樹的數據結構,加快查找速度,TreeMap中保存的對象類型需繼承Comparator接口。


# HashSet,LinkedHashSet,TreeSet

分別使用HashMap,LinkedHashMap,TreeMap 來實現,取Map中的keyset,將value填充成一個對象。


# 雙向隊列

ArrayDeque:使用數組來實現,環形數組,即 0 的左邊是 length - 1

LinkedBlockingDeque:使用雙向鏈表來實現,線程安全,使用Lock來實現


# Java 線程安全容器

同步容器(使用asynchronized):HashTable,Vector。由於add() 和 get() 加的是同一把鎖,因此會造成等待,效率低下。

併發容器:高效率

ConcurrentHashMap:對HashMap 數據進行分段(Segment),對段內數據單獨加鎖,訪問非段內的數據不需要持鎖。

CopyOnWriteArrayList:add() 使用Lock加鎖並對當前數組拷貝,在拷貝的數組中添加元素,並將原數組引用指向這個拷貝數組。get() 方法不需要加鎖。

阻塞容器(使用Lock):會對當前線程產生阻塞,比如一個線程從一個空的阻塞隊列中取元素,此時線程會被阻塞直到阻塞隊列中有了元素。當隊列中有元素後,被阻塞的線程會自動被喚醒(不需要我們編寫代碼去喚醒)

ArrayBlockingQueue:基於數組實現的一個阻塞隊列,在創建ArrayBlockingQueue對象時必須制定容量大小。並且可以指定公平性與非公平性,默認情況下爲非公平的,即不保證等待時間最長的隊列最優先能夠訪問隊列。

LinkedBlockingQueue:基於鏈表實現的一個阻塞隊列,在創建LinkedBlockingQueue對象時如果不指定容量大小,則默認大小爲Integer.MAX_VALUE。

PriorityBlockingQueue:以上2種隊列都是先進先出隊列,而PriorityBlockingQueue卻不是,它會按照元素的優先級對元素進行排序,按照優先級順序出隊,每次出隊的元素都是優先級最高的元素。注意,此阻塞隊列爲無界阻塞隊列,即容量沒有上限(通過源碼就可以知道,它沒有容器滿的信號標誌),前面2種都是有界隊列。

DelayQueue:基於PriorityQueue,一種延時阻塞隊列,DelayQueue中的元素只有當其指定的延遲時間到了,才能夠從隊列中獲取到該元素。DelayQueue也是一個無界隊列,因此往隊列中插入數據的操作(生產者)永遠不會被阻塞,而只有獲取數據的操作(消費者)纔會被阻塞。


# Unmodify 容器

使用Collections.unmofify… 方法來構造一個不可修改的容器。本質上返回的容器類型繼承自重載了 add(),put() 等操作容器方法的類型,在重載的這些方法裏面直接拋出 UnsupportedOperationException 異常。


# Java 鎖類型

  • 公平鎖 / 非公平鎖
  • 可重入鎖
  • 獨享鎖 / 共享鎖
  • 互斥鎖 / 讀寫鎖
  • 樂觀鎖 / 悲觀鎖
  • 分段鎖
  • 偏向鎖 / 輕量級鎖 / 重量級鎖
  • 自旋鎖

公平鎖 / 非公平鎖

公平鎖是指多個線程按照申請鎖的順序來獲取鎖。非公平鎖是指多個線程獲取鎖的順序並不是按照申請鎖的順序,有可能後申請的線程比先申請的線程優先獲取鎖。有可能,會造成優先級反轉或者飢餓現象。

對於 ReentrantLock 而言,通過構造函數指定該鎖是否是公平鎖,默認是非公平鎖。非公平鎖的優點在於吞吐量比公平鎖大

對於 Synchronized 而言,也是一種非公平鎖。由於其並不像 ReentrantLock 是通過 AQS 的來實現線程調度,所以並沒有任何辦法使其變成公平鎖。

可重入鎖

可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖。

對於ReentrantLock而言, 他的名字就可以看出是一個可重入鎖,其名字是Re entrant Lock重新進入鎖。

對於Synchronized而言,也是一個可重入鎖。可重入鎖的一個好處是可一定程度避免死鎖。

synchronized void setA() throws Exception{
    Thread.sleep(1000);
    setB();
}

synchronized void setB() throws Exception{
    Thread.sleep(1000);
}

上面的代碼就是一個可重入鎖的一個特點,如果不是可重入鎖的話,setB可能不會被當前線程執行,可能造成死鎖。

獨享鎖/共享鎖

獨享鎖是指該鎖一次只能被一個線程所持有。
共享鎖是指該鎖可被多個線程所持有。

對於Java ReentrantLock而言,其是獨享鎖。但是對於Lock的另一個實現類ReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨享鎖。讀鎖的共享鎖可保證併發讀是非常高效的,讀寫,寫讀 ,寫寫的過程是互斥的。

獨享鎖與共享鎖也是通過AQS來實現的,通過實現不同的方法,來實現獨享或者共享。
對於Synchronized而言,當然是獨享鎖。

互斥鎖/讀寫鎖

上面講的獨享鎖/共享鎖就是一種廣義的說法,互斥鎖/讀寫鎖就是具體的實現。互斥鎖在Java中的具體實現就是ReentrantLock,讀寫鎖在Java中的具體實現就是ReadWriteLock

樂觀鎖/悲觀鎖

樂觀鎖與悲觀鎖不是指具體的什麼類型的鎖,而是指看待併發同步的角度。

悲觀鎖認爲對於同一個數據的併發操作,一定是會發生修改的,哪怕沒有修改,也會認爲修改。因此對於同一個數據的併發操作,悲觀鎖採取加鎖的形式。悲觀的認爲,不加鎖的併發操作一定會出問題。

樂觀鎖則認爲對於同一個數據的併發操作,是不會發生修改的。在更新數據的時候,會採用嘗試更新,不斷重新的方式更新數據。樂觀的認爲,不加鎖的併發操作是沒有事情的。

悲觀鎖適合寫操作非常多的場景,樂觀鎖適合讀操作非常多的場景,不加鎖會帶來大量的性能提升。

悲觀鎖在Java中的使用,就是利用各種鎖。樂觀鎖在Java中的使用,是無鎖編程,常常採用的是CAS算法,典型的例子就是原子類,通過CAS自旋實現原子操作的更新。

分段鎖

分段鎖其實是一種鎖的設計,並不是具體的一種鎖,對於ConcurrentHashMap而言,其併發的實現就是通過分段鎖的形式來實現高效的併發操作。

我們以ConcurrentHashMap來說一下分段鎖的含義以及設計思想,ConcurrentHashMap中的分段鎖稱爲Segment,它即類似於HashMap(JDK7與JDK8中HashMap的實現)的結構,即內部擁有一個Entry數組,數組中的每個元素又是一個鏈表;同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。

當需要put元素的時候,並不是對整個hashmap進行加鎖,而是先通過hashcode來知道他要放在那一個分段中,然後對這個分段進行加鎖,所以當多線程put的時候,只要不是放在一個分段中,就實現了真正的並行的插入。

但是,在統計size的時候,可就是獲取hashmap全局信息的時候,就需要獲取所有的分段鎖才能統計。
分段鎖的設計目的是細化鎖的粒度,當操作不需要更新整個數組的時候,就僅僅針對數組中的一項進行加鎖操作。

偏向鎖/輕量級鎖/重量級鎖

這三種鎖是指鎖的狀態,並且是針對Synchronized。在Java 5通過引入鎖升級的機制來實現高效Synchronized。這三種鎖的狀態是通過對象監視器在對象頭中的字段來表明的。

偏向鎖是指一段同步代碼一直被一個線程所訪問,那麼該線程會自動獲取鎖。降低獲取鎖的代價。

輕量級鎖是指當鎖是偏向鎖的時候,被另一個線程所訪問,偏向鎖就會升級爲輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,提高性能。

重量級鎖是指當鎖爲輕量級鎖的時候,另一個線程雖然是自旋,但自旋不會一直持續下去,當自旋一定次數的時候,還沒有獲取到鎖,就會進入阻塞,該鎖膨脹爲重量級鎖。重量級鎖會讓其他申請的線程進入阻塞,性能降低。

自旋鎖

在Java中,自旋鎖是指嘗試獲取鎖的線程不會立即阻塞,而是採用循環的方式去嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗,缺點是循環會消耗CPU。


# 死鎖產生的原因及四個必要條件

產生死鎖的原因主要是:
(1) 因爲系統資源不足。
(2) 進程運行推進的順序不合適。
(3) 資源分配不當等。

如果系統資源充足,進程的資源請求都能夠得到滿足,死鎖出現的可能性就很低,否則
就會因爭奪有限的資源而陷入死鎖。其次,進程運行推進順序與速度不同,也可能產生死鎖。
產生死鎖的四個必要條件:
(1) 互斥條件:一個資源每次只能被一個進程使用。
(2) 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
(3) 不剝奪條件:進程已獲得的資源,在末使用完之前,不能強行剝奪。
(4) 循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關係。

這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之
一不滿足,就不會發生死鎖。

例子:getActiveNetworkInfo MiuiFirewall 死鎖問題,32個binder線程全被消耗完


# Java 線程池

① corePoolSize,表示核心大小,如果運行的線程少於 corePoolSize,則創建新線程來處理請求,即使其他輔助線程是空閒的。否則,新的任務會被填充到workQueue中,thread在空閒時會阻塞在workQueue的take函數,直到有新的任務添加。

② maxPoolSzie,表示阻塞隊列的大小,如果運行的線程多於 corePoolSize 而少於 maximumPoolSize,則僅當阻塞隊列滿時才創建新線程。如果設置的 corePoolSize 和 maximumPoolSize 相同,則創建了固定大小的線程池(如本例的FixThreadPool)。如果將 maximumPoolSize 設置爲基本的無界值(如 Integer.MAX_VALUE),則允許池適應任意數量的併發任務。

③ largestPoolSize,表示曾經同時存在在線程池的worker的大小,爲workers集合(維護worker)的大小。

④ 關於shutdown函數和shutdownNow函數的區別,shutdown會設置線程池的運行狀態爲SHUTDOWN,並且中斷所有空閒的worker,由於worker運行時會進行相應的檢查,所以之後會退出線程池,並且其會繼續運行之前提交到阻塞隊列中的任務,不再接受新任務。而shutdownNow則會設置線程池的運行狀態爲STOP,並且中斷所有的線程(包括空閒和正在運行的線程),在阻塞隊列中的任務將不會被運行,並且會將其轉化爲List返回給調用者,也不再接受新任務,其不會停止用戶任務(只是發出了中斷信號),若需要停止,需要用戶自定義停止邏輯。

https://www.cnblogs.com/leesf456/p/5585627.html


# String 對象存儲的位置

String 對象既可以存儲在 方法區 的常量池中,也可以存儲在 Java 堆中,根據初始化方式來確定:

String a = "123";		// 存儲在常量池中
String b = new String("123"); // 存儲在Java堆中
String c = new String("123"); // 存儲在Java堆中
String d = c.intern();	    // 存儲在常量池中

a != b != c;
a == d;

# String,StringBuilder 和 StringBuffer

String:多個字符串常量的 加法 更加耗時,原因是對象的不斷創建耗時。(String str = “12” + “3”; 這種不耗時。),適合少量的字符串操作。

StringBuilder:線程不安全,適合單線程下在字符緩衝區進行大量操作的情況。

StringBuffer:線程安全,適合多線程下載字符緩衝區進行大量操作的情況。


# Daemon 線程

用戶線程: 用戶線程可以簡單的理解爲用戶定義的線程

Daemon線程: 爲創建的用戶線程提供服務的線程,比如說jvm的GC等等

Daemon 線程的特點:

  1. 守護線程創建的過程中需要先調用setDaemon方法進行設置,然後再啓動線程,否則會報出IllegalThreadStateException異常。
  2. Daemon線程創建的子線程仍然是daemon線程
  3. 進程退出的條件是進程中沒有用戶進程存活;當沒有用戶線程時,Daemon線程也會隨進程退出。

# Java 最多支持多少個線程

  1. java的線程開啓,默認的虛擬機會分配1M的內存,但是在4G的windows上線程最多也就開到300多 ,是因爲windows本身的一些限制導致。

  2. 虛擬機給每個線程分配的內存(棧空間)是由虛擬機參數-Xss來指定的,在不同平臺上對應的默認大小可以 在oracle的官方文檔上查詢到:
    http://docs.oracle.com/cd/E13150_01/jrockit_jvm/jrockit/jrdocs/refman /optionX.html
    其中,Linux64位默認Xss值爲256K,並非1M或10M

  3. 一個Java進程可以啓動的線程數可以通過如下公式計算:

(系統剩餘內存 - 最大堆容量Xmx - 最大方法區容量MaxPermSize)/ 最大棧空間Xss

這樣,4G的服務器單個進程可以開多少線程,可以粗略計算出來,大概是5000個線程。


# Runnable 和 Callable 的區別

  1. Callable 有返回值,但需要結合FutureTask使用,通過FutureTask.get() 或者 Future.get() 阻塞主線程並獲取子線程執行結果。
  2. Callable可以拋出異常,可以在主線程中通過try…catch… “Future.get()” 來獲取子線程執行的異常。
@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}


@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

https://www.cnblogs.com/frinder6/p/5507082.html


# Java 如何 catch 子線程異常

由於

方式一: 在子線程中try…catch…

方式二: 爲線程設置“未捕獲異常處理器”UncaughtExceptionHandler

  1. Thread.setUncaughtExceptionHandler 設置當前線程的異常處理器
  2. Thread.setDefaultUncaughtExceptionHandler 爲整個程序設置默認的異常處理器

方式三: 通過Future的get方法捕獲異常(推薦)

  1. 使用線程池提交一個能獲取到返回信息的方法,也就是ExecutorService.submit(Callable)
  2. 在submit之後可以獲得一個線程執行結果的Future對象,而如果子線程中發生了異常,通過future.get()獲取返回值時,可以捕獲到ExecutionException異常,從而知道子線程中發生了異常。

http://www.cnblogs.com/yangfanexp/p/7594557.html


# Java 動態綁定

綁定:一個方法的調用與方法所在的類關聯起來。java中的綁定分爲靜態綁定和動態綁定,又被稱作前期綁定和後期綁定。

靜態綁定:(final、static、private)在程序執行前已經被綁定,也就是說在編譯過程中就已經知道這個方法是哪個類的方法,此時由編譯器獲取其他連接程序實現。

動態綁定:在運行根據具體對象的類型進行綁定。


# Java 泛型 及 類型擦除

https://www.cnblogs.com/drizzlewithwind/p/6101081.html


# Java反射

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

反射慢的原因:

java反射是要解析字節碼,將內存中的對象進行解析,包括了一些動態類型,所以JVM無法對這些代碼進行優化。因此,反射操作的效率要比那些非反射操作低得多。

java反射需要將內存中的對象進行解析,涉及到與底層c語言的交互,速度會比較慢。
java反射得到的每一個Method都有一個root,不暴漏給外部,而是每次copy一個Method。具體的反射調用邏輯是委託給MethodAccessor的,而accessor對象會在第一次invoke的時候才創建,是一種lazy init方式。而且默認Class類會cache method對象。目前MethodAccessor的實現有兩種,通過設置inflation,一個native方式,一種生成java bytecode方式。native方式啓動快,但運行時間長了不如java方式,個人感覺應該是java方式運行長了,jit compiler可以進行優化。所以JDK6的實現,在native方式中,有一個計數器,當調用次數達到閥值,就會轉爲使用java方式。默認值是15。java方式的實現,基本和非反射方式相同。


# 遞歸的優點和缺點

優點:代碼優雅
缺點:效率較低,另外容易產生StackOverFlowError(棧溢出)錯誤,請求的棧深度大於虛擬機允許的最大深度。這裏不等同於 OOM,虛擬機棧深度是對單個線程的限定。


# jvm運行時數據區域

在這裏插入圖片描述

程序計數器: 是一塊較小的內存空間。它的作用可以看做是當前線程所執行的字節碼的行號指示器。字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令。分支、循環、跳轉、異常處理、線程恢復都是依賴這個計數器來完成。

Java虛擬機棧: 線程私有,生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法被執行的時候都會同時創建一個 棧幀 用於存儲局部變量表、操作棧、動態鏈接、方法出口等信息。每一個方法被調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。

局部變量表: 存放了在編譯期可知的各種基本數據類型(boolean、byte、int、long等)、對象引用(不是對象本身,可能是指向對象起始地址的一個引用指針)。局部變量表所需的內存在編譯期就已經完成了分配,當進入一個方法時,這個方法在棧幀中分配多大的局部變量空間是確定的。

StackOverFlow: 虛擬機棧或本地方法棧線程請求的棧深度大於虛擬機允許的深度,就會拋出StackOverFlow異常。如大量的遞歸就有可能觸發該異常。

本地方法棧: 與虛擬機棧的作用類似,執行的是Native方法。

Java堆: 線程共享的一塊數據區域,在虛擬機啓動時創建,用於存放對象實例,幾乎所有的對象實例都在這裏分配內存(String.intern()在方法區的常量池中進行分配))。Java堆是垃圾收集器管理的主要區域,因此也被稱作GC堆。如果堆中沒有內存完成實例分配,並且堆也無法再進行擴展時,將會拋出OutOfMemoryError異常。

方法區: 線程共享的一塊數據區域,用於存儲已被虛擬機加載的類信息、常量、靜態變量、JIT編譯後的代碼等數據。垃圾收集器在這個區域的行爲比較少出現,類似於永久代,回收的目標主要是針對常量池的回收和對類型的卸載。

運行時常量池: 方法區的一部分,Class文件中除了有類的版本、字段、方法、接口等信息外,還有一項信息是常量池,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後存放到方法區的運行時常量池中。運行時常量池相對於Class文件常量池的另外一個重要特性是具有動態性,並非預置入Class文件常量池中的內容才能進入方法區運行時常量池,運行期間可能將新的常量放入池中,如String.intern()方法。

直接內存: 非虛擬機運行時數據區的一部分,NIO(New Input/Output)使用Native函數庫直接分配堆外內存,然後通過一個存儲在Java堆裏面的DirectByteBuffer對象作爲這塊內存的引用進行操作。這樣避免了在Java堆和Native堆中來回複製數據,在一些場景中提高了性能。


# jvm對象訪問

在這裏插入圖片描述
在這裏插入圖片描述


# GC對象的確定

1). 引用計數算法:給對象一個引用計數器,每當有一個地方引用它,計數器加1,引用失效時,計數器減1。任何時刻計數器爲0的對象都是不可能再被使用的。缺點:很難解決對象之間相互循環引用的問題。

2). 根搜索算法(GC Root Tracking):通過一系列名爲"GC Roots"的對象作爲起始點,從這些節點開始向下搜索,搜索所有走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可達的。
在這裏插入圖片描述
可作爲GC Roots的對象包括以下幾種:

  1. 虛擬機棧(棧幀中的本地變量表)中引用的對象
  2. 方法區中的類靜態屬性引用的對象
  3. 方法區中的常量引用的對象
  4. 本地方法棧中JNI(一般說的Native方法)的引用的對象

# Java 強弱引用

強引用:類似"Object obj = new Object()"這類的引用。只要強引用還在,垃圾回收期就永遠不會回收掉被引用的對象。

軟引用(SoftReference):描述一些還有用,但並非必須的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中並進行第二次回收。如果這次回收還是沒有足夠的內存,纔會拋出內存溢出異常。

弱引用(WeakReference):強度比軟引用更弱,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。

虛引用(PhantomReference):一個對象是否有虛引用的存在,完全不會對其生存時間造成影響,也無法通過虛引用來取的一個對象實例。爲一個對象設置虛引用關聯的唯一目的是希望能在這個對象被回收器回收時收到一個系統通知。


# finalize() 方法

finalize()方法:只在虛擬機GC之前纔有可能被調用。調用的前提是該方法被覆寫過,並且該方法沒有被執行過,也就是說,finalize()方法只能被執行一次。可以在finalize()方法中引用該對象來防止一次GC,但如果下一次GC時仍然GC Roots不可達,由於不會再執行finalize()方法,因此無法防止被GC。

如果希望在代碼段執行後執行,可以採用try {} finally {} 這種方式,finalize()方法時不可靠的。


# 方法區的GC

永久代的垃圾收集主要是兩部分:廢棄常量 和 無用的類。

廢棄常量:

與Java堆的對象回收類似,以常量池字面量的回收爲例,假如一個字符串"adc"已經進入了常量池,但是當前系統沒有任何一個String對象時叫做"abc"的,這個字符串就可以被回收

無用的類: 需要同時滿足以下3個條件:

  1. 該類所有的實例都已經被回收,Java堆中不存在該類的任何實例

  2. 加載該類的ClassLoader已經被回收

  3. 該類對應的java.lang.Class對象沒有在任何地方被引用,無法再任何地方通過反射訪問該類的成員


# 垃圾回收算法

1). 標記 - 清除算法: 標記出所有需要回收的對象,在標記完成後統一回收掉所有被標記的對象。

缺點:效率問題(標記和清除效率都不高),空間問題(標記清除後會產生大量不連續的內存碎片,空間碎片太多會導致程序在以後的運行過程中需要分配較大對象時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作)。
在這裏插入圖片描述

2). 複製算法: 適用於回收新生代,將可用內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間(8:1:1),每次使用Eden和其中的一塊Survivor。當回收時,將Eden和Survivor中還存活着的對象一次性地拷貝到另外一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。

內存的分配擔保:如果另一塊Survivor空間沒有足夠的空間存放上一次新生代收集下來的存活對象,這些對象將通過分配擔保機制進入老年代

3). 標記 - 整理算法: 適用於老年代,讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存。這樣可以有效減少內存碎片並提高清理時候的效率。
在這裏插入圖片描述

4). 分代收集算法: 根據對象存活週期的不同將內存劃分爲幾塊,一般把Java堆分爲新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。在新生代中,每次垃圾收集時都發現大批對象死去,只有少量存活,那就選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。而老年代因爲對象存活率高,沒有額外空間對它進行分配擔保,就必須使用 “標記 - 清理” 或 “標記 - 整理” 算法啦進行回收。


# 內存分配策略

  1. 對象優先在Eden分配。當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC

  2. 大對象直接進入老年代。大對象是指大量連續內存空間的Java對象。通過 -XX:PretenureSizeThreshold 參數來設置閾值大小,大於該值內存的對象直接在老年代中分配。目的是爲了避免在Eden區及兩個Survivor區之間發生大量的內存拷貝。

  3. 長期存活的對象將進入老年代。虛擬機給每個對象定義了一唱歌對象年齡(Age)計數器,如果對象在Eden出生並經過第一次Minor GC後仍然存活,並且能被Survivor容納,則移動到Survivor空間並將對象年齡設爲1,對象在Survivor區中每熬過一次Minor GC,年齡就增加1歲。當它的年齡增加到一定程度(默認是15歲,通過--XX:MaxTenuringThredshold來設置),就會被晉升到老年代。

  4. 動態對象年齡判定。如果Surviror空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代。

  5. 空間分配擔保。當發生Minor GC時,虛擬機會檢測之前每次晉升到老年代的平均大小是否大於老年代的剩餘空間大小,如果大於,則改爲直接進行一次Full GC。如果小於,則查看HandlePromotionFailure設置是否允許擔保失敗:如果允許,則是會進行Minor GC,否則也要改爲進行一次Full GC。


# jvm 類加載

在這裏插入圖片描述

類加載的時機不確定,Android在應用啓動時會對所有的類通過ClassLoader加載到dexElements中。

類初始化的時機:

  1. 遇到new、getstatic、putstaitic、invokestatic這4條字節碼指令時。如:使用new實例化對象時、讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)、調用一個類的靜態方法時。

  2. 使用java.lang.reflect包的方法對類進行反射調用時

  3. 當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需要先觸發父類的初始化

  4. 當虛擬機啓動時,用戶需要制定一個需要執行的主類(包含main方法的那個類),虛擬機會先初始化這個類。

加載:

  1. 通過一個類的權限定名來獲取此類的二進制字節流
  2. 將這個字節流所代表的的靜態存儲結構轉換爲方法區的運行時數據結構
  3. 在Java堆中生成一個代表這個類的java.lang.Class對象,作爲方法區這些數據的訪問入口

驗證:

  1. 文件格式驗證:是否以魔數OxCAFEBABE開頭;主次版本號是否在當前虛擬機處理範圍內;常量池的常量中是否有不被支持的常量類型(檢查常量tag標誌)
  2. 元數據驗證:這個類是否有父類;這個類的父類是否是final修飾的類;如果這個類不是抽象類,是否實現了父類或接口中要求實現的所有方法;類中的字段、方法是否與父類產生矛盾(如final修飾的字段和方法)
  3. 字節碼驗證
  4. 符號引用驗證(保證解析的順利進行):符號引用中通過字符串描述的權限定名是否能找到對應的類;在制定類中是否存在符合方法的字段描述符及簡單名稱所描述的方法和字段;符號引用中的類、字段和方法的訪問性(private、protected、public、default)是否可被當前類訪問

準備:

正式爲類變量(被static修飾的類變量,不包括實例變量,實例變量會在對象實例化時隨着對象一起分配在Java堆中)分配內存並設置類變量初始值,這些內存都將在方法區進行分配。

public static int value = 123; 在準備後 value 的值時0而不是123

public static final int value = 123; 在準備後value的值是123

public static final Object obj = new Object(); 在準備後value的值是null。引用類型的實際值將在類cinit時執行和賦值。

解析:

虛擬機將常量池內的符號引用替換成直接引用。

符號引用:以一組符號來描述所引用的目標,符號引用可以是任何形式的字面量,只要能準確定位到目標即可。符號引用的目標不一定已經加載到內存中。

直接引用:可以直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。如果有直接引用,則引用的目標一定已經在內存中存在。

類初始化(cinit):

初始化類變量(static)的實際值和其他資源。

  1. 先父類,後子類
  2. 不是必須被執行的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那麼編譯期可以不爲這個類生成cinit方法
  3. 接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此也可能會生成cinit方法。但接口執行cinit方法之前不需要先執行父接口的cinit,只有當父接口定義的變量被使用時,父接口才會被初始化。另外,接口的實現類在類初始化時也不需要執行接口的cinit方法
  4. 虛擬機保證一個類的cinit方法在多線程下是被加鎖和同步的。

# 類加載器

種類:

  1. 啓動類加載器(Bootstrap ClassLoader):負責加載< JAVA_HOME >\lib 目錄中的類庫。啓動類加載器無法被Java程序直接引用。
  2. 擴展類加載器:負責加載< JAVA_HOME >\lib\ext 目錄中的類庫。開發者可以直接使用擴展類加載器。
  3. 應用程序類加載器(sun.misc.Launcher$AppClassLoader),負責加載用戶類路徑(Classpath)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,則默認使用該加載器。

雙親委派:

使用組合的方式實現,如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的類加載請求都應該傳送到頂層的啓動類加載器中,只有當父類加載器反饋自己無法完成加載請求時(它的搜索範圍中沒有找到所需的類),子類加載器纔會嘗試自己去加載。

優點:Java類隨着它的類加載器一起具備了一種帶有優先級的層次關係。如java.lang.Object,它存放在rt.jar中,無論哪個類加載器加載這個類,最終都是委派給啓動類加載器進行加載,因此Object類在程序的各種類加載器環境中都是同一個類。相反,如果用戶自己編寫了java.lang.Object類,並放在程序的ClassPath中,那系統中就會出現多個不同的Object類。

在這裏插入圖片描述


# jvm相關資料

https://www.cnblogs.com/superfj/category/1260039.html


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