Class装载系统
系统装载Class类型
分为以下几个步骤:
- 加载
- 验证
- 准备
- 解析
- 初始化
类装载
一个类或者接口在初次使用(主动使用)前,必须进行初始化,主动使用仅限下列几种情况:
- 创建一个类的实例时,如
new 反射 克隆 反序列化
- 调用类的静态方法时
- 在使用类或者接口的静态字段时(final常量除外)
- 使用
java.lang.reflect
包中的方法反射类的方法时 - 当初始化子类时,要求先初始化父类
- 作为启动虚拟机含有
main
方法的类
除了以上情况其他均属于被动使用,被动使用不会引起类的初始化,通过-XX:+TraceClassLoading
参数跟踪类加载过程
- 不会引起类初始化的例子
public class NoClassLoad {
static {
System.out.println("super class init");
} // this will be first init
public static String v = "200";
}
public class SubClassLoad extends NoClassLoad{
static {
System.out.println("sub class init");
}
}
public class UseClassLoad {
public static void main(String[] args) {
System.out.println(SubClassLoad.v);
}
}
//print
super class init
200
此例中虽然通过SubClassLoad.v
调用但只加载了SubClassLoad
并没有初始化此类
//...
[0.515s][info ][class,load] com.alleyz.jdk8.NoClassLoad source: file:/G:/workspace/idea-dev/datastructures/target/classes/
[0.515s][info ][class,load] com.alleyz.jdk8.SubClassLoad source: file:/G:/workspace/idea-dev/datastructures/target/classes/
super class init
200
[0.569s][info ][class,load] java.net.DualStackPlainSocketImpl source: jrt:/java.base
- 不会引起类加载的例子
public class FinalConst {
public final static int v = 100;
static {
System.out.println("final const init");
}
}
public class UseClassLoad {
public static void main(String[] args) {
// System.out.println(SubClassLoad.v);
System.out.println(FinalConst.v);
}
}
// print
100
此例中虽然显式调用了FinalConst.v
但是并没有加载与初始化类FinalConst
。
虚拟机编译后,将final常量直接存放到常量池中,并植入目标类。
//...
[0.365s][info ][class,load] java.net.SocksSocketImpl source: jrt:/java.base
100
[0.366s][info ][class,load] java.net.StandardSocketOptions source: jrt:/java.base
//...
加载类
加载类处于类装载的第一个阶段,加载类时虚拟机会完成以下工作:
- 通过类的全名获取类的二进制流
- 将二进制流解析为方法区内的数据结构
- 创建java.lang.Class
类的实例,表示该类型
完成类加载,获取类信息的组件是
ClassLoader
验证类
当类加载到系统中,就开始连接操作的第一步:验证类,它的目的是保证加载的字节码是符合规范的
- 格式检查,主要检查二进制数据是否符合格式要求和规范
- 语义检查,是否所有的类都存在,是否存在语法错误等
- 字节码检查,主要检查字节码是否可被正确的执行,尽可能的检查出预知的明显的问题
- 符号引用验证,主要检查类常量池中引用的其他类、方法是否存在,是否有权限访问
准备
准备阶段虚拟机为类分配默认的内存空间,并设置初始值,如果存在常量字段,那么也会被赋上正确的值
解析类
解析类阶段的工作就是将类、接口、字段、方法的符号引用转为直接引用,就是通过字面量引用转为目标方法在类中方法表中的位置使得方法被成功调用
初始化
此阶段执行类的字节码,主要执行类的方法,它由类静态成员的赋值语句以及static语句块合并而成。多线程场景下,多个线程进行类初始化,可能导致死锁,如下部分代码:
Class A {
static {
sleep(2000);
Class.forName("B");
}
}
Class B {
static {
sleep(2000);
Class.forName("A");
}
}
Thread a = () -> Class.forName("A");
Thread b = () -> Class.forName("b");
a.start(); b.start();
ClassLoader
ClassLoader是Java的核心组件,主要从外部系统获取Class的二进制数据流,只影响类加载阶段。
在类加载过程中,采用双亲委托模式进行加载。
ClassLoader的分类
ClassLoader有4种:
Bootstrap ClassLoader
:启动类加载器,完全由C语言编写,且Java中没有与之对应的对象,系统核心类是由其加载的,如rt.jar
,通过-xbootclasspath:/home/boot/lib
。可以修改其扫描的目录。Extension ClassLoader
:扩展类加载器,加载$JAVA_HOME/ext下的类App ClassLoader
:应用类加载器,加载当前应用的类- 自定义ClassLoader:自定义类加载器
类加载器的工作流程:
- 自底向上的检查类是否加载
- 自上向下的尝试加载类
流程说明:检查类是否加载 加载一个类时,先由自定义类加载器开始进行判断,是否加载,如果没有加载则继续询问其双亲AppClassLoader,如果AppClassLoader也么加载,则询问AppClassLoader的双亲ExtensionClassLoader是否加载,如果ExtensionClassLoader未加载,则询问其双亲BootstrapCLassLoader是否加载,如果BootstrapClassLoader也未加载,则进行类加载。 类加载流程 首先Boostrap ClassLoader尝试加载目标类,如果加载失败,则交由ExtensionClassLoader加载,如果ExtensionClassLoader加载失败则交由APPClassLoader加载,如果APPClassLoader加载失败则交由自定义ClassLoader加载,如果也加载失败则抛出
ClassNotFoundException
.
应用:通过ClassLoader进行类的热替换
自定义类加载器:
public class HotClassLoader extends ClassLoader{
private static final String OS_WINDOWS = "Windows";
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
Class<?> clazz = this.findLoadedClass(className);
if(clazz == null){
byte[] bytes = readClass2Bytes(className);
if(bytes == null){
throw new ClassNotFoundException();
}
clazz = defineClass(className, bytes, 0, bytes.length);
}
return clazz;
}
private byte[] readClass2Bytes(String className){
String classPath = getClassPath(className);
try(FileInputStream fis = new FileInputStream(classPath)){
FileChannel channel = fis.getChannel();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
WritableByteChannel wbc = Channels.newChannel(bos);
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
int i = channel.read(buffer);
if(i == 0 || i == -1) {
break;
}
buffer.flip();
wbc.write(buffer);
buffer.clear();
}
return bos.toByteArray();
}catch (IOException e){
e.printStackTrace();
}
return null;
}
private String getClassPath(String className) {
String classPath = className.replaceAll("\\.", "/");
URL url = this.getClass().getResource("/");
classPath = url.getPath() + classPath + ".class";
String osx = System.getProperties().getProperty("os.name");
if(osx != null && osx.contains(OS_WINDOWS)) {
classPath = classPath.substring(1);
}
return classPath;
}
}
测试类:
public class ChangeClassDemo {
public void print(){
System.out.println("AbCdEfGhIjKlM==--6666-==");
}
}
测试,每次加载一次类:
public static void main(String[] args) {
HotClassLoader classLoader = new HotClassLoader();
while (true){
try {
Class clazz = classLoader.findClass("com.alleyz.clsload.ChangeClassDemo");
Object obj = clazz.newInstance();
Method method = clazz.getMethod("print");
method.invoke(obj);
Thread.sleep(10000);
}catch (NoSuchMethodException | ClassNotFoundException | IllegalAccessException |
InstantiationException | InvocationTargetException | InterruptedException e){
e.printStackTrace();
}
}
}
结果:
首先打印
AbCdEfGhIjKlM==--6666-==
将ChangeClassDemo 的print方法改为:
System.out.println("AbCdEfGhIjKlM==--==");
然后输出:
AbCdEfGhIjKlM==--==
参考资料《实战Java虚拟机–JVM故障诊断与性能调优》