调试内存泄漏和使用情况

概览

调试内存泄漏和使用情况可以帮助您识别和解决应用程序中与内存管理相关的问题。内存泄漏发生在已分配但未正确释放的内存时,导致内存使用量随时间逐渐增加。这会严重影响应用程序的性能和稳定性。

然而,重要的是要注意,内存使用量随时间逐渐增加并不总是表明存在泄漏。相反,这可能是应用程序的内存配置文件。例如,当应用程序的缓存随时间逐渐填满时,它会显示相同的内存逐渐增加。因此,配置缓存使其不会超出指定的限制将导致内存使用量趋于稳定。此外,由于性能或其他原因,分配器库并不总是立即将内存反馈返回给系统。但它会随着时间的推移而稳定下来。

工具和技术

在 macOS 和 Linux 环境中,可以使用不同的工具和技术来调试 Swift 中的内存泄漏,每种工具和技术都有其独特的优势和可用性。

基本故障排除包括:

1. 使用各个操作系统和开发环境提供的性能分析工具来识别和分析内存使用情况。

对于 macOS内存图调试器 和此 检测和诊断内存问题 视频很有帮助。您还可以使用 Xcode Instruments 工具进行各种性能分析,包括 Allocations instrument,以跟踪 Swift 代码中的内存分配和释放。

对于 Linux:您可以使用 ValgrindHeaptrack 等工具来分析您的应用程序,如下面的示例所示。虽然这些工具主要用于 C/C++ 代码,但它们也可以与 Swift 一起使用。

2. 审查代码并识别潜在的泄漏,检查您的代码中可能发生内存泄漏的任何潜在区域。泄漏的常见来源包括保留的引用或不平衡的 retain-release 循环,这在 Swift 中很少适用,因为它执行 自动引用计数 (ARC)

注意:如果对象之间存在涉及闭包的实质性引用循环,或者对象持有对未正确释放的外部资源的引用,则 Swift 中可能会发生内存泄漏。但是,通过自动内存管理添加和删除引用的能力,这种问题的可能性大大降低,使得诸如保留的引用和不平衡的 retain-release 循环之类的泄漏源在 Swift 代码中不太常见。

3. 启用调试内存分配功能 允许您获取有关对象及其内存分配的更多信息。

在 macOS 上:您可以使用 Xcode 启用僵尸对象,或者使用 MallocStackLogging 来检测过度释放或访问已释放的对象。

要启用僵尸对象

  1. 打开您的 Xcode 项目。
  2. 通过单击工具栏中的方案下拉菜单,转到 Edit Scheme 菜单。
  3. 在方案编辑器窗口中,选择 Run 选项卡。
  4. 选择 Diagnostics 选项卡。
  5. Memory Management 下,选中 Enable Zombie Objects 旁边的复选框。

在 Linux 上:Swift 具有内置的 LeakSanitizer 支持,可以使用 -sanitize=leak 编译器标志启用。

故障排除

本节旨在为您提供有用的服务器端故障排除技术,以使用 ValgrindLeakSanitizerHeaptrack 调试泄漏和使用情况。

以下示例程序会泄漏内存。我们仅将其用作示例,以说明下面提到的各种故障排除方法。

public class MemoryLeaker {
   var closure: () -> Void = { () }
   
   public init() {}
   
   public func doNothing() {}
   
   public func doSomethingThatLeaks() {
      self.closure = {
         // This will leak as it'll create a permanent reference cycle:
         //
         //     self -> self.closure -> self
         self.doNothing()
      }
   }
}
@inline(never) // just to be sure to get this in a stack trace
func myFunctionDoingTheAllocation() {
   let thing = MemoryLeaker()
   thing.doSomethingThatLeaks()
}

myFunctionDoingTheAllocation()

使用 Valgrind 调试泄漏

Valgrind 是一个用于调试和分析 Linux 应用程序的开源框架。它提供了多种工具,包括 Memcheck,可以检测内存泄漏、无效内存访问和其他内存错误。虽然 Valgrind 主要关注 C/C++ 应用程序,但它也可以在 Linux 上与 Swift 一起使用。

要在 Linux 上使用 Valgrind 调试 Swift 的内存泄漏,请在您的系统上安装它。

  1. 在您的 Linux 系统上安装 Swift。您可以从 官方网站 下载并安装 Swift。
  2. 使用您的包管理器在您的 Linux 系统上安装 Valgrind。例如,如果您使用的是 Ubuntu,您可以运行以下命令
    sudo apt-get install valgrind
    
  3. 安装 Valgrind 后,运行以下命令
    valgrind --leak-check=full swift run
    

valgrind 命令分析程序是否存在内存泄漏,并显示有关泄漏的相关信息,包括发生分配的堆栈跟踪,如下所示

==1== Memcheck, a memory error detector
==1== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==1== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==1== Command: ./test
==1==
==1==
==1== HEAP SUMMARY:
==1==     in use at exit: 824 bytes in 4 blocks
==1==   total heap usage: 5 allocs, 1 frees, 73,528 bytes allocated
==1==
==1== 32 bytes in 1 blocks are definitely lost in loss record 1 of 4
==1==    at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==1==    by 0x52076B1: swift_slowAlloc (in /usr/lib/swift/linux/libswiftCore.so)
==1==    by 0x5207721: swift_allocObject (in /usr/lib/swift/linux/libswiftCore.so)
==1==    by 0x108E58: $s4test12MemoryLeakerCACycfC (in /tmp/test)
==1==    by 0x10900E: $s4test28myFunctionDoingTheAllocationyyF (in /tmp/test)
==1==    by 0x108CA3: main (in /tmp/test)
==1==
==1== LEAK SUMMARY:
==1==    definitely lost: 32 bytes in 1 blocks
==1==    indirectly lost: 0 bytes in 0 blocks
==1==      possibly lost: 0 bytes in 0 blocks
==1==    still reachable: 792 bytes in 3 blocks
==1==         suppressed: 0 bytes in 0 blocks
==1== Reachable blocks (those to which a pointer was found) are not shown.
==1== To see them, rerun with: --leak-check=full --show-leak-kinds=all
==1==
==1== For counts of detected and suppressed errors, rerun with: -v
==1== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

以下跟踪块(来自上方)指示内存泄漏。

==1== 32 bytes in 1 blocks are definitely lost in loss record 1 of 4
==1==    at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==1==    by 0x52076B1: swift_slowAlloc (in /usr/lib/swift/linux/libswiftCore.so)
==1==    by 0x5207721: swift_allocObject (in /usr/lib/swift/linux/libswiftCore.so)
==1==    by 0x108E58: $s4test12MemoryLeakerCACycfC (in /tmp/test)
==1==    by 0x10900E: $s4test28myFunctionDoingTheAllocationyyF (in /tmp/test)
==1==    by 0x108CA3: main (in /tmp/test)

但是,由于 Swift 对函数和符号名称使用名称修饰,因此堆栈跟踪可能不容易理解。

要解修饰堆栈跟踪中的 Swift 符号,请运行 swift demangle 命令

swift demangle <mangled_symbol>

<mangled_symbol> 替换为堆栈跟踪中显示的修饰符号名称。例如

swift demangle $s4test12MemoryLeakerCACycfC

注意: swift demangle 是一个 Swift 命令行实用程序,如果您安装了 Swift 工具链,则应该可用。

该实用程序将解修饰符号并显示人类可读的版本,如下所示

==1== 32 bytes in 1 blocks are definitely lost in loss record 1 of 4
==1==    at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==1==    by 0x52076B1: swift_slowAlloc (in /usr/lib/swift/linux/libswiftCore.so)
==1==    by 0x5207721: swift_allocObject (in /usr/lib/swift/linux/libswiftCore.so)
==1==    by 0x108E58: test.MemoryLeaker.__allocating_init() -> test.MemoryLeaker (in /tmp/test)
==1==    by 0x10900E: test.myFunctionDoingTheAllocation() -> () (in /tmp/test)
==1==    by 0x108CA3: main (in /tmp/test)

通过分析解修饰的符号,我们可以了解代码的哪个部分负责内存泄漏。在本例中,valgrind 命令指示泄漏的分配来自

test.myFunctionDoingTheAllocation 调用 test.MemoryLeaker.__allocating_init()

局限性

使用 LeakSanitizer 调试泄漏

LeakSanitizer 是集成到 AddressSanitizer 中的内存泄漏检测器。要使用启用 Address Sanitizer 的 LeakSanitizer 调试 Swift 中的内存泄漏,您需要设置适当的环境变量,使用必要的选项编译您的 Swift 包,然后运行您的应用程序。

以下是步骤

  1. 打开终端会话并导航到您的 Swift 包目录。
  2. 设置 ASAN_OPTIONS 环境变量以启用 AddressSanitizer 并配置其行为。您可以通过运行以下命令来执行此操作
    export ASAN_OPTIONS=detect_leaks=1
    
  3. 使用附加选项 swift build 启用 Address Sanitizer
    swift build --sanitize=address
    

构建过程将使用启用 AddressSanitizer 的代码编译您的代码,这将自动查找泄漏的内存块。如果在构建期间检测到任何内存泄漏,它将输出信息(类似于 Valgrind),如下面的示例所示

=================================================================
==478==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 32 byte(s) in 1 object(s) allocated from:
    #0 0x55f72c21ac8d  (/tmp/test+0x95c8d)
    #1 0x7f7e44e686b1  (/usr/lib/swift/linux/libswiftCore.so+0x3cb6b1)
    #2 0x55f72c24b2ce  (/tmp/test+0xc62ce)
    #3 0x55f72c24a4c3  (/tmp/test+0xc54c3)
    #4 0x7f7e43aecb96  (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)

SUMMARY: AddressSanitizer: 32 byte(s) leaked in 1 allocation(s).

目前,输出未提供函数名称的人类可读表示形式,因为 LeakSanitizer 不会符号化 Linux 上的堆栈跟踪。但是,如果您安装了 binutils,则可以使用 llvm-symbolizeraddr2line 对其进行符号化。

要在运行 Linux 的服务器上为 Swift 安装 binutils,请按照以下步骤操作

步骤 1:使用终端通过 SSH 连接到您的 Swift 服务器。

步骤 2:通过运行以下命令更新包列表

sudo apt update

步骤 3:通过运行以下命令安装 binutils

sudo apt install binutils

这将安装 binutils 及其相关工具,用于处理二进制文件、目标文件和库,这对于在 Linux 上开发和调试 Swift 应用程序非常有用。

您现在可以运行以下命令来解修饰堆栈跟踪中的符号

# /tmp/test+0xc62ce
addr2line -e /tmp/test -a 0xc62ce -ipf | swift demangle

在本示例中,泄漏的分配来自

0x00000000000c62ce: test.myFunctionDoingTheAllocation() -> () at crtstuff.c:?

局限性

使用 Heaptrack 调试瞬时内存使用情况

Heaptrack 是一个开源堆内存分析工具,可帮助查找和分析内存泄漏和使用情况,其开销比 Valgrind 小。它还允许分析和调试应用程序中的瞬时内存使用情况。但是,它可能会因使分配器过载而显着影响性能。

除了命令行访问之外,GUI 前端分析器 heaptrack_gui 也可用。分析器允许在应用程序的两次不同运行之间进行差异分析,以排除 feature branchmain 之间 malloc 行为的差异。

使用不同的示例,以下是使用 Ubuntu 分析瞬时使用情况的简短操作指南。

步骤 1:通过运行此命令安装 heaptrack

sudo apt-get install heaptrack

步骤 2:使用 heaptrack 运行二进制文件两次。第一次运行为 main 提供基线。

heaptrack .build/x86_64-unknown-linux-gnu/release/test_1000_autoReadGetAndSet
heaptrack output will be written to "/tmp/.nio_alloc_counter_tests_GRusAy/heaptrack.test_1000_autoReadGetAndSet.84341.gz"
starting application, this might take some time...
...
heaptrack stats:
    allocations:              319347
    leaked allocations:       107
    temporary allocations:    68
Heaptrack finished! Now run the following to investigate the data:

  heaptrack --analyze "/tmp/.nio_alloc_counter_tests_GRusAy/heaptrack.test_1000_autoReadGetAndSet.84341.gz"

步骤 3:然后再次运行,针对 feature branch,方法是更改分支并重新编译。

heaptrack .build/x86_64-unknown-linux-gnu/release/test_1000_autoReadGetAndSet
heaptrack output will be written to "/tmp/.nio_alloc_counter_tests_GRusAy/heaptrack.test_1000_autoReadGetAndSet.84372.gz"
starting application, this might take some time...
...
heaptrack stats:
    allocations:              673989
    leaked allocations:       117
    temporary allocations:    341011
Heaptrack finished! Now run the following to investigate the data:

  heaptrack --analyze "/tmp/.nio_alloc_counter_tests_GRusAy/heaptrack.test_1000_autoReadGetAndSet.84372.gz"
ubuntu@ip-172-31-25-161 /t/.nio_alloc_counter_tests_GRusAy>

输出显示 feature branch 版本中有 673989 个分配,而 main 中有 319347 个,表明存在回归。

步骤 4:运行以下命令,以分析这些运行的输出,使用 heaptrack_print 作为差异,并通过 swift demangle 管道传输以提高可读性

heaptrack_print -T -d heaptrack.test_1000_autoReadGetAndSet.84341.gz heaptrack.test_1000_autoReadGetAndSet.84372.gz | swift demangle

注意: -T 输出临时分配,提供瞬态分配而非泄漏。如果检测到泄漏,请移除 -T

向下滚动以查看瞬态分配(输出可能很长)

MOST TEMPORARY ALLOCATIONS
307740 temporary allocations of 290324 allocations in total (106.00%) from
swift_slowAlloc
  in /home/ubuntu/bin/usr/lib/swift/linux/libswiftCore.so
43623 temporary allocations of 44553 allocations in total (97.91%) from:
    swift_allocObject
      in /home/ubuntu/bin/usr/lib/swift/linux/libswiftCore.so
    NIO.ServerBootstrap.(bind0 in _C131C0126670CF68D8B594DDFAE0CE57)(makeServerChannel: (NIO.SelectableEventLoop, NIO.EventLoopGroup) throws -> NIO.ServerSocketChannel, _: (NIO.EventLoop, NIO.ServerSocketChannel) -> NIO.EventLoopFuture<()>) -> NIO.EventLoopFuture<NIO.Channel>
      at /home/ubuntu/swiftnio/swift-nio/Sources/NIO/Bootstrap.swift:295
      in /tmp/.nio_alloc_counter_tests_GRusAy/.build/x86_64-unknown-linux-gnu/release/test_1000_autoReadGetAndSet
    merged NIO.ServerBootstrap.bind(host: Swift.String, port: Swift.Int) -> NIO.EventLoopFuture<NIO.Channel>
      in /tmp/.nio_alloc_counter_tests_GRusAy/.build/x86_64-unknown-linux-gnu/release/test_1000_autoReadGetAndSet
    NIO.ServerBootstrap.bind(host: Swift.String, port: Swift.Int) -> NIO.EventLoopFuture<NIO.Channel>
      in /tmp/.nio_alloc_counter_tests_GRusAy/.build/x86_64-unknown-linux-gnu/release/test_1000_autoReadGetAndSet
    Test_test_1000_autoReadGetAndSet.run(identifier: Swift.String) -> ()
      at /tmp/.nio_alloc_counter_tests_GRusAy/Sources/Test_test_1000_autoReadGetAndSet/file.swift:24
      in /tmp/.nio_alloc_counter_tests_GRusAy/.build/x86_64-unknown-linux-gnu/release/test_1000_autoReadGetAndSet
    main
      at Sources/bootstrap_test_1000_autoReadGetAndSet/main.c:18
      in /tmp/.nio_alloc_counter_tests_GRusAy/.build/x86_64-unknown-linux-gnu/release/test_1000_autoReadGetAndSet
22208 temporary allocations of 22276 allocations in total (99.69%) from:
    swift_allocObject
      in /home/ubuntu/bin/usr/lib/swift/linux/libswiftCore.so
    generic specialization <Swift.UnsafeBufferPointer<Swift.Int8>> of Swift._copyCollectionToContiguousArray<A where A: Swift.Collection>(A) -> Swift.ContiguousArray<A.Element>
      in /home/ubuntu/bin/usr/lib/swift/linux/libswiftCore.so
    Swift.String.utf8CString.getter : Swift.ContiguousArray<Swift.Int8>
      in /home/ubuntu/bin/usr/lib/swift/linux/libswiftCore.so
    NIO.URing.getEnvironmentVar(Swift.String) -> Swift.String?
      at /home/ubuntu/swiftnio/swift-nio/Sources/NIO/LinuxURing.swift:291
      in /tmp/.nio_alloc_counter_tests_GRusAy/.build/x86_64-unknown-linux-gnu/release/test_1000_autoReadGetAndSet
    NIO.URing._debugPrint(@autoclosure () -> Swift.String) -> ()
      at /home/ubuntu/swiftnio/swift-nio/Sources/NIO/LinuxURing.swift:297
...
22196 temporary allocations of 22276 allocations in total (99.64%) from:

查看上面的输出,我们可以看到额外的瞬态分配是由于额外的调试打印和环境变量查询造成的,如下所示

NIO.URing.getEnvironmentVar(Swift.String) -> Swift.String?
  at /home/ubuntu/swiftnio/swift-nio/Sources/NIO/LinuxURing.swift:291
  in /tmp/.nio_alloc_counter_tests_GRusAy/.build/x86_64-unknown-linux-gnu/release/test_1000_autoReadGetAndSet
NIO.URing._debugPrint(@autoclosure () -> Swift.String) -> ()

在此示例中,调试打印仅用于测试,在合并分支之前将从代码中删除。

提示: Heaptrack 也可以安装在基于 RPM 的发行版上,以调试瞬态内存使用情况。您可能需要查阅发行版的文档,以了解具体的存储库设置步骤。当 Heaptrack 正确安装后,它应显示其版本和使用信息。

局限性