Trending
Opinion: How will Project 2025 impact game developers?
The Heritage Foundation's manifesto for the possible next administration could do great harm to many, including large portions of the game development community.
By abandoning a few of the noble ideas behind Java (in effect, adopting "dirty" Java techniques), you can make a Java-based game run faster. This article illustrates performance enhancements based on techniques that Sun might not have told you about.
Despite improvements like Just-In-Time (JIT) compilation, and despite HotSpot blueprints, Java today is by and large still inadequate for some applications, due to performance problems. To speed up a Java application, there are two possible avenues. The first option is to abandon the portability of "pure" Java in favor of faster, "dirty" Java techniques; you use native (C/C++) code in conjunction with the Java Native Interface (which will be introduced in the July issue of Game Developer magazine), or compile Java bytecode to native code.
The second option is to abandon a few of the noble ideas behind Java, and write Java source code that is "dirty" in a different way. You can, for instance, avoid the delays imposed by creating objects and having them garbage collected in order to make your game run faster. This latter category of performance enhancements is what I will explain in this article.
Pure Java?
Java is often praised for its theoretical ability run on any hardware. In practice, this "write once, run anywhere" promise hardly covers more than a few platforms. Depending on the packages and APIs used, it might turn out to be Win32 and Solaris, or maybe only the former or the latter. For many, if not most, game developers, portability is still not considered a worthwhile goal anyway.
The Holy Grail of Java portability is "100% Pure Java". At first glance, this is usually interpreted as "stay away from native code". But upon closer look, it turns out that there are quite a few concepts in Java which, while not exactly set in stone, are strongly encouraged by the language design. It turns out that you can gain quite a bit of speed working around the way Java is meant to be used. Even in the absence of native code, though, using these techniques forces you to write "dirty" Java code.
A Thin Thread
Sun boasts about Java's built-in support for multithreading, and it is definitely a nice feature. What is usually not hyped are the problematic scheduling behaviors. It is convenient to rely on a native thread implementation when porting a Java Virtual Machine (JVM), but multithreaded code written with, say, priorities or time slicing in mind may behave very different when those features aren't supported within the operating system. Writing multithreaded applications and threadsafe code is difficult enough to begin with, but having to write possibly time-critical code without any scheduler specification to rely on is a challenging task, indeed.
To make things worse, there isn't really that much use for multithreading in most games. There is no reason to design a multiplayer game server like a multithreaded web server. Certain client loops (for example, sampling, timing and queuing user input) might run at a fixed rate, but they also might require real-time behavior that is not guaranteed – and often not possible – with a JVM. A game might wind up having asynchronous sender/receiver networking threads, but there surely is no point in running a thread for each NPC or server-side bot.
The death sentence for multithreading is that presently the performance penalty for thread-safe code is significant. Symmetric multiprocessing is not in widespread use, speed-ups with multiprocessing are difficult to accomplish, and many JVM themselves run only on a single processor. The gains, if any, usually do not compensate for the overhead of mutex handling and synchronization. So it is safe to say that, until the Java multithreading and the underlying hardware improve substantially, your game will perform better without threading.
Synchronizing The Bin
Java comes with a huge core API. Some of these classes come in quite handy, and it makes prototyping easier to just, for instance, take a container or utility class from the core API. However, many core classes offer nothing but synchronized methods, in compliance with the noble goal of providing threadsafe code throughout the API. Frequently used examples are java.util.Vector and java.util.Hashtable.
Eschewing multithreading means that you could get rid of its associated synchronization overhead. Note that JIT, HotSpot, or even compiling to native code will not help you at all. Benchmarks usually show that the Java virtual machine comes with heavily optimized mutex handling and synchronization code – what you are facing is the inherent, inevitable overhead that no optimization can remove as long as your code has to be threadsafe.
Your first thought might be to subclass, say, the container class, thereby providing additional asynchronous accessor and mutator methods or overriding the threadsafe ones. But core classes do not always offer access to protected fields, or expose asynchronous protected methods. They might even be final.
There is an option available to you, though: replacing the Java classes with your own custom classes. In this way you could create a class that is not thread-safe, and wrap it with another class using thread-safe methods when necessary. You could also provide the base implementation in an abstract base class with protected methods, and subclass it twice, like this:
Queue extends Container
SyncQueue extends Container
Depending on the JVM you use, you will realize a performance increase of up to ten times, and even better performance for simple synchronized accessors (even without a JIT compiler).
Reinventing the Wheel
There might be other good reasons to replace core classes. Take, for example, java.lang.String. There really aren't that many uses for Unicode and UTF in games yet. The chat messages and console inputs from game clients will be transmitted to a game server using ASCII, and with bandwidth and latency restrictions in many games, UTF isn't practical. Further, the custom fonts you likely use in OpenGL and other APIs probably won't support internationalization, either. String comparisons are twice as expensive with UTF, too. Depending on your parsing needs and networking protocol, it might be worth implementing your own ASCII string class (possibly with fixed-length, matching UDP packet size, which could be reusable). Outside of network packet parsing, string operations are usually far from being critical, yet it be wise to keep an eye on the innocent-looking "+" concatenation operator.
String illustrates another key concept of clean and safe Java programming: the Immutable object, a class that comes with constructors as the only means to set the state of an object. java.lang.String provides only accessor methods, no mutators or setters. Each "+" concatenation in all likelihood creates a new temporary object, which is immediately left behind for the garbage collector. If you use String to put together a lot of formatted output, you might accumulate a significant memory debt here. That's why Sun added a java.lang.StringBuffer class that provides the accessors to put together a string more efficiently. Avoid the temptation to use Object.toString() – there is no useful standard to convert complex, composite objects into a String anyway, so you might as well consider alternative methods that can re-use buffers and be chained without creating temporary immutables, like this:
public StringBuffer toLog( StringBuffer tmp ) { tmp.append("whatever"); return tmp; }
You can't use the "+" anymore, and you might want to avoid the implicit toString() conversions, but these features are leftovers from C++ operator overloading and stream operators anyway. They just hide the creation of additional String objects. It's quite a task just to print a stringbuffer directly, anyway:
/**
There is no PrintStream method to print a
StringBuffer without implicitly calling its
toString() method, creating a new throw-away copy
The immutable String class has a getBytes() method, but StringBuffer has not.
PrintStream has a write(byte[],int,int) method,
but none for char[] that allows for setting a length.
*/
protected final synchronized
void write( final StringBuffer msg ) {
// Get the chars from the StringBuffer.
msg.getChars( 0, msg.length(), cbuffer, 0 );
// Get the bytes from the char array.System.arraycopy( cbuffer, 0, ibuffer, 0, msg.length() );
// Now write out the byte array.
pstream.write( ibuffer, 0, msg.length() );
}
This is another good argument for creating your own ASCII string class. Fortunately, 3D game clients are not likely to do excessive console output. It might well be an issue for servers with log capability, however.
Collecting Garbage
The Java feature that probably raises the most concerns is the Garbage Collector (GC). Having a GC to take care of objects that are no longer referenced is a convenience, and prevents problems like memory leaks and corrupted memory still in use. However, one objection to garbage collection is that it is a potentially time consuming and memory-intensive task, and (in a multithreaded JVM) the GC might have to interrupt all other threads while it manipulates the heap. Often this is not a big problem. But what is disturbing is that Java does not offer a programmer any means to impart the GC with knowledge about how the application uses and releases memory. On the other hand, a pass by a non-conservative GC (for instance, before a map or mission reload in a game) might offer the benefit of heap compaction.
There are various ways to implement GCs, and some methods work better in certain applications. For example, if speed is your top priority, and you know your code will not exceed the physical memory available, the best GC is one that never does anything. The introductory article "No Silver Bullet" (found in the "References" section at the end of this article) discusses various garbage collector algorithms and how they apply to applications running in embedded systems. It is helpful to consider your Java game from this point of view, especially if it runs on an embedded JVM. The article also gives you an idea about how much effort a GC spends on identifying references and objects, without assistance from you or the run-time system.
The first thing you might want to do is bring the GC thread under your control. Unfortunately, Java is not very cooperative in this regard. The API provides the java.lang.System.gc() method to yield control to the GC, but the Java specification does not guarantee anything – the GC is free to ignore it. In any case, what you really want is to prohibit asynchronous garbage collection from happening during your game loop. In the JDK (up to version 1.1.7) there is a command-line switch for the JVM (used as java -noasyncgc), which can also be set as a flag when invoking an embedded JVM. As of JDK 1.2, however, Sun chose to remove even this tiny bit of user control.
The "verbose" GC flag is still available, and tells you how and when garbage collection actually takes place. In many cases, the GC might not be an issue at all. The "noclassgc" switch is a non-standard option as of JDK 1.2, and might not be available in other VMs. Disabling class garbage collection might simplify things (and prevent having to re-load a temporarily unused class later on, which also affects cached methodIDs and fieldIDs in native code), but you can't simply postpone the garbage collection of classes. You either have to disable it entirely, or accept it. Games that repeatedly load slightly different script classes will want to clear out the obsolete versions prior to reload.
Java VMs from other vendors are trying to offer more open and flexible architectures, so they may allow you to plug in different garbage collectors. This is a much better solution. Ideally, an Open Source Java VM like Kaffe (which you would have to license if you decided to ship it with your game) can be customized to implement the GC strategy best suited to your application. If you keep a tight lid on your object allocation and get some native control over the GC, you might postpone garbage collection until map loading or some other I/O-intensive, slow operations take place (or maybe even until your game or server shuts down). Once you get the garbage collector on the leash, you can have it run while the player is not expecting 30 frames per second anyway. But if your games last for many minutes, memory creep might start to affect the game performance.
It is worth pointing out that "100 % Pure Java" does have memory leaks. It takes a single forgotten live reference to keep an object from being removed by the garbage collector (which is the whole purpose of the pool). If one of your objects keeps a reference to an object that's no longer needed, this object will never be removed by the garbage collector. To ensure garbage collection, set all the reference fields to "null" in composite objects that point to other objects that are no longer needed. This is the only hint you can offer the GC that a certain object is no longer needed. In fact, not even objects temporarily used in method calls are marked as anything particularly transient, they exist on the same heap as any other object.
Polluting Java: Controlling Memory
Frequent garbage collection can be sidestepped simply by avoiding object creation in the first place. Parsimonious allocation means that you stay away from immutables for temporary objects. As a result, you will have to have temporary objects in methods, and this means you have be able to re-use them. In some articles (such as Chuck McManis's "Not Using Garbage Collection – Minimize Heap Trashing in Your Java program", which is referenced at the end of this article), the term "recycling" is used to describe the act of an application recycling objects once they're created. (Beware that some authors use the term to address the recycling of unused memory.)
The aforementioned java.lang.String class is an example of an Immutable class that cannot be recycled and could generate lots of waste in terms of temporary objects, as is java.lang.Number and its minor minions (classes you might want to use to handle float, int, and so on). Java encourages the use of Immutable classes, and if you subscribe to this way of coding, you will create throwaway objects by the boat load, increasing memory debt. Eventually the GC will be handed more objects to trace and more garbage to collect.
Other pitfalls are binary operators that allocate result objects and return them. Any temporary variable allocated somewhere adds to the memory debt. The further inside the loops, the less desirable this is. Vectors (the 3D ones, not the annoyingly misnamed java.util.Vector container classes) are an excellent example of a temporary variable that can be found in any Java 3D game package, as shown in this example:
class VecMath {
public static Vec3 add( Vec3 a, Vec3 b ) {
Vec3 res = new Vec3( a );
res.add( b );
return res;
}
}
This is bad programming style, and not really object-oriented anyway: stay away from class methods for convenience, as classes are not objects. Worse, however, this example also hides an allocation from the caller.
Sometimes utility classes, like those shown in the last code listing, use (possibly static) fields to reference cached objects for storing temporary results of methods, rather than allocating new objects for each method call. For example, some complicated constructive solid geometry clipping code could use internally kept Vec3 objects to store intermediate results. Sometimes it is indeed feasible to use buffer objects instead, as shown in the following example:
class ParsimoniousPrintStream {
private static byte[] ibuffer;
private static char[] bbuffer;
public void write( final StringBuffer str ) {
if ( str == null ) return;
// Lazy instantiation.try {
... // see the buffered write() method above}
catch ( NullPointerException handled ) {
cbuffer = new char[MAX];
ibuffer = new byte[2*MAX];
write( str );
}
}
}
However, it is not a good idea in most cases. I intentionally omitted the synchronized term in this example. By accident, you might write non-synchronized methods first, and use them from multiple threads later. If you have a lot of class methods, you might use a lot of buffer objects. If several class methods share the same buffer object, you have to make sure that they won't indirectly call each other, or subtle errors will occur even without multithreading. It is even worse with instance methods, since each instance might use the buffer object – or need its own. Even with lazy instantiation (as used in the above example), you might have a lot of these around.
You could create an object implementing the operation (the Action or MethodObject pattern) in some cases, but this often just shuffles around the allocation penalty, as you have to allocate the MethodObject. There has to be a better way.
In The Pool
Memory management is simply a necessity every now and then. Allocating objects can be expensive, and JIT or some other Java VM optimization will not change this. However, having a garbage collector freeze your entire game while it marks and sweeps all objects in memory is also not acceptable. Enter "pooling," a technique which can help memory management.
Pooling is a decent and worthwhile technique which I encountered first in C and C++, in attempts to avoid memory leaks and related errors. In many ways, pooling techniques can be traced back to programmers who wrote their own heap management systems, such as Doom's zone memory management for DOS extenders. The C++ pool should be class specific, and looks somewhat like this:
class PoolAlloc {
// Constructor allocates initial amount of objects.
PoolAlloc( unsigned int initial, unsigned int size );
// Destructor checks for memory leaks - all back?
~Pool_Alloc_t();
// Allocate a fixed size chunk, return as bytes.
// This can opt to grow(incr), if exhausted.
byte* Alloc();
// Release a chunk into the list.
void Free( byte* base );
}
The typical pool casts chunks of memory into internally used objects, like this:
class MemChunk { MemChunk* next; }
and maintains a linked list stored in the actual chunks. If you opt for a "zone memory" heap with a variable chunk size, you also need an int length field. A versatile, paranoid pool checks whether the size of an object handed back is larger than sizeof(MemChunk). There are many ways to customize this, and in the days of software rasterizers, such pools also came handy to handle allocation of interleaved texture memory.
In C++, it is completely transparent whether a pool is used or not. Consider this example:
class Vec3 {
public:
// Overload allocation
void* operator new(size_t) { return POOL.alloc(); }
// Overload delete operator.
void operator delete( void* p ) { POOL.Free(p); }
private:
static POOL = new Pool( 100, sizeof(Vec3) );
}
The Pool object destructor makes any memory leaks obvious, and might be verbose to provide insights into the memory requirements of your application. That in turn could be used to determine a good initial pool size and increment parameters. C++ pools can come handy if you use native code with Java, as pools might make it easier to implement memory management in cooperation with the VM. The really neat thing here is that overloading the new and delete operators makes using the pool mandatory, in a way completely transparent to the caller.
Porting this idiom to Java faces a few problems, though. The use of the destructor does not translate to Java, as execution of the finalize() method is not mandatory, and finalizers are quite different than destructors. Your application will have to query the pool explicitly to obtain usage statistics, or the newly added classFinalize() method, as we cannot count on a destructor for the pool itself, either. More important, finalization of the pooled object can not be considered a reliable way to indicate memory leaks. The following is not guaranteed to work (and useless to locate the leak anyway):
class Poolable {
protected void finalize() {
throw new MemoryLeakException();
}
}
The Java Language Specification does not guarantee if and when a finalize() method is called by the GC. It might never happen for any of your objects. For this reason you should not count on a finalizer to close files, sockets, or release other resources.
Java also does not let you override the new operator. As a consequence, you either have to use class methods for allocation, or you have to clone existing objects. You can, however, make all constructors (including the default constructor) protected – or even private – to force allocation by a factory method. I don't recommend adding the pool to the actual class in Java, however. Also, do not add static allocator methods to the pool class unless it is meant to be a final class , or you will have to cast the returned objects in every subclass. The allocators of the superclass simply clutter up any subclasses' API if you have different allocator/constructor signatures.
One possible Java implementation uses a generic pool base class which implements the mechanism, and final pool subclasses for each of the classes that you want to pool, with proper allocator methods to wrap the pool handling. See the Vec3Pool class as an example.
Mopping Up
To sum up, pools can serve several purposes. If you have a limited object budget created in one chunk, chances are that you might more coherence in memory. You generally avoid constructor overhead in inner loops. You keep the memory debt low, and the memory usage may be bounded by recycling objects. A fixed-size pool can also enforce upper limits on memory use, which is important in some situations. If your GC takes into account the actual age of an object (not testing those that have been around for a long time already), you might realize additional benefits by keeping objects around.
In may cases pooling is not worth the effort, but every game has a few of candidates that might warrant such a solution. Vectors are one possible example - another good candidate are network packet buffers. If for some reason your game (e.g. a MUD/chat RPG) turns out to be multi-threaded, you will almost surely want to pool threads, as it takes considerable time to allocate them. Re-using them might also be worth the effort, as they are potentially large (memory imprint of some native thread implementations is up to 1MB). There might also be possible termination issues for getting threads garbage collected. Finally, implementing your own poolable Thread class is also an opportunity to put all the good advice in the Thread API deprecation FAQ to good use. Remember that in a multithreaded environment, you also need pool classes with synchronized allocator and disposal methods.
Making Java safer: Const Objects
There is an issue that inevitably comes up with finals, Immutables and pooling for C++ coders (and some ANSI C coders). The const modifier contributes quite a bit to stability and robustness to C/C++ code. However, there is no similar modifier in Java, and the superficially similar final is in fact a completely different issue (remember that const is part of C++ method signatures). A declaration like:
class Vec {
public final Vec3 ORIGIN = new Vec3( 0.0, 0.0, 0.0 );
public void set( float, float, float ) {..}
...
}
accomplishes nothing more than a mutable object referenced by an immutable variable ORIGIN. Placing
Vec3.ORIGIN = new Vec3( 1.0, 1.0, 1.0 );
somewhere in the code is prohibited , but
ORIGIN.set( 1.0, 1.0, 1.0 );
is not prohibited. It is even worse for arrays: final Vec3[] normals will neither prevent changing the Vec3 objects you refer to, nor does it prevent change of the reference stored in the array. What we need is something different (see the Normal3LUT.java example).
Java thus relies heavily on Immutable objects to be secure. Only Immutables can be referenced reliably from several other objects, or else any object that counts on the object state not being changed after referencing it would have to make a copy (more memory creep). Consequently, an accessor in an immutable composite object can only return references to Immutable objects, and it would have to copy mutable objects just in case the caller used the reference to call mutator methods.
// Promiscuous composite
public class HasAVec3 {
private Vec3 vec;
public Vec3 refVec3() {
return vec;
}
}
A composite handing out references to its state in this manner can suffer any kind of abuse. From a C++ point of view, Java's lack of const modifier makes it less safe to write efficient Java. There is a good Java way to ensure at least as much safety in many cases, and, as Mark Davis points out in his much recommended "Porting C++" tutorial (see the "References" section), it is actually safer than C++'s const, which a careless or malicious C++ coder can cast away.
Ideally, our class would also have final methods which can be inlined (saving one table lookup and indirection at the very least). This is most important if you want to avoid public fields. Sun, not trusting their own technology, designed their Java3D vector objects in this way:
package javax.vecmath;
class Vec3f {
public float x,y,z;
}
An immutable Java3D Vec3 could possibly use blank finals and look like:
class Vec3fConst {
public final float x, y, z;
public Vec3fConst( float x,y,z ) {
this.x = x;
this.y = y;
this.z = z;
}
}
and would not be related to the mutable class in any way – no casts, no inheritance chain.
This design is heavily compromised, allegedly for convenience and speedy access reasons. In reality, the Java3D inheritance tree exposes all kinds of ugliness, including code recycling by inheritance (witness the way that vector and RGB color objects share a Tuple3 base class between them).
Exposing the fields makes it impossible to have an immutable base class (short of shadowing the blank finals). Immutables require data hiding, and they have to be superclasses: Mutable extends Immutable, not vice versa. Wrapping is an equally bad solution, as the Mutable can not be cast to an Immutable, which doubles the number of possible method signatures. We are also into efficiency, so a solution using interfaces (like NonConst implements Const) is also not acceptable.
The thing to do, following Mark Davis's good advice, is
class Vec3 extends Vec3Const
which has the nice side effect of separating all mutators into a different class, making the source easier to read and maintain.
Note also that Java's lack of header files and prototypes makes javadoc-generated HTML a necessity. Unfortunately, the tool does not live up to the task, which leads the coder into real-life experiences that might be called "illiteral programming": the actual source is a lot less readable and organized than a decent C++ header.
You will note that within subclasses and the package, the Vec3Const implementation is actually a bit sloppy. If you don't even trust yourself, you could opt to make the fields private, which forces subclasses to use the accessors and the few initializers provided. You could also restrict the "initialize" mutator methods to package level access, which limits subclassing in other packages. It depends on how much compile time scrutiny you require.
Whenever we have a method that is not supposed to change the referenced object, we can now safely hand it a Vec3Const parameter instead. As with C++ const, the compiler will prevent accidental mistakes. In addition, if we actually hand over Vec3Const objects, it will be impossible to cast them to Vec3 to get mutator access. In other words, you will get the C++ safety within trusted code, and can protect yourself against third-party code where necessary, at the penalty of either copying into a caller re-usable Vec3, or into a Vec3Const possibly useful for repeated calls.
// Malicious user prodding your code...
public Vec3 hackVec3( final Vec3Const allegedConst ) {
if ( allegedConst instanceof Vec )
return (Vec3)allegedConst; return null;
}
There are a few problems with pooling, however. We can pool Vec3Const just as we pool Vec3, but we have to make sure that Vec3 is not accidentally pooled as Vec3Const. Incidentally, both pools are entirely separate from each other in the example code, and separate from the actual vector classes. The vector classes are not aware of the pooling – you can use them without pooling considerations wherever memory is not an issue. The drawback of implementing pools as singletons this way is that under some circumstances you have to make sure the pool class is not garbage collected.
But we have to resist the temptation to actually re-use Vec3Const objects – that would break Immutability, introducing subtle errors if we disposed an object that was still referenced somewhere by code counting on its immutability. The Vec3Const pool is fairly useless in most situations. It might speed up allocation, and ensure that we stick to a limited memory budget, but we won't be able to re-use these objects. In a game, it might be possible to keep a separate list of objects handed out and/or disposed, and collect them at certain times (for instance, when a new map is loaded, and the game restarts). Recycling immutables might be acceptable if we can count on a limited lifetime for the information.
For the Vec3 pool, the same lesson applies in reverse: Vec3 objects are mutable, the caller should never count on them being valid later on. If you are tempted to keep a Vec3Const reference instead of copying the object, make sure to check whether it is not actually a Vec3. This is why the java.lang.String class is final and has no mutable subclass - Strings stored as keys in hash tables are not supposed to change, or somebody might manage get the Java security mechanisms into accepting a mutable string object as a supposedly safe String.
If these concerns seem a bit over the top, remember that one main benefit of a Java solution is secure custom client-side code – robustness might be worth some versatility in our base classes. Using Vec3Const, you can now provide lookup tables (see Listing NormalLUT.java) and other data to, for instance, user-written game logic and Java "scripts", without making your game vulnerable to state information that's been edited. The way initialize() methods, constructors, and setters match is helpful for class design, too. Having final methods makes accessors and mutators as fast as public members with any decent Java compiler and VM. Java still handles memory for you (if you choose to use new instead of allocate in some places, the pool will not care, and if you omit dispose, the GC will fix that), and the pool as well as the allocation counters will provide you with information about how (much) memory is used.
If pooling does not offer any benefits, or even hurts your game (the GC might slow down with additional unused objects waiting in pools), you can use the compile-time flag to have the Pool classes wrap new calls instead. This is much less elegant than its C++ equivalent, unfortunately.
Resurrecting the Dead
I mentioned that finalizers are not guaranteed to be called at all. You still might be tempted to use them for resurrecting an objects. The Java Language Specification also states that any object's finalizer is called once at most -- so something like this will not work repeatedly, if at all:
class Lazarus {
void finalize {
LazarusPool.add( this );
}
}
You may quickly recognize that you could implement a solution using auxiliary objects. There are a few ways to do that, such as:
class Resurrectable {
/** Resurrect this, promote to a strong reference. */
protected void resurrect() {
Somewhere.ref = this;
this.res = new Resurrector(this); }
/** The current resurrector. */private Resurrector res;
/** * Optional: don't allocate resurrector unless needed, * don't allocate if finalizers aren't called anyway. */ void finalize() { resurrect();
}
}
class Resurrector {
/** Assign resurrection target. */
public Resurrector( final Resurrectable res ) {
this.res = res;
}
/** The finalizer does the resurrection. */
protected void finalize() {
res.resurrect();
}
/** The resurrectable to resurrect. */
private Resurrectable res;
}
This is literally "keeping an object at any costs", and quite wasteful in case of small objects like a Vec3, since you throw away a Resurrector object for each resurrection of an object not much larger. It also does not work on any VM that chooses not to call your finalizer. Auxiliary resurrector objects are useless as an implementation technique.
Finally, there is another loose end to take care of: JDK 1.2/Java2's new java.lang.ref Reference package. The new Reference classes offer some benefits, such as cache management for images and textures, and can also help in situations where you want to release resources and can't count on the finalizer. However, as a means of letting your application interface a sophisticated GC for better resource management, the package falls short. None of the mechanisms provided by the new Reference classes can be used to improve pooling by retrieving objects the GC finds to be reclaimable. Not only does it have the same overhead as the Resurrector (you throw away immutable Reference objects now), it is also not possible to promote any such reference to a strong one.
There does not seem to be a good reason for this restriction, as the Java Language Specs have to account for one (finalizer based) resurrection per object anyway. It is possibly the "Java according to Sun" doctrine and not so much an engineering constraint that deprives Java users of more sophisticated means to interact with the GC.
Abandoning some of the noble ideas behind Java in favor of "dirty" Java can often performance improvements in Java applications. By using pooling, and by eschewing Java's multithreading features, you can see some dramatic increases in application speed. Keep in mind that it is always the large, expensive objects (like threads) that you should be wary of – instead, keep an eye on the many small wrapper objects and the immutables that accumulate in your inner loops. They are the objects that can get you in trouble. A pool of mutables, while not in perfect accordance with the Java doctrines, is a perfectly valid solution.
Bernd Kreimeier is a writer, coder and physicist who has been DOOMed to pursue immersive game technology topics since early 1994. You can share your dirty Java secrets with him at [email protected].
References
"No Silver Bullet - Garbage Collection for Java in Embedded Systems", Alexander Petit-Bianco, Cygnus Solutions, http://sourceware.cygnus.com/java/papers/nosb.html
"Not using garbage collection - Minimize heap trashing in your Java program", Chuck McManis http://www.javaworld.com/javaworld/jw-09-1996/jw-09-indepeth.html
"Under The Hood: Java's garbage-collected heap", Bill Venners http://www.javaworld.com/javaworld/jw-08-1996/jw-08-gc.html
"Porting C++ to Java", Mark Davis http://www.ibm.com/java/education/papers-java-migration.html
Pooling examples for different applications can also be found at JavaWorld:
"Build your own ObjectPool in Java to boost Application speed", Thomas E. Davis http://www.javaworld.com/javaworld/jw-06-1998/jw-06-object-pool.html
"Improve the Robustness and Performance of Your ObjectPool", Thomas E. Davis http://www.javaworld.com/javaworld/jw-08-1998/jw-08-object-pool.html
The Java Developer Connection site explains thread pooling as well as caching. It's found at: http://developer.java.sun.com/developer/onlineTraining/JDCBook/perf.html.
Read more about:
FeaturesYou May Also Like