您的位置:首页 > 编程语言 > Java开发

第7章 虚拟机类加载机制--《深入理解 Java 虚拟机》笔记

2012-02-09 22:37 661 查看
1概述

虚拟机把描述类的数据从Class文件加载到虚拟机,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。

与那些在编译时需要连接的语言不同,在Java语言里面,类的加载和连接过程都在程序运行期间完成的,这样会在类的加载稍微增加一些性能开销,但是却能为java应用程序提供高度的灵活性,java中天生就可以动态扩展的语言特性就是依赖运行动态加载和动态链接这个特点实现的。

2类加载的时机

类从加载到虚拟机内存中开始,到卸载出内存为止,他的整个生命周期包括了:

加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。

加载后会什么情况下立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

1、遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有进行过初始化,则需先触发器初始化。

2、使用java.util.reflect包的方法对类进行发射调用的时候。

3、当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

4、当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类。

除此之外所以引用类的方式,都不会被初始化,成为被动引用:

demo1:被动引用

通过子类引用父类的静态字段,不会导致子类初始化,父类的构造器也没有执行

package cn.partner4java.classloading;

public class SuperClass {
public SuperClass() {
System.out.println("SuperClass construction");
}
static{
System.out.println("SuperClass static");
}
public static int value = 1;
}

package cn.partner4java.classloading;

public class SubClass extends SuperClass {
public SubClass() {
System.out.println("SubClass construction");
}
static{
System.out.println("SubClass static");
}
}

package cn.partner4java.classloading;

/**
* 通过子类引用父类的静态字段,不会导致子类初始化,父类的构造器也没有执行
* @author partner4java
*
*/
public class NotInitialization {

public static void main(String[] args) {
System.out.println(SubClass.value);
//		后他打印:
//		SuperClass static
//		1
}

}
demo2:通过数组定义来引用类,不会触发此类的初始化

package cn.partner4java.classloading;

/**
* 通过数组定义来引用类,不会触发此类的初始化
* @author partner4java
*
*/
public class NotInitialization2 {

public static void main(String[] args) {
SuperClass[] superS = new SuperClass[10];
//		后台什么都没打印
}

}


demo3:

常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量类的初始化

package cn.partner4java.classloading;

public class ConstClass {
public ConstClass() {
System.out.println("ConstClass construction");
}
static{
System.out.println("ConstClass static area");
}
public static final String HELLO_WORLD = "hello world";
}
package cn.partner4java.classloading;

/**
* 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量类的初始化
* @author partner4java
*
*/
public class NotInitialization3 {

public static void main(String[] args) {
System.out.println(ConstClass.HELLO_WORLD);
//		后台打印:
//		hello world
}

}
demo4:

当调用静态方法时,会触发静态区域初始化,但是构造器还是没有初始化

package cn.partner4java.classloading;

public class ConstClass {
public ConstClass() {
System.out.println("ConstClass construction");
}
static{
System.out.println("ConstClass static area");
}
public static final String HELLO_WORLD = "hello world";

public static void mehtodT(){
System.out.println("ConstClass static mehtod");
}

public static void mehtodT2(){
System.out.println("ConstClass static mehtod2");
}
}

package cn.partner4java.classloading;

/**
* 当调用静态方法时,会触发静态区域初始化,但是构造器还是没有初始化
* @author partner4java
*
*/
public class NotInitialization4 {

public static void main(String[] args) {
ConstClass.mehtodT();
//		后台打印:
//		ConstClass static area
//		ConstClass static mehtod
}

}
demo5:

反射的时候是否初始化

getClass什么都不会做,Class.forName会执行静态域,但是也不会执行构造器初始化

package cn.partner4java.classloading;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
* getClass什么都不会做,Class.forName会执行静态域,但是也不会执行构造器初始化
* @author partner4java
*
*/
public class NotInitialization5 {

public static void main(String[] args) throws ClassNotFoundException, SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
Class clazz = ConstClass.class.getClass();
System.out.println("-------");
clazz.getMethods();
System.out.println("-------");

//		加上下面两行代码会打印下面的内容:报错
//		Method methodT = clazz.getMethod("mehtodT");
//		System.out.println("--------");
//		methodT.invoke(null, null);
//		-------
//		-------
//		Exception in thread "main" java.lang.NoSuchMethodException: java.lang.Class.mehtodT()
//			at java.lang.Class.getMethod(Class.java:1605)
//			at cn.partner4java.classloading.NotInitialization5.main(NotInitialization5.java:18)

System.out.println("--------");
Class clazz2 = Class.forName("cn.partner4java.classloading.ConstClass");

//		即使执行“ConstClass static area”,下面的代码也是错误的,也是会说没有初始化
//		System.out.println("--------");
//		Method methodT = clazz.getMethod("mehtodT");
//		methodT.invoke(null, null);

//		去掉报错部分后台打印:
//		-------
//		-------
//		ConstClass static area
}

}


3类加载的过程

1、加载

完成三件事情

通过一个类的权限定名来获取定义此类的二进制字节流。

将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

2、验证

验证是连接阶段的第一步,来确保包含的信息是否符合当前虚拟机的要求和是否会危害虚拟机自身的安全。

大致分为四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。

文件格式验证主要是文件格式规范和版本验证。

元数据验证主要是对字节码描述的信息进行 语义分析,以确保其描述的信息符合Java语言规范的要求。

字节码验证主要是数据流和控制流分析,任务是保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。

符号引用验证发生在虚拟机将符号引用转换为直接引用的时候,可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。目标是确保解析动作能正常执行。

3、准备

准备阶段正式为类变量分配内存并设置类变量初始化阶段,这些内存将在方法区中进行分配。

进行内存分配的仅包括类变量(被static修饰的变量),而不是实例常量,实例常量将会在对象实例化时随着对象一起分配到Java堆中。

其次这里的初始值一般是0值,如:public static int value = 123; 这里的初始值不是123,而是0,123是在初始化阶段才会被执行。

但是如果是final,这里将赋值123。

4、解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。

5、初始化

类初始化阶段是类加载过程的最后一步。

在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,则是根绝程序员通过程序制定的主观计划去初始化类变量和其他资源,或者从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。

·<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到静态语句块之前的变量,定义在他之后的变量,在前面的静态静态语句块中可以赋值,但是不能访问。

·<clinit>()方法与类的构造器(或者说实例构造器<init>()方法)不同,他不需要显示的调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。

·由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量的赋值操作。

·<clinit>()方法对于类或接口来说并不是必须的,如果一个类中并没有静态语句块,也没有对变量的赋值操作,那么编译器就可以不为这个类生成<clinit>()方法。

·接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。当接口与类不同的是,不需要先执行父类的<clinit>()。

·虚拟机会保证多线程<clinit>()方法的安全性。

4类加载器

虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机的外部去实现,以便让应用程序自己决定如果去获取锁需要的类。实现这个动作的代码模块叫做“类加载器”。

1、类和类加载器

类加载器虽然只用于实现类的加载动作,但他在Java程序中起到的作用远远不限于类加载阶段。

对于任意一个类,都需要由加载他的类加载器和类本身来确立在Java虚拟机中的唯一性。

2、双亲委派模式

站在Java虚拟机的角度讲,只存在两种不同的类加载器:

一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;

另外一种就是所有的其他类加载器,这些类加载器都是由java语言实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader。

从Java程序员的角度看,绝大部分Java程序都会使用到以下三种系统提供的类加载器:

·启动类加载器(Bootstrap ClassLoader):负责将<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接应用。

·扩展类加载器(Extension ClassLoader):这个加载器有sun.misc.Launcher$ExtClassLoader实现,他负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

·应用程序类加载器(Application ClassLoader):这个类加载器有sun.misc.Launcher$AppClassLoader实现。负责加载用户类路径(ClassPath)上所指定的类库。

双亲委派模式(Parents Delegation Model):要求除了顶层的类加载器外,其余的类加载器都应有自己的父类加载器。一般这种父子关系不是继承关系,而是组合关系来复用父加载器的代码

双亲委派模式的工作过程是:如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而且把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的请求最终都应该传送到顶层的启动类的加载器中,只要当父类返回无法完成这个加载请求时(就是查找范围没有查找到这个类),子加载器才会尝试去加载。

3、破坏双亲委派模式

如JNDI、JDBC等

OSGi而是网状结构的加载模式。

附件:通用手工类加载方式

forName

public static Class<?> forName(String name,

boolean initialize,

ClassLoader loader)

throws ClassNotFoundException使用给定的类加载器,返回与带有给定字符串名的类或接口相关联的 Class 对象。(以 getName 所返回的格式)给定一个类或接口的完全限定名,此方法会试图定位、加载和链接该类或接口。指定的类加载器用于加载该类或接口。如果参数 loader 为 null,则该类通过引导类加载器加载。只有 initialize 参数为 true 且以前未被初始化时,才初始化该类。

如果 name 表示一个基本类型或 void,则会尝试在未命名的包中定位用户定义的名为 name 的类。因此,该方法不能用于获得表示基本类型或 void 的任何 Class 对象。

如果 name 表示一个数组类,则会加载但不初始化该数组类的组件类型。

例如,在一个实例方法中,表达式:

Class.forName("Foo")

等效于:

Class.forName("Foo", true, this.getClass().getClassLoader())

注意,此方法会抛出与加载、链接或初始化相关的错误,《Java Language Specification》的第 12.2、12.3 和 12.4 节对此进行了详细说明。 注意,此方法不检查调用方是否可访问其请求的类。

如果 loader 为 null,也有安全管理器,并且调用方的类加载器不为 null,则此方法通过 RuntimePermission("getClassLoader") 权限调用安全管理器的 checkPermission 方法,以确保可以访问引导类加载器。

参数:

name - 所需类的完全限定名

initialize - 是否必须初始化类

loader - 用于加载类的类加载器

返回:

表示所需类的类对象

抛出:

LinkageError - 如果链接失败

ExceptionInInitializerError - 如果该方法激发的初始化失败

ClassNotFoundException - 如果类无法用指定的类加载器定位

从以下版本开始:

1.2

另请参见:

forName(String), ClassLoader

--------------------------------------------------------------------------------

forName

public static Class<?> forName(String className)

throws ClassNotFoundException返回与带有给定字符串名的类或接口相关联的 Class 对象。调用此方法等效于:

Class.forName(className, true, currentLoader)

其中 currentLoader 表示此类的定义类加载器。

例如,以下代码片段返回 java.lang.Thread 类的运行时 Class 描述符。

Class t = Class.forName("java.lang.Thread")

调用 forName("X") 将导致名为 X 的类被初始化。

参数:

className - 所需类的完全限定名。

返回:

具有指定名的类的 Class 对象。

抛出:

LinkageError - 如果链接失败

ExceptionInInitializerError - 如果此方法所激发的初始化失败

ClassNotFoundException - 如果找不到该类

--------------------------------------------------------------------------------

加载后可实例化:

newInstance

public T newInstance()

throws InstantiationException,

IllegalAccessException创建此 Class 对象所表示的类的一个新实例。如同用一个带有一个空参数列表的 new 表达式实例化该类。如果该类尚未初始化,则初始化之。

注意,此方法传播 nullary 构造方法所抛出的任何异常,包括已检查的异常。使用此方法可以有效地绕过编译时的异常检查,而在其他情况下编译器都会执行该检查。 Constructor.newInstance 方法将该构造方法所抛出的任何异常包装在一个(已检查的)InvocationTargetException 中,从而避免了这一问题。

返回:

此对象所表示的类的一个新分配的实例。

抛出:

IllegalAccessException - 如果此类或其 nullary 构造方法是不可访问的。

InstantiationException - 如果此 Class 表示一个抽象类、接口、数组类、基本类型或 void; 或者该类没有 nullary 构造方法; 或者由于某种其他原因导致实例化过程失败。

ExceptionInInitializerError - 如果该方法引发的初始化失败。

SecurityException - 如果存在安全管理器 s,并满足下列任一条件:

调用 s.checkMemberAccess(this, Member.PUBLIC) 拒绝创建该类的新实例

调用方的类加载器不同于也不是该类的类加载器的一个祖先,并且对 s.checkPackageAccess() 的调用拒绝访问该类的包
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: