目录
1. 类加载过程
1.1 Class类文件加载阶段
- Loading(加载)
- Linking(连接) , Linkg又分为Verfication(验证)、Preparation(准备)、Resolution(解析)
- Initializing(初始化)
1.1.1 Linking过程
-
Verfication
验证阶段大致上会完成4个阶段的校验动作:文件格式校验、元数据校验、字节码校验、符号引用校验
以其中的文件格式校验为例,会校验进来的class文件是否符合class文件的标准,如果不是以魔数0xCAFEBABE开头,则在这一步骤就被拒绝了 -
Prepration
准备阶段是为类变量分配内存并设置类变量初始值的阶段,变量所使用的内存都将在方法区中进行分配。进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中举例
1.static int i = 8,注意在这里是把i值赋值为0,而不是8, 把i赋值为8动作是在初始化完成的
2. static final int value = 123,编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123 -
Resolution
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
1.1.2 Initializing
类初始化阶段是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的Java程序代码。
静态变量在这时候才成为初始值,初始化阶段是执行类构造器<clinit>()
方法的过程
-
<clinit>()
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块
)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的 -
<clinit>()
方法与类的构造函数(或者说实例构造器<init>()
方法)不同,它不需要显示地调用父类构造器,虚拟机会保证在子类的<clinit>()
方法执行之前,父类的<clinit>()
方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()
方法的类肯定是java.lang.Object -
由于父类的
<clinit>()
方法先执行,所以父类中的定义的静态语句块优先于子类的变量赋值操作public 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); // 结果为2 } }
2. 类加载器
2.1 类加载器介绍
定义 : 通过一个类的全限定名来获取描述此类的二进制字节流这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块称为"类加载器"
- Bootstrap ClassLoader(启动类加载器):加载放在
<JAVA_HOME>\lib
,加载jdk最核心的内容比如rt.jar、charset.jar等核心类,当调用getClassLoader()得到加载器的结果是一个空值的时候说明已经达到了最顶层的加载器 - Extension ClassLoader(扩展类加载器):由
sun.misc.Launcher$ExtClassLoader
实现,它负责加载<JAVA_HOME>\lib\ext
目录中,加载扩展包的各种各样文件 - Application ClassLoader(应用程序类加载器):由
sun.misc.Launcher$AppClassLoader
实现。用于加载classpath指定的内容 - 自定义ClassLoader:加载自定义的加载器
案例演示:
public class ClassLoaderLevel {
public static void main(String[] args) {
// 查看是谁load到内存的,执行结果为null,Bootstrap使用c++实现的,Java里并没有class和他对应
System.out.println(String.class.getClassLoader()); // null
// 核心类库某个包里的类,执行结果为null,被Bootstrap类加载器加载
System.out.println(sun.awt.HKSCS.class.getClassLoader()); // null
// 位于ext目录下某个jar文件里面
System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader()); // ExtClassLoader
System.out.println(ClassLoaderLevel.class.getClassLoader()); // AppClassLoader
// ext的ClassLoader调用getClass()本身也是一个class,调用它的getClassLoader,结果是Bootstrap ClassLoader,所以显示结果为null
System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader().getClass().getClassLoader()); // null
// AppClassLoader调用getClass()本身也是一个class,调用它的getClassLoader,结果是Bootstrap ClassLoader,所以显示结果为null
System.out.println(ClassLoaderLevel.class.getClassLoader().getClass().getClassLoader()); // null
}
}
2.2 双亲委派机制
工作过程:
任何一个Class,首先尝试去自定义加载器查找,它内部维护着缓存,加载进来一次就不需要加载第二次了,如果没有加载进来,就加载进来;
如果在自己自定义的缓存没有找到的话,并不是直接加载这块内存,它会去找它的父亲application父类加载器,父类加载器会去它的缓存找有没有这个类,如果有则返回,如果没有则委托给Extension去加载,Extension只负责扩展jar包部分,找不到就委托给application加载,如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载
- 为什么会有双亲委派机制?
主要是为了安全,如果任何一个class都可以把它load进内存的话,那么我可以把java.lang.string交给自定义ClassLoader,把这个string类型load进内存,然后客户把密码存储成string类型对象,然后我就可以把密码发给我自己,此时就不安全了;加入双亲委派机制后,自定义ClassLoader加载java.lang.string会先去上面检查有没有加载过,如果有则直接返回,不让重新加载。
2.3 类加载器范围
(来自Launcher源码)
- sun.boot.class.path
- Bootstrap ClassLoader加载路径
- java.ext.dirs
- ExtensionClassLoader加载路径
- java.class.path
- Application ClassLoader加载路径
代码测试
public static void main(String[] args) {
String pathBoot = System.getProperty("sun.boot.class.path");
System.out.println(pathBoot.replaceAll(";", System.lineSeparator()));
System.out.println("------------------------------");
String pathExt = System.getProperty("java.ext.dirs");
System.out.println(pathExt.replaceAll(";", System.lineSeparator()));
System.out.println("------------------------------");
String pathApp = System.getProperty("java.class.path");
System.out.println(pathApp.replaceAll(";", System.lineSeparator()));
}

2.4 自定义类加载器
- 继承ClassLoader
- 重写模板方法findClass
- 调用definClass
ClassLoader源码解析
步骤: findInCache -> parent.loadclass -> findClass()
代码演示
public class MyClassLoader extends ClassLoader {
// 重写findClass方法
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
File f = new File("d:/test/", name.replaceAll("\\.", "/").concat(".class"));
try {
FileInputStream in = new FileInputStream(f);
ByteArrayOutputStream out = new ByteArrayOutputStream();
int i = 0;
while ((i = in.read()) != -1) {
out.write(i);
}
// 转换为二进制字节数组
byte[] byteArray = out.toByteArray();
out.close();
in.close();
return defineClass(name, byteArray, 0, byteArray.length);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return super.findClass(name); // throws ClassNotFoundException
}
public static void main(String[] args) throws Exception{
ClassLoader l = new MyClassLoader();
Class clazz = l.loadClass("cn.whc.jvm.Hello");
Object obj = clazz.newInstance();
Method method = clazz.getMethod("m");
method.invoke(obj);
System.out.println(l.getClass().getClassLoader());
System.out.println(l.getParent());
}
}
自定义类加载器又很多种方式最简单的一种从ClassLoader继承,只需要重写它的findClass
方法,findClass
方法创建了一个文件位于D盘的test目录下面,然后传递cn.whc.jvm.Hello
,把类名中的.
替换成/
,找到了它的全路径最后加上它的class文件名
如何将class文件load进内存呢?
将FileInputStream转换成InputStream
,ByteArrayOutputStream
转换成一个二进制字节数组,从文件里读出来写进字节数组里再toByteArray()
转换成一个二进制字节数组,再用defineClass
来变成一个class类对象
3. 懒初始化
lazyloading
- 严格来讲叫lazyInitializing
- new、getstatic、putstatic或invokestatic指令,访问final变量除外
- java.lang.reflect对类进行反射调用时
- 初始化子类的时候,父类首先初始化
- 虚拟机启动时,被执行的主类必须初始化
- 动态语言支持java.invoke.MethodHandle解析的结果为REF_getstatic、REF_putstatic、REF_invokestatic的方法句柄时,该类必须初始化
代码演示
public class LazyLoading { // 严格来讲应该叫lazy initialzing,因为java虚拟机规范并没有严格规范什么时候必须loading
public static void main(String[] args) throws ClassNotFoundException {
// 没有new没有访问不会被加载
// P p;
// new了会被加载
// X x = new X();
// 打印final值不需要加载整个类
// System.out.println(P.i);
// 非final会加载
// System.out.println(P.j);
// 会被加载
Class.forName("cn.whc.jvm.classloader.LazyLoading$P");
}
public static class P{
final static int i = 8;
static int j = 9;
static {
// 只要被加载过这个P一定是被打印出来的,因为一个classload内存之后他有这几个步骤
// 1.Loading 2.Linking 3.Initializing这个过程会执行静态语句块,所以被load进来这个语句块一定
// 被执行过,p一旦打印就证明这个class被load进来了
System.out.println("P");
}
}
public static class X extends P {
static {
System.out.println("X");
}
}
}
4. 混合模式
java是解释执行的,一个class文件load到内存之后通过java的解释器interpreter来执行。JIT的编译器指的是有某些代码需要把它编译成为本地代码,相当于exe。
默认情况下混合模式为: 混合使用解释器+热点代码编译
- 解释器
- bytecode interpreter
- JIT
- Just In-Time compiler
- 混合模式
- 混合使用解释器 + 热点代码编译
- 起始阶段采用解释执行
- 热点代码检测
- 多次被调用的方法(方法计数器:监测方法执行频率)
- 多次被调用的循环(循环计数器:检测循环执行频率)
- 进行编译
5. 总结
Loading
- classloading是使用双亲委派机制,从上到下再从下到上,向上找,向下委托。主要处于安全考虑
- Lazyloading五种情况
- ClassLoader源码: findCache -> parent.loadClass->findClass()
- 自定义加载器
extends ClassLoader
overwrite findClass() -> defindClass(byte[] -> Class clazz) - 混合执行 编译执行 解释执行