Saturday, May 30, 2009

JDK 1.5 adoption rates

This month, we passed the 5 year anniversary of JDK 1.5.

Java has had seven major releases in its 15 year history, and an 8th is soon to arrive (will there be significant JDK 1.7 news at Java One? There's been quite a lot of recent activity.). Here's a nice page that summarizes things. For much of this century, Java releases arrived reliably every two years, and for much of Java's lifetime, adoption of the new releases was quite rapid.

However, that adoption rate has slowed, and recent Java releases are taking much longer to achieve high penetration rates. In my experience, it's quite common to find that JDK 1.4 is still the deployed release in a certain environment.

I find this rather surprising, as 1.5 and 1.6 offer substantial benefits. In my own development work, I try to use Java 1.6 as much as possible, not only for its higher performance, but in large part because of the great work on supportability and diagnosis. Many of the problems that I study at work only arise in situations where I can't pre-configure a specially-enabled JVM, so the ability to attach to an existing JVM and get heap dumps, thread dumps, performance information, etc. is extremely valuable.

But there's no denying that JDK 1.4 remains highly common in deployment. I'm not the only one to notice this, for example see the commenters in this article. So why is this? I think that the contributing factors include at least the following:
  • In the enterprise software world, where I spend much of my time, application servers drive adoption. These are systems such as WebSphere, Weblogic, JBoss, Netweaver, Oracle AppSvr, etc. The vendors that provide these systems tend to be conservative and upgrade their base JDK level slowly, and the users of these systems tend to be even more conservative and upgrade their installed applications slowly. So even though IBM has introduced a new WebSphere version which supports JDK 1.6, most IBM customers are still in the process of upgrading from WebSphere version 5 (which supported JDK 1.3) to WebSphere version 6.0/6.1 (which support JDK 1.4 and JDK 1.5)
  • JDK 1.4 was "good enough" for many things. Although there continue to be major improvements in Java, JDK 1.4 was a very impressive release, and for an enormous class of applications, it has everything you need. So since it exists, and it's stable and reliable and widely available, it has seen wide and stable adoption, with little pressure to upgrade.
  • Slow delivery of 1.5 and 1.6 on Mac OS X, Linux, etc. Java releases on platforms other than Windows and Solaris have lagged, sometimes substantially. It will be quite interesting to see if the open source treatment of Java 7 changes this platform coverage experience, but in the past it was certainly true that platforms were rolled out unevenly. For example, I believe there is still no version of Java FX for Linux, well more than a year after it was released for Windows and Solaris.
  • Generics are intimidating. Moving past JDK 1.5 means dealing with the world of Generics, which, although extremely powerful, mean a lot of new syntax, behavior, error messages, and the like. I think Generics are a very good feature, but the "in your face" nature of generics means that many will prefer to procrastinate and stay with JDK 1.4.
  • Java update isn't pushy enough. Sun generally doesn't force Java updates on their users. They have been very good about sustaining older releases, and have not pushed very hard on their user base to upgrade.
So, given this, what should Java programmers do? The low-end solution is to stick with JDK 1.4 and avoid use of JDK 1.5 and above features. But this is a fairly severe restriction, so most people have moved to a more sophisticated implementation. In Derby, for example, there is infrastructure which enables Derby to run in JDK 1.4 environments with full functionality. But in JDK 1.5 and 1.6 configurations, Derby dynamically detects the newer JDK version and dynamically loads alternate implementations of various functionality. I'll try to provide a more complete description of how this works in a separate post.

So just because the world is stuck at JDK 1.4, don't feel like you have to stay there.

Thursday, May 28, 2009

Google I/O is rocking!

Wow! Google I/O looks like it is a really hot conference! I had sort-of thought that developer conferences had fallen by the wayside, but Google appears to have brought them back to life with some amazing content, including:
I'm sure there's a lot more even than this; this is just what I noticed in a quick skim over the Internet...

This is rather overwhelming and intimidating! I guess I know what I'll be reading about for the next few days.

Java 6 Update 14

Java 6 update 14 has apparently been released.

This is a very substantial patch, with a lot of fixes and changes.

Among the things that interested me in this list:
  • 6u14 contains the 10.4.2.1 version of Java DB. Note that this version number is very similar, but not identical, to a version number you might see on an Apache Derby release. But, there is no 10.4.2.1 release of Apache Derby. This was the subject of a long and complex discussion in the Derby community back in February. The simple summary is that 10.4.2.1 is a JavaDB release which contains some additional fixes beyond the 10.4.2.0 release available from the Apache web site, and the modifies 4th digit in the release number helps us track that.
  • 6u14 contains a substantially-updated version of the VisualVM tool for Java monitoring and profiling.
  • 6u14 contains a number of new HotSpot changes, most significantly a version of the much-anticipated G1 garbage collector.
Although I'm constrained by circumstances to do much of my work with JDK 1.4, I try to use JDK 1.6 whenever possible in my development environment, because it has so many fixes and improvements.

I'll try to get this update soon.

Implementing the SQL Standard(s)

In the Derby project, we work very hard to adhere to the SQL Standard.

Faithfully implementing the SQL Standard makes it easy for users to use Derby for applications that have been developed on other DBMS implementations, and also makes it easy for users to develop applications on Derby, then move them to other systems. Both of these are good uses of Derby, and I'm pleased to see them.

Furthermore, implementing the SQL Standard is an effective way to build a high-quality piece of software, for the SQL Standard is the result of decades of work by thousands of very intelligent and hard-working people, who have tried to describe the SQL language as accurately and thoroughly as they can.

That is, in general, the standard specifies reasonable and desirable behavior.

However, there is a major frustration in attempting to implement the SQL standard: it's not freely available. In order to access the official SQL standard, in order to contribute to the development of the official SQL standard, in order to participate in direct discussions with the team of people who maintain and enhance the official SQL standard, you have to have access to the ANSI/ISO world, which is a membership group which costs a substantial amount of money to join. As far as I know, the Apache Software Foundation does not have access to the SQL Standards process, which is a shame.

This leads to problems in:
  • access to the official text of the standard
  • access to communities for discussions involving interpretation of the standard
  • access to tools and test beds for verification of the standard
It would sure be nice if the SQL standard were openly and freely available to all.

Tuesday, May 26, 2009

Some high-level thoughts on Java Performance Tuning

So you think you want to do some performance tuning. Here's some high-level thoughts on the subject.

First, construct a benchmark. This will have at least these benefits:
  • It will force you to get specific about the software under test. For any significant and complex piece of software, there are a forest of inter-related variables: which features are you interested in tuning, what configuration will be used to run them, what workload will be applied, what duration of test, etc.
  • It will give you some real running software to measure, tune, adjust, etc. Without real running software, you won't be able to run experiments, and without experiments, you won't know if you're making progress or not.

OK, so now you have a benchmark. The next step is to run it! You need to run it, in order to start getting some data points about how it performs. You also need to run it, in order to start developing some theories about how it is behaving. Ideally, save all your benchmark results, recording as much information as you can about the configurations that were used, the software that was tested, and the results of the benchmark.

While the benchmark is running, watch it run. At this point, you're just trying to get an overall sense of how the benchmark runs. In particular, pay attention to the four major components of overall system performance:
  1. CPU cycles
  2. Disk activity
  3. Memory usage
  4. Network activity
You may, or may not, be able to see that your benchmark is constrained by one of these four resources. For example, if your machine immediately goes 100% CPU busy at the start of your benchmark, and stays there until the end, you are probably CPU-constrained. However, try not to jump to such a conclusion. Perhaps you didn't give the system enough memory? Or perhaps your benchmark is using too small of a configuration, and so everything fit tidily into memory, while in real-life your usage never experiences such a small problem, so real usage is never CPU-bound, but is always hitting the disk. Try to keep an open mind, and get a feel for whether your configuration is reasonable and your workload is realistic, and only then come to a conclusion about whether you are CPU, Disk, Memory, or Network-bound.

Perhaps, while running your benchmark, you didn't seem to be bound by any resource? Your CPU was never fully busy, you always had spare memory, your disks and network were never very active? Here are two other possibilities that you don't want to overlook at this stage:
  • Perhaps your benchmark just isn't giving the system enough work to do? Many systems are designed to handle lots of concurrently-overlapping work, so make sure that you're giving your benchmark a large enough workload so that it is busy. Try adding more work (more clients, or more requests, or larger tasks...) and see if throughput goes up, indicating that there was actually spare capacity available.
  • Perhaps your benchmark is fine, but your system has some internal contention or concurrency bottlenecks which is preventing it from using all the resources you're giving it. If it seems like you're not using all your resources, but adding more work doesn't help, then you should start to think about whether some critical resource is experiencing contention.
OK, so you have a solid benchmark, you've run it a number of times and started to develop a performance history, and you have some initial theories about where your bottlenecks might reside. Now let's start talking about Java-specific stuff.

If you're going to be this serious about Java performance tuning, you should be reading, because there's 15 years of smart people thinking about this and you can (and should) benefit from all their hard work. Here's some places to start:
OK, back to coding and tuning. We're almost to the point where we have to start talking about serious tools, but first, here's a couple simple ideas that will get you a surprisingly long way with your performance studies:

First, while you are running your benchmark, trying pressing Control-Break a few times. If you are running on a Linux or Solaris system, try sending signal QUIT (kill -QUIT my-pid) to your java process. This will tell your JVM (at least, if it's the Sun JVM) to dump a stack trace of the current location of all running threads in the virtual machine. Do this a half-dozen times, spaced randomly through your benchmark run. Then read through the stack traces, and consider what they are telling you: are you surprised about which threads are doing what? Did you see any enormously deep or intricate stack traces? Did you see a lot of threads waiting for monitor entry on some master synchronization object? I call this the "poor man's profiler", and it's amazing what it can tell you. It's particularly nice because you don't have to set anything up ahead of time; you can just walk up to a misbehaving system, toss a couple stack trace requests into it, and see if anything jumps out at you in the output.

The next step is to instrument your program, at a fairly high level, using the built-in Java clock: System.currentTimeMillis. This handy little routine is quite inexpensive to call, and for many purposes can give you a very good first guess at where problems may be lurking. Just write some code like this:


long myStartTime = System.currentTimeMillis();
int numLoops = 0;

for (some-kinda-interesting-loop)
{
.. lotsa code that might be revealing here ...
numLoops++;
}

long myElapsedTime = System.currentTimeMillis() - myStartTime;

System.out.println("My loop ran " + numLoops +
" times, and took " + myElapsedTime + " milliseconds.");


Usually it doesn't take too long to sprinkle some calls to currentTimeMillis around, and get some first-level reporting on where the time is going.

One thing to keep in mind when using this technique is that the currentTimeMillis clock ticks rather irregularly: it tends to "jump" by 15-16 milliseconds at a time. So it's hard to measure things more accurately than about 1/60th of a second. Starting with JDK 1.5, there is a System.nanoTime call which is much more precise, which gets around this problem.

If you've gotten this far, you should have:
  • A reliable benchmark, with a history of results
  • Some high-level numbers and theories about where the time is going
  • Some initial ideas about whether your system is CPU-bound, Disk-bound, memory-bound, etc.
This by itself may be enough that you can make an experimental change, to see if you can improve things (aren't you glad you kept those historical numbers, to compare with?).

But if you can't yet figure out what to change, but you at least know what code concerns you, it's time to put a serious profiler to work. You can get profilers from a number of places: there are commercial tools, such as JProfiler, or tools built into Eclipse and NetBeans, or standalone tools like VisualVM. At any rate, find one and get it installed.

When using a profiler, there are essentially 3 things you can do:

  1. You can look at elapsed time
  2. You can look at method invocation counts
  3. You can look at memory allocation behaviors
Each of them has an appeal, but each also has pitfalls to beware of. Method/line invocation counts are great for figuring out how often code is called, and for finding deeply-nested methods that are being run a lot more than you anticipated, which can lead to some very insightful realizations about how your code is working. But mindlessly trying to reduce invocation counts can lead to trying to optimize away code that in reality is very efficient; furthermore, you may be simply repeating the work that the JIT is doing automatically on your behalf for free.

Analyzing memory allocation is also a great way for finding code which is run a lot, and removing unnecessary and wasteful memory allocation can be a good way to speed up Java code. However, modern garbage collectors are just spectacularly, phenomenally efficient, and in recent years I've often found the cutting back on the generation of garbage by itself often doesn't really help much. However, if the generation of garbage is a clue (a "code smell", as they say) which leads you to locate a bunch of unnecessary and wasteful work that can be eliminated, then it's a good way to quickly locate a problem.

Lastly, there is the profiler's elapsed-time analysis. Sometimes, this can work really well, and the profiler can show you exactly where your code is spending its time. However, in a complicated server process, with lots of background threads, I/O operations, etc., figuring out how to interpret the elapsed-time graphs can be very tricky. I've often thought that the profiler was pointing a giant Flying Fickle Finger of Fate directly at some section of my code, worked industriously to isolate and improve that code, only to find that, overall, it didn't make any difference in the actual benchmark results.

Saturday, May 23, 2009

The prevalence of caching

I've been thinking about caching this week.

At work, I'm neck deep in a complex re-implementation of an intricate caching algorithm, and I realized the other day that I've been fascinated by cache implementations for almost 25 years.

My first exposure to caches was at Computer Corporation of America in Boston in the mid 80's. The team was extending the Model 204 cache algorithms as part of the new BTree implementation that was being built (previously, Model 204 had supported only entry-order and hash access methods). I learned that caches are intricate and delicate, and have to be used properly.

At Ingres, in the early 90's, I was deeply involved in several projects in the area of the cache implementation. In one project, we were re-designing the recovery system from a page-oriented physical recovery system to an ARIES-style logical recovery system, which gave me a deep appreciation for the inter-connectedness of the page cache and the recovery system. In another project we were providing shared-memory multi-processor support. Since the page cache was going to be located in shared memory, we converted it to position-independent data structures. Later, I spent some time thinking about how to handle a variety of page sizes in the page cache in an efficient manner, without excessive memory fragmentation, and while being simultaneously efficient at caching all pages sizes in the range, but didn't get to the point of building working code.

At Sybase, I worked with the Store team on implementing a page cache for the ISS system; this was my first exposure to C++ and to object-oriented methods. We tried to build a flexible cache that would be extensible and re-usable as the code evolved. We implemented a working prototype but never reached production code. However, many of the ideas behind this system traveled with Siuling, Mike, and Nat to Cloudscape (and thus to Derby).

For most of this decade, I've been extending, enhancing, and maintaining a complex Object-Relational Mapping library at work. If you haven't seen an ORM before, Hibernate is a good example. Our system isn't as sophisticated as Hibernate, but it's powerful and intricate and carefully tailored to our needs.

In particular, our ORM library provides an object cache, and does its best to return cached items when possible. As with any cache, the primary issues are:
  • Using resources (mainly, memory) efficiently
  • Managing shared access to cached objects
  • Ensuring cache coherency
A lot of work has gone into each of these general areas. When it comes to using resources efficiently, the most important topic involves the replacement policy; that is, what do you do when the cache is full? Here, you can find a rich research literature. I was lucky enough to take a class from Professor Betty O'Neil at UMB, and think that the LRU-K algorithm is among the best available.

Testing a caching algorithm is also a tricky task, as there are several hidden traps:
  • The cache may malfunction, and return stale data rather than the most up-to-date data. Only a very carefully written test will be sensitive enough to catch this.
  • The cache may also malfunction, and return dirty (not-yet-committed) data. Again, the test has to be very aware to be able to detect this behavior.
The cache may also be returning correct, accurate data, but may still have a variety of performance problems:
  • The cache may be failing to cache data, and may be needlessly re-computing data from the underlying data source.
  • The cache may be caching data effectively, but may be preferring one sort of workload over another. For example, it may be working well for a workload of mostly read queries, but may not function well when updates are being performed. Or vice versa.
  • The cache may be violating some of its configuration. For example, it may not be implementing its replacement policy as designed. Or it may be exceeding the resource limits that have been assigned to it.
  • The cache may be doing all its processing correctly, but it still may be using too many resources, for example it may be experiencing high contention on thread synchronization, or it may be using inefficient algorithms for maintaining its data structures.
It's the last problem that I've been chasing this month.

Cache coherency is an complex problem, and this is the area where I've been spending a lot of time recently. In our system, one of the problems that is most challenging involves the situation where a transaction modifies an object, and that object is referenced by some other object that is resident in the cache. We have some complex graph traversal algorithms which trace the references and locate stale data which must be refreshed. In many ways it's similar to the type of code you see in garbage collection algorithms, although our problem is much simpler than the full GC problem.

In my particular case, the issue I've been struggling with is how to verify whether a modified data value happens to be currently referenced by an object which is referenceable from some object currently resident in the cache. That is, if object D is modified, and object A points to object B points to object C points to object D, and object A is currently resident in the cache, then I need to know this fact, because the modification of object D is relevant to object A. The problem is that, since objects are complex and have lots of inter-connectedness, there can be a variety of "pointer paths" that could possibly connect from A to D. The goal is to find any such object A instances as cheaply as possible, avoiding simple-to-implement-but-unusable-in-practice algorithms such viewing every object that is reachable from every cached object to see if object D is in that set. The implementation that we use is based on 2 building blocks:
  1. We analyze the object schema to construct the possible reference paths of interest
  2. We partition the cache by type, so that we can efficiently locate the cached instances of a desired type.
Then we can examine just the necessary cached instances, and traverse only the possible paths.

Friday, May 22, 2009

A small request for compiler writers

If you're writing a compiler, and you happen to issue an error message like:


[javac] C:\src\xxx.java:230: ')' expected
[javac] }
[javac] ^

It would be really nice if you could take the time to make that error message also say something like


Unmatched left parenthesis was seen on line 184


That way I wouldn't have to fiddle around using hacky techniques like commenting out half of my program to try to figure out where I accidentally left out the parenthesis.

It seems that modern compilers do usually issue quite precise messages; maybe this particular one just fell through some crack in the parse techniques where the compiler just had no idea where it was that the parenthesis matching got out of sync.

Oh well, I eventually found it. Just wish I could have that 15 minutes of my life back.