從原始碼角度,帶你研究什麼是三級快取

雲揚四海發表於2021-12-03
  • Java開發,總會遇到問三級快取的。
  • 看了很多文章,感覺不是很透徹。打算自己寫一個自以為很詳細的對三級快取的理解。 有圖文。也有文字概括。受不了動圖的可以看文字概括哦

進入正題:

  • 在開發過程中會遇到迴圈依賴的問題。 就跟下圖一樣

image

  • Spring在為此設計了三級快取來解決以上依賴的問題

首先我們得知道 三級快取裡面分別存的什麼

一級快取裡存的是成品物件,例項化和初始化都完成了,我們的應用中使用的物件就是一級快取中的

二級快取中存的是半成品,用來解決物件建立過程中的迴圈依賴問題

三級快取中存的是 ObjectFactory<?> 型別的 lambda 表示式,用於處理存在 AOP 時的迴圈依賴問題

image

Spring 三級快取的順序

三級快取的順序是由查詢循序而來,與在類中的定義順序無關

image

所以第一級快取:singletonObjects ,第二級快取:earlySingletonObjects ,第三級快取:singletonFactories

Spring 的的注入方式有三種:構造方法注入、setter 方法注入、介面注入

介面注入的方式太靈活,易用性比較差,所以並未廣泛應用起來,大家知道有這麼一說就好,不要去細扣了

構造方法注入的方式,將例項化與初始化並在一起完成,能夠快速建立一個可直接使用的物件,但它沒法處理迴圈依賴的問題,瞭解就好

setter 方法注入的方式,是在物件例項化完成之後,再通過反射呼叫物件的 setter 方法完成屬性的賦值,能夠處理迴圈依賴的問題,是後文的基石,必須要熟悉

Spring 原始碼分析

  • 下面會從幾種不同的情況來進行原始碼跟蹤

1、 沒有依賴,有 AOP

程式碼非常簡單: spring-no-dependence

image

  • 上圖的邏輯就是 ClassPathXmlApplicationContext#refresh

  • -> this.finishBeanFactoryInitialization(beanFactory)

  • -> beanFactory.preInstantiateSingletons()

  • -> isFactoryBean 判斷bean, 然後呼叫 getBean 方法。

  • 接下來呼叫 doGetBean 方法

image

  • 上圖邏輯是 DefaultSingletonBeanRegistry#getSingleton 判斷是否存在快取當中,如果沒有則進行建立Bean
  • 可以觀察到
 Map<String, Object> singletonObjects               無 simpleBean 
 Map<String, Object> earlySingletonObjects          無 simpleBean 
 Map<String, ObjectFactory<?>> singletonFactories   無 simpleBean 
 Set<String> singletonsCurrentlyInCreation          有 simpleBean 
  • 說明bean在建立過成中
  • 我們接著從 createBean 往下跟

image

  • 關鍵程式碼在 doCreateBean 中,其中有幾個關鍵方法的呼叫值得大家去跟下
 Map<String, Object> singletonObjects               儲存的 simpleBean 的代理物件
 Map<String, Object> earlySingletonObjects          由始至終都沒有 simpleBean 物件
 Map<String, ObjectFactory<?>> singletonFactories   存入了一會資料立馬就刪除了 並未使用過  
 
 // 以下說的方法均在 AbstractAutowireCapableBeanFactory 類下
 // createBeanInstance 通過反射完成物件的例項化,獲得半成品物件。 給分配記憶體空間, 即使半成品
 // populateBean 填充半成品屬性, 如果有依賴物件則在這裡引入
 // initializeBean 初始化半成品物件
 // applyBeanPostProcessorsAfterInitialization BeanPostProcessor的後置處理,AOP 的代理物件替換就是在這裡完成的  
  • 此時:代理物件的建立是在物件例項化完成,並且初始化也完成之後進行的,是對一個成品物件建立代理物件
  • 所以《沒有依賴,有 AOP》 情況下:只用一級快取就夠了,其他兩個快取可以不要也能完成物件

2、迴圈依賴,沒有AOP

程式碼依舊非常簡單: spring-circle-simple 此時迴圈依賴的兩個類是: CircleLoop

物件的建立過程與前面的基本一致,只是多了迴圈依賴,少了 AOP,所以我們重點關注: populateBeaninitializeBean 方法

先建立的是 Circle 物件,那麼我們就從建立它的 populateBean 開始,再開始之前,我們先看看三級快取中的資料情況

 Map<String, Object> singletonObjects               無 circle 也無 loop 物件
 Map<String, Object> earlySingletonObjects          無 circle 也無 loop 物件
 Map<String, ObjectFactory<?>> singletonFactories   只有 cicle 的 lambda
 Set<String> singletonsCurrentlyInCreation          只有 circle 

image

  • 我們開始跟populateBean,它完成屬性的填充,與迴圈依賴有關,一定要仔細看,仔細跟

image

circle 物件的屬性 loop 進行填充的時候,去 Spring 容器中找 loop 物件,發現沒有則進行建立,又來到了熟悉的 createBean

此時三級快取中的資料沒有變化,但是 Set<String> singletonsCurrentlyInCreation 中多了個 loop 標識loop正在建立中

image

loop 例項化完成之後,對其屬性 circle 進行填充,去 Spring 中獲取 circle 物件,又來到了熟悉的 doGetBean

此時一、二級快取 (singletonObjects``earlySingletonObjects) 中都沒有 circleloop ,而三級快取中有這兩個

image

通過 getSingleton 獲取circle時,三級快取呼叫了 getEarlyBeanReference ,但由於沒有 AOP,所以 getEarlyBeanReference 直接返回了普通的 半成品 circle

然後將 半成品 circle 放到了二級快取,並將其返回,然後填充到了 loop 物件中

此時的 loop 物件就是一個成品物件了;接著將 loop 物件返回,填充到 circle 物件中,如下如所示

image

我們發現直接將 成品 loop 放到了一級快取中,二級快取自始至終都沒有過 loop ,三級快取雖說存了 loop ,但沒用到就直接 remove 了

此時快取中的資料,相信大家都能想到了

 Map<String, Object> singletonObjects               無 circle 有 loop 物件
 Map<String, Object> earlySingletonObjects          有 circle 無 loop 物件
 Map<String, ObjectFactory<?>> singletonFactories   無 circle 也無 loop
 Set<String> singletonsCurrentlyInCreation          有 circle 無 loop 因為loop建立完畢了
  • 當 loop 物件完成 建立bean的時候 會呼叫 DefaultSingletonBeanRegistry#getSingleton -> DefaultSingletonBeanRegistry#addSingleton
  • 將資料物件移動到一級快取中。二級快取的 circle 沒用上就刪除了, 只有 circle 存在三級快取的資料被呼叫到了。將半成品的 circle 給返回給 loop物件
  • 所以《迴圈依賴,沒有AOP》情況下:可以減少某個快取,只需要兩級快取就夠了

概括:(迴圈依賴,沒有AOP)

  • 上頭的步驟可概括為:
  • 第一步。doCreateBean 進行 circle 的建立,建立步驟為:
  • circle 的流程:
- `AbstractBeanFactory#doGetBean`                               獲取bean
- -> `AbstractAutowireCapableBeanFactory#createBean`            建立bean
- -> `AbstractAutowireCapableBeanFactory#doCreateBean`          開始建立bean
- -> `AbstractAutowireCapableBeanFactory#addSingletonFactory`   把bean的一個 lambda 到三級快取去了 singletonFactories
- -> `AbstractAutowireCapableBeanFactory#populateBean`          填充bean
- -> `AbstractAutowireCapableBeanFactory#applyPropertyValues`   檢查到有要新增的一來 進行填充
- -> `BeanDefinitionValueResolver#resolveValueIfNecessary`      注意 ! 這個位置獲取 loop 物件
```java
- 斷點 我們觀察下 三個快取 Map的儲存情況
```java 
 Map<String, Object> singletonObjects               無 circle 也無 loop 物件
 Map<String, Object> earlySingletonObjects          無 circle 也無 loop 物件
 Map<String, ObjectFactory<?>> singletonFactories   有 circle 也無 loop 物件
 Set<String> singletonsCurrentlyInCreation          有 circle 
  • 第二步 然後 get loop Bean 會重複上面的步驟
- `AbstractBeanFactory#doGetBean`                               獲取bean
- -> `AbstractAutowireCapableBeanFactory#createBean`            建立bean
- -> `AbstractAutowireCapableBeanFactory#doCreateBean`          開始建立bean
- -> `AbstractAutowireCapableBeanFactory#addSingletonFactory`   把bean的一個 lambda 到三級快取去了 singletonFactories
- -> `AbstractAutowireCapableBeanFactory#populateBean`          填充bean
- -> `AbstractAutowireCapableBeanFactory#applyPropertyValues`   檢查到有要新增的一來 進行填充
- -> `BeanDefinitionValueResolver#resolveValueIfNecessary`      注意 ! 這個位置改了。獲取的是 circle 物件
  • 斷點 我們觀察下 三個快取 Map的儲存情況
 Map<String, Object> singletonObjects               無 circle 也無 loop 物件
 Map<String, Object> earlySingletonObjects          無 circle 也無 loop 物件
 Map<String, ObjectFactory<?>> singletonFactories   有 circle 有 loop 物件
 Set<String> singletonsCurrentlyInCreation          有 circle 有 loop 說明兩個物件都在建立中 
  • 關鍵點來了:
  • 第三步 相當於程式是第二次進入 circleAbstractBeanFactory#doGetBean
- `AbstractBeanFactory#doGetBean`                                           第二次獲取 circle
- `AbstractBeanFactory#getSingleton(beanName)`                              獲取 Bean 的快取
- `DefaultSingletonBeanRegistry#getSingleton(beanName, true)`               獲取 Bean 的快取
- `DefaultSingletonBeanRegistry#isSingletonCurrentlyInCreation(beanName)`   關鍵!! 判斷 circle 這個名字的bean是不是在建立過程
- `this.singletonFactories.get(beanName)`                                   獲取這個 circle 的 lambda 建立函式
- `singletonFactory.getObject()`                                            呼叫函式 獲取了一個半成品的物件。 也就是 loop 還為空的 circle物件
- `this.earlySingletonObjects.put(beanName, singletonObject)`               將物件加入到二級快取裡面去   earlySingletonObjects 增加了物件
// 附,只有 earlySingletonObjects 新增了一個 circle 物件,其他map 無改變。 並且loop的 singletonFactories 也未使用到
  • 然後就返回了 circle 給到 loop 進行屬性填充
  • 完成 loop 建立 將 loop 在 (earlySingletonObjects、singletonFactories、singletonsCurrentlyInCreation)清除。loop新增物件到 singletonObjects
  • 返回建立好的 loop 給到 circle 的填充屬性流程
  • 填充完畢之後。在(earlySingletonObjects、singletonFactories、singletonsCurrentlyInCreation)清除。 新增circle物件到 singletonObjects
  • 注意 : circle 就算只是半成品 那他也是在bean中是唯一的。 只要 circle 的屬性在後面填充了loop 那麼在 loop 的那個單例快取裡面。就會有迴圈依賴的 circle 物件
  • 其實在整個流程中 circle 會進入到二級快取當中。但是沒使用。就被remove了
  • loop 在二級快取從來就沒有出現過。因為不會進入兩次 loop 的 doGetBean流程 。 loop的三級快取資料也沒使用過就被刪除了。

2、迴圈依賴,有AOP

程式碼還是非常簡單:spring-circle-aop ,在迴圈依賴的基礎上加了 AOP

比上一種情況多了 AOP,我們來看看物件的建立過程有什麼不一樣;同樣是先建立 Circle ,在建立Loop

建立過程與上一種情況大體一樣,只是有小部分割槽別,跟原始碼的時候我會在這些區別上有所停頓,其他的會跳過,大家要仔細看

例項化 Circle ,然後填充 半成品 circle 的屬性 loop ,去 Spring 容器中獲取 loop 物件,發現沒有

則例項化 Loop ,接著填充 半成品 loop 的屬性 circle ,去 Spring 容器中獲取 circle 物件 

這個過程與前一種情況是一致的,就直接跳過了,此時三級快取中的資料如下:

 Map<String, Object> singletonObjects               無 circle 也無 loop 物件
 Map<String, Object> earlySingletonObjects          無 circle 也無 loop 物件
 Map<String, ObjectFactory<?>> singletonFactories   有 circle 有 loop 物件
 Set<String> singletonsCurrentlyInCreation          有 circle 有 loop 說明兩個物件都在建立中 

image

我們發現從第三級快取獲取 circle 的時候,呼叫了 getEarlyBeanReference 建立了 半成品circle的代理物件

將 半成品 circle 的代理物件放到了第二級快取中,並將代理物件返回賦值給了 半成品 loopcircle 屬性

注意:此時是在進行 loop 的初始化,但卻把 半成品 circle 的代理物件提前建立出來了

loop 的初始化還未完成,我們接著往下看,又是一個重點,仔細看

image

initializeBean 方法中完成了 半成品 loop 的初始化,並在最後建立了 loop 成品 的代理物件

loop 代理物件建立完成之後會將其放入到第一級快取中(移除第三級快取中的 loop ,第二級快取自始至終都沒有 loop

然後將 loop 代理物件返回並賦值給 半成品 circle 的屬性 loop ,接著進行 半成品 circleinitializeBean

image

因為 circle 的代理物件已經生成過了(在第二級快取中),所以不用再生成代理物件了;將第二級快取中的 circle 代理物件移到第一級快取中,並返回該代理物件

此時各級快取中的資料情況如下(普通circle loop 物件在各自代理物件的 target 中)

 Map<String, Object> singletonObjects               有 circle 代理物件 有 loop 代理物件
 Map<String, Object> earlySingletonObjects          無 circle 無 loop 物件
 Map<String, ObjectFactory<?>> singletonFactories   無 circle 無 loop 物件
 Set<String> singletonsCurrentlyInCreation          無 circle 無 loop 

我們回顧下這種情況下各級快取的存在感,一級快取仍是存在感十足,二級快取有存在感,三級快取挺有存在感

      第三級快取提前建立 circle 代理物件,不提前建立則只能給 loop 物件的屬性 circle 賦值成 半成品 circle ,那麼 loop 物件中的 circle 物件就無 AOP 增強功能了

      第二級快取用於存放 circle 代理,用於解決迴圈依賴;也許在這個示例體現的不夠明顯,因為依賴比較簡單,依賴稍複雜一些,就能感受到了
      
第一級快取存放的是對外暴露的物件,可能是代理物件,也可能是普通物件

所以此種情況下:三級快取一個都不能少

概括: (2、迴圈依賴,有AOP)

  • 概括:(迴圈依賴,沒有AOP)基本一致
  • 在第三步發生變化:
- `AbstractBeanFactory#doGetBean`                                           第二次獲取 circle
- `AbstractBeanFactory#getSingleton(beanName)`                              獲取 Bean 的快取
- `DefaultSingletonBeanRegistry#getSingleton(beanName, true)`               獲取 Bean 的快取
- `DefaultSingletonBeanRegistry#isSingletonCurrentlyInCreation(beanName)`   判斷 circle 這個名字的bean是不是在建立過程
- `this.singletonFactories.get(beanName)`                                   獲取這個 circle 的 lambda 建立函式 
- `singletonFactory.getObject()`                                            呼叫函式 獲取了一個半成品的物件。(注意!! 有AOP環繞的物件在該位置會建立代理物件, 並且將代理物件 通過 AbstractAutoProxyCreator#getEarlyBeanReference 同步到AOP的建立類裡邊。為了後面的使用) 也就是 loop 還為空的 circle物件
- `this.earlySingletonObjects.put(beanName, singletonObject)`               將物件加入到二級快取裡面去   earlySingletonObjects 增加了物件
// 附,只有 earlySingletonObjects 新增了一個 circle 物件,其他map 無改變。 
  • 然後就完成了 loop 的建立。
  • 然後進行完 circle 填充之後。
- -> `AbstractAutowireCapableBeanFactory#populateBean`          填充完bean之後
- -> `AbstractAutowireCapableBeanFactory#initializeBean`        進行 circle 的初始化
- -> `AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsAfterInitialization`        bean的後置通知。此位置會進行 bean AOP的環繞 返回代理物件
- 由於在上方 loop 獲取 circle 的時候不是已經建立了個代理物件了嗎。那麼這個aop就不能在新建一個代理類了。不然不一致
- 接著往下看
- -> `AbstractAutoProxyCreator#postProcessAfterInitialization`        建立代理物件
- -> `if (this.earlyProxyReferences.remove(cacheKey) != bean)`        這個時候  二級快取派上用場了。在這裡。判斷是否已經有代理類了。如果有代理類則不新建代理類物件。
// 這樣 circle 的代理就不會被重複建立了。  二級快取也派上了用場

4、迴圈依賴 + AOP + 刪除第三級快取

沒有依賴,有AOP 這種情況中,我們知道 AOP 代理物件的生成是在成品物件建立完成之後建立的,這也是 Spring 的設計原則,代理物件儘量推遲建立

迴圈依賴 + AOP 這種情況中, circle 代理物件的生成提前了,因為必須要保證其 AOP 功能,但 loop 代理物件的生成還是遵循的 Spring 的原則

如果我們打破這個原則,將代理物件的建立邏輯提前,那是不是就可以不用三級快取了,而只用兩級快取了呢?

程式碼依舊簡單:spring-circle-custom ,只是對 Spring 的原始碼做了非常小的改動,改動如下

image

去除了第三級快取,並將代理物件的建立邏輯提前,置於例項化之後,初始化之前;

總結

  1、三級快取各自的作用

    第一級快取存的是對外暴露的物件,也就是我們應用需要用到的

    第二級快取的作用是為了處理迴圈依賴的物件建立問題,裡面存的是半成品物件或半成品物件的代理物件

    第三級快取的作用處理存在 AOP + 迴圈依賴的物件建立問題,能將代理物件提前建立

  2、Spring 為什麼要引入第三級快取

    嚴格來講,第三級快取並非缺它不可,因為可以提前建立代理物件

    提前建立代理物件只是會節省那麼一丟丟記憶體空間,並不會帶來效能上的提升,但是會破環 Spring 的設計原則

    Spring 的設計原則是儘可能保證普通物件建立完成之後,再生成其 AOP 代理(儘可能延遲代理物件的生成)

    所以 Spring 用了第三級快取,既維持了設計原則,又處理了迴圈依賴;犧牲那麼一丟丟記憶體空間是願意接受的

相關文章