<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Java class被提前加載之深度歷險記!

          共 15848字,需瀏覽 32分鐘

           ·

          2022-01-22 12:53

          你知道的越多,不知道的就越多,業(yè)余的像一棵小草!

          你來,我們一起精進(jìn)!你不來,我和你的競爭對手一起精進(jìn)!

          編輯:業(yè)余草

          juejin.cn/post/7047014740693876744

          推薦:https://www.xttblog.com/?p=5306

          1. 先說問題

          我司搭建了一個類似于Skywalking的字節(jié)碼插件平臺?;驹韰⒖?span style="color: rgb(89, 89, 89);">談?wù)凧ava Intrumentation和相關(guān)應(yīng)用[1] 。所以我們就編寫了各種神奇的插件。其中就有一個使用Sentinel限流MQ的插件。其核心邏輯就是,當(dāng)用戶空間有Sentinel相關(guān)類的時候,就使用Sentinel來做限流。

          下面這個SentinelUtil類是用來判斷是否有相關(guān)Sentinel

          import com.alibaba.csp.sentinel.common.support.CircuitBreakerSupport;

          public class SentinelUtil {
              private static boolean sentinelDisabled = true;

              static {
                  try {
                      //檢測相關(guān)類和對應(yīng)的方法是否存在
                      final Class<?> circuitBreakerSupportClass = Class.forName("com.alibaba.csp.sentinel.common.support.CircuitBreakerSupport"false, SentinelUtil.class.getClassLoader());
                      sentinelDisabled = false;
                  } catch (Throwable throwable) {
                  }
              }

              private SentinelUtil() {

              }

              public static boolean sentinelDisabled() {
                  return sentinelDisabled;
              }
          }

          下面是MQ消費(fèi)的邏輯:不存在Sentinel相關(guān)依賴就直接消費(fèi),存在的時候使用Sentinel限流消費(fèi)

          public abstract class BaseConcurrentMessageListener implements MessageListenerConcurrently {
           // ....
           @Override
              public ConsumeConcurrentlyStatus consumeMessage(final List<MessageExt> msgs, final ConsumeConcurrentlyContext context) {
                  final MessageExt messageExt = msgs.get(0);

                  // 不存在Sentinel相關(guān)依賴的時候就直接消費(fèi)
            if (SentinelUtil.sentinelDisabled()) {
             return consumeInner(messageExt);
            }

            // 存在Sentinel相關(guān)類的時候就直接使用Sentinel來限流消費(fèi)
            return CircuitBreakerSupport.syncExecute(resourceName, resourceType, origin,
             new CircuitBreakerCallback<ConsumeConcurrentlyStatus>() {
              @Override
              public ConsumeConcurrentlyStatus doWithCircuitBreaker() {
               // normal consumer logic
                              return consumeInner(messageExt);
              }
             },
             new CircuitBreakerFallback<ConsumeConcurrentlyStatus>() {
              @Override
              public ConsumeConcurrentlyStatus fallBack() {
               // fallBack logic 
              }
             });

              }
          }

          這里補(bǔ)充一下CircuitBreakerSupport用到的兩個接口的定義

          public interface CircuitBreakerCallback<T{
              doWithCircuitBreaker();
          }

          public interface CircuitBreakerFallback<T{
              fallBack();
          }

          此時,我們對Sentinel的依賴是provided級別。

          <!-- 此依賴是我司對Sentinel的簡單封裝的jar包,用來簡化Sentinel的使用 -->
              <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-common</artifactId>
            <scope>provided</scope>
          </dependency> 

          所以上面的代碼可以正常編譯,但是運(yùn)行期正常情況下會根據(jù)用戶空間有沒有scope=compile級別的該依賴來走不同的邏輯。

          我們做完這個兼容判斷后給自己的評價就是:完美。然后我們本地做了自測,測試了有Sentinel compile的依賴以及沒有該依賴的場景,都沒什么問題,完全在我們的意料之中。

          但是,當(dāng)我們把這個插件放開后真實(shí)地在開發(fā)環(huán)境跑的時候直接啟動失敗,拋出了一個java.lang.NoClassDefFoundError異常。

          2. 初步分析

          看到上面那個錯誤,我們初步分析如下:

          • 用戶應(yīng)該是沒有Sentinel的依賴的,不然不會找不到類
          • 這個錯誤的原因肯定不是運(yùn)行了BaseConcurrentMessageListener的consumeMessage方法導(dǎo)致的。因為如果是運(yùn)行時發(fā)生的話,應(yīng)該因為有了判斷Sentinel是否存在的邏輯,所以不會走到CircuitBreakerSupport的syncExecute方法。而且,我們根本就沒有發(fā)送消息,也就不會出發(fā)消費(fèi)邏輯。

          然后我們繼續(xù)看異常棧,發(fā)現(xiàn)是這一行導(dǎo)致的異常:

          我們找到那一行代碼,如下:

          public class DefaultRMQConsumer extends AbstractClientConfig {
           private DefaultMQPushConsumer createConsumer(...) throws MQClientException {
                  //...
            
                  // 就是這一行導(dǎo)致的錯誤
                  baseConcurrentMessageListener = new NormalConcurrentMessageListener(nameServerAlias, subscribeTable);
            
            //...
              }
          }

          我們發(fā)現(xiàn),這一行代碼與我們代碼發(fā)送唯一關(guān)聯(lián)的就是NormalConcurrentMessageListenerBaseConcurrentMessageListener的子類。根據(jù)周志明大大總結(jié)的類加載的知識

          「new」一個NormalConcurrentMessageListener確實(shí)會導(dǎo)致加載其父類BaseConcurrentMessageListener。但問題是:CircuitBreakerFallback只是BaseConcurrentMessageListener 類的一個方法中使用的類。按照周志明大大總結(jié)的類加載的知識,不應(yīng)該是主動使用CircuitBreakerFallback的時候才會加載該類的嗎?在沒有主動使用的時候是不應(yīng)該被加載的。

          所以總結(jié)起來,按照我掌握的常規(guī)知識與現(xiàn)象來解釋的話是自相矛盾的:

          1. 這個異常應(yīng)該是主動使用該類的時候才會拋出,也就是實(shí)際運(yùn)行BaseConcurrentMessageListener的consumeMessage方法才會拋出。
          2. 如果我們承認(rèn)上面一個結(jié)論是正確的話,那么又會導(dǎo)致實(shí)際不會執(zhí)行到CircuitBreakerFallback的方法,也就不會觸發(fā)上面的異常。

          好吧,我要崩潰了。。。

          我的內(nèi)心幾乎是崩潰的

          再用我簡單的小腦袋瓜總結(jié)一下,現(xiàn)在我們有兩個問題難以理解:

          ?
          1. 為什么本地沒有出現(xiàn)這個異常,到了開發(fā)環(huán)境就有了這個異常?
          2. 為什么方法中用到的類被提前加載了?
          ?

          3. 我的瞎想

          根據(jù)上面 的兩個問題,我自然第一步就聯(lián)想到了可能的原因:是不是JVM的鍋?

          ?

          難道是JVM在Linux平臺上的實(shí)現(xiàn)有bug,在windows(我本機(jī)是windows)和mac(其他同事用的mac也是一樣的問題)上的實(shí)現(xiàn)沒有bug?這個bug就是:某些情況下會導(dǎo)致類的提前加載。

          ?

          然后我就去JDK官方issue管理渠道(JBS - JDK Bug System:https://bugs.openjdk.java.net/projects/JDK/issues/JDK-8279300?filter=allopenissues)搜索了ClassLoader相關(guān)的issue。

          JDK官方issue

          然后我就一個個翻閱了相關(guān)的issue。果然jdk還是靠譜的。

          4. 我的猜想

          在經(jīng)過上面一輪「瞎想」之后,我開始反思這個過程可能的原因。然后我又去翻閱了周志明大大關(guān)于類加載方面的所有知識。果然,被我翻到了一點(diǎn)蛛絲馬跡:

          從這段話中,我們可以讀出兩點(diǎn):

          1. 類加載的時機(jī)是不確定的,但是類初始化的時機(jī)是由JVM規(guī)范固定的那5種情況
          2. 類加載和類的初始化大部分情況下是同時發(fā)生的,但是少數(shù)情況還是有可能只發(fā)送類的加載,不發(fā)生類的初始化的

          結(jié)合到我們這個場景下,實(shí)際上就是提前加載了類,但是估計沒有初始化。

          ?

          那到底什么情況下會提前(這里的提前是指沒有主動使用類)加載類,但是不發(fā)生類的初始化呢?

          ?

          5. 歪打正著

          那既然遇到這個問題了,而且我們還不知道是啥原因的情況下,我們又該怎么解決呢?

          你慌了嗎

          我們再來分析下,其實(shí)像我們這種處理方式,在很多其他的框架中應(yīng)該都有類似的方式。

          ?

          就判斷有沒有這個類,有的話就使用這個類提供的方法等。沒有的話走兜底邏輯。這種兼容邏輯在開源框架中應(yīng)該都有類似的解決方案。那為什么開源框架沒有出現(xiàn)這種問題呢?

          ?

          肯定有某些條件限制住了該異常的發(fā)生。那到底是什么條件呢?

          然后,我們就開始了嘗試。既然找不到類,那我把找不到的那個類隱藏到另外一個類中是不是就可以了呢?

          大體方案就是把限流邏輯隱藏到SentinelUtil中,然后調(diào)用SentinelUtil 的限流方法來做

          public class SentinelUtil {
              private static boolean sentinelDisabled = true;

              static {
                  try {
                      //檢測相關(guān)類和對應(yīng)的方法是否存在
                      final Class<?> circuitBreakerSupportClass = Class.forName("com.alibaba.csp.sentinel.common.support.CircuitBreakerSupport"false, SentinelUtil.class.getClassLoader());
                      sentinelDisabled = false;
                  } catch (Throwable throwable) {
                  }
              }

              private SentinelUtil() {

              }

              public static boolean sentinelDisabled() {
                  return sentinelDisabled;
              }
              
              /**
               * 把限流邏輯移到該方法中
               */

              public static <T> supplySyncExecute(String resourceName, int resourceType, Supplier<T> circuitBreakerCallback, Supplier<T> fallback) {
                 // 存在Sentinel相關(guān)類的時候就直接使用Sentinel來限流消費(fèi)
            return CircuitBreakerSupport.syncExecute(resourceName, resourceType, origin,
             new CircuitBreakerCallback<ConsumeConcurrentlyStatus>() {
              @Override
              public ConsumeConcurrentlyStatus doWithCircuitBreaker() {
               // normal consumer logic
                                                  return circuitBreakerCallback.get();
              }
             },
             new CircuitBreakerFallback<ConsumeConcurrentlyStatus>() {
              @Override
              public ConsumeConcurrentlyStatus fallBack() {
               return fallback.get(); 
              }
             });
              }
              
              private ConsumeConcurrentlyStatus consumeInne(...){
                  //...消費(fèi)邏輯
              }
          }

          消費(fèi)監(jiān)聽器更改如下:

          public abstract class BaseConcurrentMessageListener implements MessageListenerConcurrently {
           // ....
           @Override
              public ConsumeConcurrentlyStatus consumeMessage(final List<MessageExt> msgs, final ConsumeConcurrentlyContext context) {
                  final MessageExt messageExt = msgs.get(0);

                  // 不存在Sentinel相關(guān)依賴的時候就直接消費(fèi)
            if (SentinelUtil.sentinelDisabled()) {
             return consumeInner(messageExt);
            }
            // 限流邏輯調(diào)用SentinelUtil的方法
                          // 把限流邏輯以及
            return SentinelUtil.supplySyncExecute(...,
             new Supplier<ConsumeConcurrentlyStatus>() {
              @Override
              public ConsumeConcurrentlyStatus get() {
               // 消費(fèi)邏輯
              }
             },
             new Supplier<ConsumeConcurrentlyStatus>() {
              @Override
              public ConsumeConcurrentlyStatus get() {
                 // fall back邏輯
              }
             }
            );

              }
          }

          然后,下面就是見證奇跡的時刻了。我們在開發(fā)環(huán)境測試竟然沒有那個神奇的異常了...

          意不意外!驚不驚喜
          ?

          所以,隱藏是有用的。我只要退后一步,JVM就不需要看到我了?。?!

          ?

          6. 意外之喜

          雖然,我們也不知道為啥就解決了上面的那個問題。但是心總是懸著的。因為在本地?zé)o法復(fù)現(xiàn),只能在開發(fā)環(huán)境驗證。那就是說,隨時都有可能在本地?zé)o法復(fù)現(xiàn),在其他環(huán)境有可能復(fù)現(xiàn)。那這種風(fēng)險實(shí)際是挺大的。尤其如果沒有經(jīng)過開發(fā)和測試環(huán)境的驗證就直接上生產(chǎn)環(huán)境的話,就可能直接嗝屁了。

          我跑路了拜拜

          所以,一直在搜索,卻一直沒有任何大佬給出相關(guān)的解釋。

          ?

          然而,驗證了一句話,叫做:再牛逼的難題,也抵不住傻逼似的堅持。

          ?

          終于在某乎上搜索到了我想要的答案:

          關(guān)于Java class被提前加載的問題記錄:https://zhuanlan.zhihu.com/p/417557986

          大家有興趣可以看一下大佬的解答。這個博客不僅有實(shí)驗代碼,還有JVM規(guī)范內(nèi)容??梢哉f是牛逼大發(fā)了,正是我想要的。

          這篇文章總結(jié)起來,就以下幾點(diǎn):

          ?
          1. 在一個類中存在這種涉及類型cast,即使是隱式的子類cast成父類的行為,就可能導(dǎo)致父類和子類被提前加載。

          2. 這種提前加載的行為是發(fā)生在校驗字節(jié)碼階段

          ?

          7. 驗證結(jié)論

          我們按照上面博客的內(nèi)容,自己做了對應(yīng)的實(shí)驗,確實(shí)如博客中所說的一樣,在有類型轉(zhuǎn)換的時候,會導(dǎo)致這種提前加載類的行為。

          ?

          那既然這種行為發(fā)生在字節(jié)碼校驗階段,那是不是說我只要不校驗字節(jié)碼,這種提前加載的行為就不會發(fā)生呢?

          ?

          正好,JVM提供了相關(guān)的參數(shù)可以用來控制是否驗證字節(jié)碼

          -Xverify:none  
          // 或者 
          -noverify

          然后,我們就在開發(fā)環(huán)境中先使用我們第一版的代碼(出現(xiàn)java.lang.NoClassDefFoundError異常的代碼)跑了一下確實(shí)還是會拋出java.lang.NoClassDefFoundError異常。

          然后,我們給JVM加上-noverify參數(shù)(或者-Xverify:none )。神奇的事情發(fā)生了,沒有異常了。意不意外,驚不驚喜。

          意不意外!驚不驚喜!

          8. 峰回路轉(zhuǎn)

          再回首一下我們之前的兩個問題:

          ?
          1. 為什么本地沒有出現(xiàn)這個異常,到了開發(fā)環(huán)境就有了這個異常?
          2. 為什么方法中用到的類被提前加載了?
          ?

          現(xiàn)在第二個問題,其實(shí)我們已經(jīng)有答案了:因為在調(diào)用CircuitBreakerSupport的syncExecute方法的時候需要接受一個CircuitBreakerCallback以及CircuitBreakerFallback接口類型的參數(shù)。又因為實(shí)際傳入的是一這兩個接口類型的匿名內(nèi)部類,所以在加載BaseConcurrentMessageListener類的時候需要校驗這種存在類型轉(zhuǎn)換的情況,需要需要提前加載接口CircuitBreakerCallback以及CircuitBreakerFallback。所以發(fā)生了java.lang.NoClassDefFoundError異常。

          并且,這種情況實(shí)際上在JVM規(guī)范中是有提到的:

          鏈接如下:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.4.1。

          那既然如此,到底是為什么我們在本地沒有出現(xiàn)這個異常呢?

          允許你們停頓一下,思考研究個幾分鐘吧!

          允許你們停頓一下
          ?

          1分鐘 2分鐘 n分鐘 。。。

          ?

          好吧,不賣關(guān)子了,直接說出我當(dāng)時的想法吧。

          ?

          既然這個問題只要我們加上-noverify參數(shù)(或者-Xverify:none )就不會出現(xiàn)該問題,那我們本地開發(fā)的時候是不是ide開發(fā)工具自動幫我們加上了這個參數(shù)了呢?

          ?

          然后,一啟動,一看,世界都亮了。。。。

          9. 一探到底

          我自己又沒有加上這個參數(shù),那究竟是為啥ide要為我加上這樣一個神奇的參數(shù)呢?然后我就百度了下,真被我找到原因了,竟然是因為這個:

          一探到底

          好了,一切真相大白了。

          ?

          對于SpringBoot項目,【Enbale launch optimization】選項默認(rèn)是勾選上的。這個選項會給JVM加上兩個參數(shù)(其中一個就是-noverify參數(shù))。然后我們的異常只會出現(xiàn)在字節(jié)碼的驗證階段。由于-noverify參數(shù)關(guān)掉了字節(jié)碼校驗,所以本地是不會出現(xiàn)該異常的。

          ?

          10. 如何解決

          上面,我們討論了提前加載的原因(可能是一部分原因)。那我們編碼的時候如果規(guī)避掉提前加載的問題呢?

          1. 退后一步:將需要校驗的類放到另外一個類中(我們之前的解決方案就是這種方案)
          2. 盡量使用lamda表達(dá)式

          對于第一種解決方案其實(shí)比較好理解,那第二種解決方案究竟是什么意思呢?

          我們來看一下具體的代碼(詳細(xì)代碼加我微信:xttblog2),下面就是最核心的測試代碼:

          package com.demo.load.lambda;

          import com.alibaba.fastjson.serializer.JSONSerializer;
          import com.alibaba.fastjson.serializer.ObjectSerializer;

          import java.io.IOException;
          import java.lang.reflect.Type;

          public class InterfacesTest {
              public static void sayHello() {
                  System.out.println("hello");
              }

              public static void testInterfaces(){
                  InterfacesHolder holder = new InterfacesHolder();

                  // 不會拋出異常
                  // 原因:lambda表達(dá)式在編譯期只會生成方法名類似于lambda$0的靜態(tài)私有方法,不會生成對應(yīng)接口實(shí)現(xiàn)類的class,對應(yīng)class是在運(yùn)行期生成
                  //      所以在校驗本類的字節(jié)碼的時候是不需要校驗類型的
                  // 關(guān)于lambda表達(dá)式的實(shí)現(xiàn)原理參考:https://www.cnblogs.com/WJ5888/p/4667086.html
                  //holder.invokeInterfaces((serializer, object, fieldName, fieldType, features) -> {
                      // do nothing
                  //});

                  // 會拋出異常
                  // 原因:匿名內(nèi)部類在編譯期就生成了對應(yīng)接口的實(shí)現(xiàn)類,所以在校驗本類字節(jié)碼的時候會校驗類型
                  holder.invokeInterfaces(new ObjectSerializer() {
                      @Override
                      public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features) throws IOException {
                          // do nothing
                      }
                  });
              }

              public static void main(String[] args) {
                  InterfacesTest.sayHello();
              }

          }
          ?

          你會發(fā)現(xiàn),使用lamda時不會拋出異常的,但是使用匿名內(nèi)部類是會拋出異常的。

          ?

          是不是已經(jīng)智商不夠用了呢?

          下面我們簡單分析下(太深入分析可能需要了解比較多的lamda表達(dá)式的實(shí)現(xiàn)原理):

          對于匿名內(nèi)部類類,在編譯期會生成一個對應(yīng)的子類:

          簡單反編譯

          那實(shí)際上這個場景跟我們一開始遇到的場景是一樣的。所以還是會拋異常。

          那為什么使用lamda表達(dá)式就不會拋出異常呢?

          首先,使用lamda表達(dá)式是不會在編譯期生成對應(yīng)接口的實(shí)現(xiàn)類或者父類的子類的:

          其次,實(shí)際上lamda表達(dá)式也會生成實(shí)現(xiàn)類,但是是在運(yùn)行期動態(tài)生成的。

          所以,這樣就比較好理解了,因為lamda表達(dá)式是在運(yùn)行期生產(chǎn)的子類,所以在校驗字節(jié)碼的時候根本無法校驗。但是匿名內(nèi)部類在編譯期就生產(chǎn)了子類,所以在字節(jié)碼校驗的時候就可以校驗對應(yīng)的子類了。

          例子中,還有其他的幾種情況會導(dǎo)致類的提前加載,這里簡單總結(jié)一下:

          1. 存在類型轉(zhuǎn)換的情況
          2. catch塊中使用異常的情況(這種情況我沒有在JVM規(guī)范中找到對應(yīng)的說明)

          11. 總結(jié)一下

          1. 類的加載和類的初始化,大部分情況下是同時觸發(fā)的,少數(shù)情況下只有類的加載,沒有類的初始化
          2. 如果存在類型轉(zhuǎn)換,可能會導(dǎo)致會導(dǎo)致提前加載接口或者父類。如果catch塊中顯示使用異常的情況,那么就會導(dǎo)致提前加載異常類。
          3. 在使用SpringBoot測試的時候,對于開發(fā)字節(jié)碼植入邏輯的同學(xué)來說,一定要關(guān)掉【Enbale launch optimization】選項來測試。

          瀏覽 60
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  在线日韩一区二区 | 亚洲不卡黄色网址 | 97在线国产 | 国内精品久久久久久久久变脸 | 夜夜爽综合|