Python内存没有在Linux上释放?

Posted

技术标签:

【中文标题】Python内存没有在Linux上释放?【英文标题】:Python memory not being released on linux? 【发布时间】:2019-01-27 01:38:39 【问题描述】:

我正在尝试将一个大型 json 对象加载到内存中,然后对数据执行一些操作。但是,我注意到读取 json 文件后 RAM 大幅增加 -即使对象超出范围。

这里是代码

import json
import objgraph
import gc
from memory_profiler import profile
@profile
def open_stuff():
    with open("bigjson.json", 'r') as jsonfile:
        d= jsonfile.read()
        jsonobj = json.loads(d)
        objgraph.show_most_common_types()
        del jsonobj
        del d
    print ('d')
    gc.collect()

open_stuff()

我尝试在 Windows 中使用 Python 版本 2.7.12 和 Debian 9 中使用 Python 版本 2.7.13 运行此脚本,但我发现 Linux 中的 Python 存在问题。

在 Windows 中,当我运行脚本时,它会在读取 json 对象并在范围内时占用大量 RAM(如预期的那样),但在操作完成后会释放它(如预期的那样)。

list                       3039184
dict                       413840
function                   2200
wrapper_descriptor         1199
builtin_function_or_method 819
method_descriptor          651
tuple                      617
weakref                    554
getset_descriptor          362
member_descriptor          250
d
Filename: testjson.py

Line #    Mem usage    Increment   Line Contents
================================================
     5     16.9 MiB     16.9 MiB   @profile
     6                             def open_stuff():
     7     16.9 MiB      0.0 MiB       with open("bigjson.json", 'r') as jsonfile:
     8    197.9 MiB    181.0 MiB           d= jsonfile.read()
     9   1393.4 MiB   1195.5 MiB           jsonobj = json.loads(d)
    10   1397.0 MiB      3.6 MiB           objgraph.show_most_common_types()
    11    402.8 MiB   -994.2 MiB           del jsonobj
    12    221.8 MiB   -181.0 MiB           del d
    13    221.8 MiB      0.0 MiB       print ('d')
    14     23.3 MiB   -198.5 MiB       gc.collect()

但是在 LINUX 环境中,即使对 JSON 对象的所有引用都已删除,仍会使用超过 500MB 的 RAM。

list                       3039186
dict                       413836
function                   2336
wrapper_descriptor         1193
builtin_function_or_method 765
method_descriptor          651
tuple                      514
weakref                    480
property                   273
member_descriptor          250
d
Filename: testjson.py

Line #    Mem usage    Increment   Line Contents
================================================
     5     14.2 MiB     14.2 MiB   @profile
     6                             def open_stuff():
     7     14.2 MiB      0.0 MiB       with open("bigjson.json", 'r') as jsonfile:
     8    195.1 MiB    181.0 MiB           d= jsonfile.read()
     9   1466.4 MiB   1271.3 MiB           jsonobj = json.loads(d)
    10   1466.8 MiB      0.4 MiB           objgraph.show_most_common_types()
    11    694.8 MiB   -772.1 MiB           del jsonobj
    12    513.8 MiB   -181.0 MiB           del d
    13    513.8 MiB      0.0 MiB       print ('d')
    14    513.0 MiB     -0.8 MiB       gc.collect()

在 Debian 9 和 Python 3.5.3 中运行的相同脚本使用较少的 RAM,但会泄漏相应数量的 RAM。

list                       3039266
dict                       414638
function                   3374
tuple                      1254
wrapper_descriptor         1076
weakref                    944
builtin_function_or_method 780
method_descriptor          780
getset_descriptor          477
type                       431
d
Filename: testjson.py

Line #    Mem usage    Increment   Line Contents
================================================
     5     17.2 MiB     17.2 MiB   @profile
     6                             def open_stuff():
     7     17.2 MiB      0.0 MiB       with open("bigjson.json", 'r') as jsonfile:
     8    198.3 MiB    181.1 MiB           d= jsonfile.read()
     9   1057.7 MiB    859.4 MiB           jsonobj = json.loads(d)
    10   1058.1 MiB      0.4 MiB           objgraph.show_most_common_types()
    11    537.5 MiB   -520.6 MiB           del jsonobj
    12    356.5 MiB   -181.0 MiB           del d
    13    356.5 MiB      0.0 MiB       print ('d')
    14    355.8 MiB     -0.8 MiB       gc.collect()

是什么导致了这个问题? 两个版本的 Python 都运行 64 位版本。

编辑 - 连续多次调用该函数会导致更奇怪的数据,json.loads 函数每次调用时使用较少的 RAM,在第三次尝试后 RAM 使用稳定,但较早泄漏的 RAM 确实如此不会被释放..

list                       3039189
dict                       413840
function                   2339
wrapper_descriptor         1193
builtin_function_or_method 765
method_descriptor          651
tuple                      517
weakref                    480
property                   273
member_descriptor          250
d
Filename: testjson.py

Line #    Mem usage    Increment   Line Contents
================================================
     5     14.5 MiB     14.5 MiB   @profile
     6                             def open_stuff():
     7     14.5 MiB      0.0 MiB       with open("bigjson.json", 'r') as jsonfile:
     8    195.4 MiB    180.9 MiB           d= jsonfile.read()
     9   1466.5 MiB   1271.1 MiB           jsonobj = json.loads(d)
    10   1466.9 MiB      0.4 MiB           objgraph.show_most_common_types()
    11    694.8 MiB   -772.1 MiB           del jsonobj
    12    513.9 MiB   -181.0 MiB           del d
    13    513.9 MiB      0.0 MiB       print ('d')
    14    513.1 MiB     -0.8 MiB       gc.collect()


list                       3039189
dict                       413842
function                   2339
wrapper_descriptor         1202
builtin_function_or_method 765
method_descriptor          651
tuple                      517
weakref                    482
property                   273
member_descriptor          253
d
Filename: testjson.py

Line #    Mem usage    Increment   Line Contents
================================================
     5    513.1 MiB    513.1 MiB   @profile
     6                             def open_stuff():
     7    513.1 MiB      0.0 MiB       with open("bigjson.json", 'r') as jsonfile:
     8    513.1 MiB      0.0 MiB           d= jsonfile.read()
     9   1466.8 MiB    953.7 MiB           jsonobj = json.loads(d)
    10   1493.3 MiB     26.6 MiB           objgraph.show_most_common_types()
    11    723.9 MiB   -769.4 MiB           del jsonobj
    12    723.9 MiB      0.0 MiB           del d
    13    723.9 MiB      0.0 MiB       print ('d')
    14    722.4 MiB     -1.5 MiB       gc.collect()


list                       3039189
dict                       413842
function                   2339
wrapper_descriptor         1202
builtin_function_or_method 765
method_descriptor          651
tuple                      517
weakref                    482
property                   273
member_descriptor          253
d
Filename: testjson.py

Line #    Mem usage    Increment   Line Contents
================================================
     5    722.4 MiB    722.4 MiB   @profile
     6                             def open_stuff():
     7    722.4 MiB      0.0 MiB       with open("bigjson.json", 'r') as jsonfile:
     8    722.4 MiB      0.0 MiB           d= jsonfile.read()
     9   1493.1 MiB    770.8 MiB           jsonobj = json.loads(d)
    10   1493.4 MiB      0.3 MiB           objgraph.show_most_common_types()
    11    724.4 MiB   -769.0 MiB           del jsonobj
    12    724.4 MiB      0.0 MiB           del d
    13    724.4 MiB      0.0 MiB       print ('d')
    14    722.9 MiB     -1.5 MiB       gc.collect()


Filename: testjson.py

Line #    Mem usage    Increment   Line Contents
================================================
    17     14.2 MiB     14.2 MiB   @profile
    18                             def wow():
    19    513.1 MiB    498.9 MiB       open_stuff()
    20    722.4 MiB    209.3 MiB       open_stuff()
    21    722.9 MiB      0.6 MiB       open_stuff()

编辑 2:有人建议这是 Why does my program's memory not release? 的副本,但所讨论的内存量与另一个问题中讨论的“小页面”相去甚远。

【问题讨论】:

如果您反复调用open_stuff,内存使用是否会持续增长? @melpomene 是的,在最初的 513MB 被第一次调用占用后,看起来每次调用增加了大约 200MB 实际上看起来第一次调用将它增加到 513MB,第二次增加到 722MB,第三次保持在 723MB。每次调用 json.loads 调用都会使用更少的 RAM。 Why does my program's memory not release?的可能重复 @Miguel 上下文管理器的要点是您不必在完成后手动关闭资源。 docs.python.org/2.5/whatsnew/pep-343.html 不过,我也试过了,但没有区别。 【参考方案1】:

虽然 python 将内存释放回 glibc,但 glibc 不会每次都立即释放回操作系统,因为用户可能稍后会请求内存。你可以调用 glibc 的malloc_trim(3) 来尝试释放内存:

import ctypes

def malloc_trim():
    ctypes.CDLL('libc.so.6').malloc_trim(0) 

@profile
def load():
    with open('big.json') as f:
        d = json.load(f)
    del d
    malloc_trim()

结果:

Line #    Mem usage    Increment   Line Contents
================================================
    27     11.6 MiB     11.6 MiB   @profile
    28                             def load():
    29     11.6 MiB      0.0 MiB       with open('big.json') as f:
    30    166.5 MiB    154.9 MiB           d = json.load(f)
    31     44.1 MiB   -122.4 MiB       del d
    32     12.7 MiB    -31.4 MiB       malloc_trim()

【讨论】:

最后。在对我们的数据科学家代码进行无休止的重构之后,它在 Windows 上始终发布,而在 aws linux 上,它不断发展壮大。唯一对我有用的是这个解决方案! 最后也是!我在 Azure 上运行我的模型,它现在可以使用这个解决方案。谢谢【参考方案2】:

链接的副本可能暗示您的问题是什么,但让我们更详细一点。

首先,您应该使用json.load,而不是将文件完全加载到内存中,然后对其执行json.loads

with open('bigjson.json') as f:
    data = json.load(f)

这允许解码器在空闲时读取文件,并且很可能会减少内存使用量。在您的原始版本中,您必须至少将整个原始文件存储在内存中,然后才能开始解析 JSON。这允许文件在解码器需要时进行流式传输。

我还看到您使用的是 Python 2.7。有什么特别的原因吗? dicts 在 3 中看到了很多更新,特别是那些大大减少了内存使用的更新。如果内存使用有这么大的问题,也许也可以考虑对 3 进行基准测试。


您在这里遇到的问题不是内存没有被释放。

“内存使用”列可能表示程序的RSS(大致是进程可用的内存量,无需向操作系统请求更多空间)。 README for memory_profiler 似乎并没有准确地表明这一点,但他们做了一些模糊的陈述,暗示了这一点:“第二列(内存使用)在该行执行后 Python 解释器的内存使用情况。”

假设这一点,我们看到在所有操作系统中,在 json dict 被回收后,程序的 RSS 减半(很可疑,不是吗?我们稍后会讨论)。那是因为这里有很多层。大致来说,我们有:

Your code -> Python Runtime/GC -> userland allocator -> (syscall) -> Operating System -> Physical RAM

当某些内容超出范围时,可以从代码的角度释放它。 Python GC 不保证何时发生这种情况,但如果您调用 gc.collect() 并且对象超出范围(引用计数为 0),那么它们确实应该由 Python 运行时释放。但是,这会将内存返回给用户空间分配器。这可能会将内存还给操作系统,也可能不会。在我们在所有操作系统中回收jsonobj 之后,我们看到它这样做了。但是,它并没有归还所有内容,而是将内存使用量减半。这应该引发一个危险信号,因为那个神奇的减半数字没有出现在其他地方。这很好地表明用户空间分配器正在这里做一些工作。

回顾一些基本的数据结构,vector(一个动态大小、可增长和可收缩的数组)通常以 NULL 指针开始。然后,当您向其添加元素时,它会增长。我们通常通过将它们的大小加倍来增长向量,因为这是gives desirable amortized performance。无论向量的最终长度如何,插入平均会花费恒定的时间。 (对于任何删除都是一样的,这可能会导致缩小 2 倍)

Python 的 GC 下的内存分配器可能采用了与此类似的方法。与其回收所有使用的内存,不如猜测以后您可能需要至少一半的内存。如果你不这样做,那么是的,它确实保留了太多(但没有泄漏)。但是如果你这样做了(并且像网络服务器这样的东西的内存使用经常像这样突发),那么这个猜测可以节省你将来的分配时间(在这个级别是一个系统调用)。

在您多次运行代码的基准测试中,您会看到这种行为。它保留了足够的内存,这样初始的jsonfile.read() 就可以放入内存中,而无需请求更多。如果某处存在错误(存在内存泄漏),您会看到内存使用量随着时间的推移呈上升趋势。我不认为你的数据看起来像这样。例如,参见 another featured Python question 中的 the graph。这就是内存泄漏的样子。

如果您想更加确定,可以使用valgrind 运行您的脚本。这将为您确认用户空间中是否存在内存泄漏。但是,我怀疑情况并非如此。

编辑:顺便说一句,如果您要处理这么大的文件,也许 JSON 不是存储它们的正确格式。您可以流式传输的内容可​​能很多对内存更友好(python 生成器对此非常有用)。如果 JSON 格式是不可避免的,并且这种内存使用确实是一个问题,那么您可能希望使用一种可以更精细地控制内存布局和分配的语言,例如 C、C++ 或 Rust。与 Python dict(尤其是 2.7 dict)相比,表示您的数据的微调 C 结构在打包数据方面可能做得更好。此外,如果您经常执行此操作,您可以mmap 文件(也许将有线格式转储到文件中,以便在映射时直接从中读取)。或者,加载一次并让操作系统处理它。高内存使用不是问题,因为大多数操作系统都非常擅长在访问频率较低时分页内存。

【讨论】:

JSON 解码器不是流式解码器,因此您最初的建议会有所帮助,因为您应该已经注意到,如果您运行了一些基准来支持您的声明。只需在源代码中查找几秒钟即可轻松检查:json.load() 所做的唯一事情就是调用json.loads(fp.read(),....) (repo)。 尽管你的回答很长,但信息很薄,与问题没有直接联系......

以上是关于Python内存没有在Linux上释放?的主要内容,如果未能解决你的问题,请参考以下文章

手工释放linux内存

Linux 下释放内存,swap交换区缓存

Linux下如何释放cache内存

关于Linux下pthread线程释放内存的讨论

linux内存管理

linux何时自动释放内存