Skip to content

Commit b454f34

Browse files
authored
Prevent class loaders from leaking when using ClassValue cache (#2175)
ClassValue can transitively refer to an instance of type java.lang.Class, so this may prevent the loader from being collected during garbage collection. Using SoftReference over the value should solve this problem.
1 parent 9e344bd commit b454f34

File tree

2 files changed

+74
-26
lines changed

2 files changed

+74
-26
lines changed

core/jvmMain/src/kotlinx/serialization/internal/Caching.kt

+73-24
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package kotlinx.serialization.internal
66

77
import kotlinx.serialization.KSerializer
8+
import java.lang.ref.SoftReference
89
import java.util.concurrent.ConcurrentHashMap
910
import kotlin.reflect.KClass
1011
import kotlin.reflect.KClassifier
@@ -40,38 +41,80 @@ internal actual fun <T> createParametrizedCache(factory: (KClass<Any>, List<KTyp
4041
return if (useClassValue) ClassValueParametrizedCache(factory) else ConcurrentHashMapParametrizedCache(factory)
4142
}
4243

43-
private class ClassValueCache<T>(compute: (KClass<*>) -> KSerializer<T>?) : SerializerCache<T> {
44-
private val classValue = ClassValueWrapper(compute)
44+
private class ClassValueCache<T>(val compute: (KClass<*>) -> KSerializer<T>?) : SerializerCache<T> {
45+
private val classValue = ClassValueReferences<CacheEntry<T>>()
4546

46-
override fun get(key: KClass<Any>): KSerializer<T>? = classValue[key.java].serializer
47+
override fun get(key: KClass<Any>): KSerializer<T>? {
48+
return classValue
49+
.getOrSet(key.java) { CacheEntry(compute(key)) }
50+
.serializer
51+
}
4752
}
4853

54+
/**
55+
* A class that combines the capabilities of ClassValue and SoftReference.
56+
* Softly binds the calculated value to the specified class.
57+
*
58+
* [SoftReference] used to prevent class loaders from leaking,
59+
* since the value can transitively refer to an instance of type [Class], this may prevent the loader from
60+
* being collected during garbage collection.
61+
*
62+
* In the first calculation the value is cached, every time [getOrSet] is called, a pre-calculated value is returned.
63+
*
64+
* However, the value can be collected during garbage collection (thanks to [SoftReference])
65+
* - in this case, when trying to call the [getOrSet] function, the value will be calculated again and placed in the cache.
66+
*
67+
* An important requirement for a function generating a value is that it must be stable, so that each time it is called for the same class, the function returns similar values.
68+
* In the case of serializers, these should be instances of the same class filled with equivalent values.
69+
*/
4970
@SuppressAnimalSniffer
50-
private class ClassValueWrapper<T>(private val compute: (KClass<*>) -> KSerializer<T>?): ClassValue<CacheEntry<T>>() {
51-
/*
52-
* Since during the computing of the value for the `ClassValue` entry, we do not know whether a nullable
53-
* serializer is needed, so we may need to differentiate nullable/non-null caches by a level higher
54-
*/
55-
override fun computeValue(type: Class<*>): CacheEntry<T> {
56-
return CacheEntry(compute(type.kotlin))
71+
private class ClassValueReferences<T> : ClassValue<MutableSoftReference<T>>() {
72+
override fun computeValue(type: Class<*>): MutableSoftReference<T> {
73+
return MutableSoftReference()
5774
}
58-
}
5975

60-
private class ClassValueParametrizedCache<T>(private val compute: (KClass<Any>, List<KType>) -> KSerializer<T>?) : ParametrizedSerializerCache<T> {
61-
private val classValue = ParametrizedClassValueWrapper<T>()
76+
inline fun getOrSet(key: Class<*>, crossinline factory: () -> T): T {
77+
val ref: MutableSoftReference<T> = get(key)
78+
79+
ref.reference.get()?.let { return it }
80+
81+
// go to the slow path and create serializer with blocking, also wrap factory block
82+
return ref.getOrSetWithLock { factory() }
83+
}
6284

63-
override fun get(key: KClass<Any>, types: List<KType>): Result<KSerializer<T>?> =
64-
classValue[key.java].computeIfAbsent(types) { compute(key, types) }
6585
}
6686

67-
@SuppressAnimalSniffer
68-
private class ParametrizedClassValueWrapper<T> : ClassValue<ParametrizedCacheEntry<T>>() {
87+
/**
88+
* Wrapper over `SoftReference`, used to store a mutable value.
89+
*/
90+
private class MutableSoftReference<T> {
91+
// volatile because of situations like https://stackoverflow.com/a/7855774
92+
@JvmField
93+
@Volatile
94+
var reference: SoftReference<T> = SoftReference(null)
95+
6996
/*
70-
* Since during the computing of the value for the `ClassValue` entry, we do not know whether a nullable
71-
* serializer is needed, so we may need to differentiate nullable/non-null caches by a level higher
72-
*/
73-
override fun computeValue(type: Class<*>): ParametrizedCacheEntry<T> {
74-
return ParametrizedCacheEntry()
97+
It is important that the monitor for synchronized is the `MutableSoftReference` of a specific class
98+
This way access to reference is blocked only for one serializable class, and not for all
99+
*/
100+
@Synchronized
101+
fun getOrSetWithLock(factory: () -> T): T {
102+
// exit function if another thread has already filled in the `reference` with non-null value
103+
reference.get()?.let { return it }
104+
105+
val value = factory()
106+
reference = SoftReference(value)
107+
return value
108+
}
109+
}
110+
111+
private class ClassValueParametrizedCache<T>(private val compute: (KClass<Any>, List<KType>) -> KSerializer<T>?) :
112+
ParametrizedSerializerCache<T> {
113+
private val classValue = ClassValueReferences<ParametrizedCacheEntry<T>>()
114+
115+
override fun get(key: KClass<Any>, types: List<KType>): Result<KSerializer<T>?> {
116+
return classValue.getOrSet(key.java) { ParametrizedCacheEntry() }
117+
.computeIfAbsent(types) { compute(key, types) }
75118
}
76119
}
77120

@@ -91,8 +134,8 @@ private class ConcurrentHashMapCache<T>(private val compute: (KClass<*>) -> KSer
91134
}
92135

93136

94-
95-
private class ConcurrentHashMapParametrizedCache<T>(private val compute: (KClass<Any>, List<KType>) -> KSerializer<T>?) : ParametrizedSerializerCache<T> {
137+
private class ConcurrentHashMapParametrizedCache<T>(private val compute: (KClass<Any>, List<KType>) -> KSerializer<T>?) :
138+
ParametrizedSerializerCache<T> {
96139
private val cache = ConcurrentHashMap<Class<*>, ParametrizedCacheEntry<T>>()
97140

98141
override fun get(key: KClass<Any>, types: List<KType>): Result<KSerializer<T>?> {
@@ -101,6 +144,12 @@ private class ConcurrentHashMapParametrizedCache<T>(private val compute: (KClass
101144
}
102145
}
103146

147+
/**
148+
* Wrapper for cacheable serializer of some type.
149+
* Used to store cached serializer or indicates that the serializer is not cacheable.
150+
*
151+
* If serializer for type is not cacheable then value of [serializer] is `null`.
152+
*/
104153
private class CacheEntry<T>(@JvmField val serializer: KSerializer<T>?)
105154

106155
/**

rules/common.pro

+1-2
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,4 @@
3232
# Serialization core uses `java.lang.ClassValue` for caching inside these specified classes.
3333
# If there is no `java.lang.ClassValue` (for example, in Android), then R8/ProGuard will print a warning.
3434
# However, since in this case they will not be used, we can disable these warnings
35-
-dontwarn kotlinx.serialization.internal.ClassValueWrapper
36-
-dontwarn kotlinx.serialization.internal.ParametrizedClassValueWrapper
35+
-dontwarn kotlinx.serialization.internal.ClassValueReferences

0 commit comments

Comments
 (0)