类的动态加载
关键字: ClassLoader ClassLoading 类加载
问 : 调用 Class.forName() 与 ClassLoader.loadClass() 的区别在什么地方 ?
答 : 这两方法都是通过一个给定的类名去定位和加载这个类名对应的 java.long.Class 类对象 . 尽管如此 , 它们的在行为方式上还是有区别的 .
Ø 用哪个 java.lang.ClassLoader 进行加载 <o:p></o:p>
Ø 返回的 Class 对象是否被初始化 <o:p></o:p>
Class.forName(String) 方法(只有一个参数), 使用调用者的类加载器来加载, 也就是用加载了调用forName方法的代码的那个类加载器. 相应的, ClassLoader.loadClass()方法是一个实例方法(非静态方法), 调用时需要自己指定类加载器, 那么这个类加载器就可能是也可能不是加载调用代码的类加载器. 如果用特定的类加载器来加载类在你的设计中占有比较重要的地位, 你就应该调用ClassLoader.loadClass(String)方法或Class.forName(String, boolean, ClassLoader)方法. <o:p></o:p>
另外, Class.forName()方法对加载的类对象进行初始化. 可见的效果就是类中静态初始化段及字节码中对所有静态成员的初始工作的执行(这个过程在类的所有父类中递归地调用). 这点就与ClassLoader.loadClass()不同. ClassLoader.loadClass()加载的类对象是在第一次被调用时才进行初始化的. <o:p></o:p>
你可以利用上述的差异. 比如,要加载一个静态初始化开销很大的类, 你就可以选择提前加载该类(以确保它在classpath下), 但不进行初始化, 直到第一次使用该类的域或方法时才进行初始化. <o:p></o:p>
最常用的是Class.forName(String, boolean, ClassLoader). 设置第二个参数为false即推迟初始化, 第三个参数指定要用来进行加载的类加载器. 我建议为了最大的灵活性使用这个方法. <o:p></o:p>
类初始化错误是难处理的
成功地加载了类, 并不意味着就不会有其它问题. 静态初始化代码可以抛出异常, 异常被包装到java.long.ExceptionInInitializerError的实例中. 异常抛出后, 这个类将不可用. 这样, 如果你需要在代码中处理这些错误, 你就应该调用进行初始化的Class.forName()方法. <o:p></o:p>
但进一步说, 如果你要处理ExceptionInInitializerError并试图从错误中恢复, 很可能不如你想象的那样正常工作. 请看下面的示例代码:<o:p></o:p>
public class Main
{
public static void main (String [] args) throws Exception
{
for ( int repeat = 0; repeat < 3; ++ repeat)
{
try
{
// "Real" name for X is outer class name+$+nested class name:
Class.forName ("Main$X");
}
catch (Throwable t)
{
System.out.println ("load attempt #" + repeat + ":");
t.printStackTrace (System.out);
}
}
}
private static class X
{
static
{
if (++ s_count == 1)
throw new RuntimeException ("failing static initializer<v:shapetype id="_x0000_t75" stroked="f" filled="f" path="m@4@5l@4@11@9@11@9@5xe" o:preferrelative="t" o:spt="75" coordsize="21600,21600"><v:stroke joinstyle="miter"></v:stroke><v:formulas><v:f eqn="if lineDrawn pixelLineWidth 0"></v:f><v:f eqn="sum @0 1 0"></v:f><v:f eqn="sum 0 0 @1"></v:f><v:f eqn="prod @2 1 2"></v:f><v:f eqn="prod @3 21600 pixelWidth"></v:f><v:f eqn="prod @3 21600 pixelHeight"></v:f><v:f eqn="sum @0 0 1"></v:f><v:f eqn="prod @6 1 2"></v:f><v:f eqn="prod @7 21600 pixelWidth"></v:f><v:f eqn="sum @8 21600 0"></v:f><v:f eqn="prod @7 21600 pixelHeight"></v:f><v:f eqn="sum @10 21600 0"></v:f></v:formulas><v:path o:connecttype="rect" gradientshapeok="t" o:extrusionok="f"></v:path><o:lock aspectratio="t" v:ext="edit"></o:lock></v:shapetype><v:shape id="_x0000_i1025" style="WIDTH: 11.25pt; HEIGHT: 15pt" alt="" type="#_x0000_t75"><v:imagedata o:href="/Images/dot.gif" src="file:///C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\msohtml1\01\clip_image001.gif"></v:imagedata></v:shape>");
}
} // End of nested class
private static int s_count;
} // End of class
<o:p></o:p>
上面的代码3次尝试加载一个内部类X, 即便是X的静态初始化只在每一次加载时失败, 这3次加载都抛出了异常. <o:p></o:p>
>java Main
load attempt #0:
java.lang.ExceptionInInitializerError
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:140)
at Main.main(Main.java:17)
Caused by: java.lang.RuntimeException: failing static initializer...
at Main$X.<clinit>(Main.java:40)
... 3 more
load attempt #1:
java.lang.NoClassDefFoundError
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:140)
at Main.main(Main.java:17)
load attempt #2:
java.lang.NoClassDefFoundError
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:140)
at Main.main(Main.java:17) <o:p></o:p>
有点令人吃惊的时, 在第2, 3次进行类加载时, 抛出的异常竟然是java.lang.NoClassDefFoundError. 这里发生的事情是, 第一次加载后(在进行初始化之前), JVM发现X已经被加载, 而这个X的类实例在加载它的类加载器被垃圾回收之前是不会被卸载的. 所以这之后的对Class.forName()的调用时, JVM不会再尝试进行初始化的工作, 但是, 更令人不解的是, 抛出一个NoClassDefFoundError. <o:p></o:p>
卸载这样的类的方法是丢弃原来加载该类的类加载器实例并重新创建一个. 当然, 这只能是在你使用了Class.forName(String, boolean, ClassLoader)这个3参数的方法的时候才能办到.<o:p></o:p>
隐藏的 Class.forName() 方法
你一定用过Java中X.class的语法去获取一个在编译器就知道名字的类对象实例. 在字节码的层次上, 这一点是如何实现的就不被人熟知了. 不同的编译器有不同的实例细节, 但共同点是, 所有编译器所相应产生的代码都是调用的Class.forName(String)这一个参数的方法. 比如J2SE <st1:chsdate w:st="on" year="1899" month="12" day="30" islunardate="False" isrocdate="False">1.4.1</st1:chsdate>的javac就把Class cls = X.class; 翻译成如下等价的形式:<o:p></o:p>
<v:shape id="_x0000_i1026" style="WIDTH: 11.25pt; HEIGHT: 15pt" alt="" type="#_x0000_t75"><v:imagedata o:href="/Images/dot.gif" src="file:///C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\msohtml1\01\clip_image001.gif"></v:imagedata></v:shape>
// This is how "Class cls = X.class" is transformed:
if ( class $Main$X == null )
{
class $Main$X = class $ ("Main$X");
}
Class cls = class $Main$X;
<v:shape id="_x0000_i1027" style="WIDTH: 11.25pt; HEIGHT: 15pt" alt="" type="#_x0000_t75"><v:imagedata o:href="/Images/dot.gif" src="file:///C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\msohtml1\01\clip_image001.gif"></v:imagedata></v:shape>
static Class class $ (String s)
{
try
{
return Class.forName (s);
}
catch (ClassNotFoundException e)
{
throw new NoClassDefFoundError (e.getMessage());
}
}
static Class class $Main$X; // A synthetic field created by the compiler <o:p></o:p>
跟 Sun 的 javac 开个玩笑
从上面的例子你可以看到, 编译器调用Class.forName()方法加载类对象, 并将其缓存到一个包内可见的静态变量中. 这种令人费解的实现方式的可能是因为在早期版本的Java中, 这种X.class的语法还未被支持, so the feature was added on top of the Java 1.0 byte-code instruction set.(???)<o:p></o:p>
利用这一点, 你可以在编译器的开销上做一些有趣的事情. 用J2SE <st1:chsdate w:st="on" year="1899" month="12" day="30" islunardate="False" isrocdate="False">1.3.1</st1:chsdate>编译下面的代码片段: <o:p></o:p>
public class <st1:place w:st="on">Main</st1:place>
{
public static void main (String [] args) throws Exception
{
System.out.println ("String class: " + String. class );
class $java$lang$String = int . class ;
System.out.println ("String class: " + String. class );
}
static Class class $java$lang$String;
} // End of class <o:p></o:p>
运行它, 你会得到下面这个很荒谬的输出结果: <o:p></o:p>
>java <st1:place w:st="on">Main</st1:place><o:p></o:p>
String class: class java.lang.String<o:p></o:p>
String class: int<o:p></o:p>
在J2SE <st1:chsdate w:st="on" year="1899" month="12" day="30" islunardate="False" isrocdate="False">1.4.1</st1:chsdate>中, 上面的代码将不能被编译通过, 但你仍然可以用反射的方式戏弄它: <o:p></o:p>
public static void main (String [] args) throws Exception
{
System.out.println ("String class: " + String. class );
Main. class .getDeclaredField ("class$java$lang$String").set ( null , int . class );
System.out.println ("String class: " + String. class );
} <o:p></o:p>
<o:p> </o:p>
综上所述, 下次你再调用Class.forName()方法时, 你应该知道它的局限性可选的替代方案了. <o:p></o:p>
<o:p> </o:p>
关键字: ClassLoader ClassLoading 类加载
问 : 调用 Class.forName() 与 ClassLoader.loadClass() 的区别在什么地方 ?
答 : 这两方法都是通过一个给定的类名去定位和加载这个类名对应的 java.long.Class 类对象 . 尽管如此 , 它们的在行为方式上还是有区别的 .
Ø 用哪个 java.lang.ClassLoader 进行加载 <o:p></o:p>
Ø 返回的 Class 对象是否被初始化 <o:p></o:p>
Class.forName(String) 方法(只有一个参数), 使用调用者的类加载器来加载, 也就是用加载了调用forName方法的代码的那个类加载器. 相应的, ClassLoader.loadClass()方法是一个实例方法(非静态方法), 调用时需要自己指定类加载器, 那么这个类加载器就可能是也可能不是加载调用代码的类加载器. 如果用特定的类加载器来加载类在你的设计中占有比较重要的地位, 你就应该调用ClassLoader.loadClass(String)方法或Class.forName(String, boolean, ClassLoader)方法. <o:p></o:p>
另外, Class.forName()方法对加载的类对象进行初始化. 可见的效果就是类中静态初始化段及字节码中对所有静态成员的初始工作的执行(这个过程在类的所有父类中递归地调用). 这点就与ClassLoader.loadClass()不同. ClassLoader.loadClass()加载的类对象是在第一次被调用时才进行初始化的. <o:p></o:p>
你可以利用上述的差异. 比如,要加载一个静态初始化开销很大的类, 你就可以选择提前加载该类(以确保它在classpath下), 但不进行初始化, 直到第一次使用该类的域或方法时才进行初始化. <o:p></o:p>
最常用的是Class.forName(String, boolean, ClassLoader). 设置第二个参数为false即推迟初始化, 第三个参数指定要用来进行加载的类加载器. 我建议为了最大的灵活性使用这个方法. <o:p></o:p>
类初始化错误是难处理的
成功地加载了类, 并不意味着就不会有其它问题. 静态初始化代码可以抛出异常, 异常被包装到java.long.ExceptionInInitializerError的实例中. 异常抛出后, 这个类将不可用. 这样, 如果你需要在代码中处理这些错误, 你就应该调用进行初始化的Class.forName()方法. <o:p></o:p>
但进一步说, 如果你要处理ExceptionInInitializerError并试图从错误中恢复, 很可能不如你想象的那样正常工作. 请看下面的示例代码:<o:p></o:p>
public class Main
{
public static void main (String [] args) throws Exception
{
for ( int repeat = 0; repeat < 3; ++ repeat)
{
try
{
// "Real" name for X is outer class name+$+nested class name:
Class.forName ("Main$X");
}
catch (Throwable t)
{
System.out.println ("load attempt #" + repeat + ":");
t.printStackTrace (System.out);
}
}
}
private static class X
{
static
{
if (++ s_count == 1)
throw new RuntimeException ("failing static initializer<v:shapetype id="_x0000_t75" stroked="f" filled="f" path="m@4@5l@4@11@9@11@9@5xe" o:preferrelative="t" o:spt="75" coordsize="21600,21600"><v:stroke joinstyle="miter"></v:stroke><v:formulas><v:f eqn="if lineDrawn pixelLineWidth 0"></v:f><v:f eqn="sum @0 1 0"></v:f><v:f eqn="sum 0 0 @1"></v:f><v:f eqn="prod @2 1 2"></v:f><v:f eqn="prod @3 21600 pixelWidth"></v:f><v:f eqn="prod @3 21600 pixelHeight"></v:f><v:f eqn="sum @0 0 1"></v:f><v:f eqn="prod @6 1 2"></v:f><v:f eqn="prod @7 21600 pixelWidth"></v:f><v:f eqn="sum @8 21600 0"></v:f><v:f eqn="prod @7 21600 pixelHeight"></v:f><v:f eqn="sum @10 21600 0"></v:f></v:formulas><v:path o:connecttype="rect" gradientshapeok="t" o:extrusionok="f"></v:path><o:lock aspectratio="t" v:ext="edit"></o:lock></v:shapetype><v:shape id="_x0000_i1025" style="WIDTH: 11.25pt; HEIGHT: 15pt" alt="" type="#_x0000_t75"><v:imagedata o:href="/Images/dot.gif" src="file:///C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\msohtml1\01\clip_image001.gif"></v:imagedata></v:shape>");
}
} // End of nested class
private static int s_count;
} // End of class
<o:p></o:p>
上面的代码3次尝试加载一个内部类X, 即便是X的静态初始化只在每一次加载时失败, 这3次加载都抛出了异常. <o:p></o:p>
>java Main
load attempt #0:
java.lang.ExceptionInInitializerError
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:140)
at Main.main(Main.java:17)
Caused by: java.lang.RuntimeException: failing static initializer...
at Main$X.<clinit>(Main.java:40)
... 3 more
load attempt #1:
java.lang.NoClassDefFoundError
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:140)
at Main.main(Main.java:17)
load attempt #2:
java.lang.NoClassDefFoundError
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:140)
at Main.main(Main.java:17) <o:p></o:p>
有点令人吃惊的时, 在第2, 3次进行类加载时, 抛出的异常竟然是java.lang.NoClassDefFoundError. 这里发生的事情是, 第一次加载后(在进行初始化之前), JVM发现X已经被加载, 而这个X的类实例在加载它的类加载器被垃圾回收之前是不会被卸载的. 所以这之后的对Class.forName()的调用时, JVM不会再尝试进行初始化的工作, 但是, 更令人不解的是, 抛出一个NoClassDefFoundError. <o:p></o:p>
卸载这样的类的方法是丢弃原来加载该类的类加载器实例并重新创建一个. 当然, 这只能是在你使用了Class.forName(String, boolean, ClassLoader)这个3参数的方法的时候才能办到.<o:p></o:p>
隐藏的 Class.forName() 方法
你一定用过Java中X.class的语法去获取一个在编译器就知道名字的类对象实例. 在字节码的层次上, 这一点是如何实现的就不被人熟知了. 不同的编译器有不同的实例细节, 但共同点是, 所有编译器所相应产生的代码都是调用的Class.forName(String)这一个参数的方法. 比如J2SE <st1:chsdate w:st="on" year="1899" month="12" day="30" islunardate="False" isrocdate="False">1.4.1</st1:chsdate>的javac就把Class cls = X.class; 翻译成如下等价的形式:<o:p></o:p>
<v:shape id="_x0000_i1026" style="WIDTH: 11.25pt; HEIGHT: 15pt" alt="" type="#_x0000_t75"><v:imagedata o:href="/Images/dot.gif" src="file:///C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\msohtml1\01\clip_image001.gif"></v:imagedata></v:shape>
// This is how "Class cls = X.class" is transformed:
if ( class $Main$X == null )
{
class $Main$X = class $ ("Main$X");
}
Class cls = class $Main$X;
<v:shape id="_x0000_i1027" style="WIDTH: 11.25pt; HEIGHT: 15pt" alt="" type="#_x0000_t75"><v:imagedata o:href="/Images/dot.gif" src="file:///C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\msohtml1\01\clip_image001.gif"></v:imagedata></v:shape>
static Class class $ (String s)
{
try
{
return Class.forName (s);
}
catch (ClassNotFoundException e)
{
throw new NoClassDefFoundError (e.getMessage());
}
}
static Class class $Main$X; // A synthetic field created by the compiler <o:p></o:p>
跟 Sun 的 javac 开个玩笑
从上面的例子你可以看到, 编译器调用Class.forName()方法加载类对象, 并将其缓存到一个包内可见的静态变量中. 这种令人费解的实现方式的可能是因为在早期版本的Java中, 这种X.class的语法还未被支持, so the feature was added on top of the Java 1.0 byte-code instruction set.(???)<o:p></o:p>
利用这一点, 你可以在编译器的开销上做一些有趣的事情. 用J2SE <st1:chsdate w:st="on" year="1899" month="12" day="30" islunardate="False" isrocdate="False">1.3.1</st1:chsdate>编译下面的代码片段: <o:p></o:p>
public class <st1:place w:st="on">Main</st1:place>
{
public static void main (String [] args) throws Exception
{
System.out.println ("String class: " + String. class );
class $java$lang$String = int . class ;
System.out.println ("String class: " + String. class );
}
static Class class $java$lang$String;
} // End of class <o:p></o:p>
运行它, 你会得到下面这个很荒谬的输出结果: <o:p></o:p>
>java <st1:place w:st="on">Main</st1:place><o:p></o:p>
String class: class java.lang.String<o:p></o:p>
String class: int<o:p></o:p>
在J2SE <st1:chsdate w:st="on" year="1899" month="12" day="30" islunardate="False" isrocdate="False">1.4.1</st1:chsdate>中, 上面的代码将不能被编译通过, 但你仍然可以用反射的方式戏弄它: <o:p></o:p>
public static void main (String [] args) throws Exception
{
System.out.println ("String class: " + String. class );
Main. class .getDeclaredField ("class$java$lang$String").set ( null , int . class );
System.out.println ("String class: " + String. class );
} <o:p></o:p>
<o:p> </o:p>
综上所述, 下次你再调用Class.forName()方法时, 你应该知道它的局限性可选的替代方案了. <o:p></o:p>
<o:p> </o:p>
相关推荐
本文主要解析Java类加载的原理,分为三个部分:基础的类加载原理解析、插件环境下的类加载和线程上下文类加载器。 首先,Java虚拟机(JVM)内置了三种预定义的类加载器: 1. 启动(Bootstrap)类加载器:这是最基础的...
Java 类加载器原理 Java 类加载器是Java虚拟机(JVM)的核心组成部分,它负责将类的字节码加载到内存中并转换为可执行的Java类。类加载器的作用不仅仅是加载类,还包括确保类的唯一性,避免重复加载,并且遵循特定...
本文将深入探讨ClassLoader的工作原理和类加载机制,帮助开发者理解这个至关重要的概念。 1. 类加载机制概述 Java的类加载机制遵循“双亲委派模型”(Delegation Model)。当一个类被加载时,它首先会尝试由当前...
本文将详细解析Java类加载原理,分为三篇文章进行阐述,分别是:Java类加载原理解析、插件环境下类加载原理解析和线程上下文类加载器。 首先,我们来了解Java虚拟机(JVM)的类加载器结构。JVM预定义了三种主要的类...
Java类加载原理是Java虚拟机(JVM)的重要组成部分,它负责将类的.class文件从磁盘或网络中加载到内存,并转化为运行时的数据结构,以便执行程序代码。本文将深入探讨类加载机制,包括加载、连接、初始化三个主要...
### Java 类加载器详解 #### 一、类加载器概述 在Java中,类加载器(Class ...通过理解和掌握类加载器的工作原理及其不同类型的加载器,可以帮助开发者更好地管理Java应用程序的依赖关系,提高程序的性能和可维护性。
本篇文章将深入探讨如何实现自定义类加载器以及其背后的原理。 1. **类加载器的工作原理** - 类加载过程分为三个阶段:加载、验证、准备、解析和初始化。在加载阶段,类加载器寻找并加载类的二进制数据。 - Java...
通过掌握类加载的过程以及类加载器的工作原理,可以帮助开发者更好地处理类加载过程中可能出现的问题,同时也可以为实现某些特殊需求提供支持。希望本文能够帮助读者更加深入地理解和应用Java类加载机制。
JAVA类加载器的工作原理 JAVA类加载器是JAVA虚拟机(JVM)的一部分,负责加载类文件,并将其转换为JVM可以执行的字节码。类加载器基于三个机制:委托、可见性和单一性。 委托机制是指将加载一个类的请求交给父类...
本文将深入探讨自定义类加载器的基本概念、工作原理以及如何创建一个简单的基于磁盘的自定义类加载器。 ### 类加载器的层次结构 Java中的类加载器通常形成一个树状的层次结构,其中Bootstrap ClassLoader是最顶层...
Java 虚拟机类加载器的工作方式...理解类加载器的工作原理和双亲委派模型对于优化程序性能、解决类冲突以及构建复杂的模块化系统至关重要。在实际开发中,掌握这些知识可以帮助我们编写更高效、更健壮的Java应用程序。
通过掌握类加载的过程、类加载器的工作原理以及类加载的线程安全性等方面的知识,开发者能够更好地利用Java的动态特性,优化程序性能,并避免常见的异常问题,如`ClassNotFoundException`等。此外,对于自定义类加载...
展类加载器的加载顺序来理解。系统类加载器在加载类时,会先尝试让扩展类加载器加载,如果...理解类加载器的工作原理有助于我们更好地进行程序设计和优化,特别是在开发插件系统、模块化应用以及处理动态加载需求时。
当遇到类找不到或冲突的问题时,理解类加载器的工作原理至关重要。通过设置JVM参数(-verbose:class)或使用工具如jconsole、VisualVM等,可以帮助分析类加载的过程。 8. 源码分析: 要深入理解类加载器,阅读JVM...
类加载器是Java虚拟机(JVM)的重要组成部分,它负责将类的字节码从文件系统或网络中加载到JVM中,并转换为运行时的java.lang.Class...深入理解类加载器的工作原理和机制,对于开发高效、稳定的Java应用具有重要意义。
类加载机制不仅涉及到类的加载、验证、准备、解析和初始化等步骤,还包括类加载时机的选择及类加载器的具体工作原理等内容。 #### 一、类加载时机 Java类的生命周期主要包括以下几个阶段:加载、验证、准备、解析...