Large unexplained memory in the memory dump of a .NET process











up vote
2
down vote

favorite












I can't explain most of the memory used by a C# process. The total memory is 10 GB, but the total reachable and unreachable objects altogether total 2.5 GB. I wonder what these 7.5 GB could be?



I'm looking for the most likely explanations or a method to find out what this memory can be. It is also possible there is something I don't understand about dumps and memory profilers.



Here is the precise situation. The process is .NET 4.5.1. It downloads pages from internet and process them with machine learning. Possible issue: I know the web objects are not properly disposed (HttpClient, HttpResponse...) but before I get into cleaning all this, I wish I could relate the memory problem to it. The memory is almost entirely in the Managed Heap as shown by VMMap. This seems to rule out unmanaged memory leak.
enter image description here



The process has been running for days and the memory slowly grew. At some point, the memory is 11 GB. I stop everything running in the process. I run garbage collections including large object heap compaction several times (with one minute of interval):



GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();


The memory goes down to 10 GB. Then I create the dump:




procdump -ma psid




The dump is 10 GB as expected.



I open the dump with .NET memory profiler (version 5.6). The dump shows a total of 2.2 GB reachable objects and 0.3 GB unreachable objects. What could explain the remaining 7.5 GB ?



For those not familiar to .NET memory profiler, it is similar to WinDbg. It seems to contain most objects including strings, arrays... and in most situation I've studied, the total memory occupied by the objects displayed in the profiler was very close to the dump size.



Possible explanations I've been thinking of :




  • the LOH does not really get fully compacted

  • some memory is used beyond the objects displayed by the profiler










share|improve this question




















  • 2




    This is kind of equivalent to "got memory leak, plz fix.". What does your service do? Does it call into anything unmanaged? Does it use COM?
    – fstam
    2 days ago






  • 3




    You should not have that many unreachable objects after a GC. A deadlocked finalizer thread causes memory explosions, use WinDbg to see what it is doing.
    – Hans Passant
    2 days ago










  • Thanks Hans. This unreachable memory was indeed suspicious to me. I'll have a look.
    – Benoit Sanchez
    2 days ago






  • 1




    Sounds like unmanaged memory usage. The JetBrains profiler can show that. You could also use the free VMMap.exe to test that theory.
    – usr
    2 days ago










  • Thanks usr. I checked with VMMap. 95% of my 10 GB are in the managed heap. This seems to rule out unmanaged memory usage.
    – Benoit Sanchez
    2 days ago















up vote
2
down vote

favorite












I can't explain most of the memory used by a C# process. The total memory is 10 GB, but the total reachable and unreachable objects altogether total 2.5 GB. I wonder what these 7.5 GB could be?



I'm looking for the most likely explanations or a method to find out what this memory can be. It is also possible there is something I don't understand about dumps and memory profilers.



Here is the precise situation. The process is .NET 4.5.1. It downloads pages from internet and process them with machine learning. Possible issue: I know the web objects are not properly disposed (HttpClient, HttpResponse...) but before I get into cleaning all this, I wish I could relate the memory problem to it. The memory is almost entirely in the Managed Heap as shown by VMMap. This seems to rule out unmanaged memory leak.
enter image description here



The process has been running for days and the memory slowly grew. At some point, the memory is 11 GB. I stop everything running in the process. I run garbage collections including large object heap compaction several times (with one minute of interval):



GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();


The memory goes down to 10 GB. Then I create the dump:




procdump -ma psid




The dump is 10 GB as expected.



I open the dump with .NET memory profiler (version 5.6). The dump shows a total of 2.2 GB reachable objects and 0.3 GB unreachable objects. What could explain the remaining 7.5 GB ?



For those not familiar to .NET memory profiler, it is similar to WinDbg. It seems to contain most objects including strings, arrays... and in most situation I've studied, the total memory occupied by the objects displayed in the profiler was very close to the dump size.



Possible explanations I've been thinking of :




  • the LOH does not really get fully compacted

  • some memory is used beyond the objects displayed by the profiler










share|improve this question




















  • 2




    This is kind of equivalent to "got memory leak, plz fix.". What does your service do? Does it call into anything unmanaged? Does it use COM?
    – fstam
    2 days ago






  • 3




    You should not have that many unreachable objects after a GC. A deadlocked finalizer thread causes memory explosions, use WinDbg to see what it is doing.
    – Hans Passant
    2 days ago










  • Thanks Hans. This unreachable memory was indeed suspicious to me. I'll have a look.
    – Benoit Sanchez
    2 days ago






  • 1




    Sounds like unmanaged memory usage. The JetBrains profiler can show that. You could also use the free VMMap.exe to test that theory.
    – usr
    2 days ago










  • Thanks usr. I checked with VMMap. 95% of my 10 GB are in the managed heap. This seems to rule out unmanaged memory usage.
    – Benoit Sanchez
    2 days ago













up vote
2
down vote

favorite









up vote
2
down vote

favorite











I can't explain most of the memory used by a C# process. The total memory is 10 GB, but the total reachable and unreachable objects altogether total 2.5 GB. I wonder what these 7.5 GB could be?



I'm looking for the most likely explanations or a method to find out what this memory can be. It is also possible there is something I don't understand about dumps and memory profilers.



Here is the precise situation. The process is .NET 4.5.1. It downloads pages from internet and process them with machine learning. Possible issue: I know the web objects are not properly disposed (HttpClient, HttpResponse...) but before I get into cleaning all this, I wish I could relate the memory problem to it. The memory is almost entirely in the Managed Heap as shown by VMMap. This seems to rule out unmanaged memory leak.
enter image description here



The process has been running for days and the memory slowly grew. At some point, the memory is 11 GB. I stop everything running in the process. I run garbage collections including large object heap compaction several times (with one minute of interval):



GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();


The memory goes down to 10 GB. Then I create the dump:




procdump -ma psid




The dump is 10 GB as expected.



I open the dump with .NET memory profiler (version 5.6). The dump shows a total of 2.2 GB reachable objects and 0.3 GB unreachable objects. What could explain the remaining 7.5 GB ?



For those not familiar to .NET memory profiler, it is similar to WinDbg. It seems to contain most objects including strings, arrays... and in most situation I've studied, the total memory occupied by the objects displayed in the profiler was very close to the dump size.



Possible explanations I've been thinking of :




  • the LOH does not really get fully compacted

  • some memory is used beyond the objects displayed by the profiler










share|improve this question















I can't explain most of the memory used by a C# process. The total memory is 10 GB, but the total reachable and unreachable objects altogether total 2.5 GB. I wonder what these 7.5 GB could be?



I'm looking for the most likely explanations or a method to find out what this memory can be. It is also possible there is something I don't understand about dumps and memory profilers.



Here is the precise situation. The process is .NET 4.5.1. It downloads pages from internet and process them with machine learning. Possible issue: I know the web objects are not properly disposed (HttpClient, HttpResponse...) but before I get into cleaning all this, I wish I could relate the memory problem to it. The memory is almost entirely in the Managed Heap as shown by VMMap. This seems to rule out unmanaged memory leak.
enter image description here



The process has been running for days and the memory slowly grew. At some point, the memory is 11 GB. I stop everything running in the process. I run garbage collections including large object heap compaction several times (with one minute of interval):



GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();


The memory goes down to 10 GB. Then I create the dump:




procdump -ma psid




The dump is 10 GB as expected.



I open the dump with .NET memory profiler (version 5.6). The dump shows a total of 2.2 GB reachable objects and 0.3 GB unreachable objects. What could explain the remaining 7.5 GB ?



For those not familiar to .NET memory profiler, it is similar to WinDbg. It seems to contain most objects including strings, arrays... and in most situation I've studied, the total memory occupied by the objects displayed in the profiler was very close to the dump size.



Possible explanations I've been thinking of :




  • the LOH does not really get fully compacted

  • some memory is used beyond the objects displayed by the profiler







c# memory-profiling large-object-heap






share|improve this question















share|improve this question













share|improve this question




share|improve this question








edited 2 days ago

























asked 2 days ago









Benoit Sanchez

234111




234111








  • 2




    This is kind of equivalent to "got memory leak, plz fix.". What does your service do? Does it call into anything unmanaged? Does it use COM?
    – fstam
    2 days ago






  • 3




    You should not have that many unreachable objects after a GC. A deadlocked finalizer thread causes memory explosions, use WinDbg to see what it is doing.
    – Hans Passant
    2 days ago










  • Thanks Hans. This unreachable memory was indeed suspicious to me. I'll have a look.
    – Benoit Sanchez
    2 days ago






  • 1




    Sounds like unmanaged memory usage. The JetBrains profiler can show that. You could also use the free VMMap.exe to test that theory.
    – usr
    2 days ago










  • Thanks usr. I checked with VMMap. 95% of my 10 GB are in the managed heap. This seems to rule out unmanaged memory usage.
    – Benoit Sanchez
    2 days ago














  • 2




    This is kind of equivalent to "got memory leak, plz fix.". What does your service do? Does it call into anything unmanaged? Does it use COM?
    – fstam
    2 days ago






  • 3




    You should not have that many unreachable objects after a GC. A deadlocked finalizer thread causes memory explosions, use WinDbg to see what it is doing.
    – Hans Passant
    2 days ago










  • Thanks Hans. This unreachable memory was indeed suspicious to me. I'll have a look.
    – Benoit Sanchez
    2 days ago






  • 1




    Sounds like unmanaged memory usage. The JetBrains profiler can show that. You could also use the free VMMap.exe to test that theory.
    – usr
    2 days ago










  • Thanks usr. I checked with VMMap. 95% of my 10 GB are in the managed heap. This seems to rule out unmanaged memory usage.
    – Benoit Sanchez
    2 days ago








2




2




This is kind of equivalent to "got memory leak, plz fix.". What does your service do? Does it call into anything unmanaged? Does it use COM?
– fstam
2 days ago




This is kind of equivalent to "got memory leak, plz fix.". What does your service do? Does it call into anything unmanaged? Does it use COM?
– fstam
2 days ago




3




3




You should not have that many unreachable objects after a GC. A deadlocked finalizer thread causes memory explosions, use WinDbg to see what it is doing.
– Hans Passant
2 days ago




You should not have that many unreachable objects after a GC. A deadlocked finalizer thread causes memory explosions, use WinDbg to see what it is doing.
– Hans Passant
2 days ago












Thanks Hans. This unreachable memory was indeed suspicious to me. I'll have a look.
– Benoit Sanchez
2 days ago




Thanks Hans. This unreachable memory was indeed suspicious to me. I'll have a look.
– Benoit Sanchez
2 days ago




1




1




Sounds like unmanaged memory usage. The JetBrains profiler can show that. You could also use the free VMMap.exe to test that theory.
– usr
2 days ago




Sounds like unmanaged memory usage. The JetBrains profiler can show that. You could also use the free VMMap.exe to test that theory.
– usr
2 days ago












Thanks usr. I checked with VMMap. 95% of my 10 GB are in the managed heap. This seems to rule out unmanaged memory usage.
– Benoit Sanchez
2 days ago




Thanks usr. I checked with VMMap. 95% of my 10 GB are in the managed heap. This seems to rule out unmanaged memory usage.
– Benoit Sanchez
2 days ago












1 Answer
1






active

oldest

votes

















up vote
0
down vote













After investigation, the problem happens to be heap fragmentation because of pinned buffers. I'll explain how to investigate and what pinned buffers are.



All profilers I've used agreed to say most of the heap is free. Now I needed to look at fragmentation. You can do it with WinDbg for example:



!dumpheap -stat


Then look at the "Fragmented blocks larger than..." section. WinDbg tells you what objects lie between the free blocks making compaction impossible. Then look at what is holding these objects and if they are pinned, here for example object at address 0000000bfaf93b80:



!gcroot 0000000bfaf93b80


It displays the reference graph:



00000004082945e0 (async pinned handle)
-> 0000000535b3a3e0 System.Threading.OverlappedData
-> 00000006f5266d38 System.Threading.IOCompletionCallback
-> 0000000b35402220 System.Net.Sockets.SocketAsyncEventArgs
-> 0000000bf578c850 System.Net.Sockets.Socket
-> 0000000bf578c900 System.Net.SocketAddress
-> 0000000bfaf93b80 System.Byte

00000004082e2148 (pinned handle)
-> 0000000bfaf93b80 System.Byte


The last two lines tell you the object is pinned. Pinned objects are buffers than can't be moved because their address is shared with non-managed code. Here you can guess it is the system HTTP or TCP layer. When managed code needs to send the address of a buffer to external code, it needs to "pin" the buffer so that the address remains valid: the GC cannot move it.



These buffers, while being a very small part of the memory make compaction impossible and thus cause large memory "leak", even if it is not exactly a leak, more a fragmentation problem. In this case, this does not happen on the LOH, since the pinned objects are small (check with !do addr in WinDbg). Now the question is: what is causing these pinned objects to live forever: find the root cause of the leak that causes the fragmentation. This is beyond the scope of the question. Obviously here, it seems to be related to web access.



You can read similar questions here:




  • .NET Does NOT Have Reliable Asynchronouos Socket Communication?


  • https://ayende.com/blog/181761-C/the-curse-of-memory-fragmentation


  • .NET deletes pinned allocated buffer (good explanation of pinned objects in the answer)







share|improve this answer























    Your Answer






    StackExchange.ifUsing("editor", function () {
    StackExchange.using("externalEditor", function () {
    StackExchange.using("snippets", function () {
    StackExchange.snippets.init();
    });
    });
    }, "code-snippets");

    StackExchange.ready(function() {
    var channelOptions = {
    tags: "".split(" "),
    id: "1"
    };
    initTagRenderer("".split(" "), "".split(" "), channelOptions);

    StackExchange.using("externalEditor", function() {
    // Have to fire editor after snippets, if snippets enabled
    if (StackExchange.settings.snippets.snippetsEnabled) {
    StackExchange.using("snippets", function() {
    createEditor();
    });
    }
    else {
    createEditor();
    }
    });

    function createEditor() {
    StackExchange.prepareEditor({
    heartbeatType: 'answer',
    convertImagesToLinks: true,
    noModals: true,
    showLowRepImageUploadWarning: true,
    reputationToPostImages: 10,
    bindNavPrevention: true,
    postfix: "",
    imageUploader: {
    brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
    contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
    allowUrls: true
    },
    onDemand: true,
    discardSelector: ".discard-answer"
    ,immediatelyShowMarkdownHelp:true
    });


    }
    });














     

    draft saved


    draft discarded


















    StackExchange.ready(
    function () {
    StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53350298%2flarge-unexplained-memory-in-the-memory-dump-of-a-net-process%23new-answer', 'question_page');
    }
    );

    Post as a guest















    Required, but never shown

























    1 Answer
    1






    active

    oldest

    votes








    1 Answer
    1






    active

    oldest

    votes









    active

    oldest

    votes






    active

    oldest

    votes








    up vote
    0
    down vote













    After investigation, the problem happens to be heap fragmentation because of pinned buffers. I'll explain how to investigate and what pinned buffers are.



    All profilers I've used agreed to say most of the heap is free. Now I needed to look at fragmentation. You can do it with WinDbg for example:



    !dumpheap -stat


    Then look at the "Fragmented blocks larger than..." section. WinDbg tells you what objects lie between the free blocks making compaction impossible. Then look at what is holding these objects and if they are pinned, here for example object at address 0000000bfaf93b80:



    !gcroot 0000000bfaf93b80


    It displays the reference graph:



    00000004082945e0 (async pinned handle)
    -> 0000000535b3a3e0 System.Threading.OverlappedData
    -> 00000006f5266d38 System.Threading.IOCompletionCallback
    -> 0000000b35402220 System.Net.Sockets.SocketAsyncEventArgs
    -> 0000000bf578c850 System.Net.Sockets.Socket
    -> 0000000bf578c900 System.Net.SocketAddress
    -> 0000000bfaf93b80 System.Byte

    00000004082e2148 (pinned handle)
    -> 0000000bfaf93b80 System.Byte


    The last two lines tell you the object is pinned. Pinned objects are buffers than can't be moved because their address is shared with non-managed code. Here you can guess it is the system HTTP or TCP layer. When managed code needs to send the address of a buffer to external code, it needs to "pin" the buffer so that the address remains valid: the GC cannot move it.



    These buffers, while being a very small part of the memory make compaction impossible and thus cause large memory "leak", even if it is not exactly a leak, more a fragmentation problem. In this case, this does not happen on the LOH, since the pinned objects are small (check with !do addr in WinDbg). Now the question is: what is causing these pinned objects to live forever: find the root cause of the leak that causes the fragmentation. This is beyond the scope of the question. Obviously here, it seems to be related to web access.



    You can read similar questions here:




    • .NET Does NOT Have Reliable Asynchronouos Socket Communication?


    • https://ayende.com/blog/181761-C/the-curse-of-memory-fragmentation


    • .NET deletes pinned allocated buffer (good explanation of pinned objects in the answer)







    share|improve this answer



























      up vote
      0
      down vote













      After investigation, the problem happens to be heap fragmentation because of pinned buffers. I'll explain how to investigate and what pinned buffers are.



      All profilers I've used agreed to say most of the heap is free. Now I needed to look at fragmentation. You can do it with WinDbg for example:



      !dumpheap -stat


      Then look at the "Fragmented blocks larger than..." section. WinDbg tells you what objects lie between the free blocks making compaction impossible. Then look at what is holding these objects and if they are pinned, here for example object at address 0000000bfaf93b80:



      !gcroot 0000000bfaf93b80


      It displays the reference graph:



      00000004082945e0 (async pinned handle)
      -> 0000000535b3a3e0 System.Threading.OverlappedData
      -> 00000006f5266d38 System.Threading.IOCompletionCallback
      -> 0000000b35402220 System.Net.Sockets.SocketAsyncEventArgs
      -> 0000000bf578c850 System.Net.Sockets.Socket
      -> 0000000bf578c900 System.Net.SocketAddress
      -> 0000000bfaf93b80 System.Byte

      00000004082e2148 (pinned handle)
      -> 0000000bfaf93b80 System.Byte


      The last two lines tell you the object is pinned. Pinned objects are buffers than can't be moved because their address is shared with non-managed code. Here you can guess it is the system HTTP or TCP layer. When managed code needs to send the address of a buffer to external code, it needs to "pin" the buffer so that the address remains valid: the GC cannot move it.



      These buffers, while being a very small part of the memory make compaction impossible and thus cause large memory "leak", even if it is not exactly a leak, more a fragmentation problem. In this case, this does not happen on the LOH, since the pinned objects are small (check with !do addr in WinDbg). Now the question is: what is causing these pinned objects to live forever: find the root cause of the leak that causes the fragmentation. This is beyond the scope of the question. Obviously here, it seems to be related to web access.



      You can read similar questions here:




      • .NET Does NOT Have Reliable Asynchronouos Socket Communication?


      • https://ayende.com/blog/181761-C/the-curse-of-memory-fragmentation


      • .NET deletes pinned allocated buffer (good explanation of pinned objects in the answer)







      share|improve this answer

























        up vote
        0
        down vote










        up vote
        0
        down vote









        After investigation, the problem happens to be heap fragmentation because of pinned buffers. I'll explain how to investigate and what pinned buffers are.



        All profilers I've used agreed to say most of the heap is free. Now I needed to look at fragmentation. You can do it with WinDbg for example:



        !dumpheap -stat


        Then look at the "Fragmented blocks larger than..." section. WinDbg tells you what objects lie between the free blocks making compaction impossible. Then look at what is holding these objects and if they are pinned, here for example object at address 0000000bfaf93b80:



        !gcroot 0000000bfaf93b80


        It displays the reference graph:



        00000004082945e0 (async pinned handle)
        -> 0000000535b3a3e0 System.Threading.OverlappedData
        -> 00000006f5266d38 System.Threading.IOCompletionCallback
        -> 0000000b35402220 System.Net.Sockets.SocketAsyncEventArgs
        -> 0000000bf578c850 System.Net.Sockets.Socket
        -> 0000000bf578c900 System.Net.SocketAddress
        -> 0000000bfaf93b80 System.Byte

        00000004082e2148 (pinned handle)
        -> 0000000bfaf93b80 System.Byte


        The last two lines tell you the object is pinned. Pinned objects are buffers than can't be moved because their address is shared with non-managed code. Here you can guess it is the system HTTP or TCP layer. When managed code needs to send the address of a buffer to external code, it needs to "pin" the buffer so that the address remains valid: the GC cannot move it.



        These buffers, while being a very small part of the memory make compaction impossible and thus cause large memory "leak", even if it is not exactly a leak, more a fragmentation problem. In this case, this does not happen on the LOH, since the pinned objects are small (check with !do addr in WinDbg). Now the question is: what is causing these pinned objects to live forever: find the root cause of the leak that causes the fragmentation. This is beyond the scope of the question. Obviously here, it seems to be related to web access.



        You can read similar questions here:




        • .NET Does NOT Have Reliable Asynchronouos Socket Communication?


        • https://ayende.com/blog/181761-C/the-curse-of-memory-fragmentation


        • .NET deletes pinned allocated buffer (good explanation of pinned objects in the answer)







        share|improve this answer














        After investigation, the problem happens to be heap fragmentation because of pinned buffers. I'll explain how to investigate and what pinned buffers are.



        All profilers I've used agreed to say most of the heap is free. Now I needed to look at fragmentation. You can do it with WinDbg for example:



        !dumpheap -stat


        Then look at the "Fragmented blocks larger than..." section. WinDbg tells you what objects lie between the free blocks making compaction impossible. Then look at what is holding these objects and if they are pinned, here for example object at address 0000000bfaf93b80:



        !gcroot 0000000bfaf93b80


        It displays the reference graph:



        00000004082945e0 (async pinned handle)
        -> 0000000535b3a3e0 System.Threading.OverlappedData
        -> 00000006f5266d38 System.Threading.IOCompletionCallback
        -> 0000000b35402220 System.Net.Sockets.SocketAsyncEventArgs
        -> 0000000bf578c850 System.Net.Sockets.Socket
        -> 0000000bf578c900 System.Net.SocketAddress
        -> 0000000bfaf93b80 System.Byte

        00000004082e2148 (pinned handle)
        -> 0000000bfaf93b80 System.Byte


        The last two lines tell you the object is pinned. Pinned objects are buffers than can't be moved because their address is shared with non-managed code. Here you can guess it is the system HTTP or TCP layer. When managed code needs to send the address of a buffer to external code, it needs to "pin" the buffer so that the address remains valid: the GC cannot move it.



        These buffers, while being a very small part of the memory make compaction impossible and thus cause large memory "leak", even if it is not exactly a leak, more a fragmentation problem. In this case, this does not happen on the LOH, since the pinned objects are small (check with !do addr in WinDbg). Now the question is: what is causing these pinned objects to live forever: find the root cause of the leak that causes the fragmentation. This is beyond the scope of the question. Obviously here, it seems to be related to web access.



        You can read similar questions here:




        • .NET Does NOT Have Reliable Asynchronouos Socket Communication?


        • https://ayende.com/blog/181761-C/the-curse-of-memory-fragmentation


        • .NET deletes pinned allocated buffer (good explanation of pinned objects in the answer)








        share|improve this answer














        share|improve this answer



        share|improve this answer








        edited 16 hours ago

























        answered 17 hours ago









        Benoit Sanchez

        234111




        234111






























             

            draft saved


            draft discarded



















































             


            draft saved


            draft discarded














            StackExchange.ready(
            function () {
            StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53350298%2flarge-unexplained-memory-in-the-memory-dump-of-a-net-process%23new-answer', 'question_page');
            }
            );

            Post as a guest















            Required, but never shown





















































            Required, but never shown














            Required, but never shown












            Required, but never shown







            Required, but never shown

































            Required, but never shown














            Required, but never shown












            Required, but never shown







            Required, but never shown







            Popular posts from this blog

            Costa Masnaga

            Fotorealismo

            Sidney Franklin