`
leonzhx
  • 浏览: 793445 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

The Secret Life Of The Finalizer

阅读更多

http://www.fasterj.com/articles/finalizer1.shtml

 

In this article, I'm going to have a look into exactly what the JVM does when you create a finalizable object, following its lifecycle through until it gets garbage collected. Note this article only covers what happens in the Sun JVM, other JVMs may 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 and dereferences instances of Test1. It runs just as you would expect it to - creating lots of garbage instances, with the garbage collector 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 that method from my code. But as I'm sure you all know, it's the special finalize() method that is called by the garbage collector when the object can 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 two invocations. Test1 happily sails along, creating objects continuously, interrupted occasionally by very fast young generations GCs - just exactly as you would expect from looking at the code.

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

You might well be expecting this pattern if you have previously read about 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 program that has very little code, and is definitely not holding on to more than one object according to the code, produces an OutOfMemoryError! Even those of you who were expecting that might still be surprised at the exact details of what is happening here.

A Simple Lifecycle

Let's start with Test1. This is a straightforward class, with instances that have a straightforward lifecycle. In the main() method, a Test1 object is 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 in the next loop iteration when obj is set to point at the next Test1 instance, but I wanted to be explicit in the example).




Figure 1: New objects getting created in Eden space

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



Figure 2: A minor GC empties out Eden

Creation Of A Finalizer

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

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

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

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




 
Figure 3: Finalizable objects have a Finalizer created with them

Sound bad? Well, it isn't particularly. I mean, okay we create two objects instead of one each time, but honestly the modern JVM is fantastically efficient at creating objects, so this really isn't a 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 instances are dereferenced, so can be garbage collected. So at some point the young generation Eden gets full, and a minor GC happens. But this time round, the GC has extra work to do. Firstly, instead of a bunch of objects which aren't being referenced, we have lots of objects which are referenced from Finalizer objects, which in turn are referenced from the Finalizer class. So everything stays alive! The GC 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 to the old generation (which has a much more expensive GC cycle). Straight away we have significantly more work and are also storing up significantly more work for later.

But that's not even the end of it. Because the GC recognizes that nothing 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 Test2 instances". So the GC adds each of those Finalizer objects to the reference queue at java.lang.ref.Finalizer.ReferenceQueue. Now the GC is finally finished, having done quite a bit more work than when we didn't have Finalizers in Test1 - and look at the state of our JVM. Test2 instances are hanging around, spread all over the place in survivor space and the old generation too; Finalizer instances are hanging around too, as they are still referenced from the Finalizer class (see figure 4). The GC is finished, but nothing seems to have been cleared up!




 
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 application threads are Test2's "main" thread, and several threads started by the JVM - and one in particular concerns us: the "Finalizer" daemon thread. In that same java.lang.ref.Finalizer class is an inner class called FinalizerThread, which starts the "Finalizer" daemon thread when the java.lang.ref.Finalizer is loaded in to the JVM.

The "Finalizer" daemon thread is quite a simple thread. It sits in a loop which is blocked waiting for something to become available to 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 handle various 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 more Test2 instances, with their associated Finalizer objects, while at the same time the "Finalizer" daemon thread is running through the Finalizer objects that the last GC pushed onto the Finalizer reference queue.

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

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

But our Test2 class is a pathological Finalizable class. It does create Finalizable objects too fast for the "Finalizer" daemon thread to catch up, and so that Finalizer reference queue just keeps on building in size.

The Second GC

Nevertheless, some Finalizer objects will get off the queue, and the Test2 instances they point to will get their finalize() methods called. At this point, the "Finalizer" daemon thread also does one more job - it removes the reference from the Finalizer class to that Finalizer instance it just processed - remember, that is what was keeping the Finalizer instance alive. Now nothing points to the Finalizer instance, and it can be collected in the next GC - as can the Test2 instance since nothing else points to that.

So eventually, after all that, another GC will happen. And this time round, those Finalizer objects that have been processed will get cleared out by the GC. That's the end of the lifecycle for the ones that get to that stage. The others, still in the Finalizer reference queue, will get cycled through the Survivor spaces, where some will get processed and cleared, but most (in our Test2 application) will eventually end up in the old generation. Eventually, the old generation will 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 OutOfMemoryError occurs. That queue just keeps growing, and eventually there isn't sufficient space to create new objects, and pow, we hit the OOME.

(In 1.6, at least in some configurations, it looks like the garbage collector has enough smarts to be able to try and let the finalizer thread run a bit so that it can limp along, with the 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. The actual example we used was pathological, so that I could demonstrate finalizable objects lifecycle in 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 is worth considering how to deal with it. One obvious way is to increase the priority of the "Finalizer" daemon thread - there is no API for this, so you have to run through all the threads to find it by name, then increase it's priority.

You could also take explicit control over finalization by removing the finalize() method and using your own explicit queue using your own Reference objects in 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 mind that: 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 to try 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.

  • 大小: 4.1 KB
  • 大小: 4.4 KB
  • 大小: 4.9 KB
  • 大小: 5.4 KB
分享到:
评论

相关推荐

    .NET 常见问答:完成器(Finalizer)、程序集名、方法信息等等

    在我的类中何时需要实现一个完成器?我是否一定要实现完成器,或者只是在我控制着 非托管资源时才需要实现它?我是否一定要在我的完成器中实现 IDisposable 接口?...关键字:finalizer,assembly names,methodinfo

    python3.6.5参考手册 chm

    PEP 486: Make the Python Launcher aware of virtual environments PEP 488: Elimination of PYO files PEP 489: Multi-phase extension module initialization Other Language Changes New Modules typing ...

    Java内存泄漏排除工具mat

    Java内存泄漏是开发过程中常见的问题,可能导致系统性能下降,甚至应用程序崩溃。为了有效地排查和解决这类问题,开发者通常会借助专业工具,而“Java内存泄漏排除工具MAT”(Memory Analyzer Tool)就是一个强大的...

    finalizers:愚蠢的终结者

    愚蠢的终结者建造go build 跑# List all objects blocked by a finalizer./finalizers# List all objects with finalizers./finalizers --all例子./finalizersNAMESPACE NAME APIVERSION KIND FINALIZERSp-nf5gh ...

    teclast-f5-ubuntu-finalizer:当前的HWSW KernelSystemACPI修改,以获取$ 400的Dream Ubuntu Linux笔记本电脑

    标题中的“teclast-f5-ubuntu-finalizer”是一个针对Teclast F5笔记本电脑的项目,目的是优化Ubuntu Linux在该设备上的运行效果。这个项目可能包含特定的硬件和软件调整,尤其是对Kernel(内核)、System ACPI(系统...

    flexipatch-finalizer:自定义预处理器,用于从flexipatch版本中删除未选择的补丁,从而保留最终的补丁版本

    flexipatch-finalizer是一个自定义预处理器,它使用相同的配置文件,并剥离所有未使用代码的flexipatch构建,从而保留应用了选定补丁的软件构建。 该终结器的示例flexipatch构建可用于: :warning: 请务必注意,...

    内存管理,GC机制,内存释放过程

    3. 如果在Finalizer表上有记录,那么将记录移到另外的一张表上,在这里我们叫它Finalizer2。 4. 如果不在Finalizer2表上有记录,那么释放内存。 Finalizer的概念: 在.NET中,Destructor的概念已经不存在了,它...

    Troubleshooting Memory Problems-Poonam Parhar.pdf

    2. Excessive use of Finalizers:Finalizer是一种特殊的对象,它可以在垃圾回收器清理对象时执行一些操作。但是,如果Finalizer使用不当,可以导致内存泄漏和性能问题。 3. Explicit GC Invocations:垃圾回收器是...

    免费wordpress采集工具.zip

    《WordPress免费采集与批量发布工具详解》 在数字化信息时代,内容创作与更新的速度日益加快,对于WordPress用户来说,寻找一款高效、便捷的采集和批量发布工具显得尤为重要。本篇文章将详细解读“免费WordPress...

    免费迅睿CMS采集工具.zip

    迅睿CMS采集工具是一款专为网站内容管理系统的用户设计的免费工具,主要针对迅睿CMS系统,但也可能兼容其他CMS平台。这款工具的核心功能在于其强大的数据采集与批量发布的特性,能够有效地帮助用户自动化处理网站...

    Aspirant-of-memory

    10. **Finalizers(终结器)**:当对象包含需要手动释放的非托管资源时,可以定义一个Finalizer,以确保在对象被垃圾回收前执行必要的清理工作。然而,Finalizers应谨慎使用,因为它们会影响性能,并且不能保证在...

    JPL_Coding_Standard_Java

    13. **R13:不使用终结器(finalizer)** - 终结器在某些情况下可能导致不确定的行为和性能问题。 - 应该优先使用垃圾回收机制来管理内存。 14. **R14:不实现Cloneable接口** - 克隆对象时,推荐使用更安全的方法...

    如何排查Java内存泄漏?看完我给跪了!

    Java内存管理是一个复杂而微妙的主题,尤其是当涉及到内存泄漏时。Java的自动垃圾回收机制虽然在大多数情况下工作得很好,但并不能完全防止内存泄漏的发生。内存泄漏通常发生在程序中,当不再需要的对象仍然被引用,...

    dirty_scheduler:R17.3 +的Erlang脏调度程序示例

    dirty_scheduler 不要忘记使用“ --enable-dirty-schedulers”... 从Erlang 17.03开始,enif_schedule_dirty_nif,enif_schedule_dirty_nif_finalizer和enif_dirty_nif_finalizer被删除(erl_nif.h)。 更多信息: :

    基于.NET程序默认启动线程数讲解

    调试器帮助线程3.Finalizer线程 代码如下:class Program { static void Main(string[] args) { Console.WriteLine(“Main thread: {0}”, Thread.CurrentThread.ManagedThreadId); Console.ReadKey(); } } ...

    Findbugs缺陷详解与英文代号的对照表

    - **解释**: 如果子类覆盖了超类的finalizer并将超类的finalizer设置为空,可能会导致资源泄漏。 - **建议**: 保留超类的finalizer,避免覆盖或设置为空。 以上列出的是FindBugs检测到的部分问题及其英文代号的详细...

    findbugs报告问题含义

    22. **FI_NULLIFY_SUPER**:空 Finalizer 禁用了超类的 finalizer。如果需要,应调用 super.finalize()。 23. **MTIA_SUSPECT_STRUTS_INSTANCE_FIELD**:继承 Struts Action 类使用实例变量,可能导致多线程问题。...

    编写高效优雅Java程序.docx

    * 使类和成员的可访问性最小化,避免使用终结方法finalizer方法。 * 使可变性最小化,尽量使类不可变。 三、成员变量设计 在成员变量设计中,需要注意以下几点: * 使用static成员变量,避免创建不必要的对象。 *...

Global site tag (gtag.js) - Google Analytics