1 概述
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行加载、解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
2 类加载的时机
类从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期如下图所示:
[img]http://dl2.iteye.com/upload/attachment/0105/1395/764dafe0-5e59-3ee5-b249-85005bf5e2e5.jpg[/img]
加载、验证、准备、初始化、卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的[color=red][b]开始[/b][/color],而解析则可能在"初始化"阶段之后。这里说的是按部就班的"开始",而不是按部就班的"进行"或者"完成",因为这些阶段都是交叉的进行的,通常会在一个阶段的执行过程中调用或者激活另外一个阶段。
虚拟机规范规定的[color=red][b]有且仅有的四种[/b][/color]对类立即进行"初始化"的场景:
1)遇到new,getstatic,putstatic或者invokestatic这4条字节码指令时,如果类没有进行初始化,则需要先触发其初始化。对应的java代码分别是:使用new关键字实例化对象的时候、读取或者设置一个静态字段的(final修饰的静态字段除外)时、以及调用一个类的静态方法时。
2)使用java.lang.Reflect对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
3)当初始化一个类时,如果发现其父类还没有进行初始化,则先触发其父类的初始化。
4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类。
三个不会触发类初始化的例子。
1)对于静态字段,只有直接定义这个字段的类才会被初始化
public class SuperClass {
static{
System.out.println("superclass init");
}
public static int value = 123;
}
public class SubClass extends SuperClass{
static{
System.out.println("subclass init");
}
}
public class NotInitialization {
public static void main(String[] args){
System.out.println(SubClass.value);
}
}
输出结果:
只输出"superclass init",而不输出" subclass init "
2)new 类的数组,并不触发类的初始化。
public class NotInitialization {
public static void main(String[] args){
SuperClass[] superClasses = new SuperClass[10];
}
}
输出结果:
什么都不输出。
3)由final修饰的静态常量,会在编译器加入到引用类的常量池中,运行期间两个类就没有任何关系了。
public class ConstClass {
static {
System.out.println("constclass init");
}
public final static String HELLO_WORLD= "helloworld";
}
public class NotInitialization {
public static void main(String[] args){
System.out.println(ConstClass.HELLO_WORLD);
}
}
输出结果:
并没有输出" constclass init "
3 类的加载过程
3.1 加载
加载阶段虚拟机主要完成三件事:
1)通过类的全限定名来获取定义此类的二进制字节码。
2)将这个字节码所代表的静态存储结构转化为方法区的运行时数据结构。
3)在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
3.2 验证
验证的目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段又分为了四个验证过程。
1)文件格式验证。例如Class的版本号是否当前虚拟机能够处理。
2)元数据验证。例如是否继承了final修饰的类。
3)字节码验证。例如类型转换是否有效。
4)符号引用验证。例如符号引用中的类,方法,字段的访问性(private,public等)是否可被当前类访问。
可以通过-Xverify:none来关闭类验证,缩短虚拟机类加载的时间。
3.3 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都在方法区中进行分配。
3.4 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
3.5 初始化
到了初始化阶段才真正开始执行类中定义的Java代码。
初始化阶段就是执行类构造器<clinit>()方法的过程。
1)<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{})中的语句自动生成的。
2)虚拟机会保证父类的<clinit>优先执行于子类的<clinit>
3)如果类中没有静态语句块或者静态变量赋值操作,那么就不生成<clinit>
4)接口的<clinit>,不要求父接口先执行。
5)虚拟机会保证<clinit>方法是线程安全的。
4 类加载器
对于同一个类,如果加载它的类加载器不是同一个,那么虚拟机也视为不是同一个类。
4.1双亲委派模型
类加载器的双亲委派模型如下图所示:
[img]http://dl2.iteye.com/upload/attachment/0105/1397/f2e2f037-6be6-3d43-ae77-4eb883e531f7.jpg[/img]
1)启动类加载器(Bootstrap ClassLoader):这个类加载器负责加载<JAVA_HOME>\lib目录中的,并且虚拟机识别的(如rt.jar)类库到虚拟机内存中。启动类加载器是虚拟机自身的一部分,是由C++程序编写的,Java程序无法使用。如果你将自己定义的xxoo.jar扔到这个目录下,启动类加载器也不认。
2) 扩展类加载器(Extension ClassLoader):它负责加载<JAVA_HOME>\lib\ext目录中的类库。
3)应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现,它负责加载用户类路径上指定的类库,也成为系统类加载器。
public class NotInitialization {
public static void main(String[] args) throws Exception{
// System.out.println(ConstClass.HELLO_WORLD);
System.out.print(NotInitialization.class.getClassLoader());
}
}
输出结果:sun.misc.Launcher$AppClassLoader@192d342
如果一个类加载器收到了类加载的请求,它不会首先尝试加载这个类,而是把这个请求委派给父类加载器去完成,只有当父加载器无法完成这个加载请求时,才会尝试自己去加载,这就是类加载器的双亲委派模型。这就保证了rt.jar只能有启动类加载器加载,用户无法自己写一个和rt.jar同名的类来搞破坏。
另外你也无法定义一个属于java.lang包下的类,例如:
public class Frank1234 {
public static void main(String[] args){
System.out.print("111");
}
}
运行输出:
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:480)
...
4.2 Tomcat5的类加载器结构
web服务器需要解决如下问题:
1)同一个服务器中的多个web应用使用的类库之间的隔离性。例如WEB-INF\lib目录下的jar是隔离的。
2)同一个服务器中的多个web应用使用的类库的共享性。例如%TOMCAT_HOME%\common\lib目录下的jar是共享的。
等等。
tomcat的类加载器结构,如下图所示:
[img]http://dl2.iteye.com/upload/attachment/0105/1399/a4ea6fd9-18d7-38c7-84a1-9a7a2c895c32.jpg[/img]
如图所示,因为有CommonClassLoader所有可以保证common\lib目录下的jar的共享,可以每个web应用启动一个WebappClassLoader,这样就保证了每个web应用WEB-INF\lib目录下jar的相互隔离性。JasperLoader来保证jsp文件修改后的热部署。
《深入理解Java虚拟机》 -周志明
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行加载、解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
2 类加载的时机
类从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期如下图所示:
[img]http://dl2.iteye.com/upload/attachment/0105/1395/764dafe0-5e59-3ee5-b249-85005bf5e2e5.jpg[/img]
加载、验证、准备、初始化、卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的[color=red][b]开始[/b][/color],而解析则可能在"初始化"阶段之后。这里说的是按部就班的"开始",而不是按部就班的"进行"或者"完成",因为这些阶段都是交叉的进行的,通常会在一个阶段的执行过程中调用或者激活另外一个阶段。
虚拟机规范规定的[color=red][b]有且仅有的四种[/b][/color]对类立即进行"初始化"的场景:
1)遇到new,getstatic,putstatic或者invokestatic这4条字节码指令时,如果类没有进行初始化,则需要先触发其初始化。对应的java代码分别是:使用new关键字实例化对象的时候、读取或者设置一个静态字段的(final修饰的静态字段除外)时、以及调用一个类的静态方法时。
2)使用java.lang.Reflect对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
3)当初始化一个类时,如果发现其父类还没有进行初始化,则先触发其父类的初始化。
4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类。
三个不会触发类初始化的例子。
1)对于静态字段,只有直接定义这个字段的类才会被初始化
public class SuperClass {
static{
System.out.println("superclass init");
}
public static int value = 123;
}
public class SubClass extends SuperClass{
static{
System.out.println("subclass init");
}
}
public class NotInitialization {
public static void main(String[] args){
System.out.println(SubClass.value);
}
}
输出结果:
只输出"superclass init",而不输出" subclass init "
2)new 类的数组,并不触发类的初始化。
public class NotInitialization {
public static void main(String[] args){
SuperClass[] superClasses = new SuperClass[10];
}
}
输出结果:
什么都不输出。
3)由final修饰的静态常量,会在编译器加入到引用类的常量池中,运行期间两个类就没有任何关系了。
public class ConstClass {
static {
System.out.println("constclass init");
}
public final static String HELLO_WORLD= "helloworld";
}
public class NotInitialization {
public static void main(String[] args){
System.out.println(ConstClass.HELLO_WORLD);
}
}
输出结果:
并没有输出" constclass init "
3 类的加载过程
3.1 加载
加载阶段虚拟机主要完成三件事:
1)通过类的全限定名来获取定义此类的二进制字节码。
2)将这个字节码所代表的静态存储结构转化为方法区的运行时数据结构。
3)在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
3.2 验证
验证的目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段又分为了四个验证过程。
1)文件格式验证。例如Class的版本号是否当前虚拟机能够处理。
2)元数据验证。例如是否继承了final修饰的类。
3)字节码验证。例如类型转换是否有效。
4)符号引用验证。例如符号引用中的类,方法,字段的访问性(private,public等)是否可被当前类访问。
可以通过-Xverify:none来关闭类验证,缩短虚拟机类加载的时间。
3.3 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都在方法区中进行分配。
3.4 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
3.5 初始化
到了初始化阶段才真正开始执行类中定义的Java代码。
初始化阶段就是执行类构造器<clinit>()方法的过程。
1)<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{})中的语句自动生成的。
2)虚拟机会保证父类的<clinit>优先执行于子类的<clinit>
3)如果类中没有静态语句块或者静态变量赋值操作,那么就不生成<clinit>
4)接口的<clinit>,不要求父接口先执行。
5)虚拟机会保证<clinit>方法是线程安全的。
4 类加载器
对于同一个类,如果加载它的类加载器不是同一个,那么虚拟机也视为不是同一个类。
4.1双亲委派模型
类加载器的双亲委派模型如下图所示:
[img]http://dl2.iteye.com/upload/attachment/0105/1397/f2e2f037-6be6-3d43-ae77-4eb883e531f7.jpg[/img]
1)启动类加载器(Bootstrap ClassLoader):这个类加载器负责加载<JAVA_HOME>\lib目录中的,并且虚拟机识别的(如rt.jar)类库到虚拟机内存中。启动类加载器是虚拟机自身的一部分,是由C++程序编写的,Java程序无法使用。如果你将自己定义的xxoo.jar扔到这个目录下,启动类加载器也不认。
2) 扩展类加载器(Extension ClassLoader):它负责加载<JAVA_HOME>\lib\ext目录中的类库。
3)应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现,它负责加载用户类路径上指定的类库,也成为系统类加载器。
public class NotInitialization {
public static void main(String[] args) throws Exception{
// System.out.println(ConstClass.HELLO_WORLD);
System.out.print(NotInitialization.class.getClassLoader());
}
}
输出结果:sun.misc.Launcher$AppClassLoader@192d342
如果一个类加载器收到了类加载的请求,它不会首先尝试加载这个类,而是把这个请求委派给父类加载器去完成,只有当父加载器无法完成这个加载请求时,才会尝试自己去加载,这就是类加载器的双亲委派模型。这就保证了rt.jar只能有启动类加载器加载,用户无法自己写一个和rt.jar同名的类来搞破坏。
另外你也无法定义一个属于java.lang包下的类,例如:
public class Frank1234 {
public static void main(String[] args){
System.out.print("111");
}
}
运行输出:
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:480)
...
4.2 Tomcat5的类加载器结构
web服务器需要解决如下问题:
1)同一个服务器中的多个web应用使用的类库之间的隔离性。例如WEB-INF\lib目录下的jar是隔离的。
2)同一个服务器中的多个web应用使用的类库的共享性。例如%TOMCAT_HOME%\common\lib目录下的jar是共享的。
等等。
tomcat的类加载器结构,如下图所示:
[img]http://dl2.iteye.com/upload/attachment/0105/1399/a4ea6fd9-18d7-38c7-84a1-9a7a2c895c32.jpg[/img]
如图所示,因为有CommonClassLoader所有可以保证common\lib目录下的jar的共享,可以每个web应用启动一个WebappClassLoader,这样就保证了每个web应用WEB-INF\lib目录下jar的相互隔离性。JasperLoader来保证jsp文件修改后的热部署。
《深入理解Java虚拟机》 -周志明
本文介绍了Java虚拟机的类加载机制,包括类加载的过程、时机及类加载器的工作原理。详细解释了类从加载到虚拟机内存中开始,到卸载出内存为止的整个生命周期。
498

被折叠的 条评论
为什么被折叠?



