-
-
Notifications
You must be signed in to change notification settings - Fork 143
Fix jackson ion memory leak issue #306
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
@cowtowncoder could you help me review this PR? |
@neoremind Yes, I will -- it may take a bit due to holidays but I will prioritize this as it looks like an easy but important patch. One quick thing: unless we have a CLA from you, we would need one from: https://github.com/FasterXML/jackson/blob/master/contributor-agreement.pdf so I can accept the contribution. This only ever needs to be done once and CLA works for all future contributions. Looking forward to merging this patch and thank you once again! |
@cowtowncoder I have signed the contributor agreement. I understand this is holiday period, so no hurry and take your time to help me review. Thank you very much and happy holidays! 😄 |
@neoremind Thank you for sending the CLA, I got it. But now I realized I do need to get someone with more knowledge with IonReader to code review this. @mcliedtke or @jobarr-amzn could you help here (or know someone who can)? My main question is that ownership of passed-in
and it might be necessary to do something similar. Or maybe instances should always be closed; this depends on intent, and how readers are constructed and passed to A quick look suggests it is probably safe (and/or things are more complicated), as we have calls like:
in which actual underlying input source is further wrapped by |
Will wait for a bit, but unless I hear anything against the patch will probably merge. |
@cowtowncoder I'll represent the ion-java development team on this one. Your concerns about ownership of the IonReader are valid. It would not be correct for the The tricky part is that some of the data sources the user can provide are themselves Absent such an option, it feels like we'd have to either 1) deprecate the @neoremind While we're sorting this out, can you use the workaround of creating your |
Thank you @tgregg. This also makes me think that changes may need to go in 2.14 -- deprecation markers for sure cannot be added in a patch. |
@tgregg @cowtowncoder Thanks for your deep dive into this patch. I will investigate on the workaround of creating JsonParser via I agree that who is the authority of IonReader should be responsible to close the resource. I understand the patch we implement currently might cause of misbehavior in terms of configuration semantics. Anyway, Java finalizer could be the fallback mechanism to guardrail the release of resource, I suppose we are able to get by without too much churn. However the risk to prod stability is still there, hope this issue could be solved in 2.13 and 2.14, so that the risk alongside will be mitigated for all Ion users. |
I will defer on this to Ion maintainers; will be happy to help as needed but will not merge patch without approval. |
@tgregg @neoremind There probably hasn't been any progress here, but thought I'd ping -- I am happy to help if I can. 2.14 is still a bit off so there is no hurry but obviously would be nice to proceed if there is something to improve here. |
Closing in favor of #325. |
I wanted to report a memory leak issue of ion when uncompressing gzip payload.
How to reproduce the problem
Write a simple program to serialize data with gzip compressed content,
IonReader
is able to tell the compression format and create correspondingGzipInputStream
, but unfortunatelyIonParser
'sclose()
method leaks unclosedGzipInputStream
.If you enable Native Memory Tracking (NMT) by adding tuning flags
-XX:NativeMemoryTracking=summary
in JVM options, for instance, native memory information might look like below.From the output, JVM only takes about 23 GB in total including ~20G heap and ~3G metadata space + thread stack + code cache + symbols + GC + direct memory.
But when I run
pmap -x <pid> | sort -nr -k2
to check native memory usage. As a result, there are many anon memory chunk with size of 64MB which being filled up completely (or in pair, there could be chunk X and Y where sizeof(X+Y)=64MB).The machine runs the program has CPU of 8 physical cores (16core HT) with 64GB physical memory, the JVM is
Take a snapshot of any of memory chunks by running below command to check what stores inside.
Execute
view
to see human-readable content, as a result it is extacly what we ser/deserlized for.The default native memory allocator is glibc's
malloc
, JVM will call malloc backed by glibc in order to allocate memory. Basically, to optimize for SMP architecture with multiple threads, glibc will scale up the number of available arenas to do allocation from to avoid lock contention. An arena is 64MB large, the upper limit is to create 8 times the number of cores arena. Arena will be created on demand when a thread access an arena that is already locked so it grows the time, here with 16 cores CPU, the upper limit is 8GB.Let's do a math, JVM manages 23GB, since we run 128 threads, which could make up to 8*16=128 arena that takes up to 8GB, the maximum size would be ~31GB, but in my test environment, it could occupy up to 36GB and grows as time passes. This is because when glibc arena is full due to memory leak, it will extend a new heap based on the old heap (with pointer points to the new one), so the number of heap could exceed the limit of 8 times number of cores (refer to link Thread Arena with multiple heap part )
How to verify the fix?
After the fix, rerun the program, memory excessive consumption disappears, and pmap shows the RES of those 64MB memory chunks are mostly 0.
Why the problem is hard to find?
If I run
jmap histo:live <pid>
, there will be manyFinalizer
instances. This is becausejava.util.zip.Inflater
leveragesfinalize
method to free underlying heap or mmap allocated memory (see below JNI code).Inflater
only frees memory by callingend()
http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/native/java/util/zip/Inflater.c
From garbage collect’s point of view, JVM creates a “watchdog” for each one of the Finalizable instances, which is a instance of Finalizer. When Inflater is eligible for GC, GC will add Finalizer instances (here it is Inflater) to a special queue named java.lang.ref.Finalizer.ReferenceQueue. Then the object is marked as finalized. There is a Finalizer daemon thread running a loop to dequeue Finalizer instances from the queue and execute finalize() method. So Inflater will be reclaimed by GC to clean and end() does have chance to be executed, and the underlying heap or mmap could be freed up.
It is turn out to be an indeterministic problem, if Finalizer daemon thread calls finalize() method faster than we actually create new instances of Inflater, it could work. But is not guaranteed, because:
When GC is not able to catch up with the Inflater creation speed, Inflaters cannot be cleaned up quickly, somehow the balance is very fragile, memory allocated could grow up and once it grew up, it could be managed by memory allocator like glibc for reusing purpose which may never return back to OS, so the overall memory of the process grows over time.
So, the leaked memory won’t be freed until JVM GC Finalizer runs, which could explain the symptoms why the memory leak grows over time at a low speed.