咱们知道class文件中存储了类的描述信息和各类细节的数据,在运行Java程序时,虚拟机须要先将类的这些数据加载到内存中,并通过校验、转换、解析和初始化事后,最终造成能够直接使用的Java类型。java
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析3个部分统称为链接。
android
类的加载机制实际上就是类的生命周期中加载、验证、准备、解析、初始化5个过程。c++
加载是类的加载过程的第一个阶段,在加载阶段,虚拟机须要完成如下3件事情:程序员
java.lang.Class
对象,做为方法区这个类的各类数据的访问入口。经过全限定名来获取二进制流能够有不少种方式,好比从JAR、EAR、WAR文件包中读取,从网络获取,也能够由其余文件来生成(jsp文件生成对应的Servlet类),甚至还能够经过运行时动态生成(Java动态代理)。安全
相比类加载过程的其余阶段,加载阶段是可控性最强的。由于开发者既能够利用系统提供的启动类加载器来完成,也能够经过自定义类加载去完成(重写loadClass
方法,控制字节流的获取方式)。bash
关于类加载器的详细介绍将放在文章最后。网络
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中。而后在内存中实例化一个java.lang.Class
类的对象,这样就能够经过这个对象来访问方法区中的这些数据。数据结构
验证是链接阶段的第一步,这一阶段的目的是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。验证阶段大体上会完成下面4个阶段的检验动做:文件格式验证、元数据验证、字节码验证、符号引用验证。多线程
准备阶段是正式为类变量(静态变量)分配内存并设置初始值的阶段,这些类变量所使用的内存都将在方法区中进行分配。jsp
这里有两点须要注意:
好比:
public class Test {
public int number = 111;
public static int sNumber = 111;
}复制代码
成员变量number
在这个阶段就不会进行内存分配和初始化。而类变量sNunber
会在方法区中分配内存,并设置为int类型的零值0而不是111,赋值为111是在初始化阶段才会执行。
好比:
public class Test {
public static final int NUMBER = 111;
}复制代码
此时,就会在准备阶段将NUMBER
的值设置为111。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
解析动做主要就是在常量池中寻找类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符等7类符号引用,把这些符号引用替换为直接引用。下面主要介绍下类或接口、字段、类方法、接口方法的解析:
A
经过符号X引用了类B
,虚拟机会把表明类B
的全限定名传递给A
的类加载器去加载B
,B
通过加载、验证、准备过程,在解析过程又可能会触发B
引用的其余的类的加载过程,至关于一个类引用链的递归加载过程,整个过程只要不出现异常,B
的就是一个加载成功的类或接口了,也就是能够获取到表明B
的java.lang.Class
对象。在验证了A
具有对B
的访问权限后,就将符号引用X替换为B
的直接引用。类的初始化类加载过程的最后一步,在前面的过中,除了在加载阶段开发者能够自定义加载器以外,其他的动做都是彻底有虚拟机主导和控制完成。到了初始化阶段,才真正开始执行类中定义的Java代码。
在准备阶段,类变量已经设置了系统要求的零值,而在初始化阶段,则根据程序员经过程序制定的主观计划去初始化类变量和其余资源,或者能够从另一个角度来表达:初始化阶段是执行类构造器<clinit>()
方法的过程。
<clinit>()
方法是由编译器自动收集类中全部的类变量(static
变量)和静态代码块(static{}
块)中的语句合并生成的。编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态代码块中只能访问到定义在静态代码块以前的变量,定义在它以后的变量,在前面的静态代码块能够赋值,可是不能访问。
public class Test {
static {
number = 111; // 能够赋值
System.out.println(number); // 不能读取,编辑器或报错Illegal forward reference
}
static int number;
}复制代码
<clinit>()
方法与类的构造函数(或者说实例构造器<init>()
方法)不一样,它不须要显式地调用父类的<clinit>()
方法,虚拟机会保证在子类的<clinit>()
方法执行以前,父类的<clinit>()
方法已经执行完毕。因此,父类定义的静态代码块要先与子类的赋值操做。
class Parent {
public static int A = 1;
static {
A = 2;
}
}
class Sub extends Parent {
public static int B = A;
public static void main(String[] args) {
System.out.println(Sub.B);
}
}复制代码
<clinit>()
方法对于类或接口来讲并非必需的,若是一个类中没有静态语句块,也没有对变量的赋值操做,那么编译器能够不为这个类生成<clinit>()
方法。
接口中不能使用静态语句块,但仍然有变量初始化的赋值操做,所以接口与类同样都会生成<clinit>()
方法。但接口与类不一样的是,执行接口的<clinit>()
方法不须要先执行父接口的<clinit>()
方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也同样不会执行接口的<clinit>()
方法。
虚拟机会保证一个类的<clinit>()
方法在多线程环境中被正确地加锁、同步,若是多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()
方法,其余线程都须要阻塞等待,直到活动线程执行<clinit>()
方法完毕。若是在一个类的<clinit>()
方法中有耗时很长的操做,就可能形成多个进程阻塞。
在以前的加载过程当中,提到了类加载器经过一个类的全限定名来获取描述此类的二进制字节流,这个过程可让开发中自定义类加载器来决定如何获取须要的字节流。那么,什么是类加载器呢?
对于任意一个Java类,都必须经过类加载器加载到方法区,并生成java.lang.Class
对象才能使用类的各个功能,因此咱们能够把类加载器理解为一个将class
类文件转换为java.lang.Class
对象的工具。
对于任意一个类,都须要由加载它的类加载器和这个类自己一同确立其在Java虚拟机中的惟一性,每个类加载器,都拥有一个独立的类名称空间。也就是说,若是两个类“相等”,那么这两个类必须是被同一个虚拟机中的同一个类加载器加载,而且来自同一个class
文件。
在Java当中,已经有3个预制的类加载器,分别是BootStrapClassLoader
、ExtClassLoader、AppClassLoader
。
ExtClassLoader
做为类加载器,但它也是一个Java类,是由BootStrapClassLoader
来加载的,因此,ExtClassLoader
的parent是BootStrapClassLoader
。可是因为BootStrapClassLoader
是c++
实现的,咱们经过ExtClassLoader.getParent
获取到的是null
。一样地,AppClassLoader
是由ExtClassLoader
加载,AppClassLoader
的parent是ExtClassLoader
。
public class Test {
public static void main(String[] args) {
ClassLoader cl = Test.class.getClassLoader();
while (cl != null) {
System.out.println(cl);
cl = cl.getParent();
}
}
}复制代码
打印结果:
sun.misc.Launcher$AppClassLoader@232204a1
sun.misc.Launcher$ExtClassLoader@74a14482复制代码
同时咱们能够定义本身的类加载器CustomClassLoader
,那么它的parent确定就是AppClassLoader
了。类加载器的这种层次关系称为双亲委派模型。
双亲委派模型要求除了顶层的启动类加载器外,其他的类加载器都应当有本身的父类加载器。这里类加载器之间的父子关系不是以继承的关系来实现,而是都使用递归的方式来调用父加载器的代码。
双亲委派模型的工做过程是:若是一个类加载器收到了类加载的请求,它首先不会本身去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每个层次的类加载器都是如此,所以全部的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈本身没法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试本身去加载。
ClassLoader的源码:
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()
方法,依次向上递归。若父类加载器为空则说明递归到启动类加载器了。若是从父类加载器到启动类加载器的上层次的全部加载器都加载失败,则调用本身的findClass()
方法进行加载。
使用双亲委派模型能使Java类随着加载器一块儿具有一种优先级的层次关系,保证同一个类只加载一次,避免了重复加载,同时也能阻止有人恶意替换加载系统类。
通常地,在ClassLoader
方法的loadClass
方法中已经给开发者实现了双亲委派模型,在自定义类加载器的时候,只须要复写findClass
方法便可。
public class CustomClassLoader extends ClassLoader {
private String root;
public CustomClassLoader(String root) {
this.root = root;
}
@Override
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 name) {
String fileName = root + File.separatorChar
+ name.replace('.', File.separatorChar)
+ ".class";
try {
InputStream ins = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length;
while ((length = ins.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}复制代码
新建一个类com.xiao.U
,编译成class文件,放到桌面,来测试一下:
public class Test {
public static void main(String[] args) {
CustomClassLoader customClassLoader = new CustomClassLoader("C:\\Users\\PC\\Desktop");
try {
Class clazz = customClassLoader.loadClass("com.xiao.U");
Object o = clazz.newInstance();
System.out.println(o.getClass().getClassLoader());
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
}
}复制代码
打印结果:
CustomClassLoader@1540e19d复制代码
自定义类加载器在能够实现服务端的热部署,在移动端好比android也能够实现热更新。
参考: