写了不少java代码,以前一直都是关心java程序跑起来后的情况步骤,今天我们就来探究一下:
在java程序还是一个个存在于静态.class文件中的类(也可以是任何被JVM认可的文件,但本质还是要的到类)的时候,JVM是在怎么将它们变成一个个存在于内存的对象的。
为此,JVM专门有一个类加载机制,用于处理.class文件加载到内存这个过程。
JVM类加载机制分为5个步骤:
1.加载
2.验证
3.准备
4.解析
5.初始化
分别简单介绍一下他们的概念,
1.加载
通常使用显式的加载Class.forName()
隐性加载:new对象时加载{在jvm开启后通过文件加载}
就是JVM通过一个文件得到一个类并在内存中进行生成java.lang.Class对象,一定在堆上。
既是通过文件加载到内存
注意,对于文件我们有一定的要求:
首先就是要满足JVM指定的规范,既是.class文件遵循的规范,
文件不一定是.class文件,JVM还能在jar包,war包一类的文件中获取到加载类需要的class信息,还有jsp文件也能通过动态代理产生一个可识别的class。
我们需要知道类加载器有三种:
启动类加载器(jdk的bin目录)
扩展类加载器(jdk的lib\ext目录)
应用程序类加载器(自定义的加载器,继承java.lang.ClassLoader)
它们的关系是双亲委派,什么是双亲委派,就是当自己需要完成一个任务时不先自己考虑能否处理,先向上一代抛出任务,直到顶层(既是启动类加载器),然后再判断每个层能否处理这个任务**(在自己路径下能否找到类)**,不能则向下抛,直到能够处理为止
我们举个栗子,java.lang.Object对象位于rt.jar,是jdk\bin目录下的,该启动类加载器管理,我们都知道不管哪个对象都可以认为是Object对象。
那是因为不管哪一级的加载器想要加载Object对象
在双亲委派模式下都会被委托给启动类加载器,始终能得到同一个Object对象。
好了,现在我们在不重写的情况下实现一个自己的类加载器(此处被封装为一个方法,功能类似于Class.forName())
首先准备一个.class文件,源码功能如下:
package com.j.test;
/**
* @Description 作为类加载的实验对象
* @Date 2020-06-10 -- 20:27
* @Author joker
* @Version 1.0
*/
public class PrintJVMLoad {
public static void main(String[] args) {
}
public void loadTest() {
System.out.println("我是被用作JVM类加载机制研究的类--类加载器");
}
}
然后封装一个方法(加载到类并返回):
/**
* @Description: 手动加载一个.class文件(以test.PrintJVMLoad为例,其他的不做实验)
* ;类加载器
* @Param:
* @Return: 加载到的类
* @Date: 2020/6/11 0011
*/
public static Object loadClassByFile(String Filename){
//class文件位置
String constUrl="\\target\\classes\\com\\j\\test";
Object object=null;
try {
//URL
URL url = new URL("file:\\"+System.getProperty("user.dir")+constUrl);
//得到加载器
URLClassLoader loader=new URLClassLoader(new URL[]{url});
//得到对应class对象
Class c = loader.loadClass(Filename);
//得到对象
object = c.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return object;
}
然后检验是否加载成功:
//测试类加载,一定是完整的java-package路径
Object o=JVMClassLoad.loadClassByFile("com.j.test.PrintJVMLoad");
if (o!=null){
((PrintJVMLoad)o).loadTest();
}else {
System.out.println("类加载失败");
}
2.验证
验证,准备,解析三步合称链接。
验证是为了是的.class文件是被JVM认可的,不危害JVM的。
3.准备
通常是为了将定义的类成员变量(静态的,在方法区)进行一个赋值或者初始化。
例如:
public static int a=80
在准备阶段,其仅仅会将a设为0,而不是80,80这个赋值操作将在初始化时放在类构造器()执行
public static final int a=80
此时会直接将a赋值为80,
因为此时在编译时变量a会产生一个ConstantValue属性,是直接赋值的依据。
4.解析
是JVM通过常量池将符号引用转为直接引用的过程
符号引用不一定存在于内存中(符号引用是一种.class文件规范规定),但直接引用是一个指针,一定指向了一个具体地址,一定在内存中。
这么理解就容易了
就是将.class文件中的规范翻译成JVM认得的类构造规范(常量池的那些常量)
既是—翻译
5.初始化
此时以执行类构造器方法()为主。
此时在类构造器方法中会执行类似对类成员赋值(准备阶段的传递的任务),静态的代码。
类构造器遵循继承原则,在父类完成执行后,才执行子类。
一般无静态模块(任何静态的变量,代码块)的方法可不执行类构造器方法。
显式的调用也不会。
//////////////////////////////////////////////////////////////
JVM中的类加载机制大抵是这样了。
感谢耐心的阅读,希望能帮到你。
//////////////////////////////////////////////////////////////