JVM--类加载机制
对类加载机制的学习(2023.3.10改)
对类加载模块的学习,要理解以下几个部分:class文件结构,类加载生命周期,类加载机制层次
1、字节码文件结构
对于java语言(或者其他基于jvm的语言,如scala)的编译运行过程为
java语言----->编辑器-------->字节码文件
字节码文件----->jvm ------->机器码
所谓字节码文件本质上是一种二进制流,jvm根据自己的规则来解析这个二进制文件
首先的cafebabe成为魔数,表示这个文件是字节码文件
class文件除了类的版本、字段、方法和接口等描述信息外,还有一个信息是常量池,用于存放编译期间生成的各种字面量和符号引用,字面量包含字符串(String a="b")和基本类型的常用(直接赋值的变量,比如int a=1),而符号引用包括了类的全限定名,方法和字段的名称、描述符等。
2、类加载生命周期
类加载过程分为五个部分:加载,验证,准备,解析,初始化
加载
加载阶段就是将Class文件的内容放入方法区中,这句话不同版本的JDK有不同的实现,比如JDK1.7及以前,方法区是由JVM内存(准确来说JVM Heap)的永久代实现,但是JDK8之后,方法区分成了两个部分,第一个部分依旧在JVM内存中,即运行时常量池,这个保存Class文件的常量池(也叫类常量池和Class常量池)中的字面量和符号引用,第二个部分则是由元空间实现,这部分属于操作系统直接管理的内存空间,不属于JVM,这里存放Class文件中类的基本信息。
验证
验证,这个阶段的工作是确保class文件的子节流中包含的信息符合当前虚拟机要求,并且不会危害虚拟机
准备
准备阶段是对静态变量分配内存,并且初始为默认值
Static变量也叫类变量,与类的生命周期保持一致,有两种赋值方法
- 在构造方法中赋值,也就是clinit方法
- 如果是static+final修饰的基本变量和String变量则使用ConstantValue赋值
对应着有两种时刻,第一种在初始化阶段赋值,第二种则在准备阶段就赋值了,但这是HotSpot VM的实现,如果是JVM规范中都是在初始化阶段赋值的(也不用太在意)
解析
解析阶段将类中的符号引用转换为直接引用.
初始化
初始化指得就是对类中的静态变量赋值真正的初值,这个过程就是类构造器clinit方法的执行过程,所谓clinit方法就是收集了代码中静态代码块和静态方法
3、类加载器机制
-
Bootstrap加载器加载JAVA_HOME/jre中的类,Extension加载类加载JAVA_HOME/jre/ext,Application加载器负责加载用户路径上的类,其中启动类加载器是cpp写的,其余的是java写的,如果想要自定义加载器,那么就继承ClassLoader类,并重写findClass方法
-
其次,类加载的方式也分为三种
第一种就是静态加载,就是通过new关键字创建对象,第二种就是通过class.forName动态加载,第三种就是classloader.loadClass动态加载,动态加载要通过newInstance方法创建对象
classLoader.loadClass(),他的源码流程是
resolve是解析的意思,默认是false
这里紫色的框是双亲委派的关键代码,可以看出直接抛给上一层加载,如果最后没有则执行自定义的findClass方法加载
红色框的代码为
这个方法有这样的注释,说明loadClass默认只执行第一步"加载",后面的连接(验证,准备,解析)是可以选择的
接下来看forName的源码
关键看initialize参数,这个参数表示初始化,默认是进行初始化的
因此,loadClass和forname最大的区别是loadClass只负责加载,forname是要进行初始化的,foname常用在jdbc连接mysql时,而loadClass可以避免初始化对象,可以选择在使用该类对象时再初始化.
此外,也该注意new和newInstance的区别,new是静态加载,而newInstance是动态加载,需要保证类已经加载并已经建立连接,newInstance有两种用法Class.newInstance(),这个Class对象只能执行无参构造,另一种需要获得含参的构造器,如Construct cs=Dog.class.getConstructor(String.class)//获取含有一个String类型参数的构造器,然后执行cs.newInstance(“小明”)
- 类加载方式叫做双亲委派机制,也就是说优先委托较高层次的加载器加载,这样的目的是为了重复加载类,如果想破坏双亲委派机制,那么自定义加载器的时候,就不是重写findClass方法而是重写loadClass方法