[Java基礎]Java總結篇

陶程發表於2016-03-24

本文來自於清華大神(瀟澗)的Java總結,已得到其本人允許轉載


1.JVM


JVM記憶體模型:


PC(程式計數器),虛擬機器棧,本地方法棧,Java堆,方法區

PC:位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。

虛擬機器棧:每個方法被執行的時候都會建立一個棧幀(Stack Frame)用於儲存區域性變數表、操作棧、動態連結方法、方法出口等資訊。每一個方法被呼叫直至執行完成的過程,就對應著一個幀棧在虛擬機器棧中從入棧到出棧的過程。區域性變數所需的記憶體空間在編譯期間完成分配。當進入一個方法時,這個方法需要在幀中分配多大的區域性變數是完全確定的,在方法執行區間不會改變。

本地方法棧:虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務,而本地方法則是為虛擬機器使用到的Native方法服務。如果從記憶體分配的角度看,執行緒共享的Java堆可能劃分出多個執行緒私有的分配緩衝區(ThreadLocalAllocationBuffer)。

Java堆:幾乎所有的物件和陣列都是在堆中分配空間的,分為老生代和新生代,新生代可分為eden,survivor space 0,survivor space 1

方法區:它用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料,其資訊大部分來自class檔案。在Hot spot虛擬機器中,方法區也叫永久區,但是也會被GC,GC主要有兩類:對常量池的回收,對類後設資料的回收。

如果VM確認所有該類的例項都被回收並且裝載該類的類載入器也被回收了,那麼就回收該類的後設資料。

執行時常量池(Runtime Constant Pool)是方法區的一部分。(不一定)

Java語言並不要求常量一定只能在編譯期產生,也就是並非預置入Class檔案中常量池的內容才能進入方法區執行時常量池,執行期間也可能將新的常量放入池中,這種特性被開發人員利用的比較多的便是String類的intern()方法。

JVM引數:設定最大堆記憶體Xmx,最小堆記憶體Xms,新生代大小Xmn,老年代大小PermSize,執行緒棧大小Xss,新生帶eden和s0空間大小比例以及老年代和新生代的空間大小比例

垃圾回收演算法


(1)引用計數法:缺點是無法處理迴圈引用問題

(2)引用-清除法:標記所有從根結點開始的可達物件,清除所有未標記的物件,缺點是會造成記憶體空間不連續,不連續的記憶體空間的工作效率低於連續的記憶體空間,不容易分配記憶體

(3)複製演算法:將記憶體空間分為兩塊,每次將正在使用的記憶體中的存活物件複製到未使用的記憶體塊中,之後清除正在使用的記憶體塊演算法效率高,但是代價是將系統記憶體摺半。適用於新生代(存活物件少,垃圾物件多)

(4)標記-壓縮演算法:標記-清除的改進,清除未標記的物件時還將所有的存活物件壓縮到記憶體的一端,之後,清除邊界外所有空間既避免碎片產生,又不需要兩塊同樣大小的記憶體快,價效比高。適用於老年代。

(5)分代

JVM–標記-清除演算法Mark-Sweep

首先是mutator和collector,這兩個名詞經常出現在垃圾收集回收演算法中出現,collector指的就是垃圾收集器,而mutator是指除了垃圾收集器以外的部分,比如說我們程式本身。mutator的職責一般是NEW(分配記憶體),READ(從記憶體中讀取內容),WRITE(將內容寫入記憶體),而Collector則就是回收不再使用的記憶體來供mutator進行NEW操作的使用。

第二個概念是關於mutator roots(mutator根物件),mutator根物件一般指的是分配在堆記憶體之外,可以直接被mutator直接訪問到的物件,一般是指靜態/全域性變數以及Thread-Local變數(在Java中,儲存在java.lang.ThreadLocal中的變數和分配在棧上的變數-方法內部的臨時變數都屬於此類)。

第三個概念是關於可達物件的定義,從mutator根物件開始進行遍歷,可以被訪問到的物件都稱為是可達物件。這些物件也是mutator(你的應用程式)正在使用的物件。

垃圾回收器的型別


(1)執行緒數:序列,並行 並行:開啟多個執行緒同時進行垃圾回收,縮短GC停頓時間

(2)工作模式:併發,獨佔 併發:垃圾回收執行緒和應用程式執行緒交替工作

(3)碎片處理:壓縮,非壓縮

(4)分代:新生代,老年代

CMS:Concurrent Mark Sweep 併發標記清除,減少GC造成的停頓時間
過程:初始標記,併發標記,重新標記,併發清理,併發重置

2.多執行緒


參考文章

JAVA多執行緒和併發基礎面試問答(轉載)

生產者和消費者:

Java BlockingQueue Example implementing Producer Consumer Problem | JournalDev

Java併發程式設計:執行緒間協作的兩種方式:wait、notify、notifyAll和Condition

java.util.concurrent.BlockingQueue的特性是:當佇列是空的時,從佇列中獲取或刪除元素的操作將會被阻塞,或者當佇列是滿時,往佇列裡新增元素的操作會被阻塞。

阻塞佇列不接受空值,當你嘗試向佇列中新增空值的時候,它會丟擲NullPointerException。

阻塞佇列的實現都是執行緒安全的,所有的查詢方法都是原子的並且使用了內部鎖或者其他形式的併發控制。

BlockingQueue介面是java collections框架的一部分,它主要用於實現生產者-消費者問題。

執行緒排程器是一個作業系統服務,它負責為Runnable狀態的執行緒分配CPU時間。一旦我們建立一個執行緒並啟動它,它的執行便依賴於執行緒排程器的實現。時間分片是指將可用的CPU時間分配給可用的Runnable執行緒的過程。分配CPU時間可以基於執行緒優先順序或者執行緒等待的時間。執行緒排程並不受到Java虛擬機器控制,所以就是說不要讓你的程式依賴於執行緒的優先順序。

執行緒之間如何通訊的?當執行緒間是可以共享資源時,執行緒間通訊是協調它們的重要手段。Object類中wait()/notify()/notifyAll()方法可以用於執行緒間通訊關於資源的鎖的狀態。

如何確保執行緒安全?在Java中可以有很多方法來保證執行緒安全-同步,使用原子類(atomic concurrent classes),實現併發鎖(Lock),使用volatile關鍵字,使用不變類和執行緒安全類。

Lock設定檔案共享鎖,Lock確保當一個執行緒位於程式碼的臨界區時,另一個執行緒不進入臨界區。如果其他執行緒試圖進入鎖定的程式碼,則它將一直等待(即被阻止),直到該物件被釋放。

vilatile並不能保證執行緒安全 jvm虛擬機器棧->執行緒棧->方法棧

jvm有一個記憶體區域是jvm虛擬機器棧,每一個執行緒執行都有 一個執行緒棧,執行緒棧儲存了執行緒執行時變數值資訊。當執行緒訪某一個物件時候值的時候,首先通過物件的引用找到對應在堆記憶體的變數的值,然後把堆記憶體變數的值load到執行緒本地記憶體中,建立一個副本,之後執行緒就不再和物件堆記憶體變數值有任何關係,而是直接修改副本變數的值,在修改完之後的某一個時刻(執行緒退出之前),自動把執行緒變數副本的值回寫到物件在堆中變數。這樣在堆中的物件的值就產生變化了。

當一個執行緒訪問object的一個synchronized(this)同步程式碼塊時,它就獲得了這個object的物件鎖。結果,其他執行緒對該object物件所有的同步程式碼部分的訪問都被暫時阻塞。

synchronized方法控制對類成員的變數的訪問:每個類例項對應一把鎖,每個synchronized方法都必須獲得呼叫該方法的類例項鎖方能執行,否則所屬執行緒阻塞,方法一旦執行,就獨佔該鎖,直到從該方法返回時才將鎖釋放,此後被阻塞的執行緒方能獲得該鎖,重新進入可執行狀態。這種機制確保了同一時刻對於每一個類例項,其所有宣告為synchronized的成員函式中國至多隻有一個處於可執行狀態(因為至多隻有一個能夠獲得該類例項對應的鎖),從而有效避免了類成員變數的訪問衝突(只要所有可能訪問類成員變數的方法均被宣告為synchronized)。同步函式使用的鎖是this,靜態同步函式的鎖是該類的位元組嗎檔案物件。

synchronized塊是這樣一個程式碼塊,其中的程式碼必須獲得物件syncObject(如前所述,可以是類例項或類)的鎖方能執行,具體機制同前所述。利用同步程式碼塊可以解決執行緒安全問題。

等待喚醒機制

wait:將同步中的執行緒處於凍結狀態。釋放了執行權,釋放了資格,同時將執行緒物件儲存到執行緒池中。

notify:喚醒執行緒池中某一個等待執行緒。notifyAll:喚醒的是執行緒池中的所有執行緒。

notify和notifyAll到底哪個先執行?

  1. 這些方法都需要定義在同步中。
  2. 這些方法必須要標示所屬的鎖。要知道A鎖上的執行緒被wait了,那這個執行緒就相當於處於A鎖的執行緒池中,只有A鎖的notify醒。
  3. 這三個方法都定義在Object類中。

為什麼操作執行緒的方法定義在object類中?因為這三個方法都需要定義同步內,並標示所屬的同步鎖,既然被鎖呼叫,而鎖又可以是任意物件,那麼能被任意物件呼叫的方法一定定義在Object類中。

wait和sleep區別,從執行權和鎖上來分析:

wait:可以指定時間也可以不指定時間。不指定時間,只能由對應的notify或者notifyAll來喚醒。
wait:執行緒會釋放執行權,而且執行緒會釋放鎖。
sleep:必須指定時間,時間到自動從凍結狀態blocked轉成執行狀態(臨時阻塞狀態)。
sleep:執行緒會釋放執行權,但不釋放鎖。

  1. NEW:這種情況指的是,通過NEW關鍵字建立了Thread類(或其子類)的物件
  2. RUNNABLE:這種情況指的是Thread類的物件呼叫了start()方法,這時的執行緒就等待時間片輪轉到自己這,以便獲得CPU;第二種情況是執行緒處於RUNNABLE狀態時並沒有執行完自己的run方法,時間片用完之後回到RUNNABLE狀態;還有種情況是處於BLOCKED狀態結束了當前的BLOCKED狀態之後重新回到RUNNABLE狀態。
  3. RUUNING:這時的執行緒指的是獲得CPU的RUNNABLE執行緒,RUNNING狀態是所有執行緒都希望獲得的狀態。
  4. DEAD:處於RUUNING狀態的執行緒,在執行完run方法之後,就變成了DEAD狀態了。
  5. BLOCKED:這種狀態指的是處於RUNNING狀態的執行緒,處於某種原因,比如呼叫了sleep方法、等待使用者輸入等讓出當前的CPU給其他的執行緒。

處於RUNNABLE狀態的執行緒變為BLOCKED狀態的原因,除了該執行緒呼叫了sleep方法、等待輸入原因外,還有就是在當前執行緒中呼叫了其他執行緒的join方法、當訪問一個物件的方法時,該方法被鎖定等。

相應的,當處於BLocked狀態的執行緒再滿足一下條件時就會由該狀態轉到RUNNABLE狀態,這些條件是:sleep的執行緒醒來(sleep的時間到了)、獲得了使用者的輸入、呼叫了join的其他執行緒結束、獲得了物件鎖。

一般情況下,都是處於RUNNABLEd的執行緒和處於RUNNABLE狀態的執行緒,互相切換,直到執行完run方法,執行緒結束,進入DEAD狀態。

3.集合框架

List:有序(元素存入集合的順序和取出的順序一致),元素都有索引。元素可以重複。
|–ArrayList:底層的資料結構是陣列,執行緒不同步,ArrayList替代了Vector,查詢元素的速度非常快。
|–LinkedList:底層的結構是連結串列,執行緒不同步,增刪的速度非常快
|–Vector:底層的資料結構就是陣列,執行緒同步的,Vector 無論查詢和增刪都巨慢

可變長度陣列的原理:當元素超出陣列長度,會產生一個新的陣列,將原陣列複製到新陣列中,再將新的元素新增到新陣列中。

ArrayList:是按照原陣列的50%延長。構造一個初始容量為10的空列表。

Vector:是按照原陣列的100%延長

Set 介面中的方法和Collection中方法一致的。Set介面取出方式只有一種,迭代器。
|–HashSet:底層資料結構是雜湊表,執行緒是不同步的。無序,高效;
|–LinkedHashSet:有序,hashset的子類
|–TreeSet:對Set集合中的元素進行指定順序的排序。不同步。TreeSet底層的資料結構就是二叉樹。

HashSet集合保證元素唯一性:通過元素的hashCode方法和equals完成的。當元素的hashCode值相同時,才繼續判斷元素的equals是否為true。如果為true,那麼視為相同元素,不存。如果為false,那麼儲存。如果hashCode值不同,那麼不判斷equals從而提高物件比較的速度。

對於ArrayList集合,判斷元素是否存在,或者刪除元素底層依據都是equals方法。

對於HashSet集合,判斷元素是否存在,或者刪除元素,底層依據是hashCode方法和equals方法。

4.常用的設計模式

(1)單例模式:實現方式很多,最常見的方式就是將建構函式設定為private,類中儲存一個private的靜態單例物件,然後新建一個public static的函式例如getInstance,在這個方法中返回靜態單例物件。如果考慮到延遲載入,為了在多執行緒環境保持單例需要同步關鍵字,但是這樣做反而會增加時耗。最好的實現方式:使用內部類來維護單例的例項。

當StaticSingleton被載入的時候,其內部類並不會被初始化,所以例項也不會被初始化。而當getInstance方法被呼叫時,才會載入SingletonHolder,從而初始化instance。同時,由於例項的載入是在類載入時完成的,天生對執行緒友好,getInstance方法不需要使用同步關鍵字。(類載入自身處理了多執行緒環境下的同步問題)

//雙重判斷的方式
class Single
{
    private static Single s = null;
    private Single(){}
    public static Single getInstance()
    {
        if(s == null)
        {
            syncronized(Single.class)
            {
                if(s == null)
                {
                    s = new Single();
                }
            }
        }
        return s;
    }
}
public class StaticSingleton
{
    private StaticSingleton()
    {
    }

    private static class SingletonHolder
    {
        private static StaticSingleton instance = new StaticSingleton();
    }

    public static StaticSingleton getInsatnce()
    {
        return SingletonHolder.instance;
    }
}

(2)代理模式

使用場景:(1)延遲載入,對真實物件進行封裝;(2)網路代理,RMI;(3)安全代理,遮蔽客戶端直接訪問真實物件

延遲載入的核心思想:在真正需要某個元件的時候才去對它進行載入,其他時候使用代理物件即可,這樣可以有效提高系統啟動速度。多執行緒程式設計模式中的Future模式就是使用了代理模式,一般情況下都會有個介面,真實物件和代理物件都實現了這個介面。

動態代理:使用位元組碼動態生成載入技術,在執行時生成並載入類。應用場景:在執行時動態生成並載入代理類,與靜態代理相比,這樣做的好處就是不用為每個真實物件生成一個形式上一樣的封裝類,如果要修改就都要修改。

工具:JDK自帶,CGLib,Javasist

(3)享元模式:如果在一個系統中存在多個相同的物件,那麼只需要共享一份物件的拷貝,而不必為每一次使用都建立新的物件。類似執行緒池,但是不同的是前者儲存的物件是不可以互相替換的,而後者可以。

(4)裝飾者模式:通過委託機制,複用系統中的各個元件,在執行時,可以將這些功能元件進行疊加,使其擁有所有這些元件的功能。被裝飾者是系統的核心元件,裝飾者可以在被裝飾者的方法前後加上特定的前置處理和後置處理,增強被裝飾者的功能。

使用場景:JDK的IO框架、輸出html內容

(5)觀察者模式:在單執行緒中使某一個物件及時得知自身所依賴的狀態變化。實現將觀察者新增到被觀察者維護的觀察者列表即可。

5.Java中的4種引用型別

  • 強引用:JVM寧願丟擲OOM也不會將它回收,可能導致記憶體洩漏
  • 軟引用:當記憶體空間不足的時候才會去回收軟引用的物件
  • 弱引用:在系統GC時,弱引用的物件一定會被回收,軟弱引用適合儲存那些可有可無的快取資料
  • 虛引用:虛引用跟沒有引用差不多,即時強引用物件還存在,get方法總是返回null,它最大的作用就是跟蹤物件回收,清理被銷燬物件的相關資源

WeakHashMap適用場景:如果系統需要一張很大的map表,map中的表項作為快取之用,即使沒能從map中拿到資料也沒關係的情況下。一旦記憶體不足的時候,weakhashmap會將沒有被引用的表項清除掉,從而避免記憶體溢位。它是實現快取的一種特別好的方式。實現:Entry

6.類載入過程

                ======  常見面試問題  =======

1、equals和==的區別:前者是由物件的equals方法決定的,後者是判斷兩個物件指向的記憶體空間的地址是否相同

2、string.intern方法,在JDK6中常量池是方法區的一部分,在JDK6中常量池是方法區的一部分,在JDK7及以上常量池放到了Java堆中。

intern方法呼叫時首先去常量池找這個字串,如果有就將該字串的引用返回,如果沒有就建立該字串放到池中然後返回引用

String s = “11”; //在常量池中建立字串11,並把它的引用賦給s

String s = new String(“11”); //在常量池中建立字串11,在堆中建立一個物件c,它指向常量池中的字串,而s指向c

3、執行順序:(優先順序從高到低)靜態程式碼塊>main方法>構造程式碼塊>構造方法

其中靜態程式碼塊只執行一次。構造程式碼塊在每次建立物件都會執行。

4、final

  • 這個關鍵字是一個修飾符,可以修飾類、方法、變數
  • 被final修飾的類是一個最終類,不可以被繼承
  • 被final修飾的方法是一個最終方法,不可以被覆蓋
  • 被final修飾的變數是一個常量,只能賦值一次

5、抽象類與介面的區別

  • 抽象類只能被繼承,而且只能單繼承。介面需要被實現,而且可以多實現。
  • 抽象類中可以定義非抽象方法,子類可以直接繼承使用。介面中都是抽象方法,需要實現類去實現。
  • 抽象類使用的是 is a 關係。介面使用的 like a 關係。
  • 抽象類的成員修飾符可以自定義
  • 介面中的成員修飾符是固定的,全都是public的。

6、如果內部類被靜態修飾,相當於外部類,會出現訪問侷限性,只能訪問外部類中的靜態成員。

注意:如果內部類中定義了靜態成員,那麼該內部類必須是靜態的。內部類編譯後的檔名為:”外部類名$內部類名.java”。

為什麼內部類可以直接訪問外部類中的成員呢?

那是因為內部中都持有外部類的引用。這個引用是 外部類名.this。內部類可以定義在外部類中的成員位置上,也可以定義在外部類中的區域性位置上。當內部類被定義在區域性位置上,職能訪問區域性中被final修飾的區域性變數。

7、HashMap和HashTable的區別:

  • hashtable是執行緒安全的
  • hashtable不允許key或者value為null,hashmap可以
  • 在內部演算法上,它們對key的hash演算法和hash值到記憶體索引的對映演算法不同。

8、hashcode和equals方法

在一個執行的程式中,相等的物件必須要有相同的雜湊碼;不同的物件可以有相同的雜湊碼

  1. 無論你何時實現equals方法,你必須同時實現hashCode方法
  2. 永遠不要把雜湊碼誤用作為key,雜湊衝突是件很正常的事情,hashmap中的contains方法的實現!
  3. 雜湊碼可變,hashcode並不保證在不同的應用執行中得到相同的結果
  4. 在分散式應用中不要使用雜湊碼

替代雜湊碼:SHA1,加密的雜湊碼,160位祕鑰,衝突幾乎是不可能的。

9、RuntimeException和其他Exception的區別

Error體系:Error體系描述了Java執行系統中的內部錯誤以及資源耗盡的情形。應用程式中不應該丟擲這種型別的物件(一般是由虛擬機器丟擲)。如果出現這種錯誤,除了盡力使程式安全退出外,在其他方面是無能為力的。所以,在進行程式設計時,應該更關注Exception體系。

Exception體系包括RuntimeException體系和其他非RuntimeException的體系:

  1. RuntimeException:RuntimeException體系包括錯誤的型別轉換、陣列越界和試圖訪問空指標等等。處理RuntimeException的原則是:如果出現RuntimeException,那麼一定是程式設計師的錯誤。例如,可以通過檢查陣列下標和陣列邊界來避免陣列越界訪問異常。
  2. 其他非RuntimeException(IOException等等):這類異常一般是外部錯誤,例如試圖從檔案尾後讀取資料等,這並不是程式本身的錯誤,而是在應用環境中出現的外部錯誤。

10、集合框架

這裡寫圖片描述

生產者與消費者


public class ProducerConsumerTest {
    public static void main(String[] args) {
        PublicResource resource = new PublicResource();
        new Thread(new ProducerThread(resource)).start();
        new Thread(new ConsumerThread(resource)).start();
        new Thread(new ProducerThread(resource)).start();
        new Thread(new ConsumerThread(resource)).start();
        new Thread(new ProducerThread(resource)).start();
        new Thread(new ConsumerThread(resource)).start();
    } 
}


/** 
 * 生產者執行緒,負責生產公共資源  
 */
class ProducerThread implements Runnable {
    private PublicResource resource;
    public ProducerThread(PublicResource resource) {
        this.resource = resource;
    }
    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep((long) (Math.random() * 1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            resource.increase();
        }
    } 
}

/**
 * 消費者執行緒,負責消費公共資源
 */
class ConsumerThread implements Runnable {
    private PublicResource resource;
    public ConsumerThread(PublicResource resource) {
        this.resource = resource;
    }
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep((long) (Math.random() * 1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            resource.decrease();
        }
    } 
}


/**
 *  公共資源類
 */
class PublicResource {
    private int number = 0;
    private int size = 10;
    /**
     *  增加公共資源  
     */
    public synchronized void increase() {
        while (number >= size) {
            try { 
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } 
        }
        number++;
        System.out.println("1" + number); 
        notifyAll();
        }
        /**
         *  減少公共資源
         */
        public synchronized void decrease() {
            while (number <= 0) {
                try { 
                    wait();
                } catch (InterruptedException e) {
                e.printStackTrace();
                } 
            }
            number--;
            System.out.println("1" + number); notifyAll();
        }
 }

實現LRU Cache

public class LRUCache {
    private class Node{
        Node prev;
        Node next;
        int key;
        int value;
        public Node(int key, int value) {
            this.key = key;
            this.value = value;
            this.prev = null;
            this.next = null;
        } 
    }
    private int capacity;
    private HashMap<Integer, Node> hs = new HashMap<Integer, Node>();
    private Node head = new Node(-1, -1);
    private Node tail = new Node(-1, -1);
    public LRUCache(int capacity) {
        this.capacity = capacity;
        tail.prev = head;
        head.next = tail;
    }
    public int get(int key) {
        if( !hs.containsKey(key)) {
        return -1; 
        }
        // remove current
        Node current = hs.get(key);
        current.prev.next = current.next;
        current.next.prev = current.prev;
        // move current to tail
        move_to_tail(current);
        return hs.get(key).value;
    }
    public void set(int key, int value) {
        if( get(key) != -1) {
            hs.get(key).value = value;
            return; 
        }
        if (hs.size() == capacity) {
            hs.remove(head.next.key);
            head.next = head.next.next;
            head.next.prev = head;
        }
        Node insert = new Node(key, value);
        hs.put(key, insert);
        move_to_tail(insert);
        }
    private void move_to_tail(Node current) {
        current.prev = tail.prev;
        tail.prev = current;
        current.prev.next = current;
        current.next = tail;
        } 
}

相關文章