概述
- 虚拟机的类加载机制就是把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型
- 不同于编译时需要进行连接工作的语言,在java语言中,类型的加载、连接和初始化过程都是在程序运行期间完成的
类加载过程
过程 | 描述 |
---|---|
加载 | 加载阶段虚拟机需要完成3件事: 通过一个类的全限定名来获取定义此类的二进制字节流 将这个字节流所代表的静态存储结构转为方法区的运行时数据结构 在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口 |
验证 | 1. 文件格式验证 是否以魔数开头,主次版本号是否在当前虚拟机处理范围之内 常量池的常量中是否有不被支持的常量类型(检查常量tag标志) 指向常量的各种索引值中是否有指向不存在的常量和不符合类型的常量 CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据 Class文件中各个部分及文件本身是否有被删除的或附加的其它信息 … 2. 元数据验证 这个类是否有父类(除java.lang.Object之外,所有类都应当有父类) 这个类的父类是否继承了不允许继承的类(被final修饰的类) 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回类型却不同等) … 3. 字节码验证 – 通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作 保证跳转指令不会跳转到方法体以外的字节码指令上 保证方法体的类型转换是有效的… 4. 符号引用验证 – 发生在虚拟机将符号引用转为直接引用的时候 符合引用通过字符串描述的全限定名是否能找到对应的类 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可以被当前类访问 … |
准备 | 正式为类变量分配内存并设置类变量初始值的阶段,这些变量使用的内存都将在方法区中进行分配。实例变量将会在对象实例化时随对象一起分配在java堆中。其次这里所说的初始值是指变量默认的初始值(final除外,因为该字段的字段属性表中存在ConstantValue属性) |
解析 | 解析阶段在某些情况下是在初始化阶段之后再开始,这是为了支持java语言的运行时绑定 解析阶段是虚拟机将常量池中的符号引用转为直接引用的过程 符号引用的目标不一定已经加载到内存中 符号引用可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可 直接引用可以是直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄 |
初始化 | 执行类构造器<clinit>()方法的过程: <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生,编译器收集的顺序由语句在源文件中出现的顺序所决定的,静态语句块只能访问定义在静态语句块之前的变量。定义在静态语句块之后的变量,在静态语句块中可以赋值,但是不能访问。 <clinit>()方法与类的构造函数(或者说实例构造器<init>()方法)不同 ,它不需要显示地调用父类构造器,虚拟机会保证在子类<clinit>()执行之前,父类的<clinit>()方法已经执行完毕。因此虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。 接口中不能使用静态语句块,但是仍有变量初始化的赋值操作(接口中的成员变量的修饰符默认是 publin static final。成员方法的修饰符默认是 public abstract。这些是固定的,即使不写,系统也会给默认加上)。但是接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。 |
使用 | 对象的创建(仅限普通对象创建,不包括数组和Class对象等): 在类加载完成后,对象所需内存的大小便可完全确定(对此理解有些疑惑:类加载完成后不是只是执行了<clinit>()方法吗?<init>()方法是在什么时候执行的),然后根据此,虚拟机将为新生对象分配内存。 虚拟机将分配到的内存空间都初始化为零值(不包括对象头),此步骤保证对象的实例字段在java代码中不赋初始值就能直接使用。 虚拟机对对象进行必要的设置,如该对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。 从虚拟机角度看,一个新的对象已经产生了,但从java程序角度看,对象创建才刚刚开始——<init>()方法还没有执行,所有字段都还为零。一般来说(由字节码是否跟随invokespecial指令所决定),执行new指令之后会接着执行<init>()方法,把对象按照程序员的意思进行初始化,如此,一个真正可用的对象才算产生出来。 |
卸载 |
类加载器
1、类与类加载器:
- 对于任意一个类,都需要由加载它的类加载器及这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间
- 比较两个类是否”相等“(其指代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况)只有在这两个类是由同一个类加载器加载的前提下才有意义。
2、双亲委派模型:
- 从java虚拟机角度讲,只存在两种不同的类加载器:
a. 一种是启动类加载器(Bootstrap ClassLoader),该类加载器使用C++语言实现,是虚拟机自身的一部分。
b. 另一种是所有其他的类加载器,这些类加载器都有Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。 - 从Java开发人员角度看,绝大多数Java程序都会使用以下3种系统提供的类加载器
#set Java environment
export JAVA_HOME=/usr/lib/jvm/jdk1.7.0_79
export JRE_HOME=/usr/lib/jvm/jdk1.7.0_79/jre
#export JAVA_HOME=/usr/lib/jvm/jdk1.8.0_131
#export JRE_HOME=/usr/lib/jvm/jdk1.8.0_131/jre
export CLASSPATH=.:$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATH
export PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$JAVA_HOME:$PATH
a. 启动类加载器(Bootstrap ClassLoader),其将负责存放在<JAVA_HOME>\lib(?此处存疑?查看自己安装的环境,因为安装jdk的原因,是在<JRE_HOME>\lib)目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机是识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。
b. 扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext(同上,JRE_HOME目录下才找到lib\ext)t目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
c. 应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上指定的类库。开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中的默认加载器。
破坏双亲委派模型
1、双亲委派模型的第一次”被破坏“发生在双亲委派模型出现之前——即Jdk1.2发布之前。而类加载器和抽象类java.lang.ClassLoader则在jdk1.0时代就已经存在。虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),这个方法唯一的逻辑就是调用自己的loadClass。jdk1.2之后的java.lang.ClassLoader添加了一个新的protected方法的findClass(),在此之前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法。
/* 私有方法loadClassInternal() */
private Class<?> loadClassInternal(String name)
throws ClassNotFoundException
{
// For backward compatibility, explicitly lock on 'this' when
// the current class loader is not parallel capable.
if (parallelLockMap == null) {
synchronized (this) {
return loadClass(name);
}
} else {
return loadClass(name);
}
}
/* findClass方法 */
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
/* loadClass方法 */
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
双亲委派的具体逻辑实现就是在上面的loadClass()方法的代码中,jdk1.2之后已经不提倡用户再去覆盖loadClass()代码了,而应当把自己的类加载逻辑写在findClass()方法中。
2、双亲委派模型的第二次"被破坏"是由这个模型自身的缺陷所导致的,双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载)。但为了解决基础类又要调用回用户代码的问题,java引用了一个线程上下文类加载器(Thread Context Classloader)的设计。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果这个应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
通过这个方式,父类加载器请求子类加载器去完成类加载的动作。
3、第三次”被破坏“是由于用户对应用程序的动态性的追求而导致的(如代码热替换(HotSwap)、模块热部署(Hot Deployment))等。