The Secret Life Of The Finalizer

发现一篇好文章,讲Finalizer的生命周期的,好文章必须要转:http://www.fasterj.com/articles/finalizer1.shtml


In this article, Jack Shirazi looks into exactly what the JVM doeswhen you create a finalizable object and then have it garbagecollected. And it is surprisingly bizarre at times.

Published November 2007, Author Jack Shirazi

Page 1 of 2
next page: The Finalizer Lifecycle

In this article, I'm going to have a look into exactly what the JVM doeswhen you create a finalizable object, following its lifecycle through until it gets garbagecollected. Note this article only covers what happens in the Sun JVM, other JVMsmay differ in procedure.

A Simple Example

I'll start with a small class that doesn't cause any finalization, so that we can clearly see the differences.My Test1 class (listing 1) is a simple loop that just creates anddereferences instances of Test1. It runs just as you would expectit to - creating lots of garbage instances, with the garbagecollector kicking in periodically to clean up the garbage.

public class Test1
{
  static long NumberOfCreatedInstances = 0;
  static long NumberOfDeletedInstances = 0;
  public Test1() {NumberOfCreatedInstances++;}

  static public void main(String args[])
  {
    System.out.println("starting....");
    for (int i = 0; ; i++) {
      Test1 obj = new Test1();
      obj = null;
      if (i%10000000 == 0)
      {
        System.out.println(
          NumberOfCreatedInstances-NumberOfDeletedInstances);
      }
    }
  }
}
Listing 1: The Test1 class

Now I'll change Test1 very slightly by making it finalizable.I've called this class Test2 (listing 2). I've added just one tiny method,highlighted in the code. Doesn't seem like much, I don't even call thatmethod from my code. But as I'm sure you all know, it's the specialfinalize() method that is called by the garbage collector when the objectcan be reclaimed.

class Test2
{
  static long NumberOfCreatedInstances = 0;
  static long NumberOfDeletedInstances = 0;
  public Test2() {NumberOfCreatedInstances++;}

  static public void main(String args[])
  {
    System.out.println("starting....");
    for (int i = 0; ; i++) {
      Test2 obj = new Test2();
      obj = null;
      if (i%10000000 == 0)
      {
        System.out.println(
          NumberOfCreatedInstances-NumberOfDeletedInstances);
      }
    }
  }

  protected void finalize()
  {
	  NumberOfDeletedInstances++;
  }

}
Listing 2: The Test2 class - the only difference from the Test1 class is the addition of a finalize() method

It seems like there is not much of a difference between Test1 and Test2.But if I run the two programs, (the -verbose:gc option is particularly useful here),then I see very very different activities between the twoinvocations. Test1 happily sails along, creating objects continuously,interrupted occasionally by very fast young generations GCs - just exactlyas you would expect from looking at the code.

Test2, on the other hand, slows down to a crawl in comparison (you maywant to limit the heap to 64m with -Xmx64m, or even lower). If you runTest2 on a JVM earlier than a 1.6 JVM, then Test2 will very likelyproduce an OutOfMemoryError! (It might take a while.) On a 1.6+ JVM,it will probably limp along indefinitely, going quite slowly, or itmight produce an OutOfMemoryError, depending on the system you runit on.

You might well be expecting this pattern if you have previously readabout or encountered the cost of finalizers. Or maybe it's a surprise.It was certainly a surprise to me when I first saw it - a Java programthat has very little code, and is definitely not holding on to morethan one object according to the code, produces an OutOfMemoryError!Even those of you who were expecting that might still besurprised at the exact details of what is happening here.

A Simple Lifecycle

Let's start with Test1. This is a straightforward class, with instancesthat have a straightforward lifecycle. In the main() method, a Test1 objectis created in the Eden space of the young generation at new Test1() (see figure 1). Then the explicit dereference in the very next line(obj = null;) eliminates any reference to that object(actually if that line wasn't there, the dereference would occur inthe next loop iteration when obj is set to point atthe next Test1 instance, but I wanted to be explicit in the example).


New objects get created in Eden space
Figure 1: New objects getting created in Eden space

At some point, we have created enough Test1 instances that Eden getsfull - and that triggers a young generation garbage collection (usuallytermed "minor GC"). As nothing points to any of the objects in Eden (or possibly oneinstance is still referenced depending on when the GC occurs),Eden is simply set to empty very efficiently by the garbagecollector (if one object is referenced, that single object willbe copied to a survivor space first, see figure 2)And presto, we are very efficiently back to an empty Eden, and themain loop continues with further object creation.


A minor GC moves live objects into Survivor space and empties Eden
Figure 2: A minor GC empties out Eden


Creation Of A Finalizer

Test2, on the other hand, looks rather different. First off when a Test2instance is created, the JVM detects it has a finalize() method which isdifferent from the one defined in the Object class. Yes, defining anon-trivial finalize() method in a class - or inheriting one for thatmatter - is on it's own sufficient to change how objects are created inthe JVM.

The JVM will ignore a trivial finalize() method (e.g. one which justreturns without doing anything, like the one defined in the Objectclass). Otherwise, if an instance is being created, and that instancehas a non-trivial finalize() method defined or inherited, then theJVM will do the following:

  • The JVM will create the instance
  • The JVM will also create an instance of the java.lang.ref.Finalizerclass, pointing to that object instance just created (and pointing toa queue that it will be put on by the GC)
  • The java.lang.ref.Finalizer class holds on to thejava.lang.ref.Finalizer instance that was just created (so that it iskept alive, otherwise nothing would keep it alive and it would beGCed at the next GC).

So that is what happens here with Test2. With every instance of Test2created, we get a separate instance of java.lang.ref.Finalizerpointing to the Test2 instance. (see figure 3)


New finalizable objects are created in Eden toegther with the Finalizer referencing them
Figure 3: Finalizable objects have a Finalizer created with them

Sound bad? Well, it isn't particularly. I mean, okay we create twoobjects instead of one each time, but honestly the modern JVM isfantastically efficient at creating objects, so this really isn'ta big deal.

The First GC

Well that was the creation of an object with a non-trivial finalize()method, but what happens next? Just as before, the Test2 instancesare dereferenced, so can be garbage collected. So at some point theyoung generation Eden gets full, and a minor GC happens. But thistime round, the GC has extra work to do. Firstly, instead of abunch of objects which aren't being referenced, we have lots of objectswhich are referenced from Finalizer objects, which in turn arereferenced from the Finalizer class. So everything stays alive! TheGC will copy everything into the survivor space. And if that isn't big enough to hold all of the objects, it will have to move some tothe old generation (which has a much more expensive GC cycle).Straight away we have significantly more work and are alsostoring up significantly more work for later.

But that's not even the end of it. Because the GC recognizes thatnothing else points to the Test2 instances apart from the Finalizers, so it says to itself"aha! I can process any finalizers that point to those Test2instances". So the GC adds each of those Finalizerobjects to the reference queue at java.lang.ref.Finalizer.ReferenceQueue. Now the GC is finally finished, having done quite a bit more workthan when we didn't have Finalizers in Test1 - and look at the state of our JVM. Test2 instances are hangingaround, spread all over the place in survivor space and the oldgeneration too; Finalizer instances are hanging around too, as theyare still referenced from the Finalizer class (see figure 4). The GC is finished, but nothing seems to have been cleared up!


Finalizable objects and their associated Finalizer get moved to Survivor space or the old generation on their first GC
Figure 4: Finalizable objects don't get cleared on the first GC

The Finalizer Thread

Now that the minor GC is finished, the application threads start up again(they were suspended while the minor GC was running). Amongst the applicationthreads are Test2's "main" thread, andseveral threads started by the JVM - and one in particular concerns us:the "Finalizer" daemon thread. In that same java.lang.ref.Finalizerclass is an inner class called FinalizerThread, which starts the"Finalizer" daemon thread when the java.lang.ref.Finalizer is loadedin to the JVM.

The "Finalizer" daemon thread is quite a simple thread. It sits ina loop which is blocked waiting for something to become availableto be popped from the java.lang.ref.Finalizer.ReferenceQueue queue.Conceptually, it looks like the loop in listing 3(the actual loop is a bit more complicated to handlevarious error and access issues).

for(;;)
{
  Finalizer f = java.lang.ref.Finalizer.ReferenceQueue.remove();
  f.get().finalize();
}
Listing 3: A conceptual version of the Finalizer thread loop

So now we have a bit of a contention issue for the JVM heap resources.The main() loop has kicked back in and is producing lots moreTest2 instances, with their associated Finalizer objects, whileat the same time the "Finalizer" daemon thread is running throughthe Finalizer objects that the last GC pushed onto the Finalizerreference queue.

That would probably be fine - calling our Test2.finalize() methodshould be quicker than creating a new Test2 instance, so we'dexpect to be able to catch up and get ahead by the next GC. Onlythere is one little problem with that optimistic expectation.The "Finalizer" daemon thread is run at a lower priority by theJVM than the default thread priority. Which means that the "main"thread gets lots more CPU time than the "Finalizer" daemon threadso, in Test2, it will never catch up.

In a normal application, this imbalance doesn't matter. Finalizableobjects aren't usually created at such a high rate, and even where theymight be created at a high rate in bursts as you can get in someapplications, your average application will have some idle CPUtime which will let the "Finalizer" daemon thread catch up.

But our Test2 class is a pathological Finalizable class. It doescreate Finalizable objects too fast for the "Finalizer" daemon thread tocatch up, and so that Finalizer reference queue just keeps on buildingin size.

The Second GC

Nevertheless, some Finalizer objects will get off the queue, and theTest2 instances they point to will get their finalize() methodscalled. At this point, the "Finalizer" daemon thread also doesone more job - it removes the reference from the Finalizer classto that Finalizer instance it just processed - remember, that iswhat was keeping the Finalizer instance alive. Now nothingpoints to the Finalizer instance, and it can be collected in the next GC - as can the Test2 instance since nothing else pointsto that.

So eventually, after all that, another GC will happen. And this timeround, those Finalizer objects that have been processed will getcleared out by the GC. That's the end of the lifecycle for theones that get to that stage. The others, still in the Finalizerreference queue, will get cycled through the Survivor spaces, wheresome will get processed and cleared, but most (in our Test2 application)will eventually end up in the old generation. Eventually, the old generationwill also be full, and a major GC will occur. But the major GC,while capable of being a bit more thorough than the minor GCs,doesn't really help much here - now you can see now how the OutOfMemoryErroroccurs. That queue just keeps growing, and eventually there isn'tsufficient space to create new objects, and pow, we hit the OOME.

(In 1.6, at least in some configurations, it looks like thegarbage collector has enough smarts to be able to try and letthe finalizer thread run a bit so that it can limp along, withthe heap almost full indefinitely. It's better than an OOME crash,but not by much).

Fixing It

This article was primarily about seeing how finalizable objects are processed. Theactual example we used was pathological, so that I could demonstrate finalizable objects lifecyclein a dramatic way - the vast majority of apps using finalizers will never see a significant queue build up.Nevertheless, there are some apps that have hit this finalizer queue build up problem in the past, so it isworth considering how to deal with it. One obvious way is to increasethe priority of the "Finalizer" daemon thread - there is no API for this, so you have to runthrough all the threads to find it by name, then increase it'spriority.

You could also take explicit control over finalization by removing the finalize() method andusing your own explicit queue using your own Reference objectsin a very similar way that the Finalizer class processes the objects and their finalize() methods(see for example Tony Printezis' article). That way you control your finalization processing thread's priority and schedule.

Note that neither of these techniques reduce the overheads in having finalizable objects,they just avoid the queue building up because of the lower priority thread. Bear in mindthat: the vast majority of apps using finalizers will never see a significant queue build up;but that the overheads of having finalizable objects are significant. It's a good idea totry to keep the number of finalizable objects to a minimum. A few finalizable objects normally don't matter;too many can seriously stress the GC.


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值