50

I am building a Java web app, using the Play! Framework. I'm hosting it on playapps.net. I have been puzzling for a while over the provided graphs of memory consumption. Here is a sample:

Heap Memory

The graph comes from a period of consistent but nominal activity. I did nothing to trigger the falloff in memory, so I presume this occurred because the garbage collector ran as it has almost reached its allowable memory consumption.

My questions:

  • Is it fair for me to assume that my application does not have a memory leak, as it appears that all the memory is correctly reclaimed by the garbage collector when it does run?
  • (from the title) Why is java waiting until the last possible second to run the garbage collector? I am seeing significant performance degradation as the memory consumption grows to the top fourth of the graph.
  • If my assertions above are correct, then how can I go about fixing this issue? The other posts I have read on SO seem opposed to calls to System.gc(), ranging from neutral ("it's only a request to run GC, so the JVM may just ignore you") to outright opposed ("code that relies on System.gc() is fundamentally broken"). Or am I off base here, and I should be looking for defects in my own code that is causing this behavior and intermittent performance loss?

UPDATE
I have opened a discussion on PlayApps.net pointing to this question and mentioning some of the points here; specifically @Affe's comment regarding the settings for a full GC being set very conservatively, and @G_H's comment about settings for the initial and max heap size.

Here's a link to the discussion, though you unfortunately need a playapps account to view it.

I will report the feedback here when I get it; thanks so much everyone for your answers, I've already learned a great deal from them!

Resolution
Playapps support, which is still great, didn't have many suggestions for me, their only thought being that if I was using the cache extensively this may be keeping objects alive longer than need be, but that isn't the case. I still learned a ton (woo hoo!), and I gave @Ryan Amos the green check as I took his suggestion of calling System.gc() every half day, which for now is working fine.

Elrond_EGLDer
  • 47,430
  • 25
  • 189
  • 180
goggin13
  • 6,996
  • 6
  • 26
  • 43
  • I don't know the play framework enough to give you a definite answer on its memory footprint, but SAP has offered a nice tool to analyze memory allocation, now at http://www.eclipse.org/mat/ I think that could help you to make an assertion rather than an assumption about the memory leak. – rds Aug 18 '11 at 22:03
  • 2
    One thing that may be causing the performance issue is swapping; if you don't have enough physical memory for that heap (e.g., it's a shared machine with lots of other processes using a lot of memory), swapping pages to disk will kill you VM's performance. – vanza Aug 18 '11 at 22:06
  • @rds Play prides itself on a very small memory footprint, and PlayApps.net (the hosting service I'm using) itself runs off of a smaller footprint than the one I'm showing. Thanks for the MAT recommendation, I will take a look! – goggin13 Aug 18 '11 at 22:06
  • @vanza it is running on a shared machine; if swapping pages is the performance bottleneck, how would I combat that? Or how can I verify if that is indeed the issue? Thanks for your thoughts! If you want to elaborate in an answer I'll happily upvote you – goggin13 Aug 18 '11 at 22:10
  • And stupid question: what database are you running on this environment. – rds Aug 18 '11 at 22:13
  • I thought it might be a in-memory database that stores lots of stuff, but I didn't see the huge reduction of heap at now-2h. – rds Aug 18 '11 at 22:28
  • 2
    Also, I agree with +vanza about swapping disaster. From my (awful) experience on SharePoint, I remember that giving too much memory to a process is not _always_ the best strategy for performances. Can you change the JVM settings and set -Xmx 80m, and see how it behaves? – rds Aug 18 '11 at 22:30
  • @vanza Wouldn't swapping have a very obvious impact on speed but leave memory usage mostly the same? – G_H Aug 18 '11 at 22:30
  • @rds I am working through playapps.net, which doesn't offer me any special configs for the JVM :(. And I am unable to reproduce the performance issues locally. – goggin13 Aug 18 '11 at 22:33
  • @G_H: swapping doesn't affect memory usage. But since he has a large heap, his process will grow until the GC runs, using more physical memory, which can lead to swapping if there's not that much of it available. Which looks like what his graph + comments seem to indicate. – vanza Aug 18 '11 at 22:40
  • Possibly related [examples](http://stackoverflow.com/questions/6309407/remove-top-level-container-on-runtime/6310284#6310284). – trashgod Aug 19 '11 at 04:05
  • IIRC, the maximum heap size for a Java JVM is (typically, it depends on the JVM) a particular fractionf of the RAM, so swapping should not be an issue by default. – Raedwald Aug 19 '11 at 11:52
  • 1
    @Raedwald: I haven't heard of that before. There's a limit to the maximum heap size for 32-bit JVMs that's quite lower than the average RAM memory capacity these days. That is no longer the case for 64-bit JVMs. But even so, if many processes are running on the host and its memory is getting full, swapping will start to occur. This is handled by the OS, the JVM has no awareness of it. It'll just allocate memory, unknowing that some of this could be mapped to disk. – G_H Aug 24 '11 at 08:56

6 Answers6

22

Any detailed answer is going to depend on which garbage collector you're using, but there are some things that are basically the same across all (modern, sun/oracle) GCs.

Every time you see the usage in the graph go down, that is a garbage collection. The only way heap gets freed is through garbage collection. The thing is there are two types of garbage collections, minor and full. The heap gets divided into two basic "areas." Young and tenured. (There are lots more subgroups in reality.) Anything that is taking up space in Young and is still in use when the minor GC comes along to free up some memory, is going to get 'promoted' into tenured. Once something makes the leap into tenured, it sits around indefinitely until the heap has no free space and a full garbage collection is necessary.

So one interpretation of that graph is that your young generation is fairly small (by default it can be a fairly small % of total heap on some JVMs) and you're keeping objects "alive" for comparatively very long times. (perhaps you're holding references to them in the web session?) So your objects are 'surviving' garbage collections until they get promoted into tenured space, where they stick around indefinitely until the JVM is well and good truly out of memory.

Again, that's just one common situation that fits with the data you have. Would need full details about the JVM configuration and the GC logs to really tell for sure what's going on.

Affe
  • 45,134
  • 11
  • 79
  • 81
  • That sounds a lot like what I see when I profile my app locally (using YourKit). The playapps.net provided graphs are not that detailed, as you can see, but YourKit shows me the saw pattern of memory for the "young" generation, and the "old" generation grows steadily until garbage collection. None of this should cause performance degradation though, correct? Should I be looking elsewhere to explain why the app slows down once the memory is in the top 1/4th of the graph, and then speeds back up once the memory is reclaimed? – goggin13 Aug 18 '11 at 22:15
  • It is odd that the JVM would be 'stressing' itself to that extent to eek out every bit of space before running full collection. Do you control the JVM or is the environment provided by your hosting company? It's possible they configured it to do that for their own reasons. – Affe Aug 18 '11 at 22:17
  • All controlled by the hosting company, PlayApps.net, on a VPS server. Do you think it's worth contacting them to see if they have advice? – goggin13 Aug 18 '11 at 22:20
  • 2
    Yeah, you could mention it looks like the settings for when a full collection triggers are so stingy that the frequency of minor collections is impacting performance long before the full collection happens. It's also possible that it's using a concurrent garbage collector with only one processor core, which could be expected to behave in a way consistent with your problem. – Affe Aug 18 '11 at 22:26
  • @goggin13 "It is odd that the JVM would be 'stressing' itself to that extent to eek out every bit of space before running full collection" I'm not so sure it can easilly be done any other way. Once the GC has determined that something is garbage, it would be a waste not to remove that garbage. Determining *which* objects are garbage can be expensive. – Raedwald Aug 19 '11 at 11:55
  • Right, it's just that there's an appearance that it's thrashing around doing minor collections that are only able to free a very small amount of heap. It's really impossible to be more detailed than that without knowing which collector it is. – Affe Aug 19 '11 at 15:43
19

Java won't run the garbage cleaner until it has to, because the garbage cleaner slows things down quite a bit and shouldn't be run that frequently. I think you would be OK to schedule a cleaning more frequently, such as every 3 hours. If an application never consumes full memory, there should be no reason to ever run the garbage cleaner, which is why Java only runs it when the memory is very high.

So basically, don't worry about what others say: do what works best. If you find performance improvements from running the garbage cleaner at 66% memory, do it.

Ryan Amos
  • 5,164
  • 4
  • 31
  • 53
  • Thanks! I'll give it a shot and see if that helps things. – goggin13 Aug 18 '11 at 22:19
  • 2
    Upvoted. Don't listen to people who say "don't ever do this". If it works, doesn't complicate things horrendously, doesn't create an unmaintainable architecture and there's no better solution in sight, go for it. – G_H Aug 22 '11 at 15:19
  • @G_H there's no other reason people dont ask for not ever doing this :P – shabby Feb 04 '14 at 05:05
12

I am noticing that the graph isn't sloping strictly upward until the drop, but has smaller local variations. Although I'm not certain, I don't think memory use would show these small drops if there was no garbage collection going on.

There are minor and major collections in Java. Minor collections occur frequently, whereas major collections are rarer and diminish performance more. Minor collections probably tend to sweep up stuff like short-lived object instances created within methods. A major collection will remove a lot more, which is what probably happened at the end of your graph.

Now, some answers that were posted while I'm typing this give good explanations regarding the differences in garbage collectors, object generations and more. But that still doesn't explain why it would take so absurdly long (nearly 24 hours) before a serious cleaning is done.

Two things of interest that can be set for a JVM at startup are the maximum allowed heap size, and the initial heap size. The maximum is a hard limit, once you reach that, further garbage collection doesn't reduce memory usage and if you need to allocate new space for objects or other data, you'll get an OutOfMemoryError. However, internally there's a soft limit as well: the current heap size. A JVM doesn't immediately gobble up the maximum amount of memory. Instead, it starts at your initial heap size and then increases the heap when it's needed. Think of it a bit as the RAM of your JVM, that can increase dynamically.

If the actual memory use of your application starts to reach the current heap size, a garbage collection will typically be instigated. This might reduce the memory use, so an increase in heap size isn't needed. But it's also possible that the application currently does need all that memory and would exceed the heap size. In that case, it is increased provided that it hasn't already reached the maximum set limit.

Now, what might be your case is that the initial heap size is set to the same value as the maximum. Suppose that would be so, then the JVM will immediately seize all that memory. It will take a very long time before the application has accumulated enough garbage to reach the heap size in memory usage. But at that moment you'll see a large collection. Starting with a small enough heap and allowing it to grow keeps the memory use limited to what's needed.

This is assuming that your graph shows heap use and not allocated heap size. If that's not the case and you are actually seeing the heap itself grow like this, something else is going on. I'll admit I'm not savvy enough regarding the internals of garbage collection and its scheduling to be absolutely certain of what's happening here, most of this is from observation of leaking applications in profilers. So if I've provided faulty info, I'll take this answer down.

G_H
  • 11,514
  • 2
  • 32
  • 73
  • that is really interesting; I was not aware of your point about the JVM initial heap size being set the same as the maximum heap size. I do not have access to the JVM settings for this hosting account, but I think I will open a ticket with support for their two cents, and I will see if they think that may be an issue. Thank you! – goggin13 Aug 18 '11 at 22:35
  • 2
    It can happen. People sometimes think "hey, I'll just set the start and max at the same, so Java won't have to bother with this heap resizing business". But in reality, this may actually degrade performance. If you don't care about having a large batch of unused memory hanging around for some time, then it could make sense. But most of the time you'll want to play nice with other processes in the system that are hungry for memory. – G_H Aug 18 '11 at 22:38
  • Do you think this could go hand in hand with performance hits? Other posts have suggested this memory use shouldn't really affect my app. Also, do you think explicit calls to System.gc() may help kickstart earlier collections? – goggin13 Aug 18 '11 at 22:46
  • Well, since you're running on an environment with potentially a lot of other JVM processes, there's plenty of factors that could influence performance. Such as swapping, as mentioned by vanza in the comments on your post. Giving a JVM a higher max heap can actually disadvantage you, as it might use more memory than necessary, hindering other processes. Regarding System.gc(), it's often looked down upon, but I dislike absolutes. Even the worst anti-pattern is sometimes the right answer in strange situations. You could give it a try and see what happens. Have it run at predictable times. – G_H Aug 18 '11 at 23:05
  • I will do both of those and report back here later! Thanks so much for your help, I really appreciate it – goggin13 Aug 18 '11 at 23:07
2

As you might have noticed, this does not affect you. The garbage collection only kicks in if the JVM feels there is a need for it to run and this happens for the sake of optimization, there's no use of doing many small collections if you can make a single full collection and do a full cleanup.

The current JVM contains some really interesting algorithms and the garbage collection itself id divided into 3 different regions, you can find a lot more about this here, here's a sample:

Three types of collection algorithms

The HotSpot JVM provides three GC algorithms, each tuned for a specific type of collection within a specific generation. The copy (also known as scavenge) collection quickly cleans up short-lived objects in the new generation heap. The mark-compact algorithm employs a slower, more robust technique to collect longer-lived objects in the old generation heap. The incremental algorithm attempts to improve old generation collection by performing robust GC while minimizing pauses.

Copy/scavenge collection

Using the copy algorithm, the JVM reclaims most objects in the new generation object space (also known as eden) simply by making small scavenges -- a Java term for collecting and removing refuse. Longer-lived objects are ultimately copied, or tenured, into the old object space.

Mark-compact collection

As more objects become tenured, the old object space begins to reach maximum occupancy. The mark-compact algorithm, used to collect objects in the old object space, has different requirements than the copy collection algorithm used in the new object space.

The mark-compact algorithm first scans all objects, marking all reachable objects. It then compacts all remaining gaps of dead objects. The mark-compact algorithm occupies more time than the copy collection algorithm; however, it requires less memory and eliminates memory fragmentation.

Incremental (train) collection

The new generation copy/scavenge and the old generation mark-compact algorithms can't eliminate all JVM pauses. Such pauses are proportional to the number of live objects. To address the need for pauseless GC, the HotSpot JVM also offers incremental, or train, collection.

Incremental collection breaks up old object collection pauses into many tiny pauses even with large object areas. Instead of just a new and an old generation, this algorithm has a middle generation comprising many small spaces. There is some overhead associated with incremental collection; you might see as much as a 10-percent speed degradation.

The -Xincgc and -Xnoincgc parameters control how you use incremental collection. The next release of HotSpot JVM, version 1.4, will attempt continuous, pauseless GC that will probably be a variation of the incremental algorithm. I won't discuss incremental collection since it will soon change.

This generational garbage collector is one of the most efficient solutions we have for the problem nowadays.

Community
  • 1
  • 1
Maurício Linhares
  • 37,947
  • 14
  • 116
  • 153
  • Thanks for the link! And the summaries. If you see my comment @Affe's response, though this does seem to affect me. I see performance degrade as the memory consumption goes into the top 1/4th of the graph, and then it seems to bounce back after GC runs. Is this more likely a coincidence, and I should be looking elsewhere for the performance issue? – goggin13 Aug 18 '11 at 22:18
  • That's very old information, from 2002 about Java 1.3.1. Latest JVMs use different algorithms. Here is some information about Java 6 http://www.oracle.com/technetwork/java/javase/gc-tuning-6-140523.html and Java 7 has also the new G1 garbage collector. – Esko Luontola Aug 18 '11 at 22:27
  • @esko the generations (which is what's covered in this piece) are **exactly the same** and have not changed. For more information on the latest algorithm there is also this one - http://www.oracle.com/technetwork/java/gc-tuning-5-138395.html#1.1.Introduction%7Coutline – Maurício Linhares Aug 18 '11 at 22:31
2

I had an app that produced a graph like that and acted as you describe. I was using the CMS collector (-XX:+UseConcMarkSweepGC). Here is what was going on in my case.

I did not have enough memory configured for the application, so over time I was running into fragmentation problems in the heap. This caused GCs with greater and greater frequency, but it did not actually throw an OOME or fail out of CMS to the serial collector (which it is supposed to do in that case) because the stats it keeps only count application paused time (GC blocks the world), application concurrent time (GC runs with application threads) is ignored for those calculations. I tuned some parameters, mainly gave it a whole crap load more heap (with a very large new space), set -XX:CMSFullGCsBeforeCompaction=1, and the problem stopped occurring.

0

Probably you do have memory leaks that's cleared every 24 hours.

irreputable
  • 42,827
  • 9
  • 59
  • 89
  • 1
    What do you mean "cleared"? Wouldn't a memory leak be unable to be cleared? I am not restarting the app, the drop in memory (I'm assuming) is from the GC running. If there were memory leaks, wouldn't the GC not be able to reclaim that memory? Don't be shy if I'm dead wrong, I'm just trying to get a grasp on everything! Thanks for your thoughts! – goggin13 Aug 18 '11 at 23:54
  • Say your app has a List of some stuff that keeps growing; and every 24 hours the list gets emptied by your code. – irreputable Aug 19 '11 at 01:23