有父类的情况
1. 加载父类静态 1.1 为静态属性分配存储空间并赋初始值 1.2 执行静态初始化块和静态初始化语句(从上至下) 2. 加载子类静态 2.1 为静态属性分配存储空间 2.2 执行静态初始化块和静态初始化语句(从上至下) 3. 加载父类非静态 3.1 为非静态块分配空间 3.2 执行非静态块 4. 加载子类非静态 4.1 为非静态块分配空间 4.2 执行非静态块 5. 加载父类构造器 5.1 为实例属性分配存数空间并赋初始值 5.2 执行实例初始化块和实例初始化语句 5.3 执行构造器内容 6. 加载子类构造器 6.1 为实例属性分配存数空间并赋初始值 6.2 执行实例初始化块和实例初始化语句 6.3 执行构造器内容
下面看一个例子:
package jvm;
public class InstanceClass extends ParentClass{
public static String subStaticField = "子类静态变量";
public String subField = "子类非静态变量";
public static StaticClass staticClass = new StaticClass("子类");
static {
System.out.println("子类 静态块初始化");
}
{
System.out.println("子类 [非]静态块初始化");
}
public InstanceClass(){
System.out.println("子类构造器初始化");
}
public static void main(String args[]) throws InterruptedException {
new InstanceClass();
}
}
class ParentClass{
public static String parentStaticField = "父类静态变量";
public String parentField = "父类[非]静态变量";
public static StaticClass staticClass = new StaticClass("父类");
static {
System.out.println("父类 静态块初始化");
}
{
System.out.println("父类 [非]静态块初始化");
}
public ParentClass(){
System.out.println("父类 构造器初始化");
}
}
class StaticClass{
public StaticClass(String name){
System.out.println(name+" 静态变量加载");
}
}
按照上面说的规则,先自己想一想,然后再查看答案:
父类 静态变量加载
父类 静态块初始化
子类 静态变量加载
子类 静态块初始化
父类 [非]静态块初始化
父类 构造器初始化
子类 [非]静态块初始化
子类构造器初始化
抛砖引玉之后,结合《深入理解Java虚拟机》看看类加载机制
类的加载是指将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后再堆区创建一个java.lang.class对象,用来封装类的方法区内的数据结构。类的加载的最终产品是位于堆区的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。在Java语言中,类型的加载、连接、初始化过程都是在程序运行区间完成的。
类加载器并不需要等到某各类被首次主动使用时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
加载.class文件的方式:
类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生个生命周期包括:加载,验证,准备,解析,初始化,使用,卸载共7个阶段。其中验证,准备,解析统称为连接。
其中类的加载过程包括(加载、验证、准备、解析、初始化)五个阶段。这五个阶段中,加载,验证,准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化之后开始,这是为了支持Java的运行时绑定(也成为动态绑定或者晚期绑定).
另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:
1.通过一个类的全限定名来的获取定义此类的二进制字节流
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。
相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。
对于数组类而言,情况就有所不同,数组类本身不通过类加载器创建,它是由虚拟机直接创建的。但数组类和类加载器仍然有很密切的关系,因为数组类的元素类型最终要靠类加载器去创建。
加载过程完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机自行定义,虚拟机规范未规定此区域的具体数据结构。然后在内存中实例化一个java.lang.Class类的对象,这样对象将作为程序访问方法区中的这些类型数据的外部接口。
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会虚拟机自身的安全。验证阶段大致会完成4个阶段
的检验工作:
文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
符号引用验证:确保解析动作能正确执行。
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
1.这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时伴随着对象一块分配到Java堆中
2.这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
假设一个类变量的定义为:public static int value = 3;
那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,存放于类构造器<clinit>()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。
这里还需要注意如下几点:
3.如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。
例:public static final int value = 3;
编译时Javac将会value生成ConstantValue属相,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3.
可以理解static final常量在编译期就将其结果放入了调用它的类的常量池中。
解析——把类中的符号引用转换为直接引用
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。Java中对类变量进行初始化设定有两种方式:
1.声明类变量式指定初始值
2.使用静态代码块为类变量指定初始值
重点:JVM初始化步骤
1.假如这个类还没有被加载和连接,则程序先加载并连接该类
2.假设该类的直接父类还没有被初始化,则先初始化其直接父类
3.假如类中有初始化语句,则系统依此执行这些初始化语句
类的初始化时机:只有当类主动使用的时候才会导致类的初始化。类的主动使用包括以下六种:
结束生命周期
在如下几种情况下,Java虚拟机将结束生命周期
– 执行了System.exit()方法
– 程序正常执行结束
– 程序在执行过程中遇到了异常或错误而异常终止
– 由于操作系统出现错误而导致Java虚拟机进程终止
寻找类加载器的例子:
public class TestClassLoadDemo {
public static void main(String[] args){
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println(loader);
System.out.println(loader.getParent());
System.out.println(loader.getParent().getParent());
}
}
输出结果:
从上面的结果可以看出,并没有获取到ExtClassLoader的父Loader,原因是Bootstrap Loader(引导类加载器)是用C语言实现的,找不到一个确定的返回父Loader的方式,于是就返回null。
这几种类加载器的层次关系如下图所示:
注意:这里父类加载器并不是通过继承关系来实现的,而是采用组合实现的。
站在Java虚拟机的角度来讲,只存在两种不同的类加载器:
启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分;
所有其他的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。
站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:
启动类加载器:Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:
1)在执行非置信代码之前,自动验证数字签名。
2)动态地创建符合用户特定需要的定制化构建类。
3)从特定的场所取得java class,例如数据库中和网络中。
JVM类加载机制
全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
父类委托:先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类的二进制数据,并将其转换成Class对象,存入缓存区。
这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。
类的加载有三种方式:
1.命令行启动应用时候由JVM初始化加载
2.通过Class.forName方法动态加载
3.通过ClassLoader.loadClass()方法动态加载
例子:
1 public class LoadTest {
2 public static void main(String args[]) throws ClassNotFoundException {
3 ClassLoader loader = Thread.currentThread().getContextClassLoader();
4 System.out.println(loader);
5
6 // 使用ClassLoader.loadClass()来加载类,不会执行初始化块
7 loader.loadClass("jvm.TestClassLoad");
8
9 // 使用Class.forName()来加载类,默认会执行初始化块
10 Class.forName("jvm.TestClassLoad");
11
12 //使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块
13 Class.forName("jvm.TestClassLoad", false, loader);
14 }
15 }
要加载的类:
package jvm;
public class TestClassLoad {
static {
System.out.println("静态代码块被加载了");
}
}
输出结果:
Class.forName()和ClassLoader.loadClass()区别
Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
注:
Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。
双亲委派的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载类。
1.当AppClassLoader加载一个class时,他首先不会自己去尝试加载这个类,而是把类加载请求委托给父类加载器ExtClassLoader去完成。
2.当ExtClassLoader加载一个class时,他首先也不会自己去尝试加载这个类,而是把类加载请求委托给BootStrapClassLoader去完成。
3.如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
ClassLoader源码分析:
public Class<?> loadClass(String name)throws ClassNotFoundException {
return loadClass(name, false);
}
protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
// 首先判断该类型是否已经被加载
Class c = findLoadedClass(name);
if (c == null) {
//如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
try {
if (parent != null) {
//如果存在父类加载器,就委派给父类加载器加载
c = parent.loadClass(name, false);
} else {
//如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
双亲委派模型意义:
-系统类防止内存中出现多份同样的字节码
-保证Java程序安全稳定运行
Classloader
中的loadClass
方法,这样就导致每个自定义的类加载器其实是在使用自己的loadClass
方法中的加载机制来进行加载,这种模式当然是不符合双亲委派机制的,也是无法保证同一个类在jvm中的唯一性的,那么为了保证及时是由不同的类加载器(哪怕是用户自定义的类加载器加载)也是唯一的,java官方在Classloader
中添加了findClass
方法,用户只需要重新这个findClass
方法,在loadClass
方法的逻辑里,如果父类加载失败的时候,才会调用自己的findClass
方法来完成类加载,这样就完成了符合双亲委派机制。
Thread
类的setContextClassLoader
方法进行设置,如果创建线程时还未设置,它就从父线程继承一个,如果在应用全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
利用这个线程上下文类加载器
,jdni去加载需要的spi代码,也就是父类请求子类的加载器去加载。
每个程序模块(bundle)都有自己的类加载器,需要更换程序(bundle)的时候,连同类加载器一起替换,以实现代码的热部署
。osgi和双亲委派模式不同,他是一个基于网状的互相组合依赖的加载。 Osgi的加载步骤是这样的:
通常情况下,我们都是直接使用系统类加载器。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输 Java 类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。自定义类加载器一般都是继承自 ClassLoader 类,从上面对 loadClass 方法来分析来看,我们只需要重写 findClass 方法即可。下面我们通过一个示例来演示自定义类加载器的流程:
package com.classloader;
import java.io.*;
public class MyClassLoader extends ClassLoader {
private String root;
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] loadClassData(String className) {
String fileName = root + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
try {
InputStream ins = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length = 0;
while ((length = ins.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public String getRoot() {
return root;
}
public void setRoot(String root) {
this.root = root;
}
public static void main(String[] args) {
MyClassLoader classLoader = new MyClassLoader();
classLoader.setRoot("E:\\temp");
Class<?> testClass = null;
try {
testClass = classLoader.loadClass("com.classloader.Test2");
Object object = testClass.newInstance();
System.out.println(object.getClass().getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。由于这里只是演示,我并未对class文件进行加密,因此没有解密的过程。这里有几点需要注意:
1、这里传递的文件名需要是类的全限定性名称,即com.paddx.test.classloading.Test格式的,因为 defineClass 方法是按这种格式进行处理的。
2、最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。
3、这类Test 类本身可以被 AppClassLoader 类加载,因此我们不能把 com/paddx/test/classloading/Test.class 放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由 AppClassLoader 加载,而不会通过我们自定义类加载器来加载。
参考:
http://blog.csdn.net/ns_code/article/details/17881581
https://www.cnblogs.com/ityouknow/p/5603287.html