Python内存没有在linux上发布?

lon*_*aft 10 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()
Run Code Online (Sandbox Code Playgroud)

我尝试在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()
Run Code Online (Sandbox Code Playgroud)

但是在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()
Run Code Online (Sandbox Code Playgroud)

使用Python 3.5.3在Debian 9中运行的相同脚本使用较少的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()
Run Code Online (Sandbox Code Playgroud)

是什么导致了这个问题?两个版本的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()
Run Code Online (Sandbox Code Playgroud)

编辑2:有人建议这是一个副本为什么我的程序的内存不释放?,但有问题的内存量远远不是另一个问题中讨论的"小页面".

geo*_*xsh 7

当python将内存释放回glibc时,glibc不会每次都立即释放回操作系统,因为用户可能会在以后请求内存。您可以调用 glibcmalloc_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()
Run Code Online (Sandbox Code Playgroud)

结果:

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()
Run Code Online (Sandbox Code Playgroud)

  • 最后。经过我们数据科学家代码的不断重构,它在 Windows 上始终发布,而在 AWS 上 Linux 不断成长、成长、成长。唯一对我有用的是这个解决方案! (2认同)

Bai*_*ker 6

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

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

with open('bigjson.json') as f:
    data = json.load(f)
Run Code Online (Sandbox Code Playgroud)

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

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


你遇到的问题不在于内存没有被释放.

"mem usage"列可能表示程序的RSS(大致是进程可用的内存量,而无需向操作系统请求更多空间).该用于READMEmemory_profiler似乎并不准确指明这一点,但他们做一些模糊的声明,建议这样的:"第二列(内存使用),该行已执行后Python解释器的内存占用"

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

Your code -> Python Runtime/GC -> userland allocator -> (syscall) -> Operating System -> Physical RAM
Run Code Online (Sandbox Code Playgroud)

当某些内容超出范围时,可以从代码的角度发布.Python GC不保证何时发生这种情况,但是如果你调用gc.collect()并且对象超出范围(有0引用),那么它们确实应该由Python运行时发布.但是,这会将内存返回给userland分配器.这可能会也可能不会将内存返回给操作系统.在我们回收jsonobj所有操作系统之后,我们看到它就这样做了.但不是回馈一切,而是将内存使用量减半.这应该会引起一个红旗,因为那个神奇的减半数字出现了.这是一个很好的迹象,表明userland分配器正在这里做一些工作.

回顾一些基本数据结构,vector(动态大小,可增长和可收缩的数组)通常以NULL指针开始.然后,当您向其追加元素时,它会增长.我们通常通过将载体的大小加倍来生长载体,因为这样可以提供理想的摊销性能.无论向量的最终长度如何,插入平均需要恒定的时间.(对于任何删除都是一样的,这可能导致收缩2倍)

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

在您多次运行代码的基准测试中,您会看到此行为.它保留了足够的内存,使得初始内容jsonfile.read()可以适应内存而无需要求更多.如果某处存在错误(存在内存泄漏),您会看到内存使用情况随着时间推移呈上升趋势.我不认为你的数据是这样的.例如,请参阅图表另一个特色Python的问题.这就是内存泄漏的样子.

如果你想要倍加肯定,你可以用valgrind运行你的脚本.这将确认用户区中是否存在任何内存泄漏.但是,我怀疑情况并非如此.

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