title: Android unloading classloader cause crash
body: <p>I'm working on an android app and I'm trying to plugin-like feature which would allow loading additional dex files to extend app functionality. I've figured out how to load additional dex files extending PathClassLoader with few small changes to allow other modules to communicate. The issue is that when the dex files are loaded into the app for the first time when the app is running all works perfectly, then if I decide to disable this module so the classloader is unloaded the app continues to work properly for a few seconds then it throws exception (still continue working properly) and then again few seconds/minutes later (sometimes it takes even 5 minutes) the app crashes with native stack trace. If I decide to load the module which I disabled previously again it will only increase a chance to crash.</p>
  
  <p>This is what happens few seconds module classloader is unloaded:</p>
  
  <pre><code>12-27 01:57:10.839 E/System: Uncaught exception thrown by finalizer
  12-27 01:57:10.840 E/System: java.lang.AssertionError: Failed to close dex file in finalizer.
                                   at dalvik.system.DexFile.finalize(DexFile.java:336)
                                   at java.lang.Daemons$FinalizerDaemon.doFinalize(Daemons.java:250)
                                   at java.lang.Daemons$FinalizerDaemon.runInternal(Daemons.java:237)
                                   at java.lang.Daemons$Daemon.run(Daemons.java:103)
                                   at java.lang.Thread.run(Thread.java:764)
  12-27 01:57:10.840 E/System: Uncaught exception thrown by finalizer
  12-27 01:57:10.840 E/System: java.lang.AssertionError: Failed to close dex file in finalizer.
                                   at dalvik.system.DexFile.finalize(DexFile.java:336)
                                   at java.lang.Daemons$FinalizerDaemon.doFinalize(Daemons.java:250)
                                   at java.lang.Daemons$FinalizerDaemon.runInternal(Daemons.java:237)
                                   at java.lang.Daemons$Daemon.run(Daemons.java:103)
                                   at java.lang.Thread.run(Thread.java:764)
  12-27 01:57:10.841 E/System: Uncaught exception thrown by finalizer
  12-27 01:57:10.841 E/System: java.lang.AssertionError: Failed to close dex file in finalizer.
                                   at dalvik.system.DexFile.finalize(DexFile.java:336)
                                   at java.lang.Daemons$FinalizerDaemon.doFinalize(Daemons.java:250)
                                   at java.lang.Daemons$FinalizerDaemon.runInternal(Daemons.java:237)
                                   at java.lang.Daemons$Daemon.run(Daemons.java:103)
                                   at java.lang.Thread.run(Thread.java:764)
  </code></pre>
  
  <p>Then few seconds or minutes later there is native crash:</p>
  
  <pre><code>12-27 01:57:15.409 A/DEBUG: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
  12-27 01:57:15.409 A/DEBUG: Build fingerprint: 'Sony/G8341/G8341:8.0.0/47.1.A.8.49/3744219090:user/release-keys'
  12-27 01:57:15.409 A/DEBUG: Revision: '0'
  12-27 01:57:15.409 A/DEBUG: ABI: 'arm64'
  12-27 01:57:15.409 A/DEBUG: pid: 17551, tid: 17697, name: Profile Saver  &gt;&gt;&gt; com.rowl.plugdj 
  12-27 01:57:15.409 A/DEBUG: signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x657a6983
  12-27 01:57:15.409 A/DEBUG:     x0   00000000657a6973  x1   0000007bb0739460  x2   0000007bb0600000  x3   0000000000000002
  12-27 01:57:15.409 A/DEBUG:     x4   0000000000000139  x5   0000007bb073947f  x6   2f6d65747379732f  x7   726f77656d617266
  12-27 01:57:15.409 A/DEBUG:     x8   0000000013954588  x9   aec629c3012f5dc2  x10  0000000000000139  x11  000000000000004c
  12-27 01:57:15.409 A/DEBUG:     x12  656d6172662f6b72  x13  72616a2e6b726f77  x14  000000000000000d  x15  aaaaaaaaaaaaaaab
  12-27 01:57:15.409 A/DEBUG:     x16  0000007bca457cc8  x17  0000007bca3f5f60  x18  0000007bc9c07eb0  x19  0000007baecba270
  12-27 01:57:15.409 A/DEBUG:     x20  0000007ba6ab68c0  x21  0000007baecba588  x22  0000007ba6ab4298  x23  0000007baa692b18
  12-27 01:57:15.409 A/DEBUG:     x24  0000000000000000  x25  0000000000002710  x26  00000000977434b0  x27  0000000097746934
  12-27 01:57:15.409 A/DEBUG:     x28  0000007bc99b6b70  x29  0000007baecba1d0  x30  0000007bc96cf67c
  12-27 01:57:15.409 A/DEBUG:     sp   0000007baecba0c0  pc   0000007bc96cf634  pstate 0000000080000000
  12-27 01:57:15.411 A/DEBUG: backtrace:
  12-27 01:57:15.411 A/DEBUG:     #00 pc 00000000002fe634  /system/lib64/libart.so (_ZN3art3jit12JitCodeCache18GetProfiledMethodsERKNSt3__13setINS2_12basic_stringIcNS2_11char_traitsIcEENS2_9allocatorIcEEEENS2_4lessIS9_EENS7_IS9_EEEERNS2_6vectorINS_17ProfileMethodInfoENS7_ISH_EEEE+228)
  12-27 01:57:15.411 A/DEBUG:     #01 pc 000000000030ac08  /system/lib64/libart.so (_ZN3art12ProfileSaver20ProcessProfilingInfoEbPt+1452)
  12-27 01:57:15.411 A/DEBUG:     #02 pc 00000000003099ac  /system/lib64/libart.so (_ZN3art12ProfileSaver3RunEv+704)
  12-27 01:57:15.411 A/DEBUG:     #03 pc 000000000030b7b8  /system/lib64/libart.so (_ZN3art12ProfileSaver21RunProfileSaverThreadEPv+88)
  12-27 01:57:15.411 A/DEBUG:     #04 pc 00000000000667d0  /system/lib64/libc.so (_ZL15__pthread_startPv+36)
  12-27 01:57:15.411 A/DEBUG:     #05 pc 000000000001f2a4  /system/lib64/libc.so (__start_thread+68)
  </code></pre>
  
  <p>Here is the classloader which I'm using</p>
  
  <pre><code>public class ExtensionClassLoader extends PathClassLoader {
  
      private final Map&lt;String, Class&lt;?&gt;&gt; classes = new HashMap();
      private final ExtensionManager extensionManager;
  
      public ExtensionClassLoader(ExtensionManager extensionManager, String dexPath, String librarySearchPath, ClassLoader parent) {
          super(dexPath, librarySearchPath, parent);
          this.extensionManager = extensionManager;
      }
  
      @Override
      protected Class&lt;?&gt; findClass(String name) throws ClassNotFoundException {
          if (name.startsWith("com.rowl.") || name.startsWith("dj.plug.")) {
              throw new ClassNotFoundException(name);
          }
          PlugDJ.d("Find Class: " + name);
          Class clazz = classes.get(name);
          if(clazz == null){
              clazz = extensionManager.getClass(name);
  
              if(clazz == null){
                  clazz = super.findClass(name);
                  if(clazz != null){
                      extensionManager.addClass(name, clazz);
                  }
              }
              if(clazz != null){
                  classes.put(name, clazz);
              }
          }
  
          return clazz;
      }
  
      @Override
      public String findLibrary(String name) {
          return super.findLibrary(name);
      }
  
      public Set&lt;String&gt; getClasses(){
          return classes.keySet();
      }
  
      @Override
      protected void finalize() throws Throwable {
          super.finalize();
      }
  }
  </code></pre>
  
  <p>Additionally here is the class which loads these modules:</p>
  
  <pre><code>class ExtensionManager(plugDJInstance: PlugDJ) : Manager(plugDJInstance) {
  
      private val classes = HashMap&lt;String, Class&lt;*&gt;&gt;()
      private val extensionLoaders = HashMap&lt;String, ExtensionClassLoader&gt;()
  
      private val loadedExtensions = ArrayList&lt;PDJExtension&gt;()
      private val availableExtensions = ArrayList&lt;PDJExtensionInfo&gt;()
      val plugAPI = PlugAPIImpl(pdjInstance as PlugDJAndroid)
      private val context by lazy { (pdjInstance as PlugDJAndroid).context }
      private val extensionSettings by lazy { context.getSharedPreferences("ext", Context.MODE_PRIVATE)}
      private val whitelistedExtensions by lazy { extensionSettings.getStringSet("whitelistedExtensions", HashSet&lt;String&gt;()) }
  
      fun addClass(name: String, clazz: Class&lt;*&gt;){
          if(!classes.containsKey(name)){
              classes.put(name, clazz)
          }
      }
  
      fun removeClass(name: String){
          classes.remove(name)
      }
  
      fun getClass(name: String): Class&lt;*&gt;?{
          return classes[name]
      }
  
      private fun loadApk(dir: File, dexOutputDir: File, parent: ClassLoader): ExtensionClassLoader{
          val files = StringBuilder()
          dir.listFiles().filter { it.extension == "apk" }.forEach {
              files.append(it.absolutePath).append(":")
          }
          files.deleteCharAt(files.length - 1)
          return ExtensionClassLoader(this, files.toString(), dexOutputDir.absolutePath.toString(), parent)
      }
  
      fun isExtensionWhitelisted(extInfo: PDJExtensionInfo): Boolean{
          return whitelistedExtensions.contains(extInfo.appPackage)
      }
  
      @SuppressLint("ApplySharedPref")
      fun addExtensionToWhitelist(extInfo: PDJExtensionInfo){
          whitelistedExtensions.add(extInfo.appPackage)
          extensionSettings.edit().putStringSet("whitelistedExtensions", whitelistedExtensions).commit()
          d("Added " + extInfo.appPackage + " to whitelist")
      }
  
      @SuppressLint("ApplySharedPref")
      fun removeExtensionFromWhitelist(extInfo: PDJExtensionInfo){
          whitelistedExtensions.remove(extInfo.appPackage)
          extensionSettings.edit().putStringSet("whitelistedExtensions", if(whitelistedExtensions.isEmpty()) null else whitelistedExtensions).commit()
          d("Checking... " + whitelistedExtensions)
  
          d("Removed " + extInfo.appPackage + " from whitelist")
      }
  
      fun discoverExtensions() {
          try{
              whitelistedExtensions.forEach {
                  d("Whitelisted extension: $it")
              }
              val installedApps = context.packageManager.getInstalledApplications(PackageManager.GET_META_DATA)
              for(app in installedApps){
                  if(app.sourceDir.contains("system")) continue
                  if(app.metaData != null &amp;&amp; app.metaData.containsKey("pdjExtensionName")){
                      val extName = app.metaData.getString("pdjExtensionName")
                      val extMainClass = app.metaData.getString("pdjExtensionMainClass")
                      val sourceDir = File(app.sourceDir).parentFile
                      PlugDJ.i("Found extension: " + extName)
                      var extInfo = getAvailableExtensions().find { it.appPackage == app.packageName }
                      d(extInfo?.sourceDirectory.toString() )
                      d(sourceDir.toString() )
                      if(extInfo != null){
                          PlugDJ.d("Removing " + app.packageName)
                          availableExtensions.remove(extInfo)
                      }
                      extInfo = PDJExtensionInfoImpl(extName, sourceDir, app.packageName, extMainClass, "", "", null, null)
                      availableExtensions.add(extInfo)
                  }
              }
          }catch (e: Exception){
              e.printStackTrace()
          }
      }
  
  
  
      fun getAvailableExtensions(): List&lt;PDJExtensionInfo&gt;{
          return Collections.unmodifiableList(ArrayList(availableExtensions))
      }
  
      fun getLoadedExtensions(): List&lt;PDJExtension&gt;{
          return Collections.unmodifiableList(ArrayList(loadedExtensions))
      }
  
      fun loadExtension(extInfo: PDJExtensionInfo): PDJExtension{
          var cl = context.classLoader
          val dexOutputDir = context.getDir("dex", Context.MODE_PRIVATE)
          val extcl = loadApk(extInfo.sourceDirectory, dexOutputDir, cl)
          extensionLoaders[extInfo.name] = extcl
          val mainClass = extcl.loadClass(extInfo.mainClass)
          val apiField = mainClass.superclass.getDeclaredField("api")
          val clField = mainClass.superclass.getDeclaredField("classLoader")
          val extField = mainClass.superclass.getDeclaredField("extensionInfo")
          val extension = mainClass.newInstance() as PDJExtension
          apiField.apply {
              isAccessible = true
              set(extension, plugAPI)
              isAccessible = false
          }
          clField.apply {
              isAccessible = true
              set(extension, mainClass.classLoader)
              isAccessible = false
          }
          extField.apply {
              isAccessible = true
              set(extension, extInfo)
              isAccessible = false
          }
          loadedExtensions.add(extension)
          return extension
      }
  
      fun getLoadedExtension(appPackage: String): PDJExtension? {
          return getLoadedExtensions().find { it.extensionInfo.appPackage == appPackage }
      }
  
      fun enableExtension(ext: PDJExtension){
          ext.onEnable()
          ext::class.java.superclass.getDeclaredField("isEnabled").apply {
              isAccessible = true
              set(ext, true)
              isAccessible = false
          }
  
      }
  
      fun disableExtension(ext: PDJExtension){
          try{
              plugAPI.removeEventListeners(ext)
              ext.onDisable()
          }catch (e: Exception){
              e.printStackTrace()
          }
  
          ext::class.java.superclass.getDeclaredField("isEnabled").apply {
              isAccessible = true
              set(ext, false)
              isAccessible = false
          }
  
  
      }
  
      fun unloadExtension(ext: PDJExtension){
          d("Unloading extension...")
          ext::class.java.superclass.getDeclaredField("api").apply {
              isAccessible = true
              set(ext, null)
              isAccessible = false
          }
          val cl = ext.classLoader as ExtensionClassLoader
          loadedExtensions.remove(ext)
          for(className in cl.classes){
              removeClass(className)
          }
          extensionLoaders.remove(ext.extensionInfo.name)
          d("Extension unloaded.")
          System.runFinalization()
          System.gc()
      }
  
  }
  </code></pre>
  
  <p>Additionally, here is the full log <a href="https://pastebin.com/Jhpk4Wpc" rel="nofollow noreferrer">https://pastebin.com/Jhpk4Wpc</a></p>
  
  <p>Any idea what causes it?</p>