Java 基礎篇

讓程序性能優異的併發利器

線程池

創建參數對工作機制對影響

線程池構造函數:
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
參數含義:

  • corePoolSize
    線程池中的核心線程數,當提交一個任務時,線程池創建一個新線程執行任務,直到當前線程數等於corePoolSize;
    如果當前線程數=corePoolSize,繼續提交的任務會被保存到阻塞隊列中,等待被執行;
    如果執行了線程池的 prestartAllCoreThreads()方法,線程池會提前創建並啓動所有核心線程。
  • maximumPoolSize
    線程池允許的最大線程數。
    如果當前阻塞隊列滿了,且繼續提交任務,則創建新的線程執行任務,前提是當前線程數小於 maximumPoolSize;
  • keepAliveTime
    非核心線程空閒時的存活時間,當沒有任務執行時,非核心線程繼續存活的時間。
    默認情況下,該參數只在線程數大於corePoolSize時纔有用。
  • TimeUnit
    keepAliveTIme的時間單位。
  • workQueue
    workQueue必須是BlockingQueue阻塞隊列。
    當線程池中的線程數超過他的corePoolSize的時候,線程會進入阻塞隊列進行阻塞等待。通過workQueue,線程池實現了阻塞功能。

workQueue
用於保持等待執行任務的的任務阻塞隊列,儘量使用有界隊列,因爲無界隊列會對線程池有影響:
1、當線程池中的線程數達到corePoolSize後,新任務將在無界隊列中等待,因爲線程池中的線程數不會超過corePoolSize;
2、使用無界隊列時,maximumPoolSize和keepAliveTime將是無效參數
3、使用無界queue可能會耗盡系統資源,有界隊列則有助於防止資源耗盡,同時即使使用有界隊列,也要控制隊列的大小在一個合適的範圍。
所以一般使用 ArrayBlockingQueue,LinkedBlockingQueue,SynchronousQueue,PriorityBlockingQueue

  • threadFactory
    創建線程的工廠。
    通過自定義的線程工廠,可以給每個新建的線程設置一個具有識別度的線程名,當然還可以更加自由的對線程做更多的設置,比如設置所有的線程爲守護線程。
    Exexutors 靜態工廠裏默認的threadFactory,線程的命名規則是“pool-數字-thread-數字”
  • RejectedExecutionHandler
    線程池的飽和策略,當阻塞隊列滿了,且沒有空閒的工作線程,即當前線程數已經達到最大線程數,如果繼續提交任務,必須採取一種策略處理該任務。

線程池提供了4中策略:
1、AbortPolicy:直接拋出異常,默認策略
2、CallerRunsPolicy: 用調用者所在的線程來執行任務
3、DiscardOlderestPolicy: 丟棄阻塞隊列中靠最前的任務,並執行當前任務
4、DiscardPolicy: 直接丟棄任務
也可以根據應用場景實現 RejectedExecutionHandler接口,自定義飽和策略,如記錄日誌活持久化存儲不能處理的任務。

合理配置線程池

首先分析任務特性:
* 任務的性質:CPU密集型,IO密集型和混合型任務
* 任務的優先級:高,中,低
* 任務的執行時間:長,中,短
* 任務的依賴性:是否依賴其他系統資源,如數據庫鏈接
性質不同的任務可以用不同規模的線程池分開處理

CPU 密集型任務應配置儘可能小的線程,如配置Ncpu+1個線程的線程池。
IO密集型任務線程並不是一直在執行任務,則應配置儘可能多的線程,如 2*Ncpu。
混合型的任務,如果可以拆分,將其拆分爲一個CPU密集型任務和一個IO密集型任務,只要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐量將高於串行執行的吞吐量。如果這兩個任務執行時間相差太大,則沒必要進行分解。
可以通過Runtime.getRuntime().availableProcessors()方法獲得當前設備的CPU個數。

概述 ConcurrentHashMap

基本概述

ConcurrentHahsMap 是線程安全的Map,1.7 和 1.8 中實現方式不同

  • 1.7
    採用分段鎖的機制,實現併發的更新操作,底層採用數組+鏈表的存儲結構,包括兩個核心靜態內部類 Segment 和 HashEntry。
    1、segment繼承ReentrantLock(重入鎖)用來充當鎖的角色,每個Segemnt 對象守護每個散列映射表的若干個鎖。
    2、HashEntry 用來風中映射表的鍵值對
    3、每個桶是由若干個HashEntry 對象鏈接起來的鏈表
  • 1.8
    採用 Node+CAS+Synchronized 來保證併發安全。取消類Segment,直接用table數組存儲鍵值對;當HashEntry對象組成對鏈表長度超過 TREEIFY_ THRESHOLD 時,鏈表轉換爲紅黑樹,提升性能。底層變更爲數組 + 鏈表 + 紅黑樹。
    1、重要對常量:
    private transient volatile int sizeCtl;
    當爲負數時,-1表示正在初始化,-N表示 N -1 個線程正在進行擴容;
    當爲 0 時,表示 table 還沒有初始化;
    當爲其他正數時,表示初始化或者下一洗進行擴容的大小。
    2、 數據結構:
    Node時存儲結構的基本單元,實現了Map中的Entry接口,用於存儲數據;
    TreeNode繼承Node,但是數據結構換成來二叉樹結構,是紅黑樹的存儲結構,用於紅黑樹中存儲數據。
    3、存儲對象時(put() 方法)
    1、如果沒有初始化,就調用initTable()方法來進行初始化;
    2、如果沒有 hash 衝突就直接 CAS 無鎖插入;
    3、如果需要擴容,就先進行擴容;
    4、如果哦存在hash衝突,就枷鎖來保證線程安全,兩種情況:一種時鏈表形式就直接遍歷到尾端插入,一種是紅黑樹就按照紅黑樹結構插入;
    5、如果該鏈表的數量大雨閾值8,就要先轉換成紅黑樹的結構
    6、如果添加成功就調用 addCount()方法統計size,並堅持是否需要擴容。
    4、擴容方法 transfer()
    默認容量爲16,擴容是,容量變爲原來的兩倍
    helpTransfer(): 調用多個工作線程一起幫助進行擴容,效率更高
    5、獲取對象時(get()方法)
    1、計算hash值,定位到該table索引位置,如果是首節點符合就返回;
    2、如果遇到擴容時,會標記正在擴容結點 ForwardingNode.find() 方法,查找該結點,匹配就返回;
    3、以上都不符合的話,就往下遍歷結點,匹配就返回,否則最後就返回 null

爲什麼hashmap1.8 不直接使用紅黑樹而還要保留鏈表

因爲插入時紅黑樹需要進行左旋,右旋操作,而單鏈表不需要,在數量較少時,紅黑樹並沒有表現出比鏈表更好的查詢效率,而且在佔用空間上,紅黑樹的節點比鏈表的節點更大,時鏈表的兩倍。

爲什麼大於8個的時候才轉換紅黑樹

1、 按照JDK源碼的解釋:
TreeNodes佔用空間是普通Nodes的兩倍,所以只有當bin包含足夠多的節點時纔會轉成TreeNodes,而是否足夠多就是由 TREEIFY_THRESHOLD的值決定的。當bin中節點數變少時,又會轉成普通的bin。TREEIFY_THRESHOLD的值是這個空間和時間的權衡。
當hashCode離散性很好的時候,樹形bin用到的概率非常小,因爲數據均勻分佈在每個bin中,幾乎不會有bin中鏈表長度會達到閾值。
但是在隨機hahsCode下,離散性可能會變差,然而JDK又不能阻止用戶實現這種不好的hash算法,因此就可能導致不均勻的數據分佈。
不過理想情況下隨機 hashCode 算法下所以bin中節點的分佈頻率會遵循泊松分佈,一個bin中鏈表長度達到8個元素的概率爲 0.00000006,幾乎是不可能時間。所以,之所以選擇8,不是拍拍屁股決定的,而是根據概率統計決定的。
2、網上的說法:
紅黑樹的平均查找長度是 log(n), 如果長度爲8,平均查找長度爲 log(8)=3,鏈表的平均查找長度爲n/2,當長度爲8是,平均查找長度爲8/2=4,這纔有轉換成樹的必要;鏈表長度如果小於等於6,6/2=3,而log(6)=2.6, 雖然速度也很快,但是轉化爲樹結構和生成樹的時間並不會太短。

概述volatile

volatile 關鍵字的主要作用:
多線程主要圍繞可見性和原子性兩個特性而展開,使用volatile 關鍵字修飾的變量,保證了其在多線程之間的可見性,即每次讀取到volatile變量,一定是最新的數據。但是volatile不能保證操作的原子性,對任意單個volatile變量的讀/寫具有原子性,但類似++這種複合操作不具有原子性。

代碼底層在執行時爲了獲取更好的性能會對指令進行重排序,多線程下可能會出行一些意想不到的問題。使用volatile則會禁止重排序,但是會降低代碼的執行效率。
同時在內存語義上,當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量值刷新到主內存,當讀一個volatile變量時,JMM會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。
在java中,對與volatile修飾的變量,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序問題、強制刷新和讀取。
在具體實現上,volatile關鍵字修飾的變量會存在一個“lock:”的前綴。它不是一種內存屏障,但是它能完成類似內存屏障的功能,lock會對CPU總線和高速緩存加鎖,可以理解爲CPU指令級的一種鎖。
同時該指令會將當前處理器緩存行的數據直接寫回到系統內存中,且這個寫回內存的操作會使在其他CPU裏緩存了該地址的數據無效。

概述AQS

AQS是用來構建鎖或者其他同步組件的基礎框架,比如ReentrantLock、ReentrantReadWriteLock 和 CountDownLatch 就是基於AQS實現的。
它使用了一個int成員變量來表示同步狀態,通過內置的FIFO隊列來完成資源獲取線程的排隊工作。踏實CLH隊列鎖的一種變體實現。它可以實現2種同步方式:獨佔式,共享式。
AQS的主要使用方式式繼承,子類通過繼承AQS並實現它的抽象方法來管理同步狀態,同步器的設計基於模版方法模式,所以如果要實現我們自己的同步工具類就需要覆蓋其中幾個可以重寫的方法:tryAcquire,tryReleaseShared 等。
這樣設計的目的是同步組件(比如鎖)是面向使用者的,它定義來使用者於同步組件交互的接口(比如可以允許兩個線程並行訪問),隱藏來實現細節;同步器面向的是鎖的實現者,它簡化了鎖的實現方式,屏蔽了同步狀態管理、線程的排隊、等待與喚醒等底層操作。這樣可以很好的隔離使用者和實現者所需要關注的領域。
在內部,AQS維護一個共享資源state,通過內置的FIFO來完成獲取資源線程的排隊工作。該隊列由一個一個的Node節點組成,每個Node節點維護一個prev引用和next引用,分別指向自己的前驅和後繼節點,構成一個雙端雙向鏈表。
同時與Condition相關的等待隊列,節點類型也是Node,構成一個單向鏈表。

synchronized 的實現原理

synchronized 在JVM裏的實現都是基於進入和退出的Monitor對象來實現方法同步和代碼塊同步,雖然具體實現細節不一樣,但是都可以通過成對的 MonitorEnter 和 MonitorExit 指令來實現。
對同步塊,MonitorEnter指令插入在同步代碼塊的開始位置,當代碼執行到該指令時,將會嘗試獲取該對象Monitor到所有權,即嘗試獲得該對象的鎖,而monitorExit指令則插入在方法結束處和異常處,JVM保證每個MonitorEnter必須有對應的MonitorExit。
對同步方法,從同步方法反編譯對結果來看,方法對同步並沒有通過指令monitorEnter和monitoerExit來實現,相對於普通方法,其常量池中多來 ACC_SYNCHRONIZED 標示符。
JVM就是根據該標示符來實現方法的同步的: 當方法被調用時,調用指令將會堅持方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,如果設置了,執行線程將先獲取monitor,獲取成功之後才能執行方法體,方法執行完後在釋放monitor。在方法執行期間,其他任何線程都無法在獲得同一個monitor對象。
synchronized使用的鎖是存放在Java對象頭裏面,具體位置是對象頭裏面的MarkWord,MarkWord 裏默認數據是存儲對象的HashCode 等信息,但是會隨着對象的運行改變而發生變化,不同的鎖狀態對應着不同的記錄存儲方式。在具體優化上,從1.6開始引入了偏向鎖、自旋鎖等機制提升性能。

什麼是CAS操作,缺點是什麼?

CAS的基本思路是:如果這個地址上的值和期望的值相等,則給其賦予新值,否則不做任何事,但是要返回原值是多少。每個CAS操作過程都包含三個運算符:一個內存地址V, 一個期望的值A 和 一個新值B,操作的時候如果這個地址上存放的值等於這個期望的值A,則將地址上的值賦爲新值B,否則不做任何操作。

CAS 缺點:

  1. ABA問題:
    一個線程one從內存位置V中取出A,這時候另一個線程two也從內存中取出A,並且two進行了一些操作變成了B,最終又變回A,然後two又將V位置的數據變成A,這時候線程one進行CAS操作發現內存中仍然是A,然後one操作成功。儘管線程one的CAS操作成功,但可能存在潛在但問題。從Java1.5開始,JDK的atomic包裏提供了一個類的AtomicStampedReference來解決ABA問題。

  2. 循環時間長,開銷大:
    對於資源競爭嚴重(線程衝突嚴重)的情況,CAS自旋的概率會比較大,從而浪費更多的CPU資源,效率低於synchronized。

  3. 只能保證一個共享變量的原子操作:
    當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時間就可以用鎖。

性能等奠基之石,SQL優化

Mysql索引類型和區別

  • 普通索引:即一個索引只包含單個列,一個表可以有多個單列索引
  • 唯一索引:索引列的值必須唯一,但允許有空值
  • 複合索引:一個索引包含多個列
  • 聚集索引(聚蔟索引):innodb, 數據和索引放到一起
  • 非聚集索引:myisam,數據和索引文件分開存放

事務等四大特性

如果一個數據庫聲稱支持事務但操作,那麼該數據庫必須具備以下四個特性:

  1. 原子性
    是指事務包含的操作要麼全部成功,要麼全部失敗回滾。因此事務的操作如果成功就完全應用到數據庫,如果操作失敗則不能對數據庫有任何影響。

  2. 一致性
    是指事務必須使數據庫從一個一致性狀態變化到另一個一致性狀態,也就是說一個事務執行之前和執行之後都必須處於一致性狀態。
    拿轉賬來說,假設用戶A和用戶B的錢加一起是200,無論A和B直接如何轉賬,轉幾次賬,事務結束後兩個用戶的錢加一起還是200,這就是事務的一致性。

  3. 隔離性
    是當多個用戶併發訪問數據庫時,比如操作同一張表是,數據庫爲每一個用戶開啓的事務,不能被其他事務的操作所幹擾,多個併發事務之間要相互隔離。
    即要達到這麼一種效果:對於任意兩個併發的事務T1和T2,在事務T1看來,T2要麼在T1開始之前已經結束,要麼在T1結束之後纔開始,這樣每個事務都感覺不到有其他事務在併發的執行。

  4. 持久性
    是指一個事務一旦被提交列,那麼對數據庫中對數據對改變就是永久性對,即使是在數據庫系統遇到故障對情況下也不會丟失提交對事務的操作。

事務的隔離級別

不考慮事務的隔離性會發生的問題

  • 髒讀
    在一個事務處理過程裏讀取裏了另一個未提交的事務中的數據。
    當一個事務正在多次修改某個數據,而在這個事務中這多次的修改還未提交,這時一個併發的事務來訪問該數據,就會造成兩個事務得到的數據不一致。
  • 不可重複讀
    是指在對於數據庫中的某個數據,一個事務範圍內多次查詢卻返回來不同的數據值,這是由於在查詢間隔,被另一個事務修改並提交了。
    程序員拿着信用卡去享受生活(卡里當然是只有3.6萬),當他買單時(程序員事務開啓),收費系統事先檢測到他的卡里有3.6萬,就在這個時候!!程序員的妻子要把錢全部轉出充當家用,並提交。當收費系統準備扣款時,再檢測卡里的金額,發現已經沒錢了(第二次檢測金額當然要等待妻子轉出金額事務並提交完)。程序員就會很鬱悶,明明卡里是有錢的…
    不可重複讀和髒讀的區別:
    • 髒讀是某一稅務讀取了另一個事務未提交的髒數據
    • 不可重複讀是讀取了前一事務提交的數據
      在某些情況下,不可重複讀並不是問題,比如給我們多次查詢某個數據當然是以最後查詢得到的結果爲主。
  • 虛讀(幻讀)
    幻讀是事務非獨立執行時發生的一種現象。例如事務T1 對一個表中所有的行的某個數據項做了從“1”修改爲“2”的操作,這時事務T2又對這個表中插入了一行數據項,而這個數據項的數值還是“1”,並提交給了數據庫。而操作T1的用戶如果在查看剛剛修改的數據,就會發現還有一行沒有修改,其實這行是T2中添加的,就好像產生幻覺一樣,這就是發生了幻讀。
    幻讀和不可重複讀都是讀取了另一條已經提交的事務(這點就髒讀不同),所不同的是不可重複讀查詢的都是同一個數據項,而幻讀針對的是一批數據整體。

數據庫提供的隔離級別

Read uncommitted(讀未提交):

顧名思義,就是一個事務可以讀取另一個未提交的事務的數據。最低級別,任何情況都無法保證。

Read committd (讀已提交)

一個事務要等另一個事務提交後才能讀取數據。可避免髒讀等發生,但是無法避免不可重複讀。

Repeatable read(可重複讀)

就是在開始讀取數據時,不再允許修改操作,可避免髒讀、不可重複讀的發生。但是無法避免幻讀。

Serializable(串行化)

是最高的事務隔離級別,在該級別下,事務串行化執行,可避免髒讀,不可重複讀,幻讀的發生。
就是以鎖表的方式(類似於Java多線程中的鎖)使其他的線程只能在鎖外等待,這種事務隔離級別效率低下,比較耗數據庫性能,一般不使用。

以上四種隔離級別最高的是Serializable級別,最低的是 Read uncommitted 級別,當然級別越高,執行的效率越低。所以平時選用何種隔離級別應該根據實際情況。在MySql數據庫中默認的隔離級別爲Repeatable read(可重複讀)。
在Mysql數據庫中,支持上面四種隔離級別,默認的爲 Repeatable read;而在Oracle 數據庫中,只支持Serializable級別和Read committed這兩種級別,默認爲 Read committed 級別。

MySQL事務的實現原理

事務具有ACID四個特性。也就是:原子性,一致性,隔離性,持久性。ACD三個特性是通過 Redo log(重做日誌)和 Undo log 實現的。而隔離性是通過鎖來實現的。
重做日誌(Redo log)用來實現事務的持久性,即D特性。它由兩部分組成:

  1. 內存中的重做日誌緩衝
  2. 重做日誌文件
    在事務提交時,必須先將事務的所以日誌寫入到redo日誌文件中,待事務的commit操作完成纔算整個事務操作完成。

Undo log,它可以實現如下兩個功能:

  1. 事務回滾
  2. 實現MVCC(多版本併發控制)
    undo log 可以認爲當delete 一條記錄時,undo log中會記錄一條對應的 insert 記錄,反之亦然,當update 一條記錄時,它記錄一條對應相反的update 記錄。

SQL優化

常見步驟:

環境方面

  1. 儘可能的使用高速磁盤和大內存
  2. 服務器使用Linux,並且進行操作系統級別的調優,比如網絡參數,避免使用swap交換區等等

SQL相關

  1. 先找到慢查詢日誌,就是查詢慢的日誌,是指mysql記錄所有執行超過long_query_time 參數設定的時間閾值的SQL語句的日誌。該日誌能爲SQL語句的優化帶來很好的幫助。默認情況下,慢查詢日誌是關閉的,要使用慢查詢日誌功能,首先需要開啓慢查詢日誌功能。

    • slow_query_log 啓動/停止慢查詢
    • slow_query_log_file 指定慢查詢日誌的存儲路徑及文件(默認和數據文件放在一起)
    • long_query_time 指定記錄慢查詢日誌SQL執行時間的閾值(單位:秒,默認10秒)
    • log_queries_not_using_indexed 是否記錄未使用索引的SQL
    • log_output 日誌存放的地方【table】,【file】,【file,table】
  2. 分析慢查詢日誌。慢查詢的日誌記錄非常多,要從裏面找尋一條慢查詢的日誌並不少很容易的事情,一般需要一些輔助工具才能快速定位需要優化的SQL語句,比如 Mysqldumpslow

  3. SQL本身優化,比如少用子查詢,in查詢改關聯查詢,不實用外鍵與級聯等

  4. 反範式設計,字段允許適當冗餘,選擇合適的字段存儲長度等

  5. 使用執行計劃分析SQL語句,使用EXPLAN關鍵字可以模擬優化器執行SQL查詢語句,從而知道MySQL是如何處理你的SQL語句的。分析你的查詢語句或是表結構的性能平靜下來,至少可以知道:

* 表的讀取順序
* 數據讀取操作的操作類型
* 哪些索引可以使用
* 哪些索引被實際使用
* 表之間的引用
* 每張表有多少行被優化器查詢
比如,執行計劃中的type顯示的是訪問類型,是較爲重要的一個指標,結果值從最好到最壞依次是:
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > rang > index > ALL
一般來說,得保證查詢至少達到 rang 級別,要求能達到 ref。

優化10大策略

儘量全值匹配

當建立了索引列後,能在where條件中使用索引的儘量使用。

最佳左前綴法則

如果索引了多列,要遵守最左前綴法則。指的是查詢從索引的最左前列開始並且不跳過索引中的列。

不在索引列上做任何操作

不在索引列上做任何操作(計算,函數,手動/自動的類型轉換),會導致索引失效而轉向全表掃描

範圍條件放最後

中間有範圍查詢回導致後面的索引列全部失效

覆蓋索引儘量用

儘量使用覆蓋索引(指一個查詢語句的執行只用從索引中就能夠取得,不必從數據表中讀取),減少select *;

不等於要慎用

mysql 在使用不等於(!= 或 <>)的時候無法使用索引,會導致全表掃描,如果一定要使用不等於,請使用覆蓋索引。

Null/Not 有影響

使用is null 或 is not null 會導致索引失效
解決方式:覆蓋索引

Like 查詢要當心

like 以通配符開頭('%abc..'),mysql 索引會失效,變成全表掃描
解決方式:覆蓋索引

字符類型加引號
    字符串不佳單引號導致索引失效,變成全表掃描
    解決方式:加引號

OR 改 UNION 效率高

解決方式:如果一定要用OR,那麼使用覆蓋索引

JVM

JVM 內存區域

JVM在執行Java 程序的過程中會把它管理的內存分爲若干個不同的區域,這些組成部分有些是線程私有的,有些則是線程共享的。
線程私有的:程序計數器,虛擬機棧,本地方法棧
線程共享的:方法區,堆
  • 程序計數器
    較小的內存空間,當前線程執行的字節碼的行號指示器;各線程之間獨立存儲,互不影響,此內存區域是唯一一個不會出現 OutOfMemoryError 請求的區域。
  • 虛擬機棧
    每個線程私有的,線程在運行時,在執行每個方法的時候都會打包成一個棧幀,存儲了局部變量表,操作樹棧,動態鏈接,方法出口等信息,然後放入棧。每個時刻正在執行的當前方法就是虛擬機棧頂的棧幀。方法的執行就對應着棧幀在虛擬機棧中入棧和出棧的過程。
  • 本地方法棧
    各虛擬機自由實現,本地方法棧 native 方法調用 JNI 到了底層的 C/C++(c/c++ 可以出發彙編語言,然後驅動硬件)
  • 方法區/永久代
    用於存儲已經被虛擬機加載的類信息,常量(“zdy”,"124"等),靜態變量(static變量)等數據,比如類信息就包括類的完整有效名,返回值類型,修飾符(public,private。。。),變量名,方法名,方法代碼,這個類型直接父類的完整有效名(除非這個類型是 interface 或者 java.lang.Object,兩種情況下都沒有父類),類的直接接口的一個有序裂變等等。

  • 幾乎索引對象都分配在這裏,也是垃圾回收發生的主要區域

JVM垃圾回收器

JVM中是通過可達性分析算法判斷對象是否可回收的。
這個算法的基本思想就是通過一系列的稱爲“GC Roots”的對象作爲起點,從這些節點開始向下搜索,節點所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連的話,則證明此對象不可用的。
在垃圾回收上,又幾種常見的算法:

  1. 標記-清除算法
    標記-清除算法分爲“標記”和“清除”階段:首先標記出所以需要回收的對象,在標記完成後統一回收所以被標記的對象。但是會帶來兩個明顯的問題:

    1. 效率問題
    2. 空間問題(標記清除後會產生大量不連續的碎片)
  2. 複製算法
    將內存氛圍大小相同的兩塊,每次使用其中的一塊。當一塊的內存使用完後,就將還存活的對象複製到另一塊去,然後再把使用的空間一次清理掉。這樣就使每次的內存回收都是對內存區間的一半進行回收。

  3. 標記-整理算法
    根據老年代代特點設計的一種標記算法,標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象回收,而是讓所以存活的對象向一段移動,然後直接清理掉端邊界以外的內存。
    根據對象的生命週期,將java堆分爲新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集算法。
    比如在新生代中,每次收集都會有大量對象死去,所以可以選擇複製算法,只需要付出少量對象的複製成本就可以完成每次垃圾收集。而老年代的對象存活機率是比較高的,而且沒有額外的空間對它進行分配擔保,所以我們必須選擇“標記-清除”或“標記-整理”算法進行垃圾收集。
    在具體的垃圾算法的實現上又幾種垃圾回收器。
    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-th1e58cY-1591856868610)(file:///Users/yangmingyue/Documents/Gridea/post-images/1590464776107.jpg)]

Serial/Serial Old
最古老的,單線程,獨佔式,成熟,適合CPU 服務器
-XX:+UseSerialGC 新生代和老年代都用串行收集器
-XX:+UseParNewGC 新生代使用ParNew, 老年代使用 Serial Old
-XX:+UseParallelGC 新生代使用ParallerGC,老年代使用Serial Old

ParNew
和Serial 基本沒有區別,唯一區別:多線程,多CPU多,停頓時間比Serial少
-XX:+UseParNewGC 新生代使用ParNew,老年代使用Serial Old
除了性能原因外,主要是因爲除了 Serial 收集器,只有他能與CMS收集器配合工作。

Parallen Scavenge (ParallerGC) /Parallel Old
關注吞吐量的垃圾收集器,高吞吐量則可以高效的利用CPU事件,儘快完成程序的運算任務,主要適合在後臺運算而不需要太多交互的任務。
所謂吞吐量就是CPU用於運行用戶代碼的事件與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行哦用戶代碼時間 + 垃圾收集時間),虛擬機總共運行了 100 分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。
-XX:+UseParalleOldGC 則會開啓這一對組合,同時Parallel Scavenge還有一個自適應調整策略,就不需要手工指定新生代的大小(-Xmn),Eden與Survivor區的比例(-XX:SurvivorRatio),晉升老年代對象年齡(-XX:LPretenureSizeThreshold)等細節參數了,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或最大的吞吐量。通過打開-XX:+UseAdaptiveSizePolicy, 只需要把基本你的內存數據設置好(如-Xmx 設置最大堆),然後使用 MaxGCPauseMillis 參數(更關注最大停頓時間)或 GCTimeRatio參數(更關注吞吐量)給虛擬機設立一個優化目標,那具體細節參數的調節工作就由虛擬機完成了。

Concurrent Mark Sweep(CMS)
收集器是一種以獲取最短回收停頓時間爲目標的收集器。目前很大一部分的Java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤其重視服務器的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就非常符合這類應用的需求。
-XX:+UseConcMarkSweepGC, 一般新生代使用ParNew,老年代的用CMS,併發收集失敗,轉爲SerialOld.
從名字(包含“Mark Sweep”)可以看出,CMS收集器是基於“標記-清楚”算法實現的,它的運作過程相對於前面集中收集器來說更復雜一些。
整個過程分爲4個步驟:

  • 初始標記:糾結是標記一下 GC Roots 能直接關聯到的對象,速度最快,需要停頓(STW–stop the world)
  • 併發標記:從GC Roots 開始對堆中對象進行可達性分析,找到存活對象,它在整個回收過程中耗時最長,不需要停頓。
  • 重新標記:爲了修正併發標記期間因用戶程序繼續運作而導致標記變動的那一本部分對象的標記記錄,需要停頓。這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短。
  • 併發清除:不需要停頓。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-SEcXeOIX-1591856868611)(file:///Users/yangmingyue/Documents/Gridea/post-images/1590475711018.jpg)]

優點:
由於整個過程中耗時最長的併發標記和併發清除過程周幾去線程都可以和用戶線程一起工作,所以,總體來說,CMS收集器的內存回收過程是與用戶線程一起併發執行的。
缺點:

  • CPU資源敏感:因爲併發階段多線程佔用CPU資源,如果CPU資源不足,效率會明顯降低。
  • 浮動垃圾:由於CMS併發清理階段 用戶線程還在運行着,伴隨程序運行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS無法在檔次收集中處理掉他們,只好留待下一次GC時再清理掉。這一部分垃圾就稱爲“浮動垃圾”。
    由於浮動垃圾的存在,因爲需要預留出一部分內存,意味着CMS收集不能像其他收集器那樣等待老年代快滿
    的時候再回收。
    再1.6的版本中,老年代空間使用率閾值(92%)
    如果預留的內存不夠存放浮動垃圾,就會出現 Concurrent Mode Filure,這時虛擬機將臨時啓用 Serial Old來代替CMS。
  • 產生空間碎片:標記-清除算法會導致產生不聯繫的空間碎片,CMS只會刪除無用的對象,不會對內存做壓縮,會造成內存碎片。

G1垃圾回收器
主要是用在大內存和多處理器數量的服務器上。jdk9 中將G1 變成默認的垃圾收集器。
G1中重要參數:
-XX:_UseG1GC 使用G1垃圾回收器
-XX:MaxGCPauseMillis=200 設置GC的最大暫停時間爲200ms

內部佈局改變
G1把堆劃分爲多個大小相等的區域(Region),每個Region大小爲2的倍數,範圍在1MB-32MB之間,可能爲1,2,4,8,16,32MB。所有的Region有一樣的大小,JVM生命週期內不會改變。整個堆被劃分爲2048左右個Region。新生代和老年代不再物理隔離。Region可以說是G1回收器一次回收的最小單元。
算法:標記-整理(old,humongous) 和 複製回收算法(survivor)。

Stop The World 現象
Stop The World機制,簡稱STW,主要指執行垃圾收集算法時,Java應用程序的其他所有除了垃圾回收線程之外的線程都被掛起。
此時,系統只能允許GC線程進行運行,其他線程則會全部暫停,等待GC線程執行完才能再次運行。這些工作都是由虛擬機在後臺自動發起和自動完成的,是在用戶不可見的情況下把用戶正常工作的線程全部停下,這對於很多應用程序,尤其是那些對於實時性要求很高的程序來說是難以接受的。我們GC調優的目標就是儘可能的減少STW的時間和次數。

JVM中存在哪些引用

強引用

大部分引用都是強引用,這是最普遍的引用類型。
A a = new A();
如果一個對象具有強引用,垃圾回收器絕不會回收它。當內存空間不足,Java虛擬機寧願拋出 OutOfMemoryError 錯誤,是程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足的問題。

軟引用

如果一個對象只具有軟引用,那就類似於可有可無的生活用品。如果內存空間足夠,垃圾回收器就不會回收它,如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的告訴緩存。
軟引用可以和一個引用隊列(ReferenceQuence)聯合使用,如果軟引用所引用的對象被垃圾回收,Java虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。

弱引用

可有可無。弱引用與軟引用的區別:只具有弱引用的對象擁有更短暫的生命週期。在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了至於有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由於垃圾回收器是一個優先級很低的線程,因此不一定會很快發現那些只具有弱引用的對象。
弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。

虛引用

顧名思義,就是形同虛設,徐銀銀並不會決定對象的聲明週期。如果哦一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任務時候都可能被來及回收。
虛引用主要用來跟蹤對象被垃圾回收的活動。

在程序設計中除了強引用,使用軟引用的情況較多,這是因爲軟引用可以加速JVM對垃圾內存的回收速度,可以維護系統的運行安全,防止內存溢出等問題的產生。

類加載機制

類從被加載到虛擬機內存中開始,到卸載出內存位置,它的整個生命週期包括:加載,驗證,準備,解析,初始化,使用和卸載 7個階段。其中驗證,準備,解析 3各部分統稱爲連接(Linking)

加載

  1. 通過一個類的全限定名來獲取定義此類的二進制字節流
  2. 將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構
  3. 在內存中生成一個代表這個類的java.lang.Class 對象,作爲方法區這個類的各種數據的訪問入口

驗證

連接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。但從哪個整體來看,驗證階段大致上會完成4個階段的檢驗動作:文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證。

準備

正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這個階段中有兩個容易產生混淆的概念。首先,這時候進行內存分配的僅包括類變量(static 修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一起分配在Java堆中。其次,這裏所說的初始值“通常情況”下是數據類型的零值,假設一個類變量的定義爲:
public staic int value = 123;
那變量初始階段過後的初始值爲0而不是123,因爲這時候尚未開始執行任何Java方法,而把value 賦值爲123的putstatic指令是程序被編譯後,存放於類構造器() 方法中,所以把 value 賦值爲 123 的動作將在後面的初始化階段纔會執行。
假設類變量value的定義爲:
public static final int value = 123
編譯時javac將會爲value生成 ConstantValue屬性,在準備階段虛擬機就跟根據 ConstantValue 的設置將value賦值爲123.

解析

是虛擬機將常連池內的符號引用替換爲直接引用的過程。
符合引用以一組符號來描述所引用的目標,符號可以說任何形式的字面量,只要使用時能無歧義的定位到目標即可。符號引用於與虛擬機實現的內存佈局無關,引用的目標並不一定已經加載到內存中。各種虛擬機實現的內存佈局可以各不相同,但是它們能接受的符號引用必須是一致的,因爲符號引用的字面量形式明確定義在Java虛擬機規範的Class文件格式中。
直接引用可以是直接指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄。直接引用是和虛擬機實現的內存佈局相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會像他。如果有了直接引用,那引用的目標必定已經在內存中存在。

初始化

虛擬機規範則是嚴格規定了有且只有5種情況必須立即對類進行“初始化”(而加重,驗證,準備自然需要在此之前開始):

  1. 遇到new,getstatic,putstatic或invokestatic 這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最長久的Java代碼場景是:使用new 關鍵字實例化對象的時候,讀取或設置一個類的靜態字段(被final修飾,已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
  2. 使用 java.lang.reflect 包的方法對類的進行反射調用的時候,如果類還沒有進行過初始化,則需要先觸發其初始化。
  3. 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
  4. 當虛擬機啓動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
  5. 當使用JDK1.7 的動態語言支持時,如果一個 java.lang.invoke.MethodHandle 實例最後的解析結果 REF_getStatic,RED_putstatic,REF_invokestatic的方法局部,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。

初始化也是類加載的最後一步,前面的類加載過程中,除了在加載階段用戶應用程序可以通過自定義類加載器參與外,其餘動作完全由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的Java程序代碼在準備階段,變量已經賦過一次系統要求的初始值,而在初始化階段,則根據程序員通過程序制定的主觀計劃去初始化類變量和其他資源。
從另一個角度表達:初始化階段是執行類構造器() 方法的過程。() 方法是由編譯期自動收集類中的所有類變量的賦值動作和靜態語句塊(statiic{}) 中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序決定的。
() 方法對於類或接口並不是必須的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那麼編譯器可以不爲這個類生成 () 方法。
虛擬機會保證一個類的 () 方法在多線程環境中被正確的加鎖,同步,如果多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的()方法,其他線程都需要阻塞等待直到活動線程執行() 方法完畢。如果在一個類的() 方法中有韓式很長的操作,就可能造成多個進程阻塞。所以類的初始化是線程安全的,項目中可以利用這點。

雙親委派模型

對任意一個類,都需要由加載它的類加載器和這個類本身一起確立其在Java虛擬機中的唯一性。
從Java虛擬機的角度來講,只存在兩種不同的類加載器:

  • 啓動類加載器(Bootstrap ClassLoader)
    這個類加載器使用C++語言實現,是虛擬機的一部分。這個類將負責將存放在<JAVA_HOME>/lib 目錄中的,或者被 -Xbootclasspath 參數所指定的路徑中的,並且是虛擬機識別的(僅按照文件名識別,如 rt.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載帶虛擬機內存中。啓動類加載器無法被Java程序直接引用,用戶在編寫自定義類加載器時,如果需要把加載請求委派給引導類加載器,那直接使用null代替就可以。

  • 其他類加載器
    這些類加載器都由Java語言實現,獨立於虛擬機外部,並且全部繼承自抽象類 java.lang.ClassLoader

    • 擴展類加載器 (Extension ClassLoader)
      這個加載器由 sun.misc.Launcher$ExtClassLoader 實現,它負責加載<JAVA_HOME>/lib/ext 目錄中的,或者被 java.ext.dirs 系統變量所指定的路徑中的所以類型,開發者可以直接使用擴展類加載器。
    • 應用程序類加載器 (Application ClassLoader)
      這個類加載器由 sun.misc.Launcher$AppClassLoader 實現。由於這個類加載器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以一般也稱它爲系統類加載器。他負責加載用戶類路徑(ClassPath) 上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義的類加載器,一般情況下這個就是程序中默認的類加載器。

我們的應用程序都是由這3種類加載器互相配合進行加載的,如果有必要,還可以加入自定義的類加載器。
雙親委派模型要求除了頂層的啓動類加載器外,其餘的類加載器都應該有自己的父類加載器。這裏類加載器之間的父子關係一般不會以繼承(Inheritance) 的關係來實現,而是都使用組合(omposition) 關係來複用父加載器的代碼。

使用雙親委派模型來組織類加載器之間的關係,一個顯而易見的好處就是Java類隨着它的類加載器一起將具備了一種帶有優先級的層次關係。例如類 java.lang.Object, 它存放在rt.jar 之中,無論哪一個類加載器要加載這個類,最終都會委派給處於模型最頂端的啓動類加載器進行加載,因此Object類在程序的各類加載器環境中都是同一個類。

ClassLoader 中的 loadClass 方法中的代碼邏輯就是雙親委派模型:
在自定義ClassLoader的子類的時候,我們常見的會有兩種做法,一種是重寫 loadClass 方法,另一種是重寫 findClass方法 。其實這兩種方法本質上長不大,畢竟loadClass 也會調用findClass, 但是從邏輯上講我們最好不要直接修改 loadClass 的內部邏輯。我建議的做法是隻在 findClass 裏findClass 裏重寫自定義類的加載方法。

loadClass這個方法是實現雙親委派模型邏輯的地方,擅自修改這個方法會導致模型被破壞,容易造成問題。因此我們最好是在雙親委派模型框架內進行小範圍的改掉,不破壞原有的穩定結構。同時,也避免了自己重寫 loadClass 方法的過程中鼻血重寫雙親委託的重複代碼,從代碼的複用性來看,不直接修改這個方法始終是比較好的選擇。

但是Tomcat 中沒有完全遵守雙親委派模型。

雙親委派模型的破壞

雙親委派模型很好的解決了各個類加載器的基礎類的統一問題(越基礎的類越由上層的加載器進行加載),基礎類之所以稱爲“基礎”,是因爲他們總是作爲被用戶代碼調用的API。如果基礎類要調用用戶的代碼,那怎麼辦?

比如JDBC是原生的JDBC中Driver驅動本身只是一個接口,並沒有具體實現,具體的實現由不同數據庫類型去實現的。

例如,MySQL的mysql-connector.jar 中的Driver類具體實現的。原生的JDBC中的類是放在 rt.jar 包的,是由啓動類加載器進行加載的,在JDBC中的Driver類中需要動態去加載不同數據庫類型的Driver類,而mysql-connector.jar 中的Driver類是由獨立廠商實現並部署在應用程序的ClassPath下的,那啓動類加載器肯定是不能進行加載的,既然是自己編寫的代碼,那就需要由應用程序啓動類去進行類加載。

於是,這個時候就引入線程上下文類加載器(Thread Context ClassLoader)。有了這個東西,程序就可以把原本需要由啓動類加載器進行加載的類,由應用程序類加載器去進行加載了。如果創建線程時還未設置,他將會從父線程中繼承一個,如果在應用程序的全局範圍內都沒有設置過的話,那麼這個類加載器默認就是應用程序類加載器。

Java中所以設計SPI的加載動作基本上都是採用這種方式,例如 JNDI,JDBC,JCE,JAXB和JBI等。
雙親委派模型的“被破壞”是由於用戶對程序動態性的追求而導致的,這裏說說的“動態性”指的是當前一些非常“熱門“的名詞:代碼熱替換(HotSwap),模塊熱部署(HotDeplooyment)等等。

JVM常用工具

  • jps
    列出當前機器上正在運行的虛擬機進程,Jps從操作系統的臨時目錄上去找
    * -q: 僅僅線上進程
    * -m:輸出主函數傳入的參數,
    * -l:輸出應用程序主類完整package名稱或jar完整名稱
    * -v:列出jvm參數,-Xms20m -Xmx20 是啓動程序指定的JVM參數

  • jstat
    是用於見識虛擬機各種運行狀態信息的命令行工具。它可以顯示本地或者遠程虛擬機進程中的類裝載,內存,垃圾收集,JIT編譯等運行數據,在沒有GUI圖像件,只提供了純文本控制檯環境的服務器上,它將是運行期定位虛擬機性能問題的首選工具。
    假設需要每 250 毫秒查詢一次進程 13616 垃圾收集狀況,一共查詢 10 次,
    那命令應當是:jstat -gc 13616 250 10
    常用參數:

    • -class 類加載器
    • -compiler JIT
    • -gc GC堆狀態
    • -gccapacity 各區大小
    • -gccause 最近一次GC統計和原因
    • -gcnew 新區統計
    • -gcnewcapacity 新區大小
    • -gcold 老區統計
    • -gcoldcapacity 老區大小
    • -gcpermcapacity 永久區大小
    • gcutil GC統計彙總
    • printcompilation HotSpot編譯統計
  • jinfo
    查看和修改虛擬機參數

    • -sysprops 可以查看有 System.getProperties() 取得的參數
    • -flag 未被顯示指定的參數的系統默認值
    • -flags 顯示虛擬機的參數
  • jmap
    用於生產對轉儲快照(一般稱爲heapdump或dump文件)。jmap的作用並不僅僅是爲了獲取dump文件,它還可以查詢finalize執行隊列,java堆和永久帶的詳細信息,如空間使用率,當前用的是哪種收集器等。和info命令一樣,jmap有不少功能在windows 平臺下都是受限的,除了生成dump文件的-dump選項和用於查看每個類的實例,空間佔用統計的 -histo選項在所有操作系統都提供之外,其餘選項都只能在Linux、Solaris下使用。
    jmap -dump:live,format=b,file=heap.bin
    Sun JDK 提供jhat(JVM heap Analysis Tool)命令與jmap搭配使用,來分析jamp 生成的對轉儲快照。

  • jhat
    jhat dump 文件名
    屏幕顯示 ”server is ready“的提示後,瀏覽器中訪問 http://localhost:7000/ 就可以訪問詳情
    使用jhat可以在服務器上生成堆轉儲文件分析(一般不推薦,比較佔用服務器資源)

  • jstack
    Strack Trace Java, 命令用於生產虛擬機當前時刻的線程快照。線程快照就是當前虛擬機內每一條線程正在執行的方法堆棧的集合,生成線程快照的主要目的是定位線程出現長時間停頓的原因,如線程間死鎖,死循環,請求外部資源導致的長時間等待等都是導致線程長時間停頓的常見原因。
    在代碼中可以用 java.lang.Thread 類的 getAllStrackTraces() 方法用於獲取虛擬機中所有線程的 StackTraceElement 對象。使用這個方法可以通過簡單的幾行代碼就完成 jstack 的大部分功能,在實際項目中不妨調用這個方法做一個管理員頁面,可以隨時使用瀏覽器來查看線程堆棧。

項目內存或者CPU佔用過高如何排查

  1. 針對CPU的問題:

    1. 查看問題進程,得到進程PID: top -c
    2. 查看進程裏的線程明細,並手動記下CPU異常的線程PID:top -p PID -H
    3. 使用jdk提供的jstack命令打印出項目堆棧:jstack pid > dump.log
  2. 針對內存的問題:

    1. 查看內存中的存活對象統計,找出業務相關的類名: jmap -histo:live PID > xxx.log
    2. 通過簡單的捅進還是沒辦法定位問題的話,就輸出內存明細來分析。這個命令會將內存裏的所有信息都輸出,輸出的文件大小和內存大小基本一致。而且會導致應用暫時掛起,所有謹慎使用:jmap -dump:live,format-b,file=xxx.hprof PID
    3. 最後對dump出來的文件進行分析。文件大小不是很大的話,使用jdk自帶的jhat命令即可:
      jhat -J -mx2G -port 7170
    4. dump 文件太大的話,可以使用 jprofiler 工具來分析。
  3. 需要分析GC的情況,可以使用一下命令:
    jstat -gc PID

框架源碼,CRUD和高級程序員的分水嶺

談談依賴注入和麪向切面

談談你對Spring框架的理解,談談Spring中的IOC和AOP概念
Spring 框架是一個開源而輕量級的框架,是一個IOC和AOP的容器,spring的核心就是控制反轉(IOC)和麪向切量編程(AOP)

  • IOC
    面向對象編程中的一種設計原則,用來降低代碼之間的耦合度,使整個程序體系結構更加靈活,與此同時將類的創建和依賴關係卸載配置文件裏,由配置文件注入,達到松耦合的效果。與此同時IOC也稱爲DI(依賴注入),依賴注入是一種開發模式;依賴注入提倡使用接口編程;依賴注入使得可以開發各個組件,然後根據組件之間的依賴關係注入組裝,
    所謂依賴,從程序的角度看,就是比如A要調用B的方法,那麼A就依賴於B,返回A要用到B,則A依賴於B。所謂倒置,如果不倒置,因爲A必須要用到B,所以要有B纔可以調用B的方法。不倒置的話A就需要主動獲取B的實例:B b = new B(); 這就是最簡單的獲取B實例的方法(各種設計模式也可以幫忙去獲得B的實例,比如工廠,Locator等),然後就可以調用b對象了。所以不倒置,就需要A主動獲取B,才能使用B。倒置的話,就是A要調用B的話,A並不需要主動獲取B,而是由其他人自動將B送上門。
  • AOP
    面向切面編程將安全,事物等程序邏輯相對獨立的功能抽取出來,利用Spring的配置文件將這些功能插進去,實現了按照切面編程,提高了複用性;最主要的作用: 可以再不鏽鋼源代碼的情況下,給目標方法動態添加功能。
    面向切面編程的目標就是分離關注點。什麼是關注點呢?就是你要做的事,就是關注點。
    AOP的好處就是你只需要幹你的正事,其他事情交給別人幫你幹。
    從Spring的角度來看,AOP最大的用途就在於提供了事務管理能力。事務管理就是一個關注點,你的正事是去訪問數據庫,而你不太想管事務,所以,Spring在你訪問數據庫之前,自動幫你開啓事務,當你訪問數據庫結束後,自動幫你提交、回滾事務。

Spring優點

  • 低侵入式設計,獨立於各種應用服務器
  • 依賴注入的特點將組件關係透明化,降低耦合度
  • 與第三方框架具有良好的整合效果

Spring框架中bean實例化的流程

Spring Bean的生命週期

Spring在Bean創建過程中是如何解決循環依賴的

循環依賴只會存在單例實例中,多例循環依賴直接報錯。
A類實例化後,把實例放map中,A類中有一個B類屬性,A類實例化要進行IOC依賴注入,這時候B類需要實例化,B類實例化跟A類一樣,實例化後放入map容器中。B類中有一個A類屬性,接着B類的IOC過程,又去實例化A類,這時候實例化A類過程中從map容器發現A類已經在容器中了,就直接返回了A的實例,依賴注入到B類中A屬性中,B類IOC完成後,B實例化就完全完成了,就返回給A類的IOC過程。這就是循環依賴的解決。

AOP實現流程

  1. aop:config 自定義標籤解析
  2. 自定義標籤解析時會執行到aop入口類中
  3. Bean實例化過程中會執行到aop入口類中
  4. 在aop入口類中,判斷當前正在實例化的類是否在pointcut中,pointcut可以理解爲一個模糊匹配,是一個joinpoint的集合
  5. 如果當前正在實例化的類在pointcout中,則返回該bean的代理類,同時把所有配置的advice封裝成 MethodInterceptor對象加入到容器中,封裝成一個過濾器鏈
  6. 代理對象調用,jdk動態代理會調用invocationHandler中,cglib型代理調到 MethodInterceptor的callback類中,然後在 invoke 方法中執行過濾器鏈。

Spring框架中如何基於AOP實現事務管理

事務管理,是一個切面。在aop環節中,其他環節都一樣,事務管理就是由Spring提供的advice,既是TransactionInterceptor,它一樣的會在過濾器鏈中被執行到,這個TransactionInterceptor 過濾器類是通過解析 tx:advice 自定義標籤得到的。

描述SpringMvc的整個訪問或者調用流程

  1. 發起請求到前端控制器(DispatcherServlet)
  2. 前端控制器請求HandlerMapping查找Handler(可以根據xml配置,註解查找),處理器映射器 HandlerMapping 向前端控制器返回Handler
  3. 前端控制器調用處理器適配器去執行 Handler
  4. 處理器適配器去執行Handler
  5. Handler 執行完成給適配器返回 ModelAndView,處理器適配器向前端控制器返回 ModelAndView(ModelAndView 是 springmvc 框架的一個底層對象,包括 model 和 view)
  6. 前端控制器請求視圖解析器去進行視圖解析(根據邏輯視圖名解析成真正的視圖(jsp)),視圖解析器向前端控制器返回 View
  7. 前端控制器進行視圖渲染(視圖渲染將模型數據填充到request域)
  8. 前端控制器向用戶響應結果

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Zy1vPyth-1591856868613)(file:///Users/yangmingyue/Documents/Gridea/post-images/1590658353054.jpg)]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章