Why does my Delphi program's memory continue to grow?

后端 未结 4 1173
北海茫月
北海茫月 2020-12-09 11:31

I am using Delphi 2009 which has the FastMM4 memory manager built into it.

My program reads in and processes a large dataset. All memory is freed correctly whenever

相关标签:
4条回答
  • 2020-12-09 12:09

    What sort of dataset are you using? If it's implemented completely in Delphi, (not calling out to other code with another memory manager, like Midas,) you could try deliberately leaking the dataset.

    I assume that your dataset is on a form, and it's being freed automatically when the form clears its components. Try putting MyDataset := nil; in your form's OnDestroy. This will make sure that the dataset leaks, and also everything that the dataset owns. Try that after loading once and again after loading twice and compare the leak reports, and see if that gives you anything useful.

    0 讨论(0)
  • 2020-12-09 12:15

    You are half-leaking memory; obviously. You are leaking memory while the program is running, but when you close the program, your dataset is properly freed so FastMM (rightfully) does not report it.

    See this for details: My program never releases the memory back. Why?

    0 讨论(0)
  • 2020-12-09 12:24

    The CurrentMemoryUsage utility you linked to reports your application's working set size. Working set is the total number of pages of virtual memory address space that are mapped to physical memory addresses. However, some or many of those pages may have very little actual data stored in them. The working set is thus the "upper bound" of how much memory your process is using. It indicates how much address space is reserved for use, but it does not indicate how much is actually committed (actually residing in physical memory) or how much of the pages that are committed are actually in use by your application.

    Try this: after you see your working set size creep up after several test runs, minimize your application's main window. You will most likely see the working set size drop significantly. Why? Because Windows performs a SetProcessWorkingSetSize(-1) call when you minimize an application which discards unused pages and shrinks the working set to the minimum. The OS doesn't do this while the app window is normal sized because reducing the working set size too often can make performance worse by forcing data to be reloaded from the swap file.

    To get into it in more detail: Your Delphi application allocates memory in fairly small chunks - a string here, a class there. The average memory allocation for a program is typically less than a few hundred bytes. It's difficult to manage small allocations like this efficiently on a system-wide scale, so the operating system doesn't. It manages large memory blocks efficiently, particularly at the 4k virtual memory page size and 64k virtual memory address range minimum sizes.

    This presents a problem for applications: applications typically allocate small chunks, but the OS doles out memory in rather large chunks. What to do? Answer: suballocate.

    The Delphi runtime library's memory manager and the FastMM replacement memory manager (and the runtime libraries of just about every other language or toolset on the planet) both exist to do one thing: carve up big memory blocks from the OS into smaller blocks used by the application. Keeping track of where all the little blocks are, how big they are, and whether they've been "leaked" requires some memory as well - called overhead.

    In situations of heavy memory allocation/deallocation, there can be situations in which you deallocate 99% of what you allocated, but the process's working set size only shrinks by, say, 50%. Why? Most often, this is caused by heap fragmentation: one small block of memory is still in use in one of the large blocks that the Delphi memory manager obtained from the OS and divvied up internally. The internal count of memory used is small (300 bytes, say) but since it's preventing the heap manager from releasing the big block that it's in back to the OS, the working set contribution of that little 300 byte chunk is more like 4k (or 64k depending on whether it's virtual pages or virtual address space - I can't recall).

    In a heavy memory intensive operation involving megabytes of small memory allocations, heap fragmentation is very common - particularly if memory allocations for things not related to the memory intensive operation are going on at the same time as the big job. For example, if crunching through your 80MB database operation also outputs status to a listbox as it progresses, the strings used to report status will be scattered in the heap amongst the database memory blocks. When you release all the memory blocks used by the database computation, the listbox strings are still out there (in use, not lost) but they are scattered all over the place, potentially occupying an entire OS big block for each little string.

    Try the minimize window trick to see if that reduces your working set. If it does, you can discount the apparent "severity" of the numbers returned by the working set counter. You could also add a call to SetProcessWorkingSetSize after your big compute operation to purge the pages that are no longer in use.

    0 讨论(0)
  • 2020-12-09 12:26

    You could use VMMap to trace the most allocated bytes. It helped me for an similar scenario.

    • Download VMMap
    • Compile your application with map file detailed
    • Convert the map file to dbg, to VMMap understand it. Use the map2dbg tool
    • Configure symbol (dbg) path on VMMap: Options -> Configure Symbols -> Symbol paths
    • Configure source paths on VMMap -> Options -> Configure Symbols -> Source code paths. Hint: use the "*" to include subfolders
    • In VMMap, go to File -> Select Process -> Launch and trace a new process. Configure application and any parameter that it needs. Then Ok.

    When the app opens, VMMap will trace all allocated and freed memory using detours in allocate/free methods. You can see in Timeline button (on the botton of VMMap) the timeline of memory (obviously).

    Click in Trace button. It will show all the allocations/dealocattions operations in the traced time. Order the column Bytes to show the most bytes first, and double click it. It will show the callstack of the allocation. In my case, the first item showed my problem.

    Sample app:

    private
      FList: TObjectList<TStringList>;
      ...
    procedure TForm1.Button1Click(Sender: TObject);
    var
      i: Integer;
    begin
      for i := 0 to 1000000 do
        FList.Add(TStringList.Create);
    end;
    
    procedure TForm1.FormCreate(Sender: TObject);
    var
      a: TStringList;
    begin
      FList := TObjectList<TStringList>.Create; //not leak
      a := TStringList.Create; //leak
    end;
    
    procedure TForm1.FormDestroy(Sender: TObject);
    begin
      FList.Free;
    end;
    

    When clicking in button one time, and see the Trace in VMMap, shows:

    And the callstack:

    In this case did not show exactly the code, but the Vcl.Controls.TControl.Click give an idea. In my real scenario, helped more.

    There is a lot of others functionalities in VMMap that helps analising memory problems.

    0 讨论(0)
提交回复
热议问题