1. 程式人生 > >java面試總結(資料來源網路)

java面試總結(資料來源網路)

core java:

一、集合

1、hashMap

  結構如圖:

  HashMap在Map.Entry靜態內部類實現中儲存key-value對。

  HashMap使用雜湊演算法。在put和get方法中。它使用hashCode()和equals()方法。當我們通過傳遞key-value對呼叫put方法的時候。HashMap使用Key hashCode()和雜湊演算法來找出儲存key-value對的索引。

  Entry儲存在LinkedList中,所以假設存在entry。它使用equals()方法來檢查傳遞的key是否已經存在。假設存在,它會覆蓋value。假設不存在。它會建立一個新的entry然後儲存。

  當我們通過傳遞key呼叫get方法時,它再次使用hashCode()來找到陣列中的索引,然後使用equals()方法找出正確的Entry,然後返回它的值。下面的圖片解釋了詳細內容。

  其他關於HashMap比較重要的問題是容量、負荷係數和閥值調整。HashMap預設的初始容量是32,負荷係數是0.75。

  閥值是為負荷係數乘以容量,不管何時我們嘗試加入一個entry,假設map的大小比閥值大的時候,HashMap會對map的內容進行又一次雜湊。且使用更大的容量。容量總是2的冪。所以假設你知道你須要儲存大量的key-value對,比方快取從資料庫裡面拉取的資料,使用正確的容量和負荷係數對HashMap進行初始化是個不錯的做法。

equals()和hashCode()的實現應該遵循下面規則:

(1)假設o1.equals(o2),那麼o1.hashCode() == o2.hashCode()總是為true的。

(2)假設o1.hashCode() == o2.hashCode()。並不意味著o1.equals(o2)會為true。

2、HashMap、HashTable、SynchronizedMap、ConcurrentHashMap

(1)HashMap同意key和value為null。而HashTable不同意。 

(2)HashTable是同步的,而HashMap不是。所以HashMap適合單執行緒環境,HashTable適合多執行緒環境。 

(3)在Java1.4中引入了LinkedHashMap,HashMap的一個子類,假如你想要遍歷順序,你非常easy從HashMap轉向LinkedHashMap,可是HashTable不是這種。它的順序是不可預知的。 

(4)HashMap提供對key的Set進行遍歷。因此它是fail-fast的。但HashTable提供對key的Enumeration進行遍歷,它不支援fail-fast。 

(5)HashTable被覺得是個遺留的類。假設你尋求在迭代的時候改動Map,你應該使用CocurrentHashMap。

(6)HashMap不是執行緒安全的;Hashtable執行緒安全,但效率低,因為是Hashtable是使用synchronized的,所有執行緒競爭同一把鎖;SynchronizedMap執行緒安全,其實是保持外部同步來實現的,效率也很低;而ConcurrentHashMap不僅執行緒安全而且效率高,因為它包含一個segment陣列,將資料分段儲存,給每一段資料配一把鎖,也就是所謂的鎖分段技術。

如何執行緒安全的使用HashMap,無非就是以下三種方式:   Hashtable
  Synchronized Map
  ConcurrentHashMap
---------------------
作者:huaxun66
來源:CSDN
原文:https://blog.csdn.net/huaxun66/article/details/53036625
版權宣告:本文為博主原創文章,轉載請附上博文連結!

3、HashMap和TreeMap

  對於在Map中插入、刪除和定位元素這類操作,HashMap是最好的選擇。然而。假如你須要對一個有序的key集合進行遍歷,TreeMap是更好的選擇。基於你的collection的大小,或許向HashMap中加入元素會更快。將map換為TreeMap進行有序key的遍歷。

4、ArrayList和LinkedList

(1)ArrayList是由Array所支援的基於一個索引的資料結構,所以它提供對元素的隨機訪問。複雜度為O(1),但LinkedList儲存一系列的節點資料。每一個節點都與前一個和下一個節點相連線。所以。雖然有使用索引獲取元素的方法,內部實現是從起始點開始遍歷,遍歷到索引的節點然後返回元素。時間複雜度為O(n)。比ArrayList要慢。 

(2)與ArrayList相比,在LinkedList中插入、加入和刪除一個元素會更快。由於在一個元素被插入到中間的時候,不會涉及改變陣列的大小,或更新索引。 

(3)LinkedList比ArrayList消耗很多其他的記憶體,由於LinkedList中的每一個節點儲存了前後節點的引用。

二、JVM

1、JVM物理結構:

Java程式碼編譯是由Java原始碼編譯器來完成,流程圖如下所示:

Java位元組碼的執行是由JVM執行引擎來完成,流程圖如下所示:

類載入機制:

2、記憶體管理

  (1)堆

  所有通過new建立的物件的記憶體都在堆中分配,堆的大小可以通過-Xmx和-Xms來控制。堆被劃分為新生代和舊生代,新生代又被進一步劃分為Eden和Survivor區,最後Survivor由From Space和To Space組成,結構圖如下所示:

技術分享

  • 新生代。新建的物件都是用新生代分配記憶體,Eden空間不足的時候,會把存活的物件轉移到Survivor中,新生代大小可以由-Xmn來控制,也可以用-XX:SurvivorRatio來控制Eden和Survivor的比例

  • 舊生代。用於存放新生代中經過多次垃圾回收仍然存活的物件

  • 持久帶(Permanent Space)實現方法區,主要存放所有已載入的類資訊,方法資訊,常量池等等。可通過-XX:PermSize和-XX:MaxPermSize來指定持久帶初始化值和最大值。Permanent Space並不等同於方法區,只不過是Hotspot JVM用Permanent Space來實現方法區而已,有些虛擬機器沒有Permanent Space而用其他機制來實現方法區。

技術分享

  -Xmx:最大堆記憶體,如:-Xmx512m

  -Xms:初始時堆記憶體,如:-Xms256m

  -XX:MaxNewSize:最大年輕區記憶體

  -XX:NewSize:初始時年輕區記憶體.通常為 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 個 Survivor 空間。實際可用空間為 = Eden + 1 個 Survivor,即 90%

  -XX:MaxPermSize:最大持久帶記憶體

  -XX:PermSize:初始時持久帶記憶體

  -XX:+PrintGCDetails。列印 GC 資訊

   -XX:NewRatio 新生代與老年代的比例,如 –XX:NewRatio=2,則新生代佔整個堆空間的1/3,老年代佔2/3

   -XX:SurvivorRatio 新生代中 Eden 與 Survivor 的比值。預設值為 8。即 Eden 佔新生代空間的 8/10,另外兩個 Survivor 各佔 1/10

  堆是JVM所管理的記憶體中國最大的一塊,是被所有Java執行緒鎖共享的,不是執行緒安全的,在JVM啟動時建立。

  (2)棧

  每個執行緒執行每個方法的時候都會在棧中申請一個棧幀,每個棧幀包括區域性變數區和運算元棧,用於存放此次方法呼叫過程中的臨時變數、引數和中間結果。

    -xss:設定每個執行緒的堆疊大小. JDK1.5+ 每個執行緒堆疊大小為 1M,一般來說如果棧不是很深的話, 1M 是絕對夠用了的。

  由於Java棧是與執行緒對應起來的,Java棧資料不是執行緒共有的,所以不需要關心其資料一致性,也不會存在同步鎖的問題。

  (3)本地方法棧

  用於支援native方法的執行,儲存了每個native方法呼叫的狀態

  (4)方法區

  存放了要載入的類資訊、靜態變數、final型別的常量、屬性和方法資訊。JVM用持久代(Permanet Generation)來存放方法區,可通過-XX:PermSize和-XX:MaxPermSize來指定最小值和最大值,方法區是被Java執行緒鎖共享的

  (5)程式計數器

嚴格來說是一個數據結構,用於儲存當前正在執行的程式的記憶體地址,由於Java是支援多執行緒執行的,所以程式執行的軌跡不可能一直都是線性執行。當有多個執行緒交叉執行時,被中斷的執行緒的程式當前執行到哪條記憶體地址必然要儲存下來,以便用於被中斷的執行緒恢復執行時再按照被中斷時的指令地址繼續執行下去。為了執行緒切換後能恢復到正確的執行位置,每個執行緒都需要有一個獨立的程式計數器,各個執行緒之間計數器互不影響,獨立儲存,我們稱這類記憶體區域為“執行緒私有”的記憶體,這在某種程度上有點類似於“ThreadLocal”,是執行緒安全的。

3、垃圾回收

  

引用計數(Reference Counting):

比較古老的回收演算法。原理是此物件有一個引用,即增加一個計數,刪除一個引用則減少一個計數。垃圾回收時,只用收集計數為0的物件。此演算法最致命的是無法處理迴圈引用的問題。

 

標記-清除(Mark-Sweep):

技術分享

    此演算法執行分兩階段。第一階段從引用根節點開始標記所有被引用的物件,第二階段遍歷整個堆,把未標記的物件清除。此演算法需要暫停整個應用,同時,會產生記憶體碎片。

 

複製(Copying):

技術分享

    此演算法把記憶體空間劃為兩個相等的區域,每次只使用其中一個區域。垃圾回收時,遍歷當前使用區域,把正在使用中的物件複製到另外一個區域中。演算法每次只處理正在使用中的物件,因此複製成本比較小,同時複製過去以後還能進行相應的記憶體整理,不會出現“碎片”問題。當然,此演算法的缺點也是很明顯的,就是需要兩倍記憶體空間。

 

標記-整理(Mark-Compact):

技術分享

 

    此演算法結合了“標記-清除”和“複製”兩個演算法的優點。也是分兩階段,第一階段從根節點開始標記所有被引用物件,第二階段遍歷整個堆,把清除未標記物件並且把存活物件“壓縮”到堆的其中一塊,按順序排放。此演算法避免了“標記-清除”的碎片問題,同時也避免了“複製”演算法的空間問題。

  現代商用虛擬機器基本都採用分代收集演算法來進行垃圾回收。這種演算法沒什麼特別的,無非是上面內容的結合罷了,根據物件的生命週期的不同將記憶體劃分為幾塊,然後根據各塊的特點採用最適當的收集演算法。大批物件死去、少量物件存活的(新生代),使用複製演算法,複製成本低;物件存活率高、沒有額外空間進行分配擔保的(老年代),採用標記-清理演算法或者標記-整理演算法。

垃圾回收機制詳解:https://www.cnblogs.com/xiaoxi/p/6486852.html

4、類載入機制

  

JVM類載入機制分為五個部分:載入,驗證,準備,解析,初始化,下面我們就分別來看一下這五個過程。

 

 

載入

 

載入是類載入過程中的一個階段,這個階段會在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的入口。注意這裡不一定非得要從一個Class檔案獲取,這裡既可以從ZIP包中讀取(比如從jar包和war包中讀取),也可以在執行時計算生成(動態代理),也可以由其它檔案生成(比如將JSP檔案轉換成對應的Class類)。

 

驗證

 

這一階段的主要目的是為了確保Class檔案的位元組流中包含的資訊是否符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。

 

準備

 

準備階段是正式為類變數分配記憶體並設定類變數的初始值階段,即在方法區中分配這些變數所使用的記憶體空間。注意這裡所說的初始值概念,比如一個類變數定義為:

 

1 public static int v = 8080 ;

 

實際上變數v在準備階段過後的初始值為0而不是8080,將v賦值為8080的putstatic指令是程式被編譯後,存放於類構造器<client>方法之中,這裡我們後面會解釋。
但是注意如果宣告為:

 

1 public static final int v = 8080 ;

 

在編譯階段會為v生成ConstantValue屬性,在準備階段虛擬機器會根據ConstantValue屬性將v賦值為8080。

 

解析

 

解析階段是指虛擬機器將常量池中的符號引用替換為直接引用的過程。符號引用就是class檔案中的:

 

  • CONSTANT_Class_info
  • CONSTANT_Field_info
  • CONSTANT_Method_info

 

等型別的常量。

 

下面我們解釋一下符號引用和直接引用的概念:

 

  • 符號引用與虛擬機器實現的佈局無關,引用的目標並不一定要已經載入到記憶體中。各種虛擬機器實現的記憶體佈局可以各不相同,但是它們能接受的符號引用必須是一致的,因為符號引用的字面量形式明確定義在Java虛擬機器規範的Class檔案格式中。
  • 直接引用可以是指向目標的指標,相對偏移量或是一個能間接定位到目標的控制代碼。如果有了直接引用,那引用的目標必定已經在記憶體中存在。

 

初始化

 

初始化階段是類載入最後一個階段,前面的類載入階段之後,除了在載入階段可以自定義類載入器以外,其它操作都由JVM主導。到了初始階段,才開始真正執行類中定義的Java程式程式碼。

 

初始化階段是執行類構造器<client>方法的過程。<client>方法是由編譯器自動收集類中的類變數的賦值操作和靜態語句塊中的語句合併而成的。虛擬機器會保證<client>方法執行之前,父類的<client>方法已經執行完畢。p.s: 如果一個類中沒有對靜態變數賦值也沒有靜態語句塊,那麼編譯器可以不為這個類生成<client>()方法。

 

注意以下幾種情況不會執行類初始化:

 

  • 通過子類引用父類的靜態欄位,只會觸發父類的初始化,而不會觸發子類的初始化。
  • 定義物件陣列,不會觸發該類的初始化。
  • 常量在編譯期間會存入呼叫類的常量池中,本質上並沒有直接引用定義常量的類,不會觸發定義常量所在的類。
  • 通過類名獲取Class物件,不會觸發類的初始化。
  • 通過Class.forName載入指定類時,如果指定引數initialize為false時,也不會觸發類初始化,其實這個引數是告訴虛擬機器,是否要對類進行初始化。
  • 通過ClassLoader預設的loadClass方法,也不會觸發初始化動作。

 

類載入器

 

虛擬機器設計團隊把載入動作放到JVM外部實現,以便讓應用程式決定如何獲取所需的類,JVM提供了3種類載入器:

 

  • 啟動類載入器(Bootstrap ClassLoader):負責載入 JAVA_HOME\lib 目錄中的,或通過-Xbootclasspath引數指定路徑中的,且被虛擬機器認可(按檔名識別,如rt.jar)的類。
  • 擴充套件類載入器(Extension ClassLoader):負責載入 JAVA_HOME\lib\ext 目錄中的,或通過java.ext.dirs系統變數指定路徑中的類庫。
  • 應用程式類載入器(Application ClassLoader):負責載入使用者路徑(classpath)上的類庫。

 

JVM通過雙親委派模型進行類的載入,當然我們也可以通過繼承java.lang.ClassLoader實現自定義的類載入器。

 

 

當一個類載入器收到類載入任務,會先交給其父類載入器去完成,因此最終載入任務都會傳遞到頂層的啟動類載入器,只有當父類載入器無法完成載入任務時,才會嘗試執行載入任務。

 

採用雙親委派的一個好處是比如載入位於rt.jar包中的類java.lang.Object,不管是哪個載入器載入這個類,最終都是委託給頂層的啟動類載入器進行載入,這樣就保證了使用不同的類載入器最終得到的都是同樣一個Object物件。

 

三、併發程式設計

 

原子性:一個操作或多個操作要麼全部執行完成且執行過程不被中斷,要麼就不執行。

 

可見性:當多個執行緒同時訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。

 

有序性:程式執行的順序按照程式碼的先後順序執行。

  Java記憶體模型只保證了基本讀取和賦值是原子性操作,如果要實現更大範圍操作的原子性,可以通過synchronized和Lock來實現。由於synchronized和Lock能夠保證任一時刻只有一個執行緒執行該程式碼塊,那麼自然就不存在原子性問題了,從而保證了原子性。

  對於可見性,Java提供了volatile關鍵字來保證可見性。當一個共享變數被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他執行緒需要讀取時,它會去記憶體中讀取新值。而普通的共享變數不能保證可見性,因為普通共享變數被修改之後,什麼時候被寫入主存是不確定的,當其他執行緒去讀取時,此時記憶體中可能還是原來的舊值,因此無法保證可見性。另外,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個執行緒獲取鎖然後執行同步程式碼,並且在釋放鎖之前會將對變數的修改重新整理到主存當中。因此可以保證可見性。

  在Java裡面,可以通過volatile關鍵字來保證一定的“有序性”。另外可以通過synchronized和Lock來保證有序性,很顯然,synchronized和Lock保證每個時刻是有一個執行緒執行同步程式碼,相當於是讓執行緒順序執行同步程式碼,自然就保證了有序性。另外,Java記憶體模型具備一些先天的“有序性”,即不需要通過任何手段就能夠得到保證的有序性,這個通常也稱為 happens-before 原則。如果兩個操作的執行次序無法從happens-before原則推匯出來,那麼它們就不能保證它們的有序性,虛擬機器可以隨意地對它們進行重排序。

  volatile保證可見性、不能確保原子性、保證有序性。