I work on identifying and fixing memory leaks in Android Studio. For our canary users, when Android Studio is running low on memory we’ll take a heap dump, analyze it, and send back a report with some diagnostic information. Several reports over the last few months have shown large (often multi-gigabyte) structures held by org.sonarsource.kotlin.api.regex.RegexContext.globalCache
(BindingContexts, various indices).
Here’s a leaktrace showing paths from GC roots to large structures in the heap:
Subtree size #instances
3.81GB 1 ROOT: Static field: org.sonarsource.kotlin.api.regex.RegexContext.globalCache
3.81GB 1 (root): java.util.LinkedHashMap
3.72GB 1 +-table: java.util.HashMap$Node[]
3.72GB 407 | []: java.util.LinkedHashMap$Entry
3.72GB 407 | value: org.sonarsource.analyzer.commons.regex.RegexParseResult
2.31GB 245 | +-result: org.sonarsource.analyzer.commons.regex.ast.SequenceTree
2.31GB 245 | | source: org.sonarsource.kotlin.api.regex.KotlinAnalyzerRegexSource
2.31GB 63 | | textRangeTracker: org.sonarsource.kotlin.api.regex.TextRangeTracker
2.31GB 63 | | textRange: org.sonarsource.kotlin.api.regex.TextRangeTracker$Companion$of$1
2.31GB 63 | | $kotlinFileContext: org.sonarsource.kotlin.plugin.KotlinFileContext
668MB 63 | | +-ktFile: org.jetbrains.kotlin.psi.KtFile
656MB 63 | | | myManager: org.jetbrains.kotlin.com.intellij.psi.impl.PsiManagerImpl
155MB 63 | | | +-myProject: org.jetbrains.kotlin.com.intellij.mock.MockProject
147MB 63 | | | | myPicoContainer: org.jetbrains.kotlin.com.intellij.mock.MockComponentManager$1
146MB 63 | | | | parent: org.jetbrains.kotlin.com.intellij.mock.MockComponentManager$1
146MB 63 | | | | this$0: org.jetbrains.kotlin.com.intellij.core.CoreApplicationEnvironment$1
146MB 63 | | | | myExtensionArea: org.jetbrains.kotlin.com.intellij.openapi.extensions.impl.ExtensionsAreaImpl
146MB 63 | | | | extensionPoints: java.util.concurrent.ConcurrentHashMap
146MB 63 | | | | table: java.util.concurrent.ConcurrentHashMap$Node[]
145MB 630 | | | | []: java.util.concurrent.ConcurrentHashMap$Node
145MB 630 | | | | val: org.jetbrains.kotlin.com.intellij.openapi.extensions.impl.InterfaceExtensionPoint
145MB 63 | | | | pluginDescriptor: org.jetbrains.kotlin.com.intellij.ide.plugins.IdeaPluginDescriptorImpl
145MB 63 | | | | basePath: jdk.nio.zipfs.ZipPath
145MB 63 | | | | zfs: jdk.nio.zipfs.ZipFileSystem
145MB 63 * | | | | cen: byte[]
462MB 63 | | | +-myFileIndex: org.jetbrains.kotlin.com.intellij.openapi.util.NotNullLazyValue$2
462MB 63 | | | | myValue: org.jetbrains.kotlin.com.intellij.mock.MockFileIndexFacade
462MB 63 | | | | myLibraryRoots: java.util.ArrayList
462MB 63 | | | | elementData: java.lang.Object[]
384MB 15246 | | | | +-[]: org.jetbrains.kotlin.com.intellij.openapi.vfs.impl.jar.CoreJarVirtualFile
...
1.08GB 63 | | +-bindingContext: org.jetbrains.kotlin.resolve.BindingTraceContext$1
1.08GB 63 | | | this$0: org.jetbrains.kotlin.cli.jvm.compiler.NoScopeRecordCliBindingTrace
895MB 63 | | | +-map: org.jetbrains.kotlin.util.slicedMap.SlicedMapImpl
895MB 63 | | | | map: org.jetbrains.kotlin.util.slicedMap.OpenAddressLinearProbingHashTable
895MB 63 | | | | array: java.lang.Object[]
204MB 16441 | | | | +-[]: org.jetbrains.kotlin.com.intellij.util.keyFMap.PairElementsFMap
66.6MB 1365 | | | | | +-value1: org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedSimpleFunctionDescriptor
46.2MB 126 | | | | | | containingDeclaration: org.jetbrains.kotlin.load.java.lazy.descriptors.LazyJavaPackageFragment
40.0MB 126 | | | | | | scope: org.jetbrains.kotlin.load.java.lazy.descriptors.JvmPackageScope
37.8MB 126 | | | | | | kotlinScopes$delegate: org.jetbrains.kotlin.storage.LockBasedStorageManager$LockBasedNotNullLazyValue
37.8MB 126 | | | | | | value: org.jetbrains.kotlin.resolve.scopes.MemberScope[]
37.8MB 3339 | | | | | | []: org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedPackageMemberScope
26.5MB 3339 | | | | | | impl: org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedMemberScope$OptimizedImplementation
18.2MB 3213 | | | | | | functionProtosBytes: java.util.LinkedHashMap
15.4MB 1827 | | | | | | table: java.util.HashMap$Node[]
14.5MB 42021 | | | | | | []: java.util.LinkedHashMap$Entry
8.68MB 42021 * | | | | | | value: byte[]
62.7MB 814 | | | | | \-value2: org.jetbrains.kotlin.load.java.lazy.descriptors.LazyJavaClassDescriptor
36.4MB 756 | | | | | +-jClass: org.jetbrains.kotlin.load.java.structure.impl.classFiles.BinaryJavaClass
7.37MB 756 | | | | | | context: org.jetbrains.kotlin.load.java.structure.impl.classFiles.ClassifierResolutionContext
...
Unfortunately I don’t know what version of SonarQube or sonar-kotlin-plugin is being used here, though it looks like sonar-kotlin-plugin hasn’t changed in the last 10 months so it’s likely an issue in the latest version - and looking at the code bears that out:
-
the lambda argument to the
TextRangeTracker
constructor call captureskotlinFileContext
here, which in turn holds onto theBindingContext
, which will be large - and there is a differentBindingContext
instance associated with each (i.e., it’s not a singleton long-lived object). -
the cache entries in RegexContext.globalCache appear to persist indefinitely, and there is no bound on the size of the cache.
I’d recommend trying to find a way to avoid maintaining long-lived references to BindingContext
s, and possibly consider bounding the size of the cache as well.