Kotlin/Native 内存管理

This page describes features of the new memory manager enabled by default since Kotlin 1.7.20. See our migration guide to move your projects from the legacy memory that will be removed in Kotlin 1.9.20.

Kotlin/Native uses a modern memory manager that is similar to JVM, Go, and other mainstream technologies:

  • Objects are stored in a shared heap and can be accessed from any thread.
  • Tracing garbage collector (GC) is executed periodically to collect objects that are not reachable from the "roots", like local and global variables.

The memory manager is the same across all the Kotlin/Native targets, except for wasm32, which is only supported in the legacy memory manager.

Garbage collector

The exact algorithm of GC is constantly evolving. As of 1.7.20, it is the Stop-the-World Mark and Concurrent Sweep collector that does not separate heap into generations.

GC is executed on a separate thread and kicked off based on the timer and memory pressure heuristics, or can be called manually.

Enable garbage collection manually

To force start garbage collector, call kotlin.native.internal.GC.collect(). It triggers a new collection and waits for its completion.

Monitor GC performance

There are no special instruments to monitor the GC performance yet. However, it's still possible to look through GC logs for diagnosis. To enable logging, set the following compilation flag in the Gradle build script:

-Xruntime-logs=gc=info

Currently, the logs are only printed to stderr.

Disable garbage collection

It's recommended to keep GC enabled. However, you can disable it in certain cases, for example, for testing purposes or if you encounter issues and have a short-lived program. To do that, set the following compilation flag in the Gradle build script:

-Xgc=noop

With this option enabled, GC doesn't collect Kotlin objects, so memory consumption will keep rising as long as the program runs. Be careful not to exhaust the system memory.

Memory consumption

With the Kotlin/Native memory manager, it's possible to monitor memory consumption. You can check for memory leaks and adjust memory consumption if necessary.

Check for memory leaks

To access the memory manager metrics, call kotlin.native.internal.GC.lastGCInfo(). It returns statistics for the last run of the garbage collector. The statistics can be useful for:

  • Debugging memory leaks when using global variables
  • Checking if there are any leaks when running tests
import kotlin.native.internal.*
import kotlin.test.*

class Resource

val global = mutableListOf<Resource>()

@OptIn(ExperimentalStdlibApi::class)
fun getUsage() : Long {
    GC.collect()
    return GC.lastGCInfo!!.memoryUsageAfter["heap"]!!.totalObjectsSizeBytes
}

fun run() {
    global.add(Resource())
    // The test will fail if you remove the next line
    global.clear()
}

@Test
fun test() {
    val before = getUsage()
    // A separate function is used to ensure that all temporary objects are cleared
    run()
    val after = getUsage()
    assertEquals(before, after)
}

Adjust memory consumption

If there are no memory leaks in the program, but you still see unexpectedly high memory consumption, try updating Kotlin to the latest version. We're constantly improving the memory manager, so even a simple compiler update might improve memory consumption.

Another way to fix high memory consumption is related to mimalloc, the default memory allocator for many targets. It pre-allocates and holds onto the system memory to improve the allocation speed.

To avoid that at the cost of performance, a few options are available:

  • Switch the memory allocator from mimalloc to the system allocator. For that, set the -Xallocator=std compilation option in your Gradle build script.
  • Since Kotlin 1.8.0-Beta, you can also instruct mimalloc to promptly release memory back to the system. It's a smaller performance cost, but it gives less definitive results.

    For that, enable the following binary option in your gradle.properties file:

    kotlin.native.binary.mimallocUseCompaction=true
    
  • Switch to the custom memory allocator. It is still Beta. To try it in your projects, set the -Xallocator=custom compilation option in your Gradle build script.

    For more information on the design of the new allocator, see this README.

If none of these options improved the memory consumption, report an issue in YouTrack.

Unit tests in the background

In unit tests, nothing processes the main thread queue, so don't use Dispatchers.Main unless it was mocked, which can be done by calling Dispatchers.setMain from kotlinx-coroutines-test.

If you don't rely on kotlinx.coroutines or Dispatchers.setMain doesn't work for you for some reason, try the following workaround for implementing the test launcher:

package testlauncher

import platform.CoreFoundation.*
import kotlin.native.concurrent.*
import kotlin.native.internal.test.*
import kotlin.system.*

fun mainBackground(args: Array<String>) {
    val worker = Worker.start(name = "main-background")
    worker.execute(TransferMode.SAFE, { args.freeze() }) {
        val result = testLauncherEntryPoint(it)
        exitProcess(result)
    }
    CFRunLoopRun()
    error("CFRunLoopRun should never return")
}

Then, compile the test binary with the -e testlauncher.mainBackground compiler flag.

Legacy memory manager

If it's necessary, you can switch back to the legacy memory manager. Set the following option in your gradle.properties:

kotlin.native.binary.memoryModel=strict
  • Compiler cache support is not available for the legacy memory manager, so compilation times might become worse.
  • This Gradle option for reverting to the legacy memory manager will be removed in future releases.

If you encounter issues with migrating from the legacy memory manager, or you want to temporarily support both the current and legacy memory managers, see our recommendations in the migration guide.

下一步做什么