TCL社招面經:原子性的理解?鎖的理解?

1.ArrayList和LinkedList的瞭解?

2.HashMap和Hashtable的瞭解?

3.對於上面兩個容器的初始值和每次擴充容量大小,以及為什麼HashMap的長度是那個規律?

4.HashMap多線程操作會導致什麼問題?-

5.談談synchronized關鍵字的理解?

6.原子性的理解?鎖的理解?

7.JVM的理解?Java1.8版本有什麼瞭解?

8.什麼是OOM?StackOverflowError和OutOfMemoryError?

9.JVM的常用參數調優?

10.內存快照抓取和MAT分析hprof文件?-


1. ArrayList和LinkedList的瞭解?

ArrayList底層使用的是Object數組,所以執行數據的插入和刪除元素的時間可能會移動相應的元素。LinkedList底層使用的是雙向鏈表數據結構,所以插入和刪除不受元素位置的影響。但對於訪問集合中的元素,ArrayList可以根據下標索引序號快速響應,而LinkedList不支持快速隨機訪問元素。

2. HashMap和Hashtable的瞭解?

對於不同的JDK版本有著不同的底層結構。HashMap在JDK1.8之前底層是數組和鏈表結合而成的鏈表散列。HashMap通過key的hashCode處理得到hash值,然後通過(數組長度-1)&hash判斷當前元素存放的位置,如果當前位置存在元素的話,就判斷該元素與要存入的元素的hash值以及key是否相同,如果相同的話就覆蓋,否則就通過拉鍊法解決衝突。JDK1.8之後當鏈表長度大於閥值就轉化為紅黑樹減少搜索時間。

HashMap和Hashtable的區別

HashMap和Hashtable都實現了Map接口,但決定用哪一個之前先要弄清楚它們之間的分別。主要的區別有:線程安全性,同步(synchronization),以及速度。

HashMap幾乎可以等價於Hashtable,除了HashMap是非synchronized的,並可以接受null(HashMap可以接受為null的鍵值(key)和值(value),而Hashtable則不行)。

HashMap是非synchronized,而Hashtable是synchronized,這意味著Hashtable是線程安全的,多個線程可以共享一個Hashtable;而如果沒有正確的同步的話,多個線程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的擴展性更好。

另一個區別是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以當有其它線程改變了HashMap的結構(增加或者移除元素),將會拋出ConcurrentModificationException,但迭代器本身的remove()方法移除元素則不會拋出ConcurrentModificationException異常。但這並不是一個一定發生的行為,要看JVM。這條同樣也是Enumeration和Iterator的區別。

由於Hashtable是線程安全的也是synchronized,所以在單線程環境下它比HashMap要慢。如果你不需要同步,只需要單一線程,那麼使用HashMap性能要好過Hashtable。

HashMap不能保證隨著時間的推移Map中的元素次序是不變的。

要注意的一些重要術語:

sychronized意味著在一次僅有一個線程能夠更改Hashtable。就是說任何線程要更新Hashtable時要首先獲得同步鎖,其它線程要等到同步鎖被釋放之後才能再次獲得同步鎖更新Hashtable。

Fail-safe和iterator迭代器相關。如果某個集合對象創建了Iterator或者ListIterator,然後其它的線程試圖“結構上”更改集合對象,將會拋出ConcurrentModificationException異常。但其它線程可以通過set()方法更改集合對象是允許的,因為這並沒有從“結構上”更改集合。但是假如已經從結構上進行了更改,再調用set()方法,將會拋出IllegalArgumentException異常。

結構上的更改指的是刪除或者插入一個元素,這樣會影響到map的結構。

我們能否讓HashMap同步?

HashMap可以通過下面的語句進行同步:

Map m = Collections.synchronizeMap(hashMap);

結論

Hashtable和HashMap有幾個主要的不同:線程安全以及速度。僅在你需要完全的線程安全的時候使用Hashtable,而如果你使用Java 5或以上的話,請使用ConcurrentHashMap吧。

3. 對於上面兩個容器的初始值和每次擴充容量大小,以及為什麼HashMap的長度是那個規律?

如果不指定容器的初始值,Hashtable默認為11,每次擴充為原來的2n+1,HashMap默認初始為16,每次擴充為2的冪次方大小。

為什麼長度要是2的冪次方呢

Hash 值的範圍值-2147483648到2147483647,前後加起來大概40億的映射空間。那當然是不能直接拿的。

只能通過映射,就是對數組取模。“ (n - 1) & hash”。(n代表數組長度)這是公式。

取餘(%)操作中如果除數是2的冪次則等價於與其除數減一的與(&)操作(也就是說 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 並且 採用二進制位操作 &,相對於%能夠提高運算效率,

4. HashMap多線程操作會導致什麼問題?

大多數javaer都知道HashMap是線程不安全的,多線程環境下數據可能會發生錯亂,一定要謹慎使用。這個結論是沒錯,可是HashMap的線程不安全遠遠不是數據髒讀這麼簡單,它還有可能會發生死鎖,造成內存飆升100%的問題。

死鎖的四個條件

1)互斥條件:指進程對所分配到的資源進行排它性使用,即在一段時間內某資源只由一個進程佔用。如果此時還有其它進程請求資源,則請求者只能等待,直至佔有資源的進程用畢釋放。

2)請求和保持條件:指進程已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它進程佔有,此時請求進程阻塞,但又對自己已獲得的其它資源保持不放。

3)不剝奪條件:指進程已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時由自己釋放。

4)環路等待條件:指在發生死鎖時,必然存在一個進程——資源的環形鏈,即進程集合{P0,P1,P2,···,Pn}中的P0正在等待一個P1佔用的資源;P1正在等待P2佔用的資源,……,Pn正在等待已被P0佔用的資源。

我們來分析一下鏈表的互相引用符不符合上面四個條件:

①互斥條件:鏈表上的節點同一時間此時被兩個線程佔用,兩個線程佔用訪問節點的權利,符合該條件

②請求和保持條件:Thread1保持著節點e1,又提出了佔用節點e2(此時尚未釋放e2);而Thread2此時佔用e2,又提出了佔用節點e1,Thread1佔用著Thread2接下來要用的e1,而Thread2又佔用著Thread1接下來要用的e2,符合該條件

③:不剝奪條件:線程是由自己的退出的,此時並沒有任何中斷機制(sleep或者wait方法或者interuppted中斷),只能由自己釋放,滿足條件

④:環路等待條件:e1、e2、e3等形成了資源的環形鏈條,滿足該條件

5. 談談synchronized關鍵字的理解?

解決了多線程訪問共享資源造成的數據紊亂,所以採用該關鍵字修飾的方法或者代碼塊在任何時刻只能由一個線程執行。

原子性的理解?

多線程操作的一系列複合操作,要麼全部成功,要麼全部失敗回到操作前的初始值。

鎖的理解?

鎖的加入就是為了防止多線程同時對共享資源的操作造成數據的紊亂。

6.JVM的理解?Java1.8版本有什麼瞭解?


TCL社招面經:原子性的理解?鎖的理解?


JVM運行時數據區,分為線程共享部分(方法區、堆)和線程隔離區(虛擬機棧、本地方法棧和程序計數器)。

1.方法區

用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。

運行時常量池(Runtime Constant Pool)是方法區的一部分。.Class文件中除了有類的版本/字段/方法/接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將類在加載後進入方法區的運行時常量池中存放.

運行時常量區的內容並不只是在編譯期間產生,通過String.intern()也可以實現在運行時向常量區中添加內容。

需要注意的是:從JDK8開始,方法區被元數據區替代了。

2.堆

是JVM中最大的一塊內存區域,該區域的目的只是用於存儲對象實例及數組。該區域也是GC的最主要區域。

根據Java虛擬機規範的規定,Java堆可以處於物理上不連續的內存空間中,只要邏輯上是連續的即可,就像我們的磁盤空間一樣.在實現時,既可以實現固定大小的,也可以是可擴展的,不過當前主流的虛擬機都是按照可擴展來實現的(通過-Xmx和-Xms控制).如果在堆中沒有內存完成實例分配,並且堆也無法再擴展時,就會拋出OutOfMemoryError異常。

3.虛擬機棧

每個線程方法在執行時都會創建一個棧幀,包含局部變量表、返回地址、操作數棧等信息。每個方法的執行與完成就對應的棧幀的入棧與出棧過程。局部變量表佔用空間的大小在編譯期就確定了。

這裡需要注意:如果線程請求的棧深度大於虛擬機所允許的深度,將會拋出StackOverflowError異常;如果虛擬機棧可以動態擴展(當前大部分的Java虛擬機都可動態擴展,只不過Java虛擬機規範中也允許固定長度的虛擬機棧),當擴展時無法申請到足夠的內存時將會拋出OutOfMemoryError異常。

4.本地方法棧

與虛擬機棧類似,不過其中執行是本地方法。對於HotSpot虛擬機而言,本地方法棧和虛擬機棧是統一的。

5.程序計數器

是一個小的內存空間,如果線程正在執行的是一個java方法,則此內存區域記錄正在執行的虛擬機字節碼指令的地址;如果線程正在執行的是native方法,則計算器中的值為空。此內存區域是唯一一個在JAVA虛擬機規範中沒有規定任何OutOfMemoryError情況的區域。

TCL社招面經:原子性的理解?鎖的理解?


JVM中ClassLoader類加載器的認識

1.加載

加載指的是將類的class文件讀入到內存,併為之創建一個java.lang.Class對象,也就是說,當程序中使用任何類時,系統都會為之建立一個java.lang.Class對象。

類的加載由類加載器完成,類加載器通常由JVM提供,這些類加載器也是前面所有程序運行的基礎,JVM提供的這些類加載器通常被稱為系統類加載器。除此之外,開發者可以通過繼承ClassLoader基類來創建自己的類加載器。

通過使用不同的類加載器,可以從不同來源加載類的二進制數據,通常有如下幾種來源。

1.從本地文件系統加載class文件,這是前面絕大部分示例程序的類加載方式。

2.從JAR包加載class文件,這種方式也是很常見的,前面介紹JDBC編程時用到的數據庫驅動類就放在JAR文件中,JVM可以從JAR文件中直接加載該class文件。

3.通過網絡加載class文件。

4.把一個Java源文件動態編譯,並執行加載。

類加載器通常無須等到“首次使用”該類時才加載該類,Java虛擬機規範允許系統預先加載某些類。

2.鏈接

當類被加載之後,系統為之生成一個對應的Class對象,接著將會進入連接階段,連接階段負責把類的二進制數據合併到JRE中。類連接又可分為如下3個階段。

1)驗證:驗證階段用於檢驗被加載的類是否有正確的內部結構,並和其他類協調一致。Java是相對C++語言是安全的語言,例如它有C++不具有的數組越界的檢查。這本身就是對自身安全的一種保護。驗證階段是Java非常重要的一個階段,它會直接的保證應用是否會被惡意入侵的一道重要的防線,越是嚴謹的驗證機制越安全。驗證的目的在於確保Class文件的字節流中包含信息符合當前虛擬機要求,不會危害虛擬機自身安全。其主要包括四種驗證,文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證。

四種驗證做進一步說明:

文件格式驗證:主要驗證字節流是否符合Class文件格式規範,並且能被當前的虛擬機加載處理。例如:主,次版本號是否在當前虛擬機處理的範圍之內。常量池中是否有不被支持的常量類型。指向常量的中的索引值是否存在不存在的常量或不符合類型的常量。

元數據驗證:對字節碼描述的信息進行語義的分析,分析是否符合java的語言語法的規範。

字節碼驗證:最重要的驗證環節,分析數據流和控制,確定語義是合法的,符合邏輯的。主要的針對元數據驗證後對方法體的驗證。保證類方法在運行時不會有危害出現。

符號引用驗證:主要是針對符號引用轉換為直接引用的時候,是會延伸到第三解析階段,主要去確定訪問類型等涉及到引用的情況,主要是要保證引用一定會被訪問到,不會出現類等無法訪問的問題。

2)準備:類準備階段負責為類的靜態變量分配內存,並設置默認初始值。

3)解析:將類的二進制數據中的符號引用替換成直接引用。說明一下:符號引用:符號引用是以一組符號來描述所引用的目標,符號可以是任何的字面形式的字面量,只要不會出現衝突能夠定位到就行。佈局和內存無關。直接引用:是指向目標的指針,偏移量或者能夠直接定位的句柄。該引用是和內存中的佈局有關的,並且一定加載進來的。

3.初始化

初始化是為類的靜態變量賦予正確的初始值,準備階段和初始化階段看似有點矛盾,其實是不矛盾的,如果類中有語句:private static int a = 10,它的執行過程是這樣的,首先字節碼文件被加載到內存後,先進行鏈接的驗證這一步驟,驗證通過後準備階段,給a分配內存,因為變量a是static的,所以此時a等於int類型的默認初始值0,即a=0,然後到解析(後面在說),到初始化這一步驟時,才把a的真正的值10賦給a,此時a=10。


TCL社招面經:原子性的理解?鎖的理解?


6. 什麼是OOM?StackOverflowError和OutOfMemoryError?

OOM:java.lang.OutOfMemoryError 內存溢出

TCL社招面經:原子性的理解?鎖的理解?


StackOverflowError:遞歸過深,遞歸沒有出口。

OutOfMemoryError:JVM空間溢出,創建對象速度高於GC回收速度。

JVM的常用參數調優?

在調優之前,我們需要記住下面的原則:

多數的 Java 應用不需要在服務器上進行 GC 優化; 多數導致 GC 問題的 Java 應用,都不是因為我們參數設置錯誤,而是代碼問題; 在應用上線之前,先考慮將機器的 JVM 參數設置到最優(最適合); 減少創建對象的數量; 減少使用全局變量和大對象; GC 優化是到最後不得已才採用的手段; 在實際使用中,分析 GC 情況優化代碼比優化 GC 參數要多得多。

GC 調優目的

將轉移到老年代的對象數量降低到最小; 減少 GC 的執行時間。

策略 1:將新對象預留在新生代,由於 Full GC 的成本遠高於 Minor GC,因此儘可能將對象分配在新生代是明智的做法,實際項目中根據 GC 日誌分析新生代空間大小分配是否合理,適當通過“-Xmn”命令調節新生代大小,最大限度降低新對象直接進入老年代的情況。

策略 2:大對象進入老年代,雖然大部分情況下,將對象分配在新生代是合理的。但是對於大對象這種做法卻值得商榷,大對象如果首次在新生代分配可能會出現空間不足導致很多年齡不夠的小對象被分配的老年代,破壞新生代的對象結構,可能會出現頻繁的 full gc。因此,對於大對象,可以設置直接進入老年代(當然短命的大對象對於垃圾回收老說簡直就是噩夢)。-XX:PretenureSizeThreshold 可以設置直接進入老年代的對象大小。

策略 3:合理設置進入老年代對象的年齡,-XX:MaxTenuringThreshold 設置對象進入老年代的年齡大小,減少老年代的內存佔用,降低 full gc 發生的頻率。

策略 4:設置穩定的堆大小,堆大小設置有兩個參數:-Xms 初始化堆大小,-Xmx 最大堆大小。

策略5:注意: 如果滿足下面的指標,則一般不需要進行 GC 優化:

MinorGC 執行時間不到50ms; Minor GC 執行不頻繁,約10秒一次; Full GC 執行時間不到1s; Full GC 執行頻率不算頻繁,不低於10分鐘1次。

����T��~�t


分享到:


相關文章: