David D
David D

Reputation: 13

ClassNotFoundException even though I don't use that class

I invoke the method m1 of the next class and I don't have in my classpath the class PKCSException that supposedly I don't need it:

package otropack;

import iaik.pkcs.PKCSException;
import iaik.security.provider.IAIK;

public class Test {

    public static void m1(){
        System.out.println("Method 1");
    }
    
    public static void m2() {
        try {
            IAIK.addAsProvider(false);
            System.out.println("Method 2"); 
            throw new PKCSException();
        }catch(PKCSException e) {
            e.printStackTrace();
        }
    }
}

Why do I obtain a ClassNotFountException when I call Test.m1()? I call a method which doesn't use that class

Caused by: java.lang.ClassNotFoundException: iaik.pkcs.PKCSException

If I replace PKCSException with Exception it works well without giving me an error for not having the class IAIK in the classpath. I don't have the class either IAIK.

Thank you very much!

Upvotes: 1

Views: 1268

Answers (2)

rzwitserloot
rzwitserloot

Reputation: 102903

This is a supremely interesting question. The answer is the verifier.

Let's experiment!

class Test {
    public static void main(String[] args) {
        m1();
    }

    public static void m1() { System.out.println("M1"); }

    public static void neverCalled() throws Exception {
        Exception e = new Ex1();
        // throw e; [1]
        // w1(e); [2]
        // w2(e); [3]
        // w3(e); [3]
    }

    private static void w1(Object x) {}
    private static void w2(Exception x) {}
    private static void w3(Serializable x) {}
}

class Ex1 implements Exception extends Exception {}

and then on the command line:

> javac Test.java; rm Ex1.class; java Test

This works.

However, uncomment [1] and it fails. uncomment [2] and it works. Uncomment [3] and it fails. Uncomment [4] and it works.

What. The. Flip?

The answer is, if you do almost anything interesting, it fails. In particular, 'throw it' or 'pass it as argument' counts, but not 'pass it as argument where the argument type is either j.l.Object or any interface'. Which is bizarre. It also matters not one iota what the type the variable is. Write Ex1 e = new Ex1(); or Exception e = new Ex1(); or even Object e = new Ex1();, it will make no difference.

Also, if we do not delete Ex1.class, and instead put a static initializer in there that prints 'initializing!', it never prints. That's a JVM guarantee.

So what, in the flip, is going on?

It's the verifier. Run this with java -noverify Test and it'll successfully run (prints M1) for all cases; uncomment all 4 lines if you want.

So, what is going on?

The verifier is attempting to verify the stack frames generated by javac. javac is more or less adding some notes in the produced bytecode to makes life easier for the class verifier (which is a thing that checks if executing the bytecode as written cannot possibly lead to core dumps or any security-sensitive significant code errors). It's easier to verify that these notes are correct and to then analyse that the bytecode has no heap corruption failures, than it is to surmise that no heap corruption exists without the notes.

However, as part of checking these notes, it needs to know what Ex1 is, and given that Ex1 is not there, you get your NoClass errors the moment that any stack frame note is generated that tells the verifier that the stack will have an Ex1 instance somewhere inside it. Thus the question boils down to: When does the VM generate stack frame notes, and that's a weird one, but matches precisely with the above snippets: throw and being an argument to non-Object, non-interface invocations does. The other cases don't.

You can get an actual verifier error out of the deal. It's simple, really:

> nano Test.java
class Test {
    public static void main(String[] args) {
        Ex1 e = new Ex1();
        m(e);
    }

    public static void m(Exception e) {}
}

> nano E1.java
class E1 extends Exception {}

> javac *.java
> nano E1.java
class E1 extends Thread {} // extend something else
> javac E1.java; # recompile _JUST_ E1
> java Test
VerifierError!

The general conclusion you can draw is simply this:

Any type used anywhere in a class must be present at runtime, because otherwise only usage of a type in a method that is [A] never invoked, and [B] does not show up in any stack frame notes - will avoid the error, and those two conditions together occur pretty much never. However, class inits only occur if code is actually run. So, if you want the property of writing code that, if never actually executed, won't cause any issues due to referring to non-existent classes, you can do that, but only if you isolate this code in its own class:

package otropack;

import iaik.pkcs.PKCSException;
import iaik.security.provider.IAIK;

public class Test {

    public static void m1(){
        System.out.println("Method 1");
    }
    
    public static void m2() {
        Container.m2();
    }

    private static class Container {
      public static void m2() {
          try {
              IAIK.addAsProvider(false);
              System.out.println("Method 2"); 
              throw new PKCSException();
          }catch(PKCSException e) {
             e.printStackTrace();
          }
      }
    }
}

solves your problem. No classloading errors at all.

Upvotes: 1

Iwan Roberts
Iwan Roberts

Reputation: 40

When the class loader loads your Test class it will try and find the definitions for all dependant classes. In your code it will fail with a ClassNotFoundException because you have missing classes.

Upvotes: 0

Related Questions