Java SE 基礎:
封裝、繼承、多態
封裝:
- 概念:就是把對象的屬性和操作(或服務)結合為一個獨立的整體,并盡可能隱藏對象的內部實現細節。
- 好處: 隱藏內部實現細節。
繼承:
- 概念:繼承是從已有的類中派生出新的類,新的類能吸收已有類的數據屬性和行為,并能擴展新的能力。
- 好處:提高代碼的復用,縮短開發周期。
多態:
- 概念:多態(Polymorphism)按字面的意思就是“多種狀態,即同一個實體同時具有多種形式。一般表現形式是程序在運行的過程中,同一種類型在不同的條件下表現不同的結果。多態也稱為動態綁定,一般是在運行時刻才能確定方法的具體執行對象。
- 好處:
- 將接口和實現分開,改善代碼的組織結構和可讀性,還能創建可拓展的程序。
- 消除類型之間的耦合關系。允許將多個類型視為同一個類型。
- 一個多態方法的調用允許有多種表現形式。
抽象類與接口
- 一個子類只能繼承一個抽象類,但能實現多個接口
- 抽象類可以有構造方法,接口沒有構造方法
- 抽象類可以有普通成員變量,接口沒有普通成員變量
- 抽象類和接口都可有靜態成員變量,抽象類中靜態成員變量訪問類型任意,接口只能public static final(默認)
- 抽象類可以沒有抽象方法,抽象類可以有普通方法,接口中都是抽象方法
- 抽象類可以有靜態方法,接口不能有靜態方法
- 抽象類中的方法可以是public、protected和默認;接口方法只有public abstract
靜態內部類和普通內部類
靜態內部類不需要有指向外部類的引用。但非靜態內部類需要持有對外部類的引用。非靜態內部類能夠訪問外部類的靜態和非靜態成員。靜態類不能訪問外部類的非靜態成員。他只能訪問外部類的靜態成員。
集合框架:
List集合和Set集合
List接口:
List中元素存取是有序的、可重復的;Set集合中元素是無序的,不可重復的。CopyOnWriteArrayList:COW的策略,即寫時復制的策略。適用于讀多寫少的并發場景。
Set接口:
Set集合元素存取無序,且元素不可重復。
HashSet不保證迭代順序,線程不安全;LinkedHashSet是Set接口的哈希表和鏈接列表的實現,保證迭代順序,線程不安全。
TreeSet:可以對Set集合中的元素排序,元素以二叉樹形式存放,線程不安全。
ArrayList、LinkedList、Vector的區別
首先它們均是List接口的實現。
ArrayList、LinkedList的區別:
- 隨機存取:ArrayList是基于可變大小的數組實現,LinkedList是鏈接列表的實現。這也就決定了對于隨機訪問的get和set的操作,ArrayList要優于LinkedList,因為LinkedList要移動指針。
- 插入和刪除:LinkedList要好一些,因為ArrayList要移動數據,更新索引。
- 內存消耗:LinkedList需要更多的內存,因為需要維護指向后繼結點的指針。
Vector從Java 1.0起就存在,在1.2時改為實現List接口,功能與ArrayList類似,但是Vector具備線程安全。
Map集合
- Hashtable:基于Dictionary類,線程安全,速度快。底層是哈希表數據結構。是同步的。 不允許null作為鍵,null作為值。
- Properties:Hashtable的子類。用于配置文件的定義和操作,使用頻率非常高,同時鍵和值都是字符串。
- HashMap:線程不安全,底層是數組加鏈表實現的哈希表。允許null作為鍵,null作為值。HashMap去掉了contains方法。 注意:HashMap不保證元素的迭代順序。如果需要元素存取有序,請使用LinkedHashMap
- TreeMap:可以用來對Map集合中的鍵進行排序。
- ConcurrentHashMap:是JUC包下的一個并發集合。
為什么使用ConcurrentHashMap而不是HashMap或Hashtable?
HashMap的缺點:主要是多線程同時put時,如果同時觸發了rehash操作,會導致HashMap中的鏈表中出現循環節點,進而使得后面get的時候,會死循環,CPU達到100%,所以在并發情況下不能使用HashMap。讓HashMap同步:Map m = Collections.synchronizeMap(hashMap);而Hashtable雖然是同步的,使用synchronized來保證線程安全,但在線程競爭激烈的情況下HashTable的效率非常低下。因為當一個線程訪問HashTable的同步方法時,其他線程訪問HashTable的同步方法時,可能會進入阻塞或輪詢狀態。如線程1使用put進行添加元素,線程2不但不能使用put方法添加元素,并且也不能使用get方法來獲取元素,所以競爭越激烈效率越低。
ConcurrentHashMap的原理:
HashTable容器在競爭激烈的并發環境下表現出效率低下的原因在于所有訪問HashTable的線程都必須競爭同一把鎖,那假如容器里有多把鎖,每一把鎖用于鎖容器其中一部分數據,那么當多線程訪問容器里不同數據段的數據時,線程間就不會存在鎖競爭,從而可以有效的提高并發訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術,首先將數據分成一段一段的存儲,然后給每一段數據配一把鎖,當一個線程占用鎖訪問其中一個段數據的時候,其他段的數據也能被其他線程訪問。
ConcurrentHashMap的結構:
ConcurrentHashMap是由Segment數組結構和HashEntry數組結構組成。Segment是一種可重入鎖ReentrantLock,在ConcurrentHashMap里扮演鎖的角色,HashEntry則用于存儲鍵值對數據。一個ConcurrentHashMap里包含一個Segment數組,Segment的結構和HashMap類似,是一種數組和鏈表結構, 一個Segment里包含一個HashEntry數組,每個HashEntry是一個鏈表結構的元素,當對某個HashEntry數組的數據進行修改時,必須首先獲得它對應的Segment鎖。
ConcurrentHashMap的構造、get、put操作:
構造函數:傳入參數分別為
- 初始容量,默認16;
- 裝載因子 裝載因子用于rehash的判定,就是當ConcurrentHashMap中的元素大于裝載因子*最大容量時進行擴容,默認0.75;
- 并發級別 這個值用來確定Segment的個數,Segment的個數是大于等于concurrencyLevel的第一個2的n次方的數。比如,如果concurrencyLevel為12,13,14,15,16這些數,則Segment的數目為16(2的4次方)。默認值為static final int DEFAULT_CONCURRENCY_LEVEL = 16;。理想情況下ConcurrentHashMap的真正的并發訪問量能夠達到concurrencyLevel,因為有concurrencyLevel個Segment,假如有concurrencyLevel個線程需要訪問Map,并且需要訪問的數據都恰好分別落在不同的Segment中,則這些線程能夠無競爭地自由訪問(因為他們不需要競爭同一把鎖),達到同時訪問的效果。這也是為什么這個參數起名為“并發級別”的原因。默認16.
初始化的一些動作:
初始化segments數組(根據并發級別得到數組大小ssize),默認16
初始化segmentShift和segmentMask(這兩個全局變量在定位segment時的哈希算法里需要使用),默認情況下segmentShift為28,segmentMask為15
初始化每個Segment,這一步會確定Segment里HashEntry數組的長度.put操作:
- 判斷value是否為null,如果為null,直接拋出異常。
- key通過一次hash運算得到一個hash值。將得到hash值向右按位移動segmentShift位,然后再與segmentMask做&運算得到segment的索引j。即segmentFor方法
- 使用Unsafe的方式從Segment數組中獲取該索引對應的Segment對象。向這個Segment對象中put值,這個put操作也基本是一樣的步驟(通過&運算獲取HashEntry的索引,然后set)。
get操作:
- 和put操作一樣,先通過key進行hash確定應該去哪個Segment中取數據。
- 使用Unsafe獲取對應的Segment,然后再進行一次&運算得到HashEntry鏈表的位置,然后從鏈表頭開始遍歷整個鏈表(因為Hash可能會有碰撞,所以用一個鏈表保存),如果找到對應的key,則返回對應的value值,如果鏈表遍歷完都沒有找到對應的key,則說明Map中不包含該key,返回null。
定位Segment的hash算法:(hash >>> segmentShift) & segmentMask
定位HashEntry所使用的hash算法:int index = hash & (tab.length - 1);
注:tab為HashEntry數組
Collection 和 Collections的區別
Collection是集合類的上級接口,子接口主要有Set 和List、Queue Collections是針對集合類的一個幫助類,提供了操作集合的工具方法:一系列靜態方法實現對各種集合的搜索、排序、線程安全化等操作。
Map、Set、List、Queue、Stack的特點與用法
Set集合類似于一個罐子,"丟進"Set集合里的多個對象之間沒有明顯的順序。 List集合代表元素有序、可重復的集合,集合中每個元素都有其對應的順序索引。 Stack是Vector提供的一個子類,用于模擬"棧"這種數據結構(LIFO后進先出) Queue用于模擬"隊列"這種數據結構(先進先出 FIFO)。 Map用于保存具有"映射關系"的數據,因此Map集合里保存著兩組值。
HashMap的工作原理
HashMap維護了一個Entry數組,Entry內部類有key,value,hash和next是個字段,其中next也是一個Entry類型。可以將Entry數組理解為一個個的散列桶。每一個桶實際上是一個單鏈表。當執行put操作時,會根據key的hashcode定位到相應的桶。遍歷單鏈表檢查該key是否已經存在,如果存在,覆蓋該value,反之,新建一個新的Entry,并放在單鏈表的頭部。當通過傳遞key調用get方法時,它再次使用key.hashCode()來找到相應的散列桶,然后使用key.equals()方法找出單鏈表中正確的Entry,然后返回它的值。
七、HashMap和Hashtable的區別
Hashtable是基于陳舊的Dictionary的Map接口的實現,而HashMap是基于哈希表的Map接口的實現
從方法上看,HashMap去掉了Hashtable的contains方法
HashTable是同步的(線程安全),而HashMap線程不安全
HashMap允許空鍵值,而Hashtable不允許
HashMap的iterator迭代器執行快速失敗機制,也就是說在迭代過程中修改集合結構,除非調用迭代器自身的remove方法,否則以其他任何方式的修改都將拋出并發修改異常。如果尋求迭代的時候修改Map,可以使用ConcurrentHashMap。而Hashtable返回的Enumeration不是快速失敗的。
Map的實現類的介紹
HashMap基于散列表來的實現,即使用hashCode()進行快速查詢元素的位置,顯著提高性能。插入和查詢“鍵值對”的開銷是固定的。可以通過設置容量和裝載因子,以調整容器的性能。
LinkedHashMap, 類似于HashMap,但是迭代遍歷它時,保證迭代的順序是其插入的次序,因為它使用鏈表維護內部次序。此外可以在構造器中設定LinkedHashMap,使之采用LRU算法。使沒有被訪問過的元素或較少訪問的元素出現在前面,訪問過的或訪問多的出現在后面。這對于需要定期清理元素以節省空間的程序員來說,此功能使得程序員很容易得以實現。
TreeMap, 是基于紅黑樹的實現。同時TreeMap實現了SortedMap接口,該接口可以確保鍵處于排序狀態。所以查看“鍵”和“鍵值對”時,所有得到的結果都是經過排序的,次序由自然排序或提供的Comparator決定。SortedMap接口擁有其他額外的功能,如:返回當前Map使用的Comparator比較強,firstKey(),lastKey(),headMap(toKey),tailMap(fromKey)以及可以返回一個子樹的subMap()方法等。
WeakHashMap,表示弱鍵映射,WeakHashMap 的工作與正常的 HashMap 類似,但是使用弱引用作為 key,意思就是當 key 對象沒有任何引用時,key/value 將會被回收。
ConcurrentHashMap, 在HashMap基礎上分段鎖機制實現的線程安全的HashMap。
IdentityHashMap 使用==代替equals() 對“鍵”進行比較的散列映射。專為解決特殊問題而設計。
HashTable:基于Dictionary類的Map接口的實現,它是線程安全的。
LinkedList 和 PriorityQueue 的區別
它們均是Queue接口的實現。擁有FIFO的特點,它們的區別在于排序行為。LinkedList 支持雙向列表操作, PriorityQueue 按優先級組織的隊列,元素的出隊次序由元素的自然排序或者由Comparator比較器指定。
線程安全的集合類。Vector、HashTable、Properties和Stack
BlockingQueue
Java.util.concurrent.BlockingQueue是一個隊列,在進行獲取元素時,它會等待隊列變為非空;當在添加一個元素時,它會等待隊列中的可用空間。BlockingQueue接口是Java集合框架的一部分,主要用于實現生產者-消費者模式。我們不需要擔心等待生產者有可用的空間,或消費者有可用的對象,因為它都在BlockingQueue的實現類中被處理了。Java提供了集中BlockingQueue的實現,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。
如何對一組對象進行排序
如果需要對一個對象數組進行排序,我們可以使用Arrays.sort()方法。如果我們需要排序一個對象列表,我們可以使用Collections.sort()方法。排序時是默認根據元素的自然排序(使用Comparable)或使用Comparator外部比較器。Collections內部使用數組排序方法,所有它們兩者都有相同的性能,只是Collections需要花時間將列表轉換為數組。
Comparable和Comparator接口區別
Comparator位于包java.util下,而Comparable位于包java.lang下
如果我們需要使用Arrays或Collections的排序方法對對象進行排序時,我們需要在自定義類中實現Comparable接口并重寫compareTo方法,compareTo方法接收一個參數,如果this對象比傳遞的參數小,相等或大時分別返回負整數、0、正整數。Comparable被用來提供對象的自然排序。String、Integer實現了該接口。
Comparator比較器的compare方法接收2個參數,根據參數的比較大小分別返回負整數、0和正整數。 Comparator 是一個外部的比較器,當這個對象自然排序不能滿足你的要求時,你可以寫一個比較器來完成兩個對象之間大小的比較。用 Comparator 是策略模式(strategy design pattern),就是不改變對象自身,而用一個策略對象(strategy object)來改變它的行為。
與Java集合框架相關的有哪些最好的實踐
- 根據需要選擇正確的集合類型。比如,如果指定了大小,我們會選用Array而非ArrayList。如果我們想根據插入順序遍歷一個Map,我們需要使用TreeMap。如果我們不想重復,我們應該使用Set。
- 一些集合類允許指定初始容量,所以如果我們能夠估計到存儲元素的數量,我們可以使用它,就避免了重新哈希或大小調整。
- 基于接口編程,而非基于實現編程,它允許我們后來輕易地改變實現。
- 總是使用類型安全的泛型,避免在運行時出現ClassCastException。 > 5. 使用JDK提供的不可變類作為Map的key,可以避免自己實現hashCode()和equals()。
IO和NIO
在以前的Java IO中,都是阻塞式IO,NIO引入了非阻塞式IO。 第一種方式:我從硬盤讀取數據,然后程序一直等,數據讀完后,繼續操作。這種方式是最簡單的,叫阻塞IO。 第二種方式:我從硬盤讀取數據,然后程序繼續向下執行,等數據讀取完后,通知當前程序(對硬件來說叫中斷,對程序來說叫回調),然后此程序可以立即處理數據,也可以執行完當前操作在讀取數據。
流與塊的比較
原來的 I/O 以流的方式處理數據,而 NIO 以塊的方式處理數據。面向流 的 I/O 系統一次一個字節地處理數據。一個輸入流產生一個字節的數據,一個輸出流消費一個字節的數據。這樣做是相對簡單的。不利的一面是,面向流的 I/O 通常相當慢。 一個 面向塊 的 I/O 系統以塊的形式處理數據。每一個操作都在一步中產生或者消費一個數據塊。按塊處理數據比按(流式的)字節處理數據要快得多。但是面向塊的 I/O 缺少一些面向流的 I/O 所具有的優雅性和簡單性。
通道與流
Channel是一個對象,可以通過它讀取和寫入數據。通道與流功能類似,不同之處在于通道是雙向的。而流只是在一個方向上移動(一個流必須是 InputStream 或者 OutputStream 的子類), 而通道可以用于讀、寫或者同時用于讀寫。
緩沖區Buffer
在 NIO 庫中,所有數據都是用緩沖區處理的。在 NIO 庫中,所有數據都是用緩沖區處理的。
Position: 表示下一次訪問的緩沖區位置 Limit: 表示當前緩沖區存放的數據容量。 Capacity:表示緩沖區最大容量
flip()方法:它將 limit 設置為當前 position。它將 position 設置為 0
clear方法:它將 limit 設置為與 capacity 相同。它設置 position 為 0。
線程
什么是線程
線程是操作系統能夠進行運算調度的最小單位,它被包含在進程之中,是進程中的實際運作單位。程序員可以通過它進行多處理器編程,可以使用多線程對運算密集型任務提速。比如,如果一個線程完成一個任務要100 毫秒,那么用十個線程完成改任務只需 10 毫秒。Java在語言層面對多線程提供了很好的支持。
線程和進程有什么區別
- 從概念上: 進程:一個程序對一個數據集的動態執行過程,是分配資源的基本單位。 線程:存在于進程內,是進程內的基本調度單位。共享進程的資源。
- 從執行過程中來看: 進程:擁有獨立的內存單元,而多個線程共享內存,從而提高了應用程序的運行效率。 線程:每一個獨立的線程,都有一個程序運行的入口、順序執行序列、和程序的出口。但是線程不能夠獨立的執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。
- 從邏輯角度來看:(重要區別) 多線程的意義在于一個應用程序中,有多個執行部分可以同時執行。但是,操作系統并沒有將多個線程看做多個獨立的應用,來實現進程的調度和管理及資源分配。
簡言之,一個程序至少有一個進程,一個進程至少有一個線程。進程是資源分配的基本單位,線程共享進程的資源。
如何在 Java 中實現線程
繼承Thread類或實現Runnable接口。
用 Runnable 還是 Thread
Java 不支持類的多重繼承,但允許你調用多個接口。所以如果你要繼承其他類,當然是實現Runnable接口好了。
Thread 類中的 start () 和 run () 方法有什么區別
start ()方法被用來啟動新創建的線程,而且 start ()內部調用了 run ()方法,這和直接調用 run ()方法的效果不一樣。當你調用 run ()方法的時候,只會是在原來的線程中調用,沒有新的線程啟動,start ()方法才會啟動新線程。也就是用start方法來啟動線程,才是真正實現了多線程。而run方法只是一個普通方法。
Java 中 Runnable 和 Callable 有什么不同
Runnable和 Callable 都代表那些要在不同的線程中執行的任務。Runnable 從 JDK1.0 開始就有了,Callable 是在 JDK1.5 增加的。它們的主要區別是 Callable 的 call () 方法可以返回值和拋出異常,而 Runnable 的 run ()方法沒有這些功能。
Java 中 CyclicBarrier 和 CountDownLatch 有什么不同
它們都是JUC下的類,CyclicBarrier 和 CountDownLatch 都可以用來讓一組線程等待其它線程。區別在于CountdownLatch計數無法被重置。如果需要重置計數,請考慮使用 CyclicBarrier。
Java 內存模型是什么
Java 內存模型規定和指引Java 程序在不同的內存架構、CPU 和操作系統間有確定性地行為。它在多線程的情況下尤其重要。Java內存模型對一個線程所做的變動能被其它線程可見提供了保證,它們之間是先行發生關系。這個關系定義了一些規則讓程序員在并發編程時思路更清晰。
線程內的代碼能夠按先后順序執行,這被稱為程序次序規則。
對于同一個鎖,一個解鎖操作一定要發生在時間上后發生的另一個鎖定操作之前,也叫做管程鎖定規則。
前一個對volatile的寫操作在后一個volatile的讀操作之前,也叫volatile變量規則。
一個線程內的任何操作必需在這個線程的 start ()調用之后,也叫作線程啟動規則。
一個線程的所有操作都會在線程終止之前,線程終止規則。
一個對象的終結操作必需在這個對象構造完成之后,也叫對象終結規則。
傳遞性
Java 中的 volatile 變量是什么
Java 語言提供了一種稍弱的同步機制,即volatile變量。但是volatile并不容器完全被正確、完整的理解。 一般來說,volatile具備2條語義,或者說2個特性。
第一是保證volatile修飾的變量對所有線程的可見性,這里的可見性是指當一條線程修改了該變量,新值對于其它線程來說是立即可以得知的。而普通變量做不到這一點。
第二條語義是禁止指令重排序優化,這條語義在JDK1.5才被修復。
關于第一點:根據JMM,所有的變量存儲在主內存,而每個線程還有自己的工作內存,線程的工作內存保存該線程使用到的變量的主內存副本拷貝,線程對變量的操作在工作內存中進行,不能直接讀寫主內存的變量。在volatile可見性這一點上,普通變量做不到的原因正因如此。比如,線程A修改了一個普通變量的值,然后向主內存進行回寫,線程B在線程A回寫完成后再從主內存讀取,新變量才能對線程B可見。其實,按照虛擬機規范,volatile變量依然有工作內存的拷貝,要借助主內存來實現可見性。但由于volatile的特殊規則保證了新值能立即同步回主內存,以及每次使用從主內存刷新,以此保證了多線程操作volatile變量的可見性。
關于第二點:先說指令重排序,指令重排序是指CPU采用了允許將多條指令不按規定順序分開發送給相應的處理單元處理,但并不是說任意重排,CPU需要正確處理指令依賴情況確保最終的正確結果,指令重排序是機器級的優化操作。那么為什么volatile要禁止指令重排序呢,又是如何去做的。舉例,DCL(雙重檢查加鎖)的單例模式。volatile修飾后,代碼中將會插入許多內存屏障指令保證處理器不發生亂序執行。同時由于Happens-before規則的保證,在剛才的例子中寫操作會發生在后續的讀操作之前。
除了以上2點,volatile還保證對于64位long和double的讀取是原子性的。因為在JMM中允許虛擬機對未被volatile修飾的64位的long和double讀寫操作分為2次32位的操作來執行,這也就是所謂的long和double的非原子性協定。
基于以上幾點,我們知道volatile雖然有這些語義和特性在并發的情況下仍然不能保證線程安全。大部分情況下仍然需要加鎖。
除非是以下2種情況:
- 運算結果不依賴變量的當前值,或者能夠確保只有單一線程修改變量的值;
- 變量不需要與其他的狀態變量共同參與不變約束。
Java 中,編寫多線程程序的時候你會遵循哪些最佳實踐?
- 給線程命名,這樣可以幫助調試。
- 最小化同步的范圍,而不是將整個方法同步,只對關鍵部分做同步。
- 如果可以,更偏向于使用 volatile 而不是 synchronized。
- 使用更高層次的并發工具,而不是使用 wait() 和 notify() 來實現線程間通信,如 BlockingQueue,CountDownLatch 及 Semeaphore。
- 優先使用并發集合,而不是對集合進行同步。并發集合提供更好的可擴展性。
什么是線程安全?Vector 是一個線程安全類嗎
如果你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。如果每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。一個線程安全的計數器類的同一個實例對象在被多個線程使用的情況下也不會出現計算失誤。很顯然你可以將集合類分成兩組,線程安全和非線程安全的。Vector 是用同步方法來實現線程安全的, 而和它相似的 ArrayList 不是線程安全的。
Java 中什么是競態條件? 舉個例子說明。
競態條件會導致程序在并發情況下出現一些 bugs。多線程對一些資源的競爭的時候就會產生競態條件,如果首先要執行的程序競爭失敗排到后面執行了,那么整個程序就會出現一些不確定的 bugs。這種 bugs 很難發現而且會重復出現,因為線程間的隨機競爭。一個例子就是無序處理。
Java 中如何停止一個線程
當 run () 或者 call () 方法執行完的時候線程會自動結束,如果要手動結束一個線程,你可以用 volatile 布爾變量來退出 run ()方法的循環或者是取消任務來中斷線程。其他情形:異常 - 停止執行 休眠 - 停止執行 阻塞 - 停止執行
一個線程運行時發生異常會怎樣
簡單的說,如果異常沒有被捕獲該線程將會停止執行。Thread.UncaughtExceptionHandler 是用于處理未捕獲異常造成線程突然中斷情況的一個內嵌接口。當一個未捕獲異常將造成線程中斷的時候 JVM 會使用 Thread.getUncaughtExceptionHandler ()來查詢線程的 UncaughtExceptionHandler 并將線程和異常作為參數傳遞給 handler 的 uncaughtException ()方法進行處理。
如何在兩個線程間共享數據?
通過共享對象來實現這個目的,或者是使用像阻塞隊列這樣并發的數據結構
Java 中 notify 和 notifyAll 有什么區別
notify ()方法不能喚醒某個具體的線程,所以只有一個線程在等待的時候它才有用武之地。而 notifyAll ()喚醒所有線程并允許他們爭奪鎖確保了至少有一個線程能繼續運行。
為什么 wait, notify 和 notifyAll 這些方法不在 thread 類里面
一個很明顯的原因是 JAVA 提供的鎖是對象級的而不是線程級的。如果線程需要等待某些鎖那么調用對象中的 wait ()方法就有意義了。如果 wait ()方法定義在 Thread 類中,線程正在等待的是哪個鎖就不明顯了。簡單的說,由于 wait,notify 和 notifyAll 都是鎖級別的操作,所以把他們定義在 Object 類中因為鎖屬于對象。
什么是ThreadLocal
ThreadLocal,線程局部變量。
當使用ThreadLocal維護變量時,ThreadLocal為每個使用該變量的線程提供獨立的變量副本,每個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本,是線程隔離的。線程隔離的秘密在于ThreadLocalMap類(ThreadLocal的靜態內部類)
線程局部變量是局限于線程內部的變量,屬于線程自身所有,不在多個線程間共享。Java 提供 ThreadLocal 類來支持線程局部變量,是一種實現線程安全的方式。但是在管理環境下(如 web 服務器)使用線程局部變量的時候要特別小心,在這種情況下,工作線程的生命周期比任何應用變量的生命周期都要長。任何線程局部變量一旦在工作完成后沒有釋放,Java 應用就存在內存泄露的風險。
ThreadLocal的方法:void set(T value)、T get()以及T initialValue()。
ThreadLocal是如何為每個線程創建變量的副本的:
首先,在每個線程Thread內部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個threadLocals就是用來存儲實際的變量副本的,鍵值為當前ThreadLocal變量,value為變量副本(即T類型的變量)。初始時,threadLocals為空,當通過ThreadLocal變量調用get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,并且以當前ThreadLocal變量為鍵值,以ThreadLocal要保存的副本變量為value,存到threadLocals。然后在當前線程里面,如果要使用副本變量,就可以通過get方法在threadLocals里面查找。
總結:
- 實際通過ThreadLocal創建的副本是存儲在每個線程自己的threadLocals中的
- 為何threadLocals的鍵為ThreadLocal對象,因為每個線程中可有多個threadLocal變量,就像上面代碼中的longLocal和stringLocal;
- 在進行get之前,必須先set,否則會報空指針異常;如果想在get之前不需要調用set就能正常訪問的話,必須重寫initialValue()方法
什么是 FutureTask?
在 Java 并發程序中 FutureTask 表示一個可以取消的異步運算。它有啟動和取消運算、查詢運算是否完成和取回運算結果等方法。只有當運算完成的時候結果才能取回,如果運算尚未完成 get 方法將會阻塞。一個 FutureTask 對象可以對調用了 Callable 和 Runnable 的對象進行包裝,由于 FutureTask 也是調用了 Runnable 接口所以它可以提交給 Executor 來執行。
Java 中 interrupted 和 isInterruptedd 方法的區別
interrupted是靜態方法,isInterruptedd是一個普通方法;
如果當前線程被中斷(沒有拋出中斷異常,否則中斷狀態就會被清除),你調用interrupted方法,第一次會返回true。然后,當前線程的中斷狀態被方法內部清除了。第二次調用時就會返回false。如果你剛開始一直調用isInterrupted,則會一直返回true,除非中間線程的中斷狀態被其他操作清除了。也就是說isInterrupted 只是簡單的查詢中斷狀態,不會對狀態進行修改。
為什么 wait 和 notify 方法要在同步塊中調用
如果不這么做,代碼會拋出 IllegalMonitorStateException異常。還有一個原因是為了避免 wait 和 notify 之間產生競態條件。
為什么你應該在循環中檢查等待條件?
處于等待狀態的線程可能會收到錯誤警報和偽喚醒,如果不在循環中檢查等待條件,程序就會在沒有滿足結束條件的情況下退出。因此,當一個等待線程醒來時,不能認為它原來的等待狀態仍然是有效的,在 notify ()方法調用之后和等待線程醒來之前這段時間它可能會改變。這就是在循環中使用 wait ()方法效果更好的原因。
Java 中的同步集合與并發集合有什么區別
同步集合與并發集合都為多線程和并發提供了合適的線程安全的集合,不過并發集合的可擴展性更高。在 Java1.5 之前程序員們只有同步集合來用且在多線程并發的時候會導致爭用,阻礙了系統的擴展性。Java1.5加入了并發集合像 ConcurrentHashMap,不僅提供線程安全還用鎖分離和內部分區等現代技術提高了可擴展性。它們大部分位于JUC包下。
Java 中堆和棧有什么不同
每個線程都有自己的棧內存,用于存儲本地變量,方法參數和棧調用,一個線程中存儲的變量對其它線程是不可見的。而堆是所有線程共享的一片公用內存區域。對象都在堆里創建,為了提升效率線程會從堆中弄一個緩存到自己的棧,如果多個線程使用該變量就可能引發問題,這時 volatile 變量就可以發揮作用了,它要求線程從主存中讀取變量的值。
什么是線程池? 為什么要使用它?
創建線程要花費昂貴的資源和時間,如果任務來了才創建線程那么響應時間會變長,而且一個進程能創建的線程數有限。為了避免這些問題,在程序啟動的時候就創建若干線程來響應處理,它們被稱為線程池,里面的線程叫工作線程。從 JDK1.5 開始,Java API 提供了 Executor 框架讓你可以創建不同的線程池。比如單線程池,每次處理一個任務;數目固定的線程池或者是緩存線程池(一個適合很多生存期短的任務的程序的可擴展線程池)。
如何寫代碼來解決生產者消費者問題?
在現實中你解決的許多線程問題都屬于生產者消費者模型,就是一個線程生產任務供其它線程進行消費,你必須知道怎么進行線程間通信來解決這個問題。比較低級的辦法是用 wait 和 notify 來解決這個問題,比較贊的辦法是用 Semaphore 或者 BlockingQueue 來實現生產者消費者模型。
如何避免死鎖?
死鎖是指兩個或兩個以上的進程在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。這是一個嚴重的問題,因為死鎖會讓你的程序掛起無法完成任務,死鎖的發生必須滿足以下四個條件:
- 互斥條件:一個資源每次只能被一個進程使用。
- 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
- 不剝奪條件:進程已獲得的資源,在末使用完之前,不能強行剝奪。
- 循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關系。
避免死鎖最簡單的方法就是阻止循環等待條件,將系統中所有的資源設置標志位、排序,規定所有的進程申請資源必須以一定的順序(升序或降序)做操作來避免死鎖。
Java 中活鎖和死鎖有什么區別?
活鎖和死鎖類似,不同之處在于處于活鎖的線程或進程的狀態是不斷改變的,活鎖可以認為是一種特殊的饑餓。一個現實的活鎖例子是兩個人在狹小的走廊碰到,兩個人都試著避讓對方好讓彼此通過,但是因為避讓的方向都一樣導致最后誰都不能通過走廊。簡單的說就是,活鎖和死鎖的主要區別是前者進程的狀態可以改變但是卻不能繼續執行。
怎么檢測一個線程是否擁有鎖
在 java.lang.Thread 中有一個方法叫 holdsLock (),當且僅當當前線程擁有某個具體對象的鎖時它返回true。
你如何在 Java 中獲取線程堆棧
eak 組合鍵來獲取線程堆棧,Linux 下用 kill -3 命令。你也可以用 jstack 這個工具來獲取,它對線程 id 進行操作,你可以用 jps 這個工具找到 id。
JVM內存配置參數
- -Xmx:最大堆大小
- -Xms:初始堆大小(最小內存值)
- -Xmn:年輕代大小
- -XXSurvivorRatio:3 意思是Eden:Survivor=3:2
- -Xss棧容量
- -XX:+PrintGC 輸出GC日志
- -XX:+PrintGCDetails 輸出GC的詳細日志
Java 中 synchronized 和 ReentrantLock 有什么不同
Java 在過去很長一段時間只能通過 synchronized 關鍵字來實現互斥,它有一些缺點。比如你不能擴展鎖之外的方法或者塊邊界,嘗試獲取鎖時不能中途取消等。Java 5 通過 Lock 接口提供了更復雜的控制來解決這些問題。 ReentrantLock 類實現了 Lock,它擁有與 synchronized 相同的并發性和內存語義且它還具有可擴展性。
有三個線程 T1,T2,T3,怎么確保它們按順序執行
可以用線程類的 join ()方法。具體操作是在T3的run方法中調用t2.join(),讓t2執行完再執行t3;T2的run方法中調用t1.join(),讓t1執行完再執行t2。這樣就按T1,T2,T3的順序執行了
Thread 類中的 yield 方法有什么作用
Yield 方法可以暫停當前正在執行的線程對象,讓其它有相同優先級的線程執行。它是一個靜態方法而且只保證當前線程放棄 CPU 占用而不能保證使其它線程一定能占用 CPU,執行 yield ()的線程有可能在進入到暫停狀態后馬上又被執行。
Java 中 ConcurrentHashMap 的并發度是什么
ConcurrentHashMap 把實際 map 劃分成若干部分來實現它的可擴展性和線程安全。這種劃分是使用并發度獲得的,它是 ConcurrentHashMap 類構造函數的一個可選參數,默認值為 16,這樣在多線程情況下就能避免爭用。
Java 中 Semaphore是什么
JUC下的一種新的同步類,它是一個計數信號。從概念上講,Semaphore信號量維護了一個許可集合。如有必要,在許可可用前會阻塞每一個 acquire (),然后再獲取該許可。每個 release ()添加一個許可,從而可能釋放一個正在阻塞的獲取者。但是,不使用實際的許可對象,Semaphore 只對可用許可的號碼進行計數,并采取相應的行動。信號量常常用于多線程的代碼中,比如數據庫連接池。
如果你提交任務時,線程池隊列已滿。會發會生什么?
這個問題問得很狡猾,許多程序員會認為該任務會阻塞直到線程池隊列有空位。事實上如果一個任務不能被調度執行那么 ThreadPoolExecutor’s submit ()方法將會拋出一個 RejectedExecutionException 異常。
Java 線程池中 submit () 和 execute ()方法有什么區別
兩個方法都可以向線程池提交任務,execute ()方法的返回類型是 void,它定義在 Executor 接口中, 而 submit ()方法可以返回持有計算結果的 Future 對象,它定義在 ExecutorService 接口中,它擴展了 Executor 接口,其它線程池類像 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 都有這些方法。
什么是阻塞式方法?
阻塞式方法是指程序會一直等待該方法完成期間不做其他事情,ServerSocket 的 accept ()方法就是一直等待客戶端連接。這里的阻塞是指調用結果返回之前,當前線程會被掛起,直到得到結果之后才會返回。此外,還有異步和非阻塞式方法在任務完成前就返回。
Swing 是線程安全的嗎?
你可以很肯定的給出回答,Swing 不是線程安全的。你不能通過任何線程來更新 Swing 組件,如 JTable、JList 或 JPanel,事實上,它們只能通過 GUI 或 AWT 線程來更新。這就是為什么 Swing 提供 invokeAndWait() 和 invokeLater() 方法來獲取其他線程的 GUI 更新請求。這些方法將更新請求放入 AWT 的線程隊列中,可以一直等待,也可以通過異步更新直接返回結果。
Java 中 invokeAndWait 和 invokeLater 有什么區別
這兩個方法是 Swing API 提供給 Java 開發者用來從當前線程而不是事件派發線程更新 GUI 組件用的。InvokeAndWait ()同步更新 GUI 組件,比如一個進度條,一旦進度更新了,進度條也要做出相應改變。如果進度被多個線程跟蹤,那么就調用 invokeAndWait ()方法請求事件派發線程對組件進行相應更新。而 invokeLater ()方法是異步調用更新組件的。
Swing API 中那些方法是線程安全的?
雖然Swing不是線程安全的但是有一些方法是可以被多線程安全調用的。如repaint (), revalidate ()。 JTextComponent 的 setText ()方法和 JTextArea 的 insert () 和 append () 方法也是線程安全的。
如何在 Java 中創建 Immutable 對象
Immutable 對象可以在沒有同步的情況下共享,降低了對該對象進行并發訪問時的同步化開銷。可是 Java 沒有@Immutable 這個注解符,要創建不可變類,要實現下面幾個步驟:通過構造方法初始化所有成員、對變量不要提供 setter 方法、將所有的成員聲明為私有的,這樣就不允許直接訪問這些成員、在 getter 方法中,不要直接返回對象本身,而是克隆對象,并返回對象的拷貝。
Java 中的 ReadWriteLock 是什么?
一般而言,讀寫鎖是用來提升并發程序性能的鎖分離技術的成果。Java 中的 ReadWriteLock 是 Java 5 中新增的一個接口,一個 ReadWriteLock 維護一對關聯的鎖,一個用于只讀操作一個用于寫。在沒有寫線程的情況下一個讀鎖可能會同時被多個讀線程持有。寫鎖是獨占的,你可以使用 JDK 中的 ReentrantReadWriteLock 來實現這個規則,它最多支持 65535 個寫鎖和 65535 個讀鎖。
多線程中的忙循環是什么?
忙循環就是程序員用循環讓一個線程等待,不像傳統方法 wait (), sleep () 或 yield () 它們都放棄了 CPU 控制,而忙循環不會放棄 CPU,它就是在運行一個空循環。這么做的目的是為了保留 CPU 緩存,在多核系統中,一個等待線程醒來的時候可能會在另一個內核運行,這樣會重建緩存。為了避免重建緩存和減少等待重建的時間就可以使用它了。
volatile 變量和 atomic 變量有什么不同
volatile 變量和 atomic 變量看起來很像,但功能卻不一樣。volatile 變量可以確保先行關系,即寫操作會發生在后續的讀操作之前, 但它并不能保證原子性。例如用 volatile 修飾 count 變量那么 count++ 操作并不是原子性的。而 AtomicInteger 類提供的 atomic 方法可以讓這種操作具有原子性如 getAndIncrement ()方法會原子性的進行增量操作把當前值加一,其它數據類型和引用變量也可以進行相似操作。
如果同步塊內的線程拋出異常會發生什么?
無論你的同步塊是正常還是異常退出的,里面的線程都會釋放鎖,所以對比鎖接口我更喜歡同步塊,因為它不用我花費精力去釋放鎖,該功能可以在 finally block 里釋放鎖實現。
如何在 Java 中創建線程安全的 Singleton
5種:急加載,同步方法,雙檢鎖,靜態內部類,枚舉
如何強制啟動一個線程?
這個問題就像是如何強制進行 Java 垃圾回收,目前還沒有覺得方法,雖然你可以使用 System.gc ()來進行垃圾回收,但是不保證能成功。在 Java 里面沒有辦法強制啟動一個線程,它是被線程調度器控制著且 Java 沒有公布相關的 API。
Java 中的 fork join 框架是什么?
fork join 框架是 JDK7 中出現的一款高效的工具,Java 開發人員可以通過它充分利用現代服務器上的多處理器。它是專門為了那些可以遞歸劃分成許多子模塊設計的,目的是將所有可用的處理能力用來提升程序的性能。fork join 框架一個巨大的優勢是它使用了工作竊取算法,可以完成更多任務的工作線程可以從其它線程中竊取任務來執行。
Java 多線程中調用 wait () 和 sleep ()方法有什么不同?
Java 程序中 wait 和 sleep 都會造成某種形式的暫停,它們可以滿足不同的需要。wait ()方法意味著條件等待,如果等待條件為真且其它線程被喚醒時它會釋放鎖,而 sleep ()方法僅僅釋放 CPU 資源或者讓當前線程短暫停頓,但不會釋放鎖。
雙親委派模型中的方法
findLoadedClass(),LoadClass(),findBootstrapClassOrNull(),findClass(),resolveClass()
NIO、AIO、BIO
- BIO即同步阻塞IO,適用于連接數目較小且固定的架構,這種方式對服務器資源要求比較高,并發局限于應用中,JDK1.4之前的唯一選擇,但程序直觀、簡單、易理解。
- NIO即同步非阻塞IO,適用于連接數目多且連接比較短的架構,比如聊天服務器,并發局限于應用中,編程比較復雜,JDK1.4開始支持。
- AIO即異步非阻塞IO,適用于連接數目多且連接比較長的架構,如相冊服務器,充分調用OS參與并發操作,編程比較復雜,JDK1.7開始支持
多線程、并發及線程的基礎問題
> Java 中能創建 volatile 數組嗎?
能,Java 中可以創建 volatile 類型數組,不過只是一個指向數組的引用,而不是整個數組。我的意思是,如果改變引用指向的數組,將會受到 volatile 的保護,但是如果多個線程同時改變數組的元素,volatile 標示符就不能起到之前的保護作用了。
volatile 能使得一個非原子操作變成原子操作嗎?
一個典型的例子是在類中有一個 long 類型的成員變量。如果你知道該成員變量會被多個線程訪問,如計數器、價格等,你最好是將其設置為 volatile。為什么?因為 Java 中讀取 long 類型變量不是原子的,需要分成兩步,如果一個線程正在修改該 long 變量的值,另一個線程可能只能看到該值的一半(前 32 位)。但是對一個 volatile 型的 long 或 double 變量的讀寫是原子。
volatile 修飾符的有過什么實踐?
一種實踐是用 volatile 修飾 long 和 double 變量,使其能按原子類型來讀寫。double 和 long 都是64位寬,因此對這兩種類型的讀是分為兩部分的,第一次讀取第一個 32 位,然后再讀剩下的 32 位,這個過程不是原子的,但 Java 中 volatile 型的 long 或 double 變量的讀寫是原子的。volatile 修復符的另一個作用是提供內存屏障(memory barrier),例如在分布式框架中的應用。簡單的說,就是當你寫一個 volatile 變量之前,Java 內存模型會插入一個寫屏障(write barrier),讀一個 volatile 變量之前,會插入一個讀屏障(read barrier)。意思就是說,在你寫一個 volatile 域時,能保證任何線程都能看到你寫的值,同時,在寫之前,也能保證任何數值的更新對所有線程是可見的,因為內存屏障會將其他所有寫的值更新到緩存。
volatile 類型變量提供什么保證?
volatile 變量提供順序和可見性保證,例如,JVM 或者 JIT為了獲得更好的性能會對語句重排序,但是 volatile 類型變量即使在沒有同步塊的情況下賦值也不會與其他語句重排序。 volatile 提供 happens-before 的保證,確保一個線程的修改能對其他線程是可見的。某些情況下,volatile 還能提供原子性,如讀 64 位數據類型,像 long 和 double 都不是原子的,但 volatile 類型的 double 和 long 就是原子的。
10 個線程和 2 個線程的同步代碼,哪個更容易寫?
從寫代碼的角度來說,兩者的復雜度是相同的,因為同步代碼與線程數量是相互獨立的。但是同步策略的選擇依賴于線程的數量,因為越多的線程意味著更大的競爭,所以你需要利用同步技術,如鎖分離,這要求更復雜的代碼和專業知識。
你是如何調用 wait()方法的?使用 if 塊還是循環?為什么?
wait() 方法應該在循環調用,因為當線程獲取到 CPU 開始執行的時候,其他條件可能還沒有滿足,所以在處理前,循環檢測條件是否滿足會更好。下面是一段標準的使用 wait 和 notify 方法的代碼:
// The standard idiom for using the wait methodsynchronized (obj) { while (condition does not hold) obj.wait(); // (Releases lock, and reacquires on wakeup)... // Perform action appropriate to condition }
參見 Effective Java 第 69 條,獲取更多關于為什么應該在循環中來調用 wait 方法的內容。
什么是多線程環境下的偽共享(false sharing)?
偽共享是多線程系統(每個處理器有自己的局部緩存)中一個眾所周知的性能問題。偽共享發生在不同處理器的上的線程對變量的修改依賴于相同的緩存行,如下圖所示:
圖-1.gif
偽共享問題很難被發現,因為線程可能訪問完全不同的全局變量,內存中卻碰巧在很相近的位置上。如其他諸多的并發問題,避免偽共享的最基本方式是仔細審查代碼,根據緩存行來調整你的數據結構。
GC、內存相關
對哪些區域回收 Java運行時數據區域:程序計數器、JVM棧、本地方法棧、方法區和堆。
由于程序計數器、JVM棧、本地方法棧3個區域隨線程而生隨線程而滅,對這幾個區域內存的回收和分配具有確定性。而方法區和堆則不一樣,程序需要在運行時才知道創建哪些對象,對這部分內存的分配是動態的,GC關注的也就是這部分內存。
如何判定對象需要回收
引用計數法:給對象加上一個計數器,當有一個地方引用它,計數器+1,引用失效時,計數器-1,當計數器為0時,判定該對象可回收。引用計數法優點是實現簡單,python,flashplayer等使用引用計數法進行內存管理。引用計數法的缺點在于無法解決循環引用的問題。
在Java中使用可達性分析算法法判定對象是否“死亡”。可達性分析法是指通過稱為GC-Roots的對象為起始點,從這些結點向下搜索,當從GCRoots到這個對象不可達時,被判定為可收回的對象。
可作為GC Roots的對象
可作為GC Roots的對象:虛擬機棧中引用的對象 方法區中靜態屬性引用的對象 方法區中常量引用的對象 本地方法棧中JNI引用的對象
對象的自我救贖
即使在可達性算法中判定為不可達時,也并非一定被回收。對象存在自我救贖的可能。要真正宣告對象的死亡,需要經歷2次標記的過程。如果對象經過可達性分析法發現不可達時,對象將被第一次標記被進行篩選,篩選的條件是此對象是否有必要執行finalize方法。如果對象沒有重寫finalize方法或finalize方法已經被JVM調用過,則判定為不需要執行。
如果對象被判定為需要執行finalize方法,該對象將被放置在一個叫做F-Queue的隊列中,JVM會建立一個低優先級的線程執行finalize方法,如果對象想要完成自我救贖需要在finalize方法中與引用鏈上的對象關聯,比如把自己也就是this賦值給某個類變量。當GC第二次對F-Queue中對象標記時,該對象將被移出“即將回收”的集合,完成自我救贖。簡言之,finalize方法是對象逃脫死亡命運的最后機會,并且任何對象的finalize方法只會被JVM調用一次。
垃圾回收算法
- Mark-Sweep法:標記清除法,容易產生內存碎片,導致分配較大對象時沒有足夠的連續內存空間而提前出發GC。這里涉及到另一個問題,即對象創建時的內存分配,對象創建內存分配主要有2種方法,分別是指針碰撞法和空閑列表法:
- 指針碰撞法:使用的內存在一側,空閑的在另一側,中間使用一個指針作為分界點指示器,對象內存分配時只要指針向空閑的移動對象大小的距離即可。
- 空閑列表法:使用的和空閑的內存相互交錯無法進行指針碰撞,JVM必須維護一個列表記錄哪些內存塊可用,分配時從列表中找出一個足夠的分配給對象,并更新列表記錄。
所以,當采用Mark-Sweep算法的垃圾回收器時,內存分配通常采用空閑列表法。
- Copy法:將內存分為2塊,每次使用其中的一塊,當一塊滿了,將存活的對象復制到另一塊,把使用過的那一塊一次性清除。顯然,Copy法解決了內存碎片的問題,但算法的代價是內存縮小為原來的一半。現代的垃圾收集器對新生代采用的正是Copy算法。但通常不執行1:1的策略,HotSpot虛擬機默認Eden區Survivor區8:1。每次使用Eden和其中一塊Survivor區。也就是說新生代可用內存為新生代內存空間的90%。
- Mark-Compact法:標記整理法。它的第一階段與Mark-Sweep法一樣,但不直接清除,而是將存活對象向一端移動,然后清除端邊界以外的內存,這樣也不存在內存碎片。
- 分代收集算法:將堆內存劃分為新生代,老年代,根據新生代老年代的特點選取不同的收集算法。因為新生代對象大多朝生夕死,而老年代對象存活率高,沒有額外空間進行分配擔保,通常對新生代執行復制算法,老年代執行Mark-Sweep算法或Mark-Compact算法。
垃圾收集器
通常來說,新生代老年代使用不同的垃圾收集器。新生代的垃圾收集器有Serial(單線程)、ParNew(Serial的多線程版本)、ParallelScavenge(吞吐量優先的垃圾收集器),老年代有SerialOld(單線程老年代)、ParallelOld(與ParallelScavenge搭配的多線程執行標記整理算法的老年代收集器)、CMS(標記清除算法,容易產生內存碎片,可以開啟內存整理的參數),以及當前最先進的垃圾收集器G1,G1通常面向服務器端的垃圾收集器,在我自己的Java應用程序中通過-XX:+PrintGCDetails,發現自己的垃圾收集器是使用了ParallelScavenge+ParallelOld的組合。
內存分配和回收的策略
- 對象優先在Eden區分配,默認Eden與Survivor的比例為8:1
- 大對象直接進入老年代
- 長期存活的進入老年代:JVM給每個對象定義一個年齡計數器,當對象在Eden區出生并躲過一次MinorGC,并且Survivor可以容納的話,將被移入Survivor區,年齡設為1。以后每在Survivor區躲過一次MinorGC,年齡加一歲,當對象年齡加到15歲時,晉升到老年代。當然15歲的默認值可以通過-XX虛擬機參數設置。
- 動態對象年齡判定:有的時候無需到達15歲即晉升老年代。判定方法是如果Survivor區中相同年齡的所有對象大小的總和大于Survivor區空間的一半,年齡大于或等于該年齡的對象直接進入老年代
- 空間分配擔保
在發生MinorGC之前,虛擬機會檢查老年代最大可用連續空間是否大于新生代所有對象總和,如果成立,確保這次MinorGC安全。否則,虛擬機會查看HandlePromotionFailure設置是否允許擔保失敗。如果允許,虛擬機會接著查看老年代最大連續可用空間是否大于歷次晉升到老年代對象的平均大小,如果大于,則進行一次MinorGC,盡管這次MinorGC是有風險的,如果小于或者HandlePromotionFailure設置為不允許,要改為一次FullGC
方法區的回收
方法區通常會與永久代劃等號,實際上二者并不等價,只不過是HotSpot虛擬機設計者用永久代實現方法區,并將GC分代擴展至方法區。 永久代垃圾回收通常包括兩部分內容:廢棄常量和無用的類。常量的回收與堆區對象的回收類似,當沒有其他地方引用該字面量時,如果有必要,將被清理出常量池。
判定無用的類的3個條件:
- 該類的所有實例都已經被回收,也就是說堆中不存在該類的任何實例
- 加載該類的ClassLoader已經被回收
- 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
當然,這也僅僅是判定,不代表立即卸載該類。
Java中有內存泄漏嗎?
內存泄露的定義: 當某些對象不再被應用程序所使用,但是由于仍然被引用而導致垃圾收集器不能釋放。
內存泄漏的原因:對象的生命周期不同。比如說
對象A引用了對象B,A的生命周期比B的要長得多,當對象B在應用程序中不會再被使用以后,對象 A 仍然持有著B的引用。 (根據虛擬機規范)在這種情況下GC不能將B從內存中釋放。這種情況很可能會引起內存問題,倘若A還持有著其他對象的引用,那么這些被引用的(無用)對象也不會被回收,并占用著內存空間。甚至有可能B也持有一大堆其他對象的引用。這些對象由于被 B 所引用,也不會被垃圾收集器所回收,所有這些無用的對象將消耗大量寶貴的內存空間。并可能導致內存泄漏
怎樣防止:
1、當心集合類,比如HashMap、ArrayList等,因為這是最容易發生內存泄露的地方。當集合對象被聲明為static時,他們的生命周期一般和整個應用程序一樣長。
OOM解決辦法:
內存溢出的空間:Permanent Generation和Heap Space,也就是永久代和堆區
第一種情況永久代的溢出:出現這種問題的原因可能是應用程序加載了大量的jar或class,使虛擬機裝載類的空間不夠,與Permanent Generation Space的大小有關。
解決辦法有2種:
- 通過虛擬機參數-XX:PermSize和-XX:MaxPermSize調整永久代大小
- 清理程序中的重復的Jar文件,減少類的重復加載
第二種堆區的溢出:發生這種問題的原因是java虛擬機創建的對象太多,在進行垃圾回收之間,虛擬機分配的到堆內存空間已經用滿了,與Heap Space的size有關。解決這類問題有兩種思路:
- 檢查程序,看是否存在死循環或不必要地重復創建大量對象,定位原因,修改程序和算法。
- 通過虛擬機參數-Xms和-Xmx設置初始堆和最大堆的大小
DirectMemory直接內存
直接內存并不是Java虛擬機規范定義的內存區域的一部分,但是這部分內存也被頻繁使用,而且也可能導致OOM異常的出現。
JDK1.4引入了NIO,這是一種基于通道和緩沖區的非阻塞IO模式,它可以使用Native函數庫分配直接堆外內存,然后通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內存的引用進行操作,使得在某些場合顯著提高性能,因為它避免了在Java堆和本地堆之間來回復制數據。