JVM篇之类加载机制
类加载过程
JVM类的加载过程分为五个部分:加载,验证,准备,解析,初始化,其中验证,准备,解析三个部分统称为连接。如下图所示
1.加载
- 通过一个类的全限定名来获取其定义的二进制字节流,将二进制数据读入内存,放入运行时数据区的方法区中。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在堆区创建一个代表这个类的java.lang.Class对象,用来封装类在方法区内的数据结构,作为对方法区中这些数据的访问入口。
我的理解:加载的最终结果是在堆中创建了一个Class对象,这个对象是独一无二的。
Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
具体关于Class对象的内容可以查看反射相关内容。
class Car{
}
public class Loading {
public static void main(String[] args) {
Car car1 = new Car();
Car car2 = new Car();
Car car3 = new Car();
Class<? extends Car> car1Class = car1.getClass();
Class<? extends Car> car2Class = car2.getClass();
Class<? extends Car> car3Class = car3.getClass();
System.out.println("car1:"+car1 +" car2:"+car2 +" car3:"+car3);//car1:Car@15aeb7ab car2:Car@7b23ec81 car3:Car@6acbcfc0
System.out.println("car1Class:"+car1Class +"; car2Class:"+car2Class +"; car3Class:"+car3Class);//car1Class:class Car; car2Class:class Car; car3Class:class Car
System.out.println(car1Class==car2Class);//true
System.out.println(car1Class.getClass());//class java.lang.Class
}
}
我的理解:java中最抽象的类是java.lang.Class,而Car类对象(car1Class)可以看作是java.lang.Class类的一个实例化对象
与此相对应的,我们可以把car1对象看作是Car类的一个实例化对象。堆的唯一目的就是存放对象实例。
加载.class文件的方式
- 将Java源文件动态编译为.class文件
- 从zip压缩包读取,成为日后jar ,war格式的基础
- 从本地系统中直接加载
- Class findSystemClass(String name): 方法从本地文件系统装入文件。它在本地文件系统中寻找类文件,如果存在,就使用 defineClass 将原始字节转换成 Class 对象,以将该文件转换成类。
- 通过网络下载.class文件
- 从专有数据库中提取.class文件
2.验证
确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。大概分为4个阶段的检验动作:
-
文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以 0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
-
元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了 java.lang.Object之外。
-
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
-
符号引用验证:确保解析动作能正确执行。
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用 -Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
3.准备
准备阶段是正式为类的静态变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。
注意:
- 只会为被static关键字修饰的静态成员变量设置初始值
- 可以来思考一下为什么:静态成员变量可以通过类名直接调用,因此应该先于类的实例化设置初始值。
- 这里所设置的初始值如果不被final修饰,则是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
- 对final的静态字面值常量直接赋初值,即字面值
例如:
public static int a=100;//准备阶段设为0
public static final int b=100;//准备阶段设为100
4.解析
把类中的符号引用转换为直接引用
在编译时,java类并不知道引用类的实际地址,所以用符号引用来代替。在该阶段转换为实际内存地址。
5.初始化
初始化主要完成静态块执行以及静态变量的赋值.
先初始化父类,再初始化当前类.
只有对类主动使用时才会初始化.
触发初始化的条件有:
- 创建类的实例时
- 访问类的静态方法或静态变量的时候
- 使用Class.forName反射类的时候
- 或者某个子类初始化的时候
JVM初始化步骤
- 假如这个类还没有被加载和连接,则程序先加载并连接该类
- 假如该类的直接父类还没有被初始化,则先初始化其直接父类
- 假如类中有初始化语句,则系统依次执行这些初始化语句
类加载器
虚拟机设计团队把加载动作放到 JVM 外部实现,以便让应用程序决定如何获取所需的类,JVM 提
供了 3 种类加载器:启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、应用程序类加载器(Application ClassLoader)。如下图所示:
先看一个例子
class Car{
}
public class Loading {
public static void main(String[] args) {
Car car = new Car();
ClassLoader classLoader1 = car.getClass().getClassLoader();
System.out.println(classLoader1);
System.out.println(classLoader1.getParent());
System.out.println(classLoader1.getParent().getParent());
}
}
运行结果
jdk.internal.loader.ClassLoaders$AppClassLoader@63947c6b
jdk.internal.loader.ClassLoaders$PlatformClassLoader@4eec7777
null
java9开始,将Bootstrap ClassLoader—>Platform ClassLoader
PlatformClassLoader的父类是null的原因:BootstrapClassLoader是用c语言实现的。
应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:
- 在执行非置信代码之前,自动验证数字签名。
- 动态地创建符合用户特定需要的定制化构建类。
- 从特定的场所取得java class,例如数据库中和网络中。
类的加载
类加载有三种方式:
- 命令行启动应用时候由JVM初始化加载
- 通过Class.forName()方法动态加载
- 通过ClassLoader.loadClass()方法动态加载
Class.forName()和ClassLoader.loadClass()区别
- Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
- ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
- Class.forName(name,initialize,loader)带参函数也可控制是否加载static块。
双亲委派模型
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
优点:
- 避免类的重复加载
- 避免Java的核心API被篡改