Java知識點彙總

Java基礎

Java 中堆和棧有什麼區別

堆與棧的區別很明顯:java

  1. 棧內存存儲的是局部變量而堆內存存儲的是實體;
  2. 棧內存的更新速度要快於堆內存,由於局部變量的生命週期很短;
  3. 棧內存存放的變量生命週期一旦結束就會被釋放,而堆內存存放的實體會被垃圾回收機制不定時的回收。

堆、棧說明:git

棧內存:棧內存首先是一片內存區域,存儲的都是局部變量,凡是定義在方法中的都是局部變量(方法外的是全局變量),for循環內部定義的也是局部變量,是先加載函數才能進行局部變量的定義,因此方法先進棧,而後再定義變量,變量有本身的做用域,一旦離開做用域,變量就會被釋放。棧內存的更新速度很快,由於局部變量的生命週期都很短。程序員

堆內存:存儲的是數組和對象(其實數組就是對象),凡是new創建的都是在堆中,堆中存放的都是實體(對象),實體用於封裝數據,並且是封裝多個(實體的多個屬性),若是一個數據消失,這個實體也沒有消失,還能夠用,因此堆是不會隨時釋放的,可是棧不同,棧裏存放的都是單個變量,變量被釋放了,那就沒有了。堆裏的實體雖然不會被釋放,可是會被當成垃圾,Java有垃圾回收機制不定時的收取。github

JAVA的四種引用,及應用場景

Java的對象引用,在jdk1.2以前只有強引用,在這以後加入了其餘引用,即強弱軟虛。 強引用(Strong Reference):最經常使用的引用類型,如Object obj=new Object()。只要強引用存在GC就不會這個對象,因此這也是引發OOM的緣由。算法

軟引用(Soft Reference):用於描述還有用但非必須的對象,當堆將發生OOM時則會回收軟引用所指向的內存空間,若回收後依然空間不足纔會拋出OOM。通常用於實現內存敏感的高速緩存。當真正對象被標記finalizable以及的finalize()方法調用以後而且內存已經清理, 那麼若是SoftReference object還存在就被加入到它的 ReferenceQueue。只有前面幾步完成後,軟引用和弱引用的get方法纔會返回null。編程

弱引用(Weak Reference):發生GC時一定回收弱引用指向的內存空間。 和軟引用加入隊列的時機相同。數組

虛引用(Phantom Reference) 又稱爲幽靈引用或幻影引用,虛引用既不會影響對象的生命週期,也沒法經過虛引用來獲取對象實例,僅用於在發生GC時接收一個系統通知。當一個對象的finalize方法已經被調用了以後,這個對象的虛引用會被加入到隊列中。經過檢查該隊列裏面的內容就知道一個對象是否是已經準備要被回收了。虛引用和軟引用和弱引用都不一樣,它會在內存沒有清理的時候被加入引用隊列.虛引用的創建必需要傳入引用隊列,其餘能夠沒有。緩存

BelieveFrank安全

GC在收集一個對象的時候會判斷是否有引用指向對象,在JAVA中的引用主要有四種:bash

強引用(Strong Reference)

強引用是使用最廣泛的引用。若是一個對象具備強引用,那垃圾回收器毫不會回收它。當內存空間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具備強引用的對象來解決內存不足的問題。

軟引用(Soft Reference)

若是一個對象只具備軟引用,則內存空間足夠,垃圾回收器就不會回收它;若是內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就能夠被程序使用。軟引用可用來實現內存敏感的高速緩存。

下面舉個例子,假若有一個應用須要讀取大量的本地圖片,若是每次讀取圖片都從硬盤讀取,則會嚴重影響性能,可是若是所有加載到內存當中,又有可能形成內存溢出,此時使用軟引用能夠解決這個問題。

設計思路是:用一個HashMap來保存圖片的路徑和相應圖片對象關聯的軟引用之間的映射關係,在內存不足時,JVM會自動回收這些緩存圖片對象所佔用的空間,從而有效地避免了內存溢出的問題。

軟引用能夠和一個引用隊列(ReferenceQueue)聯合使用,若是軟引用所引用的對象被垃圾回收器回收,Java虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。

弱引用(Weak Reference)

弱引用與軟引用的區別在於:只具備弱引用的對象擁有更短暫的生命週期。在垃圾回收器線程掃描它所管轄的內存區域的過程當中,一旦發現了只具備弱引用的對象,無論當前內存空間足夠與否,都會回收它的內存。不過,因爲垃圾回收器是一個優先級很低的線程,所以不必定會很快發現那些只具備弱引用的對象。

弱引用能夠和一個引用隊列(ReferenceQueue)聯合使用,若是弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。

虛引用(Phantom Reference)

「虛引用」顧名思義,就是形同虛設,與其餘幾種引用都不一樣,虛引用並不會決定對象的生命週期。若是一個對象僅持有虛引用,那麼它就和沒有任何引用同樣,在任什麼時候候均可能被垃圾回收器回收。

虛引用主要用於檢測對象是否已經從內存中刪除,跟蹤對象被垃圾回收器回收的活動。虛引用與軟引用和弱引用的一個區別在於:虛引用必須和引用隊列 (ReferenceQueue)聯合使用。當垃圾回收器準備回收一個對象時,若是發現它還有虛引用,就會在回收對象的內存以前,把這個虛引用加入到與之關聯的引用隊列中。

虛引用的惟一目的是當對象被回收時收到一個系統通知。

什麼是線程安全?保障線程安全有哪些手段?

線程安全就是當多個線程訪問一個對象時,若是不用考慮這些線程在運行時環境下的調度和交替執行,也不須要進行額外的同步,或者在調用方進行任何其餘的協調操做,調用這個對象的行爲均可以得到正確的結果,那這個對象是線程安全的。保證線程安全可從多線程三特性出發:

  1. 原子性(Atomicity):單個或多個操做是要麼所有執行,要麼都不執行
    • Lock:保證同時只有一個線程能拿到鎖,並執行申請鎖和釋放鎖的代碼
    • synchronized:對線程加獨佔鎖,被它修飾的類/方法/變量只容許一個線程訪問
  2. 可見性(Visibility):當一個線程修改了共享變量的值,其餘線程可以當即得知這個修改
    • volatile:保證新值能當即同步到主內存,且每次使用前當即從主內存刷新;
    • synchronized:在釋放鎖以前會將工做內存新值更新到主存中
  3. 有序性(Ordering):程序代碼按照指令順序執行
    • volatile: 自己就包含了禁止指令重排序的語義
    • synchronized:保證一個變量在同一個時刻只容許一條線程對其進行lock操做,使得持有同一個鎖的兩個同步塊只能串行地進入

簡述一下類加載過程

加載

加載,是指Java虛擬機查找字節流(查找.class文件),而且根據字節流建立java.lang.Class對象的過程。這個過程,將類的.class文件中的二進制數據讀入內存,放在運行時區域的方法區內。而後在堆中建立java.lang.Class對象,用來封裝類在方法區的數據結構。

類加載階段: (1)Java虛擬機將.class文件讀入內存,併爲之建立一個Class對象。

(2)任何類被使用時系統都會爲其建立一個且僅有一個Class對象。

(3)這個Class對象描述了這個類建立出來的對象的全部信息,好比有哪些構造方法,都有哪些成員方法,都有哪些成員變量等。

連接

連接包括驗證、準備以及解析三個階段。 (1)驗證階段。主要的目的是確保被加載的類(.class文件的字節流)知足Java虛擬機規範,不會形成安全錯誤。

(2)準備階段。負責爲類的靜態成員分配內存,並設置默認初始值。

(3)解析階段。將類的二進制數據中的符號引用替換爲直接引用。

說明: 符號引用。即一個字符串,可是這個字符串給出了一些可以惟一性識別一個方法,一個變量,一個類的相關信息。

直接引用。能夠理解爲一個內存地址,或者一個偏移量。好比類方法,類變量的直接引用是指向方法區的指針;而實例方法,實例變量的直接引用則是從實例的頭指針開始算起到這個實例變量位置的偏移量。

舉個例子來講,如今調用方法hello(),這個方法的地址是0xaabbccdd,那麼hello就是符號引用,0xaabbccdd就是直接引用。 在解析階段,虛擬機會把全部的類名,方法名,字段名這些符號引用替換爲具體的內存地址或偏移量,也就是直接引用。

初始化

初始化,則是爲標記爲常量值的字段賦值的過程。換句話說,只對static修飾的變量或語句塊進行初始化。

若是初始化一個類的時候,其父類還沒有初始化,則優先初始化其父類。 若是同時包含多個靜態變量和靜態代碼塊,則按照自上而下的順序依次執行。

談談你對Java中Hash碼的理解

在Java中,哈希碼錶明瞭對象的一種特徵,例如咱們判斷某兩個字符串是否==,若是其哈希碼相等,則這兩個字符串是相等的,其次,哈希碼是一種數據結構的算法,常見的哈希碼的算法有:

Object類的HashCode,返回對象的內存地址通過處理後的結構,因爲每一個對象的內存地址都不同,因此哈希碼也不同。

String類的HashCode,根據String類包含的字符串的內容,根據一種特殊的算法返回哈希碼,只要字符串的內容相同,返回的哈希碼也相同。

Integer類:返回的哈希碼就是integer對象裏所包含的那個整數的數值。 例如:Integer i1=new Integer(100) i1.hashCode的值就是100,因而可知兩個同樣大小的Integer對象返回的哈希碼也同樣。

break與continue的區別

break語句 (強行結束循環)做用:

  1. 能夠用來從循環體內跳出循環體,即提早結束循環,接着執行循環下面的語句。
  2. 使流程跳出switch結構

注意:break語句不能用於循環語句和switch語句以外的任何其餘語句中

continue語句做用:

  • 結束本次循環,即忽略循環體中continue語句下面還沒有執行的語句,接着進行下一次是否執行循環的斷定。

注意:continue語句不能用於循環語句以外的任何其餘語句中

continue語句和break語句的區別:

  • continue語句只結束本次循環,而不是終止整個循環的執行。
  • break語句則是結束整個循環過程,再也不判斷執行循環的條件是否成立。break語句能夠用在循環語句和switch語句中。在循環語句中用來結束內部循環;在switch語句中用來跳出switch語句。

注意:循環嵌套時,break和continue隻影響包含它們的最內層循環,與外層循環無關。

final, finally, finalize的區別

final 用於聲明屬性,方法和類,分別表示屬性不可變,方法不可覆蓋,類不可繼承。 內部類要訪問局部變量,局部變量必須定義成final類型.

finally是異常處理語句結構的一部分,表示老是執行。

finalize是Object類的一個方法,在垃圾收集器執行的時候會調用被回收對象的此方法,能夠覆蓋此方法提供垃圾收集時的其餘資源回收,例如關閉文件等。 JVM不保證此方法總被調用

守護線程與阻塞線程的四種狀況

守護線程 Java中有兩類線程:User Thread(用戶線程)、Daemon Thread(守護線程)

用戶線程即運行在前臺的線程,而守護線程是運行在後臺的線程。 守護線程做用是爲其餘前臺線程的運行提供便利服務,並且僅在普通、非守護線程仍然運行時才須要,好比垃圾回收線程就是一個守護線程。當VM檢測僅剩一個守護線程,而用戶線程都已經退出運行時,VM就會退出,由於沒有若是沒有了被守護這,也就沒有繼續運行程序的必要了。若是有非守護線程仍然存活,VM就不會退出。

守護線程並不是只有虛擬機內部提供,用戶在編寫程序時也能夠本身設置守護線程。用戶能夠用Thread的setDaemon(true)方法設置當前線程爲守護線程。

雖然守護線程可能很是有用,但必須當心確保其餘全部非守護線程消亡時,不會因爲它的終止而產生任何危害。由於你不可能知道在全部的用戶線程退出運行前,守護線程是否已經完成了預期的服務任務。一旦全部的用戶線程退出了,虛擬機也就退出運行了。 所以,不要在守護線程中執行業務邏輯操做(好比對數據的讀寫等)。

另外有幾點須要注意:

一、setDaemon(true)必須在調用線程的start()方法以前設置,不然會跑出IllegalThreadStateException異常。

二、在守護線程中產生的新線程也是守護線程。 三、 不要認爲全部的應用均可以分配給守護線程來進行服務,好比讀寫操做或者計算邏輯。

線程阻塞 線程能夠阻塞於四種狀態:

一、當線程執行Thread.sleep()時,它一直阻塞到指定的毫秒時間以後,或者阻塞被另外一個線程打斷;

二、當線程碰到一條wait()語句時,它會一直阻塞到接到通知(notify())、被中斷或通過了指定毫秒時間爲止(若制定了超時值的話)

三、線程阻塞與不一樣I/O的方式有多種。常見的一種方式是InputStream的read()方法,該方法一直阻塞到從流中讀取一個字節的數據爲止,它能夠無限阻塞,所以不能指定超時時間;

四、線程也能夠阻塞等待獲取某個對象鎖的排他性訪問權限(即等待得到synchronized語句必須的鎖時阻塞)。

注意,並不是全部的阻塞狀態都是可中斷的,以上阻塞狀態的前兩種能夠被中斷,後兩種不會對中斷作出反應

談談你對重入鎖的理解

重入鎖

(1)重進入: 1.定義:重進入是指任意線程在獲取到鎖以後,再次獲取該鎖而不會被該鎖所阻塞。關聯一個線程持有者+計數器,重入意味着鎖操做的顆粒度爲「線程」。

2.須要解決兩個問題: 線程再次獲取鎖:鎖須要識別獲取鎖的現場是否爲當前佔據鎖的線程,若是是,則再次成功獲取; 鎖的最終釋放:線程重複n次獲取鎖,隨後在第n次釋放該鎖後,其餘線程可以獲取該鎖。要求對鎖對於獲取進行次數的自增,計數器對當前鎖被重複獲取的次數進行統計,當鎖被釋放的時候,計數器自減,當計數器值爲0時,表示鎖成功釋放。

3.重入鎖實現重入性:每一個鎖關聯一個線程持有者和計數器,當計數器爲0時表示該鎖沒有被任何線程持有,那麼任何線程均可能得到該鎖而調用相應的方法;當某一線程請求成功後,JVM會記下鎖的持有線程,而且將計數器置爲1;此時其它線程請求該鎖,則必須等待;而該持有鎖的線程若是再次請求這個鎖,就能夠再次拿到這個鎖,同時計數器會遞增;當線程退出同步代碼塊時,計數器會遞減,若是計數器爲0,則釋放該鎖

(2)ReentrantLock是的非公平類中經過組合自定義同步器來實現鎖的獲取與釋放。

1 /** 2 * Sync中的nonfairTryAcquire()方法實現 3 * 這個跟公平類中的實現主要區別在於不會判斷當前線程是不是等待時間最長的線程 4 **/ 
 5 final boolean nonfairTryAcquire(int acquires) {
 6     final Thread current = Thread.currentThread();
 7     int c = getState();
 8     if (c == 0) {
 9         // 跟FairSync中的主要區別,不會判斷hasQueuedPredecessors()
10         if (compareAndSetState(0, acquires)) {
11             setExclusiveOwnerThread(current);
12             return true;
13         }
14     }
15     else if (current == getExclusiveOwnerThread()) {
16         int nextc = c + acquires;
17         if (nextc < 0) // overflow
18             throw new Error("Maximum lock count exceeded");
19         setState(nextc);
20         return true;
21     }
22     return false;
23 }
複製代碼

nonfairTryAcquire()方法中,增長了再次獲取同步狀態的處理邏輯,經過判斷當前線程是否爲獲取鎖的線程來決定獲取操做是否成功,若是是獲取鎖的線程再次請求,則將同步狀態值進行增長並返回true,表示獲取同步狀態成功。 成功獲取鎖的現場再次獲取鎖,只是增長了同步狀態值,要求ReentrantLock在釋放同步狀態時減小同步狀態值。

1 /** 2 * Sync中tryRelease() 3 **/
 4 protected final boolean tryRelease(int releases) {
 5     // 修改當前鎖的狀態
 6     // 若是一個線程遞歸獲取了該鎖(也就是state != 1), 那麼c可能不等0
 7     // 若是沒有線程遞歸獲取該鎖,則c == 0
 8     int c = getState() - releases;
 9 
10 // 若是鎖的佔有線程不等於當前正在執行釋放操做的線程,則拋出異常
11     if (Thread.currentThread() != getExclusiveOwnerThread())
12         throw new IllegalMonitorStateException();
13     boolean free = false;
14     // c == 0,表示當前線程釋放鎖成功,同時表示遞歸獲取了該鎖的線程已經執行完畢
15     // 則設置當前鎖狀態爲free,同時設置鎖的當前線程爲null,可讓其餘線程來獲取
16     // 同時也說明,若是c != 0,則表示線程遞歸佔用了鎖資源,
17     // 因此鎖的當前佔用線程依然是當前釋放鎖的線程(實際沒有釋放)
18     if (c == 0) {
19         free = true;
20         setExclusiveOwnerThread(null);
21     }
22     // 從新設置鎖的佔有數
23     setState(c);
24     return free;
25 }
複製代碼

若是該鎖被獲取n次,則前(n-1)次tryRelease(int releases)方法必須返回false,而只有同步狀態徹底釋放了,才返回true,該方法將同步狀態是否爲0做爲最終釋放的條件,當同步狀態爲0時,將佔有線程設置爲null,並返回true,表示釋放成功。 對於公平鎖而言

1 /** 2 * FairSync中tryAcquire()的實現 3 * 返回 4 * true: 獲取鎖成功 5 * false: 獲取鎖不成功 6 **/
 7 protected final boolean tryAcquire(int acquires) {
 8     // 獲取當前線程
 9     final Thread current = Thread.currentThread();
10     // 獲取鎖資源的狀態
11     // 0: 說明當前鎖可當即獲取,在此種狀態下(又是公平鎖)
12     // >0而且當前線程與持有鎖資源的線程是同一個線程則state + 1並返回true
13     // >0而且佔有鎖資源的不是當前線程,則返回false表示獲取不成功
14     int c = getState();
15     if (c == 0) {
16         // 在鎖能夠當即獲取的狀況下
17         // 首先判斷線程是不是剛剛釋放鎖資源的頭節點的下一個節點(線程的等待前後順序)
18         // 若是是等待時間最長的纔會立刻獲取到鎖資源,不然不會(這也是公平與不公平的主要區別所在)
19         if (!hasQueuedPredecessors() &&
20             compareAndSetState(0, acquires)) {
21             setExclusiveOwnerThread(current);
22             return true;
23         }
24     }
25     else if (current == getExclusiveOwnerThread()) {  //線程能夠遞歸獲取鎖
26         int nextc = c + acquires;
27         // 超過int上限值拋出錯誤
28         if (nextc < 0)
29             throw new Error("Maximum lock count exceeded");
30         setState(nextc);
31         return true;
32     }
33     return false;
34 }
複製代碼

與非公平惟一的區別是判斷條件中多了hasQueuedPredecessors()方法,即加入了同步隊列中當前節點是否有前驅節點的判斷,若是該方法返回了true,則表示有線程比當前線程更早地請求獲取鎖,因此須要等待前驅線程獲取並釋放鎖後才能繼續獲取該鎖。 可是非公平鎖是默認實現:非公平性鎖可能使線程「飢餓」,可是極少的線程切換,能夠保證其更大的吞吐量。而公平性鎖,保證了鎖的獲取按照FIFO原則,代價是進行大量的線程切換。

(3)synchronized可重入性 同一線程在調用本身類中其餘synchronized方法/塊或調用父類的synchronized方法/塊都不會阻礙該線程的執行,就是說同一線程對同一個對象鎖是可重入的,並且同一個線程能夠獲取同一把鎖屢次,也就是能夠屢次重入

談談數組與鏈表的區別

  1. 數組是將元素在內存中連續存放,因爲每一個元素佔用內存相同,能夠經過下標迅速訪問數組中任何元素。可是若是要在數組中增長一個元素,須要移動大量元素,在內存中空出一個元素的空間,而後將要增長的元素放在其中。一樣的道理,若是想刪除一個元素,一樣須要移動大量元素去填掉被移動的元素。若是應用須要快速訪問數據,不多或不插入和刪除元素,就應該用數組。
  2. 鏈表剛好相反,鏈表中的元素在內存中不是順序存儲的,而是經過存在元素中的指針聯繫到一塊兒。好比:上一個元素有個指針指到下一個元素,以此類推,直到最後一個元素。若是要訪問鏈表中一個元素,須要從第一個元素開始,一直找到須要的元素位置。可是增長和刪除一個元素對於鏈表數據結構就很是簡單了,只要修改元素中的指針就能夠了。若是應用須要常常插入和刪除元素你就須要用鏈表數據結構了。

From Taonce

  1. 數組是順序的存儲結構,鏈表是鏈式的存儲結構。
  2. 數組的查找效率高,時間複雜度爲O(1),插入和刪除效率低,時間複雜度爲O(n);鏈表插入和刪除效率高,時間複雜度爲O(1),查找效率低,時間複雜度爲O(n)。
  3. 數組在內存中是一塊連續的區域;而鏈表是分散的,它是經過指針指向下一個元素。
  4. 數組的空間大小是固定的,不能動態擴展;鏈表空間大小不固定,可動態擴展。

談談 static 關鍵字的用法

static 修飾的方法/變量等資源是靜態資源 在內存中存放在方法區,全部容許訪問的對象均可以訪問(參考變量的修飾符 public protected等)

static修飾的變量只存在一份,全部能夠訪問的對象都容許進行修改

static 修飾的變量/方法在內存中被root引用,所以不會被GC回收,

static修飾的變量在類被加載的時候就會被加載

被static修飾的方法/代碼塊只能引用被static修飾的方法/變量

static的主要用法

  1. 用來修飾變量 能夠不須要實例化對象就能夠直接引用變量,引用方法ClassName.field;
  2. 修飾方法 能夠不須要實例化對象就能夠直接引用方法,引用方法 ClassName.method();
  3. 靜態塊 用來實現須要在類加載時就須要加載的邏輯

談談 ArrayList 和 LinkList 的區別

  1. ArrayList是實現了基於動態數組的數據結構,LinkedList基於鏈表的數據結構。

  2. 對於隨機訪問get和set,ArrayList以爲優於LinkedList,由於LinkedList要移動指針。

  3. 對於新增和刪除操做add和remove,LinedList比較佔優點,由於ArrayList要移動數據。

兩個都是多線程不安全。

From midNightHz

一、實現原理不同 ArrayList是基於數據的實現,而LinkedList是基於鏈表的實現,二者的區別就是這兩種數據結構的差異 二、初始化:ArrayList初始化時會初始化一個必定容量的數組,而linkedlist只是定義了鏈表的頭元素和尾元素 三、添加元素 在list尾端添加元素區別並非太大,但ArrayList是基於Array來實現的,會遇到一個很蛋疼大問題就是擴容問題 四、遍歷 :遍歷arraylist和linkedlist區別並非很大 五、查詢指定位置的元素 arraylist的查詢速度爲0(1),而linkedlist則雙端的元素查詢快,中間的元素查詢慢 六、刪除元素 數組刪除元素是一個很蛋疼的問題,特別是移除數組頭端的元素 ,須要必定當前下標元素後面的全部元素,而linkedlist刪除雙端的元素則很是快 刪除中間的元素會比較慢(要遍歷查到爲指定位置的元素)

From 吉文傑 jiwenjie

  1. 由於 Array 是基於索引 (index) 的數據結構,它使用索引在數組中搜索和讀取數據是很快的。Array 獲取數據的時間複雜度是 O(1), 可是要刪除數據倒是開銷很大的,由於這須要重排數組中的全部數據。
  2. 相對於 ArrayList , LinkedList 插入是更快的。由於LinkedList不像ArrayList 同樣,不須要改變數組的大小,也不須要在數組裝滿的時候要將全部的數據從新裝入一個新的數組,這是 ArrayList 最壞的一種狀況,時間複雜度是 O(n) ,而 LinkedList 中插入或刪除的時間複雜度僅爲 O(1) 。ArrayList 在插入數據時還須要更新索引(除了插入數組的尾部)。
  3. 相似於插入數據,刪除數據時,LinkedList 也優於 ArrayList 。
  4. LinkedList 須要更多的內存,由於 ArrayList 的每一個索引的位置是實際的數據,而 LinkedList 中的每一個節點中存儲的是實際的數據和先後節點的位置( 一個LinkedList實例存儲了兩個值Node first 和 Node last 分別表示鏈表的其實節點和尾節點,每一個 Node 實例存儲了三個值:E item,Node next,Node pre)。

什麼場景下更適宜使用LinkedList,而不用ArrayList

  1. 你的應用不會隨機訪問數據 。由於若是你須要LinkedList中的第n個元素的時候,你須要從第一個元素順序數到第n個數據,而後讀取數據。
  2. 你的應用更多的插入和刪除元素,更少的讀取數據。由於插入和刪除元素不涉及重排數據,因此它要比ArrayList要快。
  3. 以上就是關於ArrayList和LinkedList的差異。你須要一個不一樣步的基於索引的數據訪問時,請儘可能使用ArrayList。ArrayList很快,也很容易使用。可是要記得要給定一個合適的初始大小,儘量的減小更改數組的大小。

簡述HashMap工做原理

HashMap是基於hashing算法的原理,經過put(key,value)和get(key)方法儲存和獲取值的。

  1. 存:咱們將鍵值對K/V 傳遞給put()方法,它調用K對象的hashCode()方法來計算hashCode從而獲得bucket位置,以後儲存Entry對象。(HashMap是在bucket中儲存 鍵對象 和 值對象,做爲Map.Entry)
  2. 取:獲取對象時,咱們傳遞 鍵給get()方法,而後調用K的hashCode()方法從而獲得hashCode進而獲取到bucket位置,再調用K的equals()方法從而肯定鍵值對,返回值對象。
  3. 碰撞:當兩個對象的hashcode相同時,它們的bucket位置相同,‘碰撞’就會發生。如何解決,就是利用鏈表結構進行存儲,即HashMap使用LinkedList存儲對象。可是當鏈表長度大於8(默認)時,就會把鏈表轉換爲紅黑樹,在紅黑樹中執行插入獲取操做。
  4. 擴容:若是HashMap的大小超過了負載因子定義的容量,就會進行擴容。默認負載因子爲0.75。就是說,當一個map填滿了75%的bucket時候,將會建立原來HashMap大小的兩倍的bucket數組(jdk1.6,但不超過最大容量),來從新調整map的大小,並將原來的對象放入新的bucket數組中。這個過程叫做rehashing,由於它調用hash方法找到新的bucket位置。

HashMap

補充答案
From midNightHz

從前有個叫媽個蛋村,村裏有個地主,有錢又有顏,有能力能夠娶不少妻妾,可是怎麼樣才能快速找到想要的妻妾呢,因而這個地主想了一個辦法; 一、先建16個房子(標上0-15),爲何是16個呢,算命的說的 二、每娶到一個妻子(put),就根據妻子的生日,每一年的第幾天(hash值)算出要讓這個妻子住在哪一個房間,具體算法是這樣的 hash%16 等於0就住0號房; 三、問題來了,這個有錢富豪娶了兩個妻子,兩個生日不一樣,可是要住同一個房間怎麼辦(hash碰撞)這個有錢的地主想了一個辦法,讓這些住同一個房間的妻子根據生日排個大小(二叉樹) 四、過來一段時間之後,這位有錢的地主娶了12房姨太太,他想着房子快不夠住了,怎麼辦,又建了16個房子(hashmap擴容),而後從新安排他們的住所

From 吉文傑 jiwenjie

HashMap的工做原理

  1. 何時會使用HashMap?他有什麼特色? 是基於Map接口的實現,存儲鍵值對時,它能夠接收null的鍵值,是非同步的,HashMap存儲着Entry(hash, key, value, next)對象。
  2. 你知道HashMap的工做原理嗎? 經過hash的方法,經過put和get存儲和獲取對象。存儲對象時,咱們將K/V傳給put方法時,它調用hashCode計算hash從而獲得bucket位置,進一步存儲,HashMap會根據當前bucket的佔用狀況自動調整容量(超過Load Facotr則resize爲原來的2倍)。獲取對象時,咱們將K傳給get,它調用hashCode計算hash從而獲得bucket位置,並進一步調用equals()方法肯定鍵值對。若是發生碰撞的時候,Hashmap經過鏈表將產生碰撞衝突的元素組織起來,在Java 8中,若是一個bucket中碰撞衝突的元素超過某個限制(默認是8),則使用紅黑樹來替換鏈表,從而提升速度。
  3. 你知道get和put的原理嗎?equals()和hashCode()的都有什麼做用? 經過對key的hashCode()進行hashing,並計算下標( n-1 & hash),從而得到buckets的位置。若是產生碰撞,則利用key.equals()方法去鏈表或樹中去查找對應的節點
  4. 你知道hash的實現嗎?爲何要這樣實現? 在Java 1.8的實現中,是經過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質量來考慮的,這麼作能夠在bucket的n比較小的時候,也能保證考慮到高低bit都參與到hash的計算中,同時不會有太大的開銷。
  5. 若是HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦? 若是超過了負載因子(默認0.75),則會從新resize一個原來長度兩倍的HashMap,而且從新調用hash方法。

關於Java集合的小抄中是這樣描述的:

以Entry[]數組實現的哈希桶數組,用Key的哈希值取模桶數組的大小可獲得數組下標。

插入元素時,若是兩條Key落在同一個桶(好比哈希值1和17取模16後都屬於第一個哈希桶),咱們稱之爲哈希衝突。

JDK的作法是鏈表法,Entry用一個next屬性實現多個Entry以單向鏈表存放。查找哈希值爲17的key時,先定位到哈希桶,而後鏈表遍歷桶裏全部元素,逐個比較其Hash值而後key值。

在JDK8裏,新增默認爲8的閾值,當一個桶裏的Entry超過閥值,就不以單向鏈表而以紅黑樹來存放以加快Key的查找速度。

固然,最好仍是桶裏只有一個元素,不用去比較。因此默認當Entry數量達到桶數量的75%時,哈希衝突已比較嚴重,就會成倍擴容桶數組,並從新分配全部原來的Entry。擴容成本不低,因此也最好有個預估值。

取模用與操做(hash & (arrayLength-1))會比較快,因此數組的大小永遠是2的N次方, 你隨便給一個初始值好比17會轉爲32。默認第一次放入元素時的初始值是16。

iterator()時順着哈希桶數組來遍歷,因此看起來是個亂序

接口和抽象類有什麼區別

  1. 共同點

    是上層的抽象層。 都不能被實例化。 都能包含抽象的方法,這些抽象的方法用於描述類具有的功能,可是不比提供具體的實現。

  2. 區別

    在抽象類中能夠寫非抽象的方法,從而避免在子類中重複書寫他們,這樣能夠提升代碼的複用性,這是抽象類的優點,接口中只能有抽象的方法。 一個類只能繼承一個直接父類,這個父類能夠是具體的類也但是抽象類,可是一個類能夠實現多個接口。

From jiwenjie

  1. 默認的方法實現 抽象類能夠有默認的方法實現徹底是抽象的。接口根本不存在方法的實現。

    抽象類中能夠有已經實現了的方法,也能夠有被abstract修飾的方法(抽象方法),由於存在抽象方法,因此該類必須是抽象類。可是接口要求只能包含抽象方法,抽象方法是指沒有實現的方法。因此就不能像抽象類那麼無賴了,接口就根本不能存在方法的實現。實現 抽象類使用extends關鍵字來繼承抽象類。若是子類不是抽象類的話,它須要提供抽象類中全部聲明的方法的實現。子類使用關鍵字implements來實現接口。它須要提供接口中全部聲明的方法的實現。抽象類雖然不能實例化來使用,可是能夠被繼承,讓子類來具體實現父類的全部抽象方法。

  2. 接口的實現,經過implements關鍵字。實現該接口的類,必須把接口中的全部方法給實現。不能再推給下一代。

  3. 抽象類能夠有構造器,而接口不能有構造器

  4. 抽象方法能夠有public、protected和default這些修飾符

  5. 接口方法默認修飾符是public。你不可使用其它修飾符。

  6. 抽象類在java語言中所表示的是一種繼承關係,一個子類只能存在一個父類,可是能夠存在多個接口。

  7. 若是你往抽象類中添加新的方法,你能夠給它提供默認的實現。所以你不須要改變你如今的代碼。 若是你往接口中添加方法,那麼你必須改變實現該接口的類。

From midNightHz

一、抽象類是個類,而接口就是一個接口,一個是三個字,一個是兩個字 二、抽象類只能單繼承,而接口能夠多實現 三、抽象類能夠有成員變量(私有 共有and so on);接口只可能有常量,並且都public 四、抽象類能夠有全部的方法能夠用全部的修飾符來修飾 public private protected static ,能夠包含0-N個抽象方法;接口的方法都是public的,JDK1.8接口容許有一個static方法和多個default方法

From Moosphan

大致區別以下:

  • 抽象類能夠提供成員方法的實現細節,而接口中只能存在 public 抽象方法;
  • 抽象類中的成員變量能夠是各類類型的,而接口中的成員變量只能是 public static final 類型的;
  • 接口中不能含有構造器、靜態代碼塊以及靜態方法,而抽象類能夠有構造器、靜態代碼塊和靜態方法;
  • 一個類只能繼承一個抽象類,而一個類卻能夠實現多個接口;
  • 抽象類訪問速度比接口速度要快,由於接口須要時間去尋找在類中具體實現的方法;
  • 若是你往抽象類中添加新的方法,你能夠給它提供默認的實現。所以你不須要改變你如今的代碼。若是你往接口中添加方法,那麼你必須改變實現該接口的類;
  • 此外,Java 8 以後的接口能夠有默認方法實現,經過 default 關鍵字代表便可。

From safier

抽象類是用來捕捉子類的通用特性的 。它不能被實例化,只能被用做子類的超類。抽象類是被用來建立繼承層級裏子類的模板。以JDK中的GenericServlet爲例:

public abstract class GenericServlet implements Servlet, ServletConfig, Serializable {
    // abstract method
    abstract void service(ServletRequest req, ServletResponse res);
 
    void init() {
        // Its implementation
    }
    // other method related to Servlet
}
複製代碼

當HttpServlet類繼承GenericServlet時,它提供了service方法的實現:

public class HttpServlet extends GenericServlet {
    void service(ServletRequest req, ServletResponse res) {
        // implementation
    }
 
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        // Implementation
    }
 
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
        // Implementation
    }
 
    // some other methods related to HttpServlet
}
複製代碼

接口

接口是抽象方法的集合。若是一個類實現了某個接口,那麼它就繼承了這個接口的抽象方法。這就像契約模式,若是實現了這個接口,那麼就必須確保使用這些方法。接口只是一種形式,接口自身不能作任何事情。以Externalizable接口爲例:

public interface Externalizable extends Serializable {
 
    void writeExternal(ObjectOutput out) throws IOException;
 
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
複製代碼

當你實現這個接口時,你就須要實現上面的兩個方法:

public class Employee implements Externalizable {
 
    int employeeId;
    String employeeName;
 
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        employeeId = in.readInt();
        employeeName = (String) in.readObject();
 
    }
 
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
 
        out.writeInt(employeeId);
        out.writeObject(employeeName);
    }
}
複製代碼

抽象類和接口的對比

參數 抽象類 接口
默認的方法實現 它能夠有默認的方法實現 接口徹底是抽象的。它根本不存在方法的實現
實現 子類使用extends關鍵字來繼承抽象類。若是子類不是抽象類的話,它須要提供抽象類中全部聲明的方法的實現。 子類使用關鍵字implements來實現接口。它須要提供接口中全部聲明的方法的實現
構造器 抽象類能夠有構造器 接口不能有構造器
與正常Java類的區別 除了你不能實例化抽象類以外,它和普通Java類沒有任何區別 接口是徹底不一樣的類型
訪問修飾符 抽象方法能夠有public、protected和default這些修飾符 接口方法默認修飾符是public。你不可使用其它修飾符。
main方法 抽象方法能夠有main方法而且咱們能夠運行它 接口沒有main方法,所以咱們不能運行它
多繼承 抽象方法能夠繼承一個類和實現多個接口 接口只能夠繼承一個或多個其它接口
速度 它比接口速度要快 接口是稍微有點慢的,由於它須要時間去尋找在類中實現的方法。
添加新方法 若是你往抽象類中添加新的方法,你能夠給它提供默認的實現。所以你不須要改變你如今的代碼。 若是你往接口中添加方法,那麼你必須改變實現該接口的類。

何時使用抽象類和接口

  • 若是你擁有一些方法而且想讓它們中的一些有默認實現,那麼使用抽象類吧。
  • 若是你想實現多重繼承,那麼你必須使用接口。因爲Java不支持多繼承,子類不可以繼承多個類,但能夠實現多個接口。所以你就可使用接口來解決它。
  • 若是基本功能在不斷改變,那麼就須要使用抽象類。若是不斷改變基本功能而且使用接口,那麼就須要改變全部實現了該接口的類。

Java8中的默認方法和靜態方法

Oracle已經開始嘗試向接口中引入默認方法和靜態方法,以此來減小抽象類和接口之間的差別。如今,咱們能夠爲接口提供默認實現的方法了而且不用強制子類來實現它。

談談 Java 中多線程實現的幾種方式

Java中多線程實現的方式主要有三種:

  1. 繼承Thread類
  2. 實現Runnable接口
  3. 使用ExecutorService、Callable、Future實現有返回結果的多線程

其中前兩種方式線程執行完沒有返回值,只有最後一種是帶返回值的。

繼承Thread類實現多線程:

繼承Thread類本質上也是實現Tunnable接口的一個實例,他表明一個線程的實例,而且啓動線程的惟一方法是經過Thread類的start()方法,start()方法是一個native方法,他將啓動一個新線程,並執行run( )方法。

實現Runnable接口方式實現多線程:

實例化一個Thread對象,並傳入實現的Runnable接口,當傳入一個Runnable target參數給Thread後,Thraed的run()方法就會調用target.run( );

使用ExecutorService、Callable、Future實現有返回結果的多線程:

可返回值的任務必須實現Callable接口,相似的無返回值的任務必須實現Runnable接口,執行Callable任務後,能夠獲取一個Future的對象,在該對象上調用get就能夠獲取到Callable任務返回的Object了,在結合線程池接口ExecutorService就能夠實現有返回結果的多線程。

繼承 Thread 類自己

public class Test {
    public static void main(String[] args) {
        MyThread mt = new MyThread();
        mt.start();
    }
}

class MyThread extends Thread {
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
複製代碼

#實現Runnable接口 *用法須要在外層包裹一層 Thread *

public class Test {
    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();
        new Thread(mr).start();
    }
}

class MyRunnable implements Runnable {
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
複製代碼

#實現 Callable 接口 比較少見,不經常使用 須要實現的是 call() 方法

代碼拷過來的,確實沒用過

public class Test {
    public static void main(String[] args) throws Exception {
        ExecutorService es = Executors.newSingleThreadExecutor();

        // 自動在一個新的線程上啓動 MyCallable,執行 call 方法
        Future<Integer> f = es.submit(new MyCallable());

        // 當前 main 線程阻塞,直至 future 獲得值
        System.out.println(f.get());

        es.shutdown();
    }
}

class MyCallable implements Callable<Integer> {
    public Integer call() {
        System.out.println(Thread.currentThread().getName());

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return 123;
    }
}
複製代碼

String,StringBuilder,StringBuffer的區別

  1. 可變不可變 String:字符串常量,在修改時不會改變自身;若修改,等於從新生成新的字符串對象。 StringBuffer:在修改時會改變對象自身,每次操做都是對 StringBuffer 對象自己進行修改,不是生成新的對 象;使用場景:對字符串常常改變狀況下,主要方法:append(),insert()等。
  2. 線程是否安全 String:對象定義後不可變,線程安全。 StringBuffer:是線程安全的(對調用方法加入同步鎖),執行效率較慢,適用於多線程下操做字符串緩衝區 大量數據。 StringBuilder:是線程不安全的,適用於單線程下操做字符串緩衝區大量數據。
  3. 共同點 StringBuilder 與 StringBuffer 有公共父類 AbstractStringBuilder(抽象類)。 StringBuilder、StringBuffer 的方法都會調用 AbstractStringBuilder 中的公共方法,如 super.append(...)。 只是 StringBuffer 會在方法上加 synchronized 關鍵字,進行同步。最後,若是程序不是多線程的,那麼使用 StringBuilder 效率高於 StringBuffer。

HashMap和Hashtable的區別

參考答案
  1. HashMap是map接口的子類,是將鍵映射到值的對象,其中鍵和值都是對象,而且不能包含重複鍵,但能夠包含重複值。HashMap容許null key和null value,而hashtable不容許。

  2. HashMap是Hashtable的輕量級實現(非線程安全的實現),他們都完成了Map接口,因爲非線程安全,效率上可能高於Hashtable。

  3. HashMap把Hashtable的contains方法去掉了,改爲containsvalue和containsKey。

  4. Hashtable繼承自Dictionary類,而HashMap是Java1.2引進的Map interface的一個實現。

  5. Hashtable的方法是Synchronize的,而HashMap不是,在多個線程訪問Hashtable時,不須要本身爲它的方法實現同步,而HashMap 就必須爲之提供外同步。可是若是使用Java 5或以上的話,能夠用ConcurrentHashMap代替Hashtable。

  6. Hashtable和HashMap採用的hash/rehash算法都大概同樣,因此性能不會有很大的差。

談談你對java三大特性的理解

封裝

封裝最好理解了。封裝是面向對象的特徵之一,是對象和類概念的主要特性。

封裝,也就是把客觀事物封裝成抽象的類,而且類能夠把本身的數據和方法只讓可信的類或者對象操做,對不可信的進行信息隱藏。

繼承

面向對象編程 (OOP) 語言的一個主要功能就是「繼承」。繼承是指這樣一種能力:它可使用現有類的全部功能,並在無需從新編寫原來的類的狀況下對這些功能進行擴展。

經過繼承建立的新類稱爲「子類」或「派生類」。

被繼承的類稱爲「基類」、「父類」或「超類」。

繼承的過程,就是從通常到特殊的過程。

多態

多態性(polymorphisn)是容許你將父對象設置成爲和一個或更多的他的子對象相等的技術,賦值以後,父對象就能夠根據當前賦值給它的子對象的特性以不一樣的方式運做。簡單的說,就是一句話:容許將子類類型的指針賦值給父類類型的指針。

實現多態,有二種方式,覆蓋,重載。

覆蓋,是指子類從新定義父類的虛函數的作法。

重載,是指容許存在多個同名函數,而這些函數的參數表不一樣(或許參數個數不一樣,或許參數類型不一樣,或許二者都不一樣)。

JVM

談談JVM的內存結構和內存分配

Java內存模型 Java虛擬機將其管轄的內存大體分三個邏輯部分:方法區(Method Area)、Java棧和Java堆。

方法區是靜態分配的,編譯器將變量綁定在某個存儲位置上,並且這些綁定不會在運行時改變。

Java Stack是一個邏輯概念,特色是後進先出。一個棧的空間多是連續的,也多是不連續的。

Java堆分配(heap allocation)意味着以隨意的順序,在運行時進行存儲空間分配和收回的內存管理模型。

java內存分配 基礎數據類型直接在棧空間分配; 方法的形式參數,直接在棧空間分配,當方法調用完成後從棧空間回收; 引用數據類型,須要用new來建立,既在棧空間分配一個地址空間,又在堆空間分配對象的類變量;

方法的引用參數,在棧空間分配一個地址空間,並指向堆空間的對象區,當方法調用完後從棧空間回收; 局部變量 new 出來時,在棧空間和堆空間中分配空間,當局部變量生命週期結束後,棧空間馬上被回收,堆空間區域等待GC回收; 方法調用時傳入的實際參數,先在棧空間分配,在方法調用完成後從棧空間釋放; 字符串常量在 DATA 區域分配 ,this 在堆空間分配;

數組既在棧空間分配數組名稱, 又在堆空間分配數組實際的大小!

From BelieveFrank

內存結構
程序計數器

程序計數器(Program Counter Register)是一塊較小的內存空間,它的做用能夠看作是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型裏(僅是概念模型,各類虛擬機可能會經過一些更高效的方式去實現),字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。 因爲Java虛擬機的多線程是經過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器(對於多核處理器來講是一個內核)只會執行一條線程中的指令。所以,爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各條線程之間的計數器互不影響,獨立存儲,咱們稱這類內存區域爲「線程私有」的內存。 若是線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是正在執行的是Natvie方法,這個計數器值則爲空(Undefined)。 此內存區域是惟一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域。

Java虛擬機棧

虛擬機棧描述的是Java方法執行的內存模型,每一個方法在執行的同時都會建立一個棧幀(Stack Frame)用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機中入棧到出棧的過程。 在Java虛擬機規範中,對這個區域規定了兩種異常情況:若是線程請求的棧深度大於虛擬機所容許的深度,將拋出StackOverflowError異常;若是虛擬機棧能夠動態擴展(當前大部分的Java虛擬機均可動態擴展,只不過Java虛擬機規範中也容許固定長度的虛擬機棧),當擴展時沒法申請到足夠的內存時會拋出OutOfMemoryError異常。

本方法棧

本地方法棧(Native Method Stacks)與虛擬機棧所發揮的做用是很是類似的,其區別不過是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的Native方法服務。虛擬機規範中對本地方法棧中的方法使用的語言、使用方式與數據結構並無強制規定,所以具體的虛擬機能夠自由實現它。甚至有的虛擬機(譬如Sun HotSpot虛擬機)直接就把本地方法棧和虛擬機棧合二爲一。與虛擬機棧同樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。

Java堆

對於大多數應用來講,Java堆(Java Heap)是Java虛擬機所管理的內存中最大的一塊。Java堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存。(棧上分配、標量替換會致使對象不分配在堆內存中) Java堆是垃圾收集器管理的主要區域,所以不少時候也被稱作「GC堆」。若是從內存回收的角度看,因爲如今收集器基本都是採用的分代收集算法,因此Java堆中還能夠細分爲:新生代和老年代;再細緻一點的有Eden空間、From Survivor空間、To Survivor空間等。 根據Java虛擬機規範的規定,Java堆能夠處於物理上不連續的內存空間中,只要邏輯上是連續的便可,就像咱們的磁盤空間同樣。在實現時,既能夠實現成固定大小的,也能夠是可擴展的,不過當前主流的虛擬機都是按照可擴展來實現的(經過-Xmx和-Xms控制)。 若是在堆中沒有內存完成實例分配,而且堆也沒法再擴展時,將會拋出OutOfMemoryError異常。

方法區

方法區(Method Area)與Java堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。雖然Java虛擬機規範把方法區描述爲堆的一個邏輯部分,可是它卻有一個別名叫作Non-Heap(非堆),目的應該是與Java堆區分開來。 根據Java虛擬機規範的規定,當方法區沒法知足內存分配需求時,將拋出OutOfMemoryError異常。

內存分配策略
對象優先在Eden分配

大多數狀況下,對象在新生代Eden去中分配,(注:java堆中的新生代可分爲Eden區和兩個Survivor區),當Eden區中沒有足夠的空間進行分配時,虛擬機將發起一次Minor GC。

Minor GC 和 Full GC的區別

  • 新生代GC(Minor GC):指的是發生在新生代中的垃圾收集動做,java對象的建立和回收很是頻繁,因此Mnior GC很是頻繁,通常回收速度也比較快。
  • 老年代GC(Major GC/Full FC):指發生在老年代中的GC,出現了Major GC常常會伴隨至少一次的Minor GC(並不是絕對),Major GC的速度通常會比Minor GC慢10倍以上。
大對象直接進入老年代

大對象是指,須要大量連續內存空間的java對象(寫程序的時候應該避免「短命大對象」),常常出現大對象,容易致使內存還有很多空間時,就提早觸發垃圾收集以獲取足夠的連續空間來分給他們。 虛擬機提供-XX:PretenureSizeThreshold參數,令大於這個設置值的對象直接進入老年代,這麼作爲目的是爲了不在Eden以及兩個Survivor區之間發生大量的內存複製(新生代的垃圾收集算法採用複製算法)。

長期存活的對象將進入老年代

虛擬機給每一個對象定義一個對象年齡(Age)的計數器,若是對象在Eden出生並通過第一次Minor GC後仍然存活,而且能被Survivor容納的話,將被移動到Survivor空間中,而且對象年齡設爲1.其在Survivor中沒經歷一次Minior GC,Age就加1,當其Age增長到必定程度(默認15歲),就將其晉升到老年代。年齡閾值能夠經過參數-XX:MxTenuringThreshold設置。

動態對象的年齡斷定

爲了能更好的適應不一樣程序的內存情況,虛擬機不是永遠地要求對象的年齡必須達到MaxTenuringThreshold才能晉升老年代,若是Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或者等於該年齡的對象就能夠直接進入老年代。

空間分配擔保

在發生Minor GC以前,虛擬機會先檢查老年代中最大可用連續空間是否大於新生代全部對象總空間,若是這個條件成立,那麼Minor GC能夠確保是安全的。若是不成立,則虛擬機會查看HandlePromotionFailure設置值是否容許擔保失敗。若是容許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,若是大於將嘗試進行一次Minor GC。若是小於或者HandlePromotionFailure設置爲不容許,那這時就改成一次Full GC。 分配擔保解釋:新生代使用複製算法完成垃圾收集,爲了節約內存Survivor的設置的比較小,當Minor GC後若是還有大量對象存活,超過了一個Survivor的內存空間,這時就須要老年代進行分配擔保,把Survivor中沒法容納的對象直接進入老年代。若虛擬機檢查老年代中最大可用連續空間大於新生代全部對象總空間那麼就能保證不須要發生Full GC,由於老年代的內存空間夠用。反之,若是老年代中最大可用連續空間小於新生代全部對象總空間就須要在嘗試Minor GC失敗後進行Full Gc或者直接Full GC。

談談4種gc算法

1:標記—清除 Mark-Sweep 過程:標記可回收對象,進行清除 缺點:標記和清除效率低,清除後會產生內存碎片

2:複製算法 過程:將內存劃分爲相等的兩塊,將存活的對象複製到另外一塊內存,把已經使用的內存清理掉 缺點:使用的內存變爲了原來的一半 進化:將一塊內存按8:1的比例分爲一塊Eden區(80%)和兩塊Survivor區(10%) 每次使用Eden和一塊Survivor,回收時,將存活的對象一次性複製到另外一塊Survivor上,若是另外一塊Survivor空間不足,則使用分配擔保機制存入老年代

3:標記—整理 Mark—Compact 過程:全部存活的對象向一端移動,而後清除掉邊界之外的內存

4:分代收集算法 過程:將堆分爲新生代和老年代,根據區域特色選用不一樣的收集算法,若是新生代朝生夕死,則採用複製算法,老年代採用標記清除,或標記整理

談談Java的垃圾回收機制以及觸發時機

內存回收機制:就是釋放掉在內存中已經沒有用的對象,要判斷怎樣的對象是沒用的,有兩種方法:(1)採用標記數的方法,在給內存中的對象打上標記,對象被引用一次,計數加一,引用被釋放,計數就減一,當這個計數爲零時,這個對象就能夠被回收,可是,此種方法,對於循環引用的對象是沒法識別出來並加以回收的,(2)採用根搜索的方法,從一個根出發,搜索全部的可達對象,則剩下的對象就是可被回收的,垃圾回收是在虛擬機空閒的時候或者內存緊張的時候執行的,何時回收並非由程序員控制的,可達與不可達的概念:分配對象使用new關鍵字,釋放對象時,只需將對象的引用賦值爲null,讓程序不可以在訪問到這個對象,則稱該對象不可達。

在如下狀況中垃圾回收機制會被觸發: (1)全部實例都沒有活動線程訪問 ;(2)沒有其餘任何實例訪問的循環引用實例;(3)Java中有不一樣的引用類型。判斷實例是否符合垃圾收集的條件都依賴於它的引用類型。

From safier

垃圾收集算法

1. Mark-Sweep(標記-清除)算法

這是最基礎的垃圾回收算法,之因此說它是最基礎的是由於它最容易實現,思想也是最簡單的。標記-清除算法分爲兩個階段:標記階段和清除階段。標記階段的任務是標記出全部須要被回收的對象,清除階段就是回收被標記的對象所佔用的空間。具體過程以下圖所示:

image

從圖中能夠很容易看出標記-清除算法實現起來比較容易,可是有一個比較嚴重的問題就是容易產生內存碎片,碎片太多可能會致使後續過程當中須要爲大對象分配空間時沒法找到足夠的空間而提早觸發新的一次垃圾收集動做。

2. Copying(複製)算法

爲了解決Mark-Sweep算法的缺陷,Copying算法就被提了出來。它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用的內存空間一次清理掉,這樣一來就不容易出現內存碎片的問題。具體過程以下圖所示:

image

這種算法雖然實現簡單,運行高效且不容易產生內存碎片,可是卻對內存空間的使用作出了高昂的代價,由於可以使用的內存縮減到原來的一半。

3. Mark-Compact(標記-整理)算法

瞭解決Copying算法的缺陷,充分利用內存空間,提出了Mark-Compact算法。該算法標記階段和Mark-Sweep同樣,可是在完成標記以後,它不是直接清理可回收對象,而是將存活對象都向一端移動,而後清理掉端邊界之外的內存。具體過程以下圖所示:

image

4. Generational Collection(分代收集)算法

分代收集算法是目前大部分JVM的垃圾收集器採用的算法。它的核心思想是根據對象存活的生命週期將內存劃分爲若干個不一樣的區域。通常狀況下將堆區劃分爲老年代(Tenured Generation)和新生代(Young Generation),老年代的特色是每次垃圾收集時只有少許對象須要被回收,而新生代的特色是每次垃圾回收時都有大量的對象須要被回收,那麼就能夠根據不一樣代的特色採起最適合的收集算法。

  目前大部分垃圾收集器對於新生代都採起Copying算法,由於新生代中每次垃圾回收都要回收大部分對象,也就是說須要複製的操做次數較少,可是實際中並非按照1:1的比例來劃分新生代的空間的,通常來講是將新生代劃分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden空間和其中的一塊Survivor空間,當進行回收時,將Eden和Survivor中還存活的對象複製到另外一塊Survivor空間中,而後清理掉Eden和剛纔使用過的Survivor空間。

  而因爲老年代的特色是每次回收都只回收少許對象,通常使用的是Mark-Compact算法。

  注意,在堆區以外還有一個代就是永久代(Permanet Generation),它用來存儲class類、常量、方法描述等。對永久代的回收主要回收兩部份內容:廢棄常量和無用的類。

相關文章
相關標籤/搜索