在Java语言里面,类型的加载,连接和初始化过程都是在程序运行期间完成的,虽然会令类加载时稍微增加一些性能开销,但是为Java应用程序提供了高度的灵活性
为什么Java天生就可以动态扩展?
依赖运行期加载和动态连接
类加载的生命周期?
加载,验证,准备,解析,初始化,使用,卸载 (验证,准备,解析)称为连接
加载,验证,准备,初始化,卸载的顺序的固定的,解析某些情况下可以在初始化之后进行,就是为了支持Java语言的运行时绑定。
初始化阶段,虚拟机规范严格规定了5种情况必须进行初始化
1)遇到new,getstatic,putstatic,invokestatic这4条字节码指令时,如果类没有过初始化,则需要先触发其初始化。生成这4条指令的常见java代码场景是:使用new关键字实例化对象,读取或设置一个类的静态字段,以及调用一个类的静态方法
2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行初始化,则需先进行初始化
3)当初始化一个类时,发现其父类还没有初始化的时候,则需先触发父类的初始化
4)当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化该主类
5)如果一个MethodHandle实例最后的解析结果REF_getStatic方法句柄所对应的类没有初始化,则要进行初始化(这条看不懂,,,)
通过引用父类的静态字段,是不会初始化子类的,只有定义这个静态字段的类才会被初始化,结果如下:
如果的调用子类的静态字段,那么子类就会被初始化,而根据(3),子类初始化则父类一定要先被初始化,所以结果如下:
而常量类会被直接存入调用类的常量池中,本质上并没有直接引用到定义常量的类,所以使用该常量时,连定义该常量的类也不会被初始化。
这里可能会说在子类中定义一个final变量,调用该常量时为什么父类还是会初始化,比如下面这样
因为这里满足了(4),注意main方法是在ATest类中的,所以虚拟机启动时就会初始化一个执行的主类。改成下面这样就不会了,我们把main方法写到另一个类中:
接口与类初始化的区别?
当一个接口初始化时,并不要求其父接口全部都完成了初始化,只有在真正的使用到了父接口的时候才初始化
类加载的具体过程
1.加载
1)通过一个类的全限定名来获取定义此类的二进制字节流
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据结构的访问入口
数组类的加载不一样,数组类不是由类加载器创建,而是java虚拟机直接创建的
2.验证
为什么要验证?
为了确保字节流中所包含的信息不会威胁到虚拟机自身的安全。java语言本身无法做危险的事,但是class未见并不一定都是java源码编写而来,也可能是甚至十六进制直接编译的,这样java本身不能做到的事就能被其他层面的做到了,所以如果不检查的话可能导致系统崩溃。
验证方式:文件格式验证,元数据验证,字节码验证,符号引用验证
3.准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的 的内存都将在方法区中进行分配。类变量仅包括被static修饰的变量,而不是实例变量,而且初始值通常情况下是零值。例如 public static int value=123,,初始化value是0,而赋值成为123是在初始化阶段才会执行的
4.解析
1)类或接口的解析
假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或者接口C的直接引用,那么虚拟机将会完成下面3个步骤
a:如果C不是数组类型,那么虚拟机会把代表N的全限定名传给D的类加载器去加载这个类C
b:如果C是一个数组类型,并且数组的元素类型为对象,需要加载的元素类型就是"java,lang.Integer",接着由虚拟机生成一个数组对象
c:如果上面的步骤都没出现异常,那么C在虚拟机中实际上已经成为一个有效的类或皆苦了
2)字段解析
a:如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用
b:否则,如果C实现了接口,将会按照继承关系从下往上递归搜索父接口,找字段
c:否则,如果C不是java.lang.Object的话,会按照继承关系从下往上递归搜索父类,找字段
d:还是没有,则解析失败
3)类方法解析(
a:类方法和接口方法引用的常量类型是分开的,如果在类方法中发现C是个接口,直接报错
b:通过a的话,那么在C中查找是否有简单名称与描述符都符合的方法,有的话直接返回
c:否则,在类C的父类中递归查找能够匹配的方法
d:否则,在类C实现的接口以及父接口中递归查找,如果存在匹配的方法,则说明C是一个抽象类,这时抛出异常
e:查找失败,抛出异常
4)接口方法解析
5.初始化
1)静态语句块中只能访问到定义在静态语句块之间的变量,定义在之后的变量在静态语句块中只能赋值,不能进行访问
报错:非法向前引用
2)由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句会优于子类的变量赋值操作
3)<clinit>()方法不是必须的,如果一个类没有静态语句块,没有对变量的复制操作,则不需要<clinit>()方法
4)接口中不能使用静态语句块,但是仍然可以有变量初始化的赋值操作,与类不同的是接口中的<clinit>()方法不需要父接口先执行<clinit>()方法
5)虚拟机会保证类的<clinit>()方法在多线程环境下正确地同步
终于开始类加载器了
什么是类加载器?
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定什么时候去获取需要的类,实现这个动作的代码块叫做“类加载器”
类与类加载器?
对应任意一个类,都需要由加载它的类加载器和这个类本身一同确立在java虚拟机中的唯一性,每一个类加载器,都有其独立的类名称空间。也就是说,比较两个类是否相等,只有在这两个类是同一个类加载器加载的前提下才有意义,只要加载他们的类加载器不相等,则两个类一定不相等(即使来自同一个class文件,被同一个虚拟机加载)
双亲委派模型
从java虚拟机的角度来讲,只存在两种不同的类加载器:启动类加载器(Bootstrap ClassLoader)由C++实现,是java虚拟机自身的一部分,另一种就是所有其他的类加载器,都是由java语言实现的,独立于虚拟机,并且全部继承自抽象类ClassLoader
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,父子关系一般都以组合的关系来实现
工作过程:如果一个类加载器收到了类加载的请求,首先不会自己去加载这个类,而是把请求委派给父类,每一层都是如此,知道到达启动类加载器,如果启动类加载器也无法加载该类,子加载器才会尝试加载。(例如你自己写了一个String方法,它会先去启动类加载器中找,如果找到了,那么你写的类永远无法正确运行,这保证了Java程序的稳定性)
可以看到我自己写了一个类String,而Test1根本就不让我运行了,删除自己写的后就可以了
双亲委派机制的作用
1:防止重复加载同一个.class 通过委托去向上面问一问,加载过了就不会再加载,保证数据安全
2:保证核心.class不会被篡改,例如上面我想重写String类就失败咯,即使我篡改了也不会让我运行,保证了Class执行的安全
3.保证了类的优先级
书籍:《深入理解java虚拟机》
来源:CSDN
作者:qq_40058686
链接:https://blog.csdn.net/qq_40058686/article/details/104222246