不能在Linux上释放Python内存吗?

时间:2018-08-20 21:51:14

标签: python linux memory-leaks

我试图将一个较大的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,在第3次尝试使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?的副本,但是所讨论的内存量与另一个问题中讨论的“小页面”相距甚远。

2 个答案:

答案 0 :(得分:6)

链接的重复项可能暗示您的问题所在,但让我们更详细些。

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

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

这使解码器可以随意读取文件,并且很可能会减少内存使用量。在原始版本中,您甚至必须至少将整个原始文件存储在内存中,然后才能开始解析JSON。这样可以根据解码器的需要对文件进行流处理。

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


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

“内存使用情况”列可能表示程序的RSS(这大致是进程可用的内存量,而无需要求OS占用更多空间)。 README for memory_profiler似乎并不能精确地表明这一点,但是它们做出了一些模糊的陈述,这暗示了这一点:“第二行(Mem用法)执行该行后Python解释器的内存使用情况。” < / p>

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

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

当超出范围时,可以从代码的角度发布它。 Python GC不能保证何时发生这种情况,但是如果调用gc.collect()并且对象超出范围(引用计数为0),那么它们确实应该由Python运行时释放。但是,这会将内存返回给用户态分配器。这可能会也可能不会将内存还给操作系统。在所有操作系统中回收jsonobj之后,我们看到它可以这样做。但是,与其将所有内容都还给别人,还不如将其占用的内存减少一半。因为那个减半的数字没有其他地方出现,所以应该发出一个红旗。这很好地表明userland分配器正在此处进行某些工作。

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

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

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

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

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

答案 1 :(得分:3)

虽然python将内存释放回glibc,但glibc不会每次都立即释放回OS,因为用户可能稍后会请求内存。您可以调用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()