public class TestDynamincLoaded {
static {
System.out.println("***** load test dynamic ");
}
public static void main(String[] args) {
new A();
System.out.println("***** load test *****");
B b = null;
}
}
class A {
static {
System.out.println("**** load A ****");
}
}
class B {
static {
System.out.println("**** load B ****");
}
}
说明了JVM的类加载是懒加载,也就是用到了某个类时才会去加载这个类。jar包或war包里的类不是一次性全部加载的,是使用到时才加载。
此程序运行过程分为如下几个过程:
类被加载到方法区中后主要包含 运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息。类加载器的引用:这个类到类加载器实例的引用对应class实例的引用:类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的对象实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。
类加载过程主要是通过类加载器来实现的,Java里有如下几种类加载器:
示例:
public class TestJDKClassLoader {
public static void main(String[] args) {
//java的核心类库是由BootStrapClassLoader加载的, BootStrapClassLoader是由c++创建的,所以返回null
System.out.println(String.class.getClassLoader());
//ExtClassLoader加载的
System.out.println(DESKeyFactory.class.getClassLoader());
//AppClassLoader
System.out.println(TestJDKClassLoader.class.getClassLoader());
System.out.println("=================");
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
ClassLoader extClassLoader = appClassLoader.getParent();
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println("the bootstrapClassLoader " + bootstrapClassLoader);
System.out.println("the extClassLoader " + extClassLoader);
System.out.println("the appClassLoader " + appClassLoader);
System.out.println("=========使用bootStrapLoader加载以下文件========");
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urLs.length; i++) {
System.out.println(urLs[i]);
}
System.out.println("========使用extClassLoader加载以下文件=========");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println("=========appClassLoader加载文件===============");
System.out.println(System.getProperty("java.class.path"));
}
}
JVM类加载器的层级结构如下:
这里类加载其实就有一个双亲委派机制,加载某个类时会先委托父加载器寻找目标类,找不到再
委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的
类加载路径中查找并载入目标类。比如我们的Math类,最先会找应用程序类加载器加载,应用程序类加载器会先委托扩展类加载器加载,扩展类加载器再委托引导类加载器,顶层引导类加载器在自己的类加载路径里找了半天没找到Math类,则向下退回加载Math类的请求,扩展类加载器收到回复就自己加载,在自己的类加载路径里找了半天也没找到Math类,又向下退回Math类的加载请求给应用程序类加载器,应用程序类加载器于是在自己的类加载路径里找Math类,结果找到了就自己加载了。
双亲委派机制说简单点就是,先找父亲加载,不行再由儿子自己加载。
双亲委派机制存在的意义:
/**
* 自定义类加载器,重写findClass方法
*/
public class MyClassLoader extends ClassLoader{
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0 , data.length);
} catch (Exception e) {
throw new ClassNotFoundException(e.getMessage() + e.getCause());
}
}
private byte[] loadByte(String name) throws IOException {
name = name.replaceAll("\\.", "/");
FileInputStream inputStream = new FileInputStream(classPath + "/" + name + ".class");
int len = inputStream.available();
byte[] data = new byte[len];
inputStream.read(data);
inputStream.close();
return data;
}
}
public class User {
public void sout() {
System.out.println("==============自定义类加载器============");
}
}
public class MyClassLoaderTest {
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("D:/test");
//需要在指定目录下放置User类编译后的class文件
Class clazz = myClassLoader.loadClass("User");
Object o = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(o, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
这里需要注意的是:如果程序编译后的User class文件没有删除,那么最后的输出结果会是AppClassLoader,这是因为AppClassLoader已经加载了程序生成的User class文件。
删除程序生成的User类的class文件,只保留test目录下的class文件,运行后的结果如下:
虽然我们自定义的类加载器继承的是ClassLoader,但是它的父类是AppClassLoader。
/**
* 自定义类加载器
*/
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
throw new ClassNotFoundException(e.getMessage() + e.getCause());
}
}
/**
* 重写类加载方法,实现自己的加载逻辑,不委派给父类加载
*
* @param name
* @param resolve
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = findLoadedClass(name);
if (clazz == null) {
long t1 = System.nanoTime();
if (!name.startsWith("mytest")) {
//委派给父类加载
clazz = this.getParent().loadClass(name);
} else {
//以mytest开头的类使用自己的类加载器加载
clazz = findClass(name);
}
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClassTime().increment();
}
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
}
private byte[] loadByte(String name) throws IOException {
name = name.replaceAll("\\.", "/");
FileInputStream inputStream = new FileInputStream(classPath + "/" + name + ".class");
int len = inputStream.available();
byte[] data = new byte[len];
inputStream.read(data);
inputStream.close();
return data;
}
}
测试
public class MyClassLoaderTest {
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("D:/");
Class clazz = myClassLoader.loadClass("mytest.User1");
Object o = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(o, null);
System.out.println(clazz.getClassLoader());
System.out.println();
MyClassLoader myClassLoader1 = new MyClassLoader("D:/");
Class clazz1 = myClassLoader1.loadClass("mytest.User1");
Object o1 = clazz1.newInstance();
Method method1 = clazz1.getDeclaredMethod("sout", null);
method1.invoke(o1, null);
System.out.println(clazz1.getClassLoader());
}
}
结果如下:可以看到使用了我们自定义的加载器加载的User1的class文件,而原本应该是使用AppClassLoader加载的。
同一个JVM内,两个相同包名和类名的类对象可以共存,因为他们的类加载器可以不一样,所以看两个类对象是否是同一个,除了看类的包名和类名是否都相同之外,还需要他们的类加载器也是同一个才能认为他们是同一个。
从图中的委派关系中可以看出: