一、类加载流程
类加载主要有五个步骤:加载、验证、准备、解析、初始化;其中验证机械、准备阶段相当于C++程序中的连接阶段。
1.1、加载
- 通过类的全限定名来获取此类的二进制字节流(由类加载器完成)
- 将这个字节流所代表的的静态存储结构转化为方法区的运行时结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口(在HotSpot虚拟机中这个Class对象是保存在方法区中的)。
1.2、验证
1.3、准备
- 如果加载的类中含有静态变量,那么这个时候JVM会给静态变量在方法中分配内存,并初始化为(0,0L,null,false)这些值,而不是我们在Java代码中声明的值(赋值成我们在代码中声明的值是类初始化阶段做的事);
- 如果加载的类定义了常量(final static 修饰的变量),那么此时就会出现在方法区的常量池中分配内存,并初始化为我们在Java代码中声明的值(与类变量不同)
1.4、解析
1.5、初始化
这个具体的过程可以看下面的类初始化
二、类初始化
2.1、执行clinit方法
一个类初始化的过程就是执行类构造器的过程;类构造器和实力构造方法是不同的,实例构造器是我们在类中声明的和类名相同且无返回值的方法,二类构造方法是类初始化阶段由JVM生成的。
- clinit()组成:当一个类需要被初始化时,JVM会自动收集一个类的静态变量和静态代码块构成该类的clinit()方法,类构造方法主要是执行类变量第二次赋值(由Java代码声明的值),执行静态代码块;对于静态代码块和静态变量之间的运行次序完全取决于代码的先后顺序,其中需要注意的是在任何时候我们都不能超前引用变量(一个变量在)。参考2.2
- 执行顺序:当我们初始化一个类时,JVM会自动去初始化父类的类构造方法clinit(),然后依次递归直到Object的类构造方法执行完毕以后,最后才执行子类的类构造方法。因此父类的静态变量和静态代码块先于子类的静态变量和静态代码块执行。参考2.2
- 线程安全:对于类构造方法clinit()的执行,当有多个线程想要初始化一个对象时,JVM保证只有一个线程执行clinit()方法,如果当前线程初始完成以后,后面的线程将不再执行此clinit方法。(其实这也是为什么单例模式的静态内部类能够采用这种方法实现的原理;代码可以参考单例模式https://blog.csdn.net/makeliwei1/article/details/81254538)也可以参考2.4
- 是否必须:对于一个类如果没有静态代码块和静态方法,那么该类就不会执行clinit()方法,但是如果父类有静态变量和静态方法,那么仍然需要先执行父类的类构造方法clinit();参考2.3
- 接口中的clinit()方法:对于接口我们不能使用静态代码块,但是我们可以使用静态变量,因此接口中也有clinit()方法;需要注意的一点是,和一般的父类不同,接口作为父类只有当其静态变量使用的时候才会执行clinit()方法。
2.2、父类和子类有静态代码块和静态方法
package JVM;
public class ClinitTest {
public static void main(String[] args){
Son son = new Son();
}
}
class Father{
static int i = 1;
static{
System.out.println("Father的<clinit>()方法执行了"+ " i = "+i);
}
public Father(){
System.out.println("Father的<init>()方法执行了");
}
}
class Son extends Father{
static int j = 1;
static{
System.out.println("Son 的<clinit>()方法执行了" + " j = "+j);
}
public Son(){
System.out.println("Son的<init>()方法执行了");
}
}
输出结果
Father的<clinit>()方法执行了 i = 1
Son 的<clinit>()方法执行了 j = 1
Father的<init>()方法执行了
Son的<init>()方法执行了
此输出结果论证了上面提到的第二点
2.3、父类有静态带买块而子类没有静态代码
package JVM;
public class ClinitTest {
public static void main(String[] args){
Son son = new Son();
}
}
class Father{
static int i = 1;
static{
System.out.println("Father的<clinit>()方法执行了"+ " i = "+i);
}
public Father(){
System.out.println("Father的<init>()方法执行了");
}
}
class Son extends Father{
public Son(){
System.out.println("Son的<init>()方法执行了");
}
}
输出结果
Father的<clinit>()方法执行了 i = 1
Father的<init>()方法执行了
Son的<init>()方法执行了
此输出结果论证了第四点
2.4、一个类的clinit方法只执行一次
package JVM;
public class ClinitTest {
public static void main(String[] args){
Father father1 = new Father();
Father father2 = new Father();
}
}
class Father{
static int i = 1;
static{
System.out.println("Father的<clinit>()方法执行了"+ " i = "+i);
}
public Father(){
System.out.println("Father的<init>()方法执行了");
}
}
输出结果
Father的<clinit>()方法执行了 i = 1
Father的<init>()方法执行了
Father的<init>()方法执行了
此结论证明了第三点
三、 类初始化的五种情况
JVM规范中严格规定有且只有以下五种情况下会初始化一个类,也就是上面提到的执行类的类构造方法(下面初始化一个类都是在类没有被初始化的情况下)
- 如果执行机器码指令new、getstatic、putstatic、invokestatic这四条字节码指令的时候,如果类没有被加载,此时就会按照clinit执行的规则去执行类构造器方法<”clinit”>(),new指定对应Java中的new一个对象,getstatic对应读取一个静态字段,putstatic对应设置一个静态字段;invokestatic表示调用类的静态方法。
- 如果使用java.lang.reflect中的Class中的方法进行反射调用某一个类的时候就会初始化一个类;
- 当一个类还没有初始化时,那么首先需要将父类进行初始化
- 当虚拟机启动时,用户需要制定一个要执行的主类,虚拟机会首先初始化这个主类
- 当使用JDK1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例解析后的结果是REF_getStatic、REF_普通Static、REF_invokeStatic方法的句柄。
3.1、当我们执行new、getStatic、putStatic、invokestatic机器码指令
package JVM;
import org.junit.Test;
public class ClinitTest {
@Test
public void testGetStatic(){
int age = Father.i;
}
@Test
public void testNew(){
new Father();
}
@Test
public void testInvokeStatic(){
Father.sayHello();
}
}
class Father{
static int i = 1;
static{
System.out.println("Father的<clinit>()方法执行了"+ " i = "+i);
}
public Father(){
System.out.println("Father的<init>()方法执行了");
}
public static void sayHello(){
System.out.println("Hello World");
}
}
输出结果
//执行getstatic机器码指令只执行了类的类构造方法<clinit>()方法
Father的<clinit>()方法执行了 i = 1
//执行new机器码指令首先执行了额类的类构造方法<clinit>()方法、和实力构造方法<init>()
Father的<clinit>()方法执行了 i = 1
Father的<init>()方法执行了
//执行invokestatic机器码指令执行了类的类构造方法<clinit>()方法和静态方法
Father的<clinit>()方法执行了 i = 1
Hello World
3.2、通过反射获取一个类会初始化一个类
Father这个类仍然使用3.1中的类,然后我们来看一下使用Class.forName()方法来反射一个类,这个类是否会执行类的类构造方法clinit();
@Test
public void testClass(){
try {
Class clazz = Class.forName("JVM.Father");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
输出结果
Father的<clinit>()方法执行了 i = 1
这里可以参考2.1中的代码实现,从那里我们可以发现父类的类构造方法确实先于子类的构造方法执行
3.3、父类的类构造方法先于子类的构造方法执行
public class ClinitTest {
static{
System.out.println("执行了主类的<clinit>()方法");
}
public static void main(String[] args){
System.out.println("执行了 main(String[] args) ");
}
}
输出结果
执行了主类的<clinit>()方法
执行了 main(String[] args)
3.4、虚拟机启动时会先去执行主类的类构造方法
四、双亲委派模型
4.1、类加载器
类加载器主要是执行类加载阶段中通过一个类的二进制全限定名来获取此类的二进制字节流这一过程。
- 启动类加载器(BootStrap ClassLoader):
- 扩展类加载器(Extension ClassLoader):
- 应用程序类加载器(Application ClassLoader):
4.2、双亲委派模型下的类加载
类加载流程:
- 如果一个类加载器收到了一个加载类的通知,首先会去检查这个类是否已经加载,如果已经加载则直接返回。如果没有,
那么将加载的任务委派给父类加载器Application ClassLoder加载, - Application ClassLoader会将任务委派给Extension ClassLoader加载,而Extension ClassLoader也不会自己加载,会将任务委派给BootStrap ClassLoader,如果BootStrap ClassLoader没有加载成功,那么就会把加载的任务返回给ExtensionClassLoader;
- 如果Extension ClassLoader加载成功,那么直接返回,如果加载失败,那么返回给Application ClassLoader;
- 如果ApplicationClassLoader加载成功,那么最后的加载将会交给自定义的类加载器来加载
类加载的主要几个方法:
loadClass()
类加载流程主要由ClassLoader这个类中的loadClass方法实现,我们看一下loadClass()是如何实现的
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//1、首先检查此类是否已经加载过了,如果是则直接返回,如果不是则转到2、
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//2、检查是否是BootStrap ClassLoader,如果不是一直进行递归直到找到BootStrapClassLoader
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//3、如果是BootStrap ClassLoader,那么由BootStrap ClassLoader进行加载,加载成功则返回Class对象,不成功则抛出异常
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.
// 如果父类加载失败了,那么就会调用findClass来加载类
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;
}
}
findClass通过指定的二进制名查找类,此方法应该被类加载器重写,通过实现此方法从而使我们的自定义类加载器也符合双亲委派模型
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
defineClass()
将一个byte数组转换为Class实例,必须分析Class,然后才能使用
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
4.3、双亲委派模型的优点
Java类随着类加载器也有了一种带有优先级层次的关系
五、自定义类加载器
我们从loadClass方法可以看到,如果我们需要自定义一个类加载器,我们只要通过继承ClassLoader抽象类然后覆盖findClass这个方法就可以。因为从ClassLoader的loadClass方法流程可以看到,如果父类加载器不成功,那么最后会调用自己的findClass方法来加载类。实现一个类加载器主要有以下几个步骤
- 继承java.lang.ClassLoader抽象类
- 覆写loadClass方法
- 将二进制流通过defineClass方法解析得到一个Class对象,最后返回。
package JVM;
import java.lang.ClassLoader;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
public class MyClassLoader extends ClassLoader{
private String minFilePath;
public MyClassLoader(){}
public MyClassLoader(String filePath){
this.minFilePath = filePath;
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = getFileName(name);
File file = new File(minFilePath,fileName);
try {
byte[] bytes = getClassBytes(file);
Class<?> clazz = defineClass(name,bytes,0,bytes.length);
return clazz;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return super.findClass(name);
}
private String getFileName(String name){
int index = name.lastIndexOf(".");
if(index == -1){
return name+".class";
}else{
return name.substring(index+1)+".class";
}
}
private byte[] getClassBytes(File file) throws IOException {
FileInputStream fis = new FileInputStream(file);
FileChannel fc = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();//最终作为输出的类
WritableByteChannel wbc = Channels.newChannel(baos);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);//缓存队列
while(true){
int i = fc.read(byteBuffer);
if(i == 0 || i == -1){
break;
}
byteBuffer.flip();
wbc.write(byteBuffer);//将读取到的byte读取到字符中
byteBuffer.clear();
fis.close();
}
return baos.toByteArray();
}
}
测试代码
package JVM;
/**
* Created by luckyboy on 2018/8/11.
*/
public class ClassLoaderTest {
public static void main(String[] args){
HelloWorld helloWorld1 = new HelloWorld();
Class clazz = null;
try {
MyClassLoader myClassLoader= new MyClassLoader("D:/Java_Program/JavaBasic/out/production/JavaBasic/JVM");
clazz = myClassLoader.loadClass("JVM.HelloWorld");
Object object= clazz.newInstance();
if(helloWorld1 instanceof JVM.HelloWorld){
System.out.println("由两个类加载器的加载,类是--不同--的类型");
}else{
System.out.println("由两个类加载器的加载,类是--相同--的类型");
}
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出结果
由两个类加载器的加载,类是--不同--的类型
六、参考文章
《深入理解Java虚拟机》
https://blog.csdn.net/xyang81/article/details/7292380
https://blog.csdn.net/briblue/article/details/54973413
https://www.cnblogs.com/szlbm/p/5504631.html