JDK加密內存溢出問題排查

在項目開發上線的過程中,最近發現一個加解密服務隔7天左右就會出現以下問題:
Exception in thread “Timer-0” java.lang.OutOfMemoryError: Java heap space

(一開始使用findBugs進行掃描,並未掃描出可用結果)

首先,介紹一個免費開源分析dump的軟件Memory Analyzer,下載地址如下所示:(同事介紹)

http://www.eclipse.org/mat/downloads.php
此工具需要依賴於dump,可以根據以下命令生成JVM的dump文件

ps aux | grep xxx 查詢進程ID

jmap -dump:live,format=b,file=文件名.bin 進程ID

可將生成的dump文件下載到本地,使用Memory Analyzer打開其文件進行分析。

最好在進程剛剛開啓時就生成一個dump文件,在服務使用一段時間後再生成一個dump文件,兩個文件進行對比排除內存漏洞問題。

經過排查發現漏洞問題如下所示:

在這裏插入圖片描述

發現問題出現在了JceSecurity的verificationResults 屬性上,在verificationResults 屬性中存在了過多的BouncyCastleProvider,隨着應用的使用正在不斷的增多,未被GC回收。

對javax.crypto.JceSecurity進行反編譯查看代碼發現verificationResults 是static 類屬性,GC不會自動對其永久代進行回收。

對項目代碼進行排查,發現項目中使用代碼BouncyCastleProvider使用代碼如下所示:

Java代碼 收藏代碼

Cipher ci = Cipher.getInstance("RSA", new BouncyCastleProvider());  

發現是坑的代碼,在服務每次使用的時候都會重新創建一個BouncyCastleProvider用來進行初始化密鑰的工具類。

public static final Cipher getInstance(String paramString, Provider paramProvider)  
    throws NoSuchAlgorithmException, NoSuchPaddingException{  
    if (paramProvider == null) {  
      throw new IllegalArgumentException("Missing provider");  
    }  
    Object localObject1 = null;  
    List localList = getTransforms(paramString);  
    int i = 0;  
    String str = null;  
    for (Iterator localIterator = localList.iterator(); localIterator.hasNext(); ) {  
      Transform localTransform = (Transform)localIterator.next();  
      Provider.Service localService = paramProvider.getService("Cipher", localTransform.transform);  
      if (localService == null)  
        continue;  
      Object localObject2;  
      Object localObject3;  
      if (i == 0){  
        localObject2 = JceSecurity.getVerificationResult(paramProvider);  
        if (localObject2 != null) {  
          localObject3 = "JCE cannot authenticate the provider " + paramProvider.getName();  
  
          throw new SecurityException((String)localObject3, (Throwable)localObject2);  
        }  
        i = 1;  
      }  
      if (localTransform.supportsMode(localService) == 0) {  
        continue;  
      }  
      if (localTransform.supportsPadding(localService) == 0) {  
        str = localTransform.pad;  
      }  
      try{  
        localObject2 = (CipherSpi)localService.newInstance(null);  
        localTransform.setModePadding((CipherSpi)localObject2);  
        localObject3 = new Cipher((CipherSpi)localObject2, paramString);  
        ((Cipher)localObject3).provider = localService.getProvider();  
        ((Cipher)localObject3).initCryptoPermission();  
        return localObject3;  
      } catch (Exception localException) {  
        localObject1 = localException;  
      }  
    }  
    if (localObject1 instanceof NoSuchPaddingException) {  
      throw ((NoSuchPaddingException)localObject1);  
    }  
    if (str != null) {  
      throw new NoSuchPaddingException("Padding not supported: " + str);  
    }  
    throw new NoSuchAlgorithmException("No such algorithm: " + paramString, localObject1);  
  }  

可查看BouncyCastleProvider代碼發現此類進行過特殊處理,每次new出的實例hashCode是相同的。又對JceSecurity.getVerificationResult方法代碼進行了分析,代碼如下所示:

static synchronized Exception getVerificationResult(Provider paramProvider){  
    Object localObject1 = verificationResults.get(paramProvider);  
    if (localObject1 == PROVIDER_VERIFIED)  
      return null;  
    if (localObject1 != null) {  
      return (Exception)localObject1;  
    }  
    if (verifyingProviders.get(paramProvider) != null)  
    {  
      return new NoSuchProviderException("Recursion during verification");  
    }Exception localException2;  
    try {  
      verifyingProviders.put(paramProvider, Boolean.FALSE);  
      URL localURL = getCodeBase(paramProvider.getClass());  
      verifyProviderJar(localURL);  
  
      verificationResults.put(paramProvider, PROVIDER_VERIFIED);  
      localException2 = null;  
  
      return localException2;  
    }  
    catch (Exception localException1){  
      verificationResults.put(paramProvider, localException1);  
      localException2 = localException1;  
      return localException2; } finally { verifyingProviders.remove(paramProvider); }  
  }  

查找到這裏發現自己越來越矛盾,每次new出來的BouncyCastleProvider具有相同的hashCode,放在verificationResults 屬性Map中怎麼會越來越多,後一個應當會將前一個覆蓋纔對,怎麼會導致內存溢出。

最終實在無頭緒請教同事,發現一個verificationResults屬性定義的居然是IdentityHashMap,此Map在存儲類的時候並不是使用類的equals方法來判斷是否Key已經存在,而是使用來判斷是否Key已經存在的。換句話說就是當兩個對象不那此Map就會將兩個對象都存進去。

找到這裏問題的解決方案就已經非常明瞭了,只要給BouncyCastleProvider定義成單例就可以了。
這裏BouncyCastleProvider只需要保證是一個,可以使用代碼Security.addProvider(new BouncyCastleProvider())來保證

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