Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

This article introduces Finalizer, a built-in concept in Java. You may be familiar with it, but you may never have heard of it, depending on whether you’ve taken the time to read the java.lang.Object class in its entirety. There is a finalize() method in java.lang.object. The implementation of this method is empty, but once implemented, it triggers the JVM’s internal behavior with both power and danger.

If the JVM finds a class that implements finalize(), then it’s time for a miracle. Let’s first create a class that implements the extraordinary Finalize () method, and then see how the JVM handles things differently in this case. Let’s start with a simple example:

import java.util.concurrent.atomic.AtomicInteger; class Finalizable { static AtomicInteger aliveCount = new AtomicInteger(0); Finalizable() { aliveCount.incrementAndGet(); } @Override protected void finalize() throws Throwable { Finalizable.aliveCount.decrementAndGet(); } public static void main(String args[]) { for (int i = 0;; i++) { Finalizable f = new Finalizable(); if ((i % 100_000) == 0) { System.out.format("After creating %d objects, %d are still alive.%n", new Object[] {i, Finalizable.aliveCount.get() }); }}}}Copy the code

This program uses an infinite loop to create objects. It also uses a static variable, aliveCount, to keep track of how many instances are created. The counter increases by 1 each time a new object is created, and decreases by 1 once the FINALIZE () method is called after GC is complete.

What do you think the output of this little piece of code will look like? Since newly created objects are quickly unreferenced, they are immediately recyclable by the GC. So you might think that this program would run forever:

After creating 345,000,000 objects, 0 are still alive. 0 are still alive. After creating 345,200,000 objects, 0 are still alive. 0 are still alive.Copy the code

Apparently not. Realistic results are completely different, in my Mac OS X JDK 1.7.0 _51, program thrown around until 1.2 million objects to create Java. Lang. OutOfMemoryError: GC overhead limitt exceeded abnormal out of the race.

After creating 900,000 objects, 791,361 are still alive. After creating 1,000,000 objects, 875,624 are still alive. After creating 1,100,000 objects, 959,024 are still alive. After creating 1,200,000 objects, 1040909 are still alive. The Exception in the thread "is the main" Java. Lang. OutOfMemoryError: GC overhead limit exceeded at java.lang.ref.Finalizer.register(Finalizer.java:90) at java.lang.Object.(Object.java:37) at eu.plumbr.demo.Finalizable.(Finalizable.java:8) at eu.plumbr.demo.Finalizable.main(Finalizable.java:19)Copy the code

The act of garbage collection

To figure out what’s going on, you have to look at how the program behaves when it runs. Let’s open the -xx :+PrintGCDetails option and run it again:

[GC [PSYoungGen: 16896K->2544K(19456K)] 16896K->16832K(62976K), 0.0857640 secs] [Times: User =0.22 sys=0.02, real= 0.09secs]\ [GC [PSYoungGen: 19440K->2560K(19456K)] 33728K->31392K(62976K), 0.0489700secs] [Times: User =0.14 sys=0.01, real=0.05 secs]\ [GC-- [PSYoungGen: 19456K->19456K(19456K)] 48288K->62976K(62976K), secs] [Times: User =0.16 sys=0.01, real= 0.06secs]\ [Full GC [PSYoungGen: 16896K->14845K(19456K)] [ParOldGen: [PSPermGen: 2567K->2567K(21504K)], 0.4954480 secs] [Times: User =1.76 sys=0.01, real=0.50 secs]\ [Full GC [PSYoungGen: 16896K->16820K(19456K)] [ParOldGen: [Times: 5361K -> 5361k (43520K)] 60257K->60181K(62976K) [PSPermGen: 2567K->2567K(21504K)], 0.1379550 secs] [Times: User =0.47 sys=0.01, real= 0.14secs]\ -- cut for brevity-- \ [Full GC [PSYoungGen: 16896K->16893K(19456K)] [ParOldGen: 43351K->43351K(43520K)] 60247K->60244K(62976K) [PSPermGen: 2567K->2567K(21504K)], 0.1231240 secs] [Times: The user sys = = 0.45 0.00, real = 0.13 secs] \ [Full GCException in thread "main" Java. Lang. OutOfMemoryError: GC overhead limit exceeded\ [PSYoungGen: 16896K->16866K(19456K)] [ParOldGen: 43351K->43351K(43520K)] 60247K->60218K(62976K) [PSPermGen: 2591K->2591K(21504K)], 0.1301790secs] [Times: User sys = = 0.44 0.00, real = 0.13 secs] \ at eu plumbr. Demo. Finalizable. Main (19) Finalizable. Java:Copy the code

As you can see from the logs, after a few new generation GCS in Eden, the JVM started using more expensive Full GCS to clean up old and persistent GCS. Why is that? Since no one references these objects anymore, why aren’t they recycled in the new generation? What’s wrong with writing the code this way?

To understand why GC behaves like this, let’s make a small change to our code to remove the finalize() method implementation first. Now the JVM finds that this class does not implement the Finalize () method, so it switches back to the “normal” mode. If you look at the GC logs, you can only see some cheap new generation GC running.

The Java heap partition

Because in this modified program, no one really refers to these newly created objects of the new generation. As a result, the Eden area is quickly emptied and the program can be executed forever.

On the other hand, things are a little different in the earlier example. These objects are not unreferenced, and the JVM creates a watchdog for each Finalizable object. This is an example of the Finalizer class. All of these watchdogs are referenced by the Finalizer class. Because there is such a chain of references, the entire object is alive.

So now Eden is full and all objects have references, GC has no choice but to copy them all to Suvivor. Even worse, once the Survivor zone is full, you can only save to the older generation. As you will recall, Eden uses a throwaway cleanup strategy, whereas older GC uses a more expensive approach.

Finalizers queue

Only after the GC is complete does the JVM realize that no one else refers to the instances we created except the Finalizer objects, so it marks the Finalizer objects that point to them as processable. GC internal will put these finalizers object in Java. Lang. Ref. Finalizers. ReferenceQueue this particular queue.

With all this hassle out of the way, our application can move on. Here’s a thread you’ll be interested in — the Finalizer daemon thread. You can see information about this thread by using JStack for Thread dump.

My Precious:~ demo$ jps 1703 Jps 1702 Finalizable My Precious:~ demo$ jstack 1702 --- cut for brevity --- "Finalizer" daemon prio=5 tid=0x00007fe33b029000 nid=0x3103 runnable [0x0000000111fd4000] java.lang.Thread.State: RUNNABLE at java.lang.ref.Finalizer.invokeFinalizeMethod(Native Method) at java.lang.ref.Finalizer.runFinalizer(Finalizer.java:101) at java.lang.ref.Finalizer.access$100(Finalizer.java:32) at Java.lang.ref. Finalizer$FinalizerThread. Run (finalizer.java :190) -- Cut for Brevity --Copy the code

As you can see above, there is a Finalizer daemon thread running. The Finalizer thread is a single responsibility thread. The cycle of this thread will keep waiting for Java. Lang. Ref. Finalizers. New objects in the ReferenceQueue. Once the Finalizer thread finds a new object in the queue, it will pop that object up and call its Finalize () method to remove the reference from the Finalizer class, so that the Finalizer instance and its reference object can be garbage collected the next time the GC executes.

Now we have two threads that are looping around. Our main thread is busy creating new objects. These objects have their own watchdog is finalizers, while the finalizers object will be added to a Java lang. Ref. Finalizers. ReferenceQueue. The Finalizer thread handles the queue by popping all objects up and calling their Finalize () method.

A lot of times you may not be aware of this situation. The call to finalize() method will happen much earlier than when you create a new object. So most of the time, the Finalizer thread can clear the queue before the next GC brings in more Finalizer objects. But in our case, that’s clearly not the case.

Why overflow? The Finalizer thread has a lower priority than the main thread. This means less CPU time is allocated to it, so its processing speed cannot keep up with the creation of new objects. This is the root of the problem — objects are created faster than the Finalizer thread can call Finalize () to finalize them, resulting in all available space in the heap being used up. The result is, our dear friend Java. Lang. OutOfMemoryError will appear in a different identity in front of you.

If you still don’t believe me, dump the heap to see what’s in it. For example, you can use – XX: + HeapDumpOnOutOfMemoryError parameters start our small programs, the MAT in my Eclipse Dominator Tree is I see in the image below:

As you can see, my 64M heap is fully occupied by the Finalizer object.

conclusion

As a refresher, the life cycle of Finalizable objects is completely different from the behavior of ordinary objects, listed as follows:

  • The JVM creates Finalizable objects
  • The JVM creates an instance of java.lang.ref.Finalizer that points to the newly created object.
  • The java.lang.ref.Finalizer class holds a newly created instance of java.lang.ref.Finalizer. This prevents the next generation GC from collecting these objects.
  • The new generation GC cannot clear the Eden region, so it moves these objects to Survivor or old generation.
  • The garbage collector finds that these objects implement the Finalize () method. Because will add them to the Java. Lang. Ref. Finalizers. ReferenceQueue queue.
  • The Finalizer thread will process the queue, pop the objects one by one, and call their Finalize () method.
  • After the Finalize () method is called, the Finalizer thread removes the reference from the Finalizer class, so the objects can be collected in the next GC.
  • The Finalizer thread competes with our main thread, but because it has a lower priority and gets less CPU time, it will never catch up with the main thread.
  • The program consumes all available resources and finally throws an OutOfMemoryError.