一、Class文件结构
Class文件结构是异构语言与Java虚拟机之间的桥梁,目前Groovy、Scala、Jython等语言都可以在Java虚拟机上运行,其本质原因可就是这些语言由源码被编译成了Class文件。
Class文件结构会随着Java虚拟机的发展做出一些调整,但是它基本的结构和框架是很稳定的。
Class文件对应名称和描述:
Class文件格式 | |||
类型 | 名称 | 数量 | 描述 |
u4 | magic | 1 | 魔数 |
u2 | minor_version | 1 | 次版本号 |
u2 | major_version | 1 | 主版本号 |
u2 | constant_pool_count | 1 | 常量池容量 |
cp_info | constant_pool | costant_pool_count-1 | 常量池 |
u2 | access_flags | 1 | 访问标志 |
u2 | this_class | 1 | 当前类常量索引 |
u2 | super_class | 1 | 超类常量索引 |
u2 | interfaces_count | 1 | 接口数量 |
u2 | interfaces | interfaces_count | 接口常量索引 |
u2 | fields_count | 1 | 字段数量 |
field_info | fields | fields_count | 字段信息 |
u2 | methods_count | 1 | 方法数量 |
method_info | methods | methods_count | 方法信息 |
u2 | attributes_count | 1 | 属性数量 |
attribute_info | attributes | attributes_count | 属性信息 |
二、Class文件装载
JVM类加载机制分为3个部分:加载,连接,初始化。其中,连接又分为验证、准备、解析三个步骤。如下图:
Java虚拟机规定,一个类或者接口在初次使用之前,必须要进行初始化,也就是说Class只有在必须要使用的时候才会被装载。“使用”包括下面几种情况
1.当需要创建一个类的实例时,例如:使用new关键字、通过反射、克隆、反序列化;
2.当调用类的静态方法时;
3.当使用类或接口的静态字段时(final字段除外),例如:使用getstatic或putstatic指令;
4.当使用java.lang.reflect包中的反射类的方法时;
5.当初始化子类时,要求先初始化父类;
6.作为启动虚拟机,含有main方法的那个类。
上面6种情况属于主动使用,会引起类的初始化,其他情况不会引起类的初始化。
举一个非主动使用类的情况:
父类:
public class Parent { public static int v = 100; static { System.out.println("ParentInit"); }}
子类:
public class Child extends Parent { static { System.out.println("ChildInit"); }}
获取父类变量:
public class UseParent { public static void main(String[] args) { System.out.println(Child.v); }}
执行结果:
可以看出:虽然直接访问了子类对象,仅仅父类被初始化,并没有引起子类的初始化操作。所以,在引用一个字段时,只有直接定义该字段的类才会被初始化。
注:虽然子类没有被初始化,但是子类Child已经被系统所加载,只是还没有进入到初始化阶段。因此,并非在执行代码中出现的类,就一定会被初始化,如果不符合上述主动使用的条件,类就不会被初始化。
我们继续看下关于Final常量的示例:
常量类:
public class Field { public static final int v = 100; static { System.out.println("FieldInit"); }}
使用类:
public class UseFiled { public static void main(String[] args) { System.out.println(Field.v); }}
执行结果:
可以看出:Field类并没有因为其final字段的使用而被初始化,这也是为何上述6条中第三条中去掉final常量的原因。因为在生成Class文件时,因为final常量的不可变特性,在编译过程中,将final常量直接放到了常量池中,需要使用的时候直接从常量池中获取,因此,不会引起Field类的加载和初始化操作。
加载类
作为类装载的第一个阶段,在加载类时,Java虚拟机必须完成三个工作:
1.通过类的全名,获取类的二进制数据流;
2.将类的二进制数据流解析为方法区类的数据结构;
3.创建java.lang.Class类的实例,表示该类型。
验证类
当类加载到系统之后,就开始连接操作,连接的第一步是验证操作:目的是保证加载的字节码是合法、合理且符合规范的。如图所示Java虚拟机验证过程:
1.通过魔数检查是否是一个Calss文件,判断大小版本号是否在当前Java虚拟机的支持范围内,检查每一个数据项的长度是否正确;
2.是否所有类都有父类存在(Java中除了Object外都有父类),是否有定义为Final的方法和类被重写和继承,非抽象类是否实现类所有的抽象方法或者接口方法;
3.判断字节码是否可以正确地被执行,例如:判断字节码执行过程中,是否会执行一条并不存在的指令;
4.校验过成中会对符号引用进行验证,判断要验证的类和方法是否确实存在,并且当前类有权限访问这些数据。如果一个类无法在系统中找到,则会抛出NoClassDefFoundError,如果一个方法无法被找到,则会抛出NoSuchMethdError。
准备
当一个需要装载的类通过验证后,虚拟机就会进入准备阶段,开始为这个类分配相应的内存空间,并设置初始值。如果类存在常量字段,那么常量字段也会在准备阶段被赋上其指定的数值(变量的初始化)。此阶段并没有任何代码在执行。Java各类型的变量默认初始值如下:
类型 | 默认初始化值 |
---|---|
boolean | false |
int | 0 |
short | 0 |
float | 0.0 |
double | 0.0 |
char | \u0000 |
long | 0 |
byte | 0 |
对象 | null |
解析类
解析阶段的工作就是将类、接口、方法的符号引用转为直接引用;
符号引用:符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,符号引用和虚拟机的布局无关。我的理解是:在编译的时候一个每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址,多以就用符号引用来代替,而在这个解析阶段就是为了把这个符号引用转化成为真正的地址的阶段。
直接引用:直接引用和虚拟机的布局是相关的,不同的虚拟机对于相同的符号引用所翻译出来的直接引用一般是不同的。如果有了直接引用,那么直接引用的目标一定被加载到了内存中。直接引用可以是:
1.直接指向目标的指针。(可以理解为:指向对象,类变量和类方法的指针)
2.相对偏移量。(指向实例的变量,方法的指针)
3.一个间接定位到对象的句柄。
初始化
完成类的加载和连接之后,开始类装载的最后一个阶段--初始化阶段。此时,类才会开始执行Java字节码。初始化阶段最重要的工作是执行类的初始化方法。方法是由编译器自动生成的(Java编译器不会为所有的类生成方法,如果一个类既没有赋值语句,也没有static语句块,那么生成的为空,编译器不会为此类插入函数)。例如:如下Filed类中的变量V1和V2都是final常量,会在准备阶段初始化,而不是在初始化阶段处理,所以生成的为null,生成的class中,不存在函数。
类的初始化什么时候会被触发呢?JVM 规范枚举了下述多种触发情况:
1.当虚拟机启动时,初始化用户指定的主类;
2.用new去实例化时;
3.调用静态方法或静态字段的指令时,初始化该静态方法或静态字段所在的类;
4.子类的初始化会关联父类的初始化;
5.如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
6.通过反射获取某个类时,会对类进行初始化;
7.当初次调用 MethodHandle 实例时,初始化该MethodHandle指向的方法所在的类。
不会生成方法的类:
public class Field { public static final int v1 = 100; public static final int v2 = 200;}
注意:
1.对于调用初始化方法,虚拟机会确保其在多线程环境下的安全性。即:当多个线程同时尝试初始化一个类的时候,只有一个线程可以进入方法,其他线程必须等待,如果第一个进入的线程成功加载并初始化类类,那么队列中其他等待的线程就没有机会再执行方法,虚拟机会直接返回给需要执行的类已经初始化完成的数据信息。
2.由于方法通过锁机制保证线程安全,那么在多线程环境下,也可能存在死锁的问题。例如:
假设两个线程A和线程B同时访问一个类,线程A尝试初始化StaticA,线程B尝试初始化StaticB,但是StaticA初始化过程中,会区尝试初始化StaticB,StaticB在初始化过程中也会尝试去初始化StaticA,而线程A和线程B都调用了类中编译器生成的方法,因此会产生了死锁的问题。
ClassLoader类加载器
ClassLoader,顾名思义,就是用来加载Class类文件的核心组件,其目的是将Class的Class 的字节码数据读入系统,转换成内存形式的Class对象,然后交给虚拟机进行连接、初始化操作。因此ClassLoader对应的阶段是类装载中的加载阶段(无法干预类装载过程中的连接和初始化操作)。
每个 Class 对象里面都有一个 classLoader 属性,用来标记当前Class类对象到底由哪个加载器来加载。所以判断两个类是否是同一个类的前提是判断这两个类是不是由同一个类加载器加载。
从代码上看,ClassLoader是一个抽象类,提供了一些重要的方法:
1.通过类名加载一个类,返回结果是加载目标类的实例,如果找不到类,则返回ClassNotFoundException异常。
public Class> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false);}
2.将指定的字节码流b定义一个类,转换成JVM内部的java.lang.Class对象。off, len分别表示byte[]数组的位置和长度,字节数组b可以从本地文件系统、远程网络获取,参数name为字节数组对应的全限定类名。
protected final Class> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{ return defineClass(name, b, off, len, null);}
注意:该方法是一个protected方法,只有在自定义的ClassLoader子类中使用。
3.同样是一个protected方法,在第一条loadClass()时调用,用于自定义查找类的逻辑。如果不需要重写类的加载默认机制,只是想改变类加载的形式,可以重载此方法。
protected Class> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name);}
4.这是一个无法被修改的final方法,用于查找已经被加载的类,如果已载入,那么返回java.lang.Class对象;否则返回null。如果强行装载某个已存在的类,那么则抛出连接错误。
protected final Class> findLoadedClass(String name) { if (!checkName(name)) return null; return findLoadedClass0(name);}
5.获取类装载器的父装载器。除根装载器外,所有的类装载器都有且仅有一个父装载器。ExtensionClassLoader的父装载器是根装载器,因为根装载器非java语言编写(C语言编写),所以无法获取,将返回null。
public final ClassLoader getParent() { if (parent == null) return null; SecurityManager sm = System.getSecurityManager(); if (sm != null) { // Check access to the parent class loader // If the caller's class loader is same as this class loader, // permission check is performed. checkClassLoaderPermission(parent, Reflection.getCallerClass()); } return parent;}
三、ClassLoader的分类
Java提供了三个重要的 ClassLoader,分别是 BootstrapClassLoader(启动类加载器)、ExtensionClassLoader (扩展类加载器)和 AppClassLoader(应用类加载器,也叫系统类加载器)。除此之外,系统可以根据需要自定义ClassLoader。其中:
1.BootstrapClassLoader:是虚拟机的核心组件,负责加载 JVM 运行时核心类,这些类位于 $JAVA_HOME/lib/rt.jar 文件中,我们常用内置库 java.xxx.* 都在里面,比如 java.util.、java.io.、java.nio.、java.lang. 等等。这个 ClassLoader 比较特殊,它是由 C 代码实现的,在Java中没有对象与之对应,我们将它称之为“根加载器”。因此,任何在启动类中加载的类是无法获取到其ClassLoader实例的,例如:String.class.getClassloader(),String属于java.lang.String,因此会返回null;
2.ExtensionClassLoader:负责加载 JVM 扩展类,比如 swing 系列、内置的 js 引擎、xml 解析器 等等,这些库名通常以 javax 开头,它们的 jar 包位于 $JAVA_HOME/lib/ext/*.jar 中,有很多 jar 包;
3.AppClassLoader :直接面向我们自己工程的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。我们自己工程中编写的代码以及使用的第三方 jar 包都是由它来加载的;
那些位于网络上静态文件服务器提供的 jar 包和 class文件,jdk 内置了一个 URLClassLoader,用户只需要传递规范的网络路径给构造器,就可以使用 URLClassLoader 来加载远程类库了。URLClassLoader 不但可以加载远程类库,还可以加载本地路径的类库,取决于构造器中不同的地址形式。ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子类,它们都是从本地文件系统里加载类库。
四、双亲委派模式
在JVM提供的内置ClassLoader加载器中,我们可以把将启动类的加载器BootstrapClassLoader作为根加载器,BootstrapClassLoader是ExtensionClassLoader加载器的双亲,ExtensionClassLoader是AppClassLoader加载器的双亲。在每个ClassLoader对象中,。除根装载器外,所有的类装载器都有且仅有一个父加载器,其内部都会有一个 parent 属性指向它的父加载器:
class ClassLoader { ... private final ClassLoader parent; ...}
当系统需要使用一个类时,会自底向上检查类是否已经被加载;当系统需要加载一个类时,会自顶向下依次加载,直到成功,如图:
例如,如果你在自己的Project中,新建了一个java.lang.String的类,那么系统中用的是你自己定义的String 类,还是JDK原生Api提供的String类,了解类加载的双亲委派模式就很容易理解了,使用的是原生Api提供的String类。
实现自定义类加载器:
实现自定义类加载器的关键是需要重写findClass方法,下面来实现用自定义类加载器热加载一段JavaCode,即生成文件并读取目标代码,并将其编译、加载到内存中。
首先,定义自定义类加载器MyClassLoader,使MyClassLoader继承于ClassLoader,并声明参数为路径path的构造方法:
五、自定义类加载器
public class MyClassLoader extends ClassLoader { private String rootPath; public MyClassLoader(String rootPath) { this.rootPath = rootPath; }}
重写 findClass方法,先根据类名从内存中寻找此类是否被加载过,没有则自定义实现类的加载,异常情况会抛出“ClassNotFoundException”:
@Override protected Class> findClass(String name) throws ClassNotFoundException { Class> aClass = findLoadedClass(name); if (aClass == null) { //内存中未找到此类,说明还未加载 //加载此类 try { aClass = findMyClass(name); } catch (IOException e) { e.printStackTrace(); } } return aClass; }
根据类的路径获取需要加载类的字节数组,并转换成JVM内部的java.lang.Class对象,返回转换后的类Class>。为了增强代码可读性,此处使用了try-with-resource语法糖来关闭文件资源(MyFileInputStream实现了AutoCloseable接口)。
private Class> findMyClass(String name) throws IOException { String path = rootPath + File.separatorChar + name.replace('.', File.separatorChar) + ".class"; ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); try (MyFileInputStream myFileInputStream = new MyFileInputStream(path)) { byte[] buffer = new byte[1024]; for (int read = 0; ; ) { read = myFileInputStream.read(buffer); if (read != -1) { outputStream.write(buffer, 0, read); } else { break; } } } catch (IOException ex) { throw new RuntimeException(ex.getMessage(), ex); } byte[] bytes = outputStream.toByteArray(); //将输出的字节码流定义成类Class>,转换成JVM内部的java.lang.Class对象 Class> defineClass = this.defineClass(null, bytes, 0, bytes.length); return defineClass; }
到此,一个简单的自定义的类加载器就已经完成类,接下来就可以将一段代码加载到内存中运行了。
package com.chengchen.web.classloader;import com.alibaba.fastjson.JSON;import javax.tools.JavaCompiler;import javax.tools.JavaFileObject;import javax.tools.StandardJavaFileManager;import javax.tools.ToolProvider;import java.io.File;import java.io.FileWriter;import java.io.IOException;public class MyClassLoaderTest { public static final String javaCode = "package com.chengchen.web.classloader;\n" + "\n" + "public class Person {\n" + "\n" + " private String name = \"chengchen\";\n" + " public int age = 26;\n" + "\n" + " public Person() {\n" + " }\n" + "\n" + " public void setAge(int age) {\n" + " this.age = age;\n" + " }\n" + "\n" + " private void setName(String name) {\n" + " this.name = name;\n" + " }\n" + "\n" + "}\n"; public static void runJavaCode() throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException { //把Java String存储到文件 String fileName = "/Users/chengchen/Desktop/project/workSapce/MyPractice/src/main/java/com/chengchen/web/classloader/Person.java"; File file = new File(fileName); FileWriter fileWriter = null; try { fileWriter = new FileWriter(file); fileWriter.write(javaCode); fileWriter.flush(); } catch (Exception e) { } finally { fileWriter.close(); } //使用JavaCompiler接口编译java源程序 JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler(); StandardJavaFileManager fileManager = javaCompiler.getStandardFileManager(null, null, null); Iterable extends JavaFileObject> iterable = fileManager.getJavaFileObjects(fileName); //执行编译任务 //参数含义:out--用于输出错误的流,默认是System.err;fileManager--标准的文件管理;diagnosticListener--编译器的默认行为;options--编译器的选项;classes:参与编译的class。 JavaCompiler.CompilationTask task = javaCompiler.getTask(null, fileManager, null, null, null, iterable); //编译源代码 task.call(); fileManager.close(); //把编译后的class文件加载到内存 MyClassLoader loader = new MyClassLoader("/Users/chengchen/Desktop/project/workSapce/MyPractice/src/main/java/com/chengchen/web"); Class> c = loader.loadClass("classloader.Person" + ""); System.out.println(JSON.toJSONString(c.newInstance())); } //写一个Main方法来看下执行结果吧 public static void main(String[] args) throws IOException, IllegalAccessException, InstantiationException, ClassNotFoundException { MyClassLoaderTest.runJavaCode(); }}
执行的结果就是在“com.chengchen.web.classloader”目录下新建了一个Person的类,同时生成了一个编译后的“.class”文件;
Persion类文件:
package com.chengchen.web.classloader;public class Person { private String name = "chengchen05"; public int age = 26; public Person() { } public void setAge(int age) { this.age = age; } private void setName(String name) { this.name = name; }}
.class文件:
//// Source code recreated from a .class file by IntelliJ IDEA// (powered by Fernflower decompiler)//package com.chengchen.web.classloader;public class Person { private String name = "chengchen05"; public int age = 26; public Person() { } public void setAge(int var1) { this.age = var1; } private void setName(String var1) { this.name = var1; }}
注:本文系本人原创,转载请表明出处,部分图文来源于网络,如有侵权,可以留言联系本人删除。
注:由于作者水平有限,如有不对之处,希望各位指正,我们共同学习讨论。