嘚不嘚:最近写的太频繁了,没有啥嘚嘚了,嘚嘚不动了。但是我有个比较好奇的问题,是什么样的信念与决心,让我的领导每天都奋战在一线,几乎都是在十点以后下班,而且每天都特别的有激情。我觉得人总会有低迷的时候吧,这时候的工作与学习效率相对能低一些或者是厌倦的状态,可能这时候大家都会相应的调整一下,散散心之类,调整我觉得怎么样也需要个一天半天的,但是我感觉我的领导没有这个时候,哪怕有这时候也调整的特别快,调整的速度都让我感觉不到他状态的低迷,这是咋做到的。。。。
概述
类加载机制:虚拟机把class文件的数据加载到内存,并对数据进行校验、转换解析和初始化,最终形成JVM使用的java类型,这就是类加载机制。
类变量和类的实例变量:类变量和类的实例变量是有本质区别的,类加载器加载的类,并且已经初始化,就表示着类变量已经被赋值了
static String str = "类变量";
,但是并不意味着类的构造器已经被调用,当初始化该类对象的时(单纯的new的过程,当然反射或者其他的方式也可能调用类的构造器),才会调用类的构造器。
public class Main{
static String str = "类变量";
String strDemo = "类的实例变量";
}
public class Demo05 {
public static void main(String[] args) {
System.out.println( Father.str);
}
static class Father {
Father() {
System.out.println("father constructor");
}
static {
System.out.println("static");
}
public static String str = "类变量";
}
}
类加载的时机
概述:
- 类型被加载到JVM内存直到类型被卸载出内存的过程:加载、连接(验证、准备、解析)、初始化、使用、回收。类的加载过程开始顺序是一定的,但并不是一个阶段的结束另一个才会开始,而可能是一个阶段未结束,而另一个阶段已经开始。
- 动态加载:JVM没有将class文件一次性的加载到内存,在类的过程中,根据类的真实类型去选择要加载的类,这样的语言充满了扩展性。
初始化类的时机:也称为主动引用,只有在这几种情况下才会对类型进行加载,同时也伴随着类加载的其他步骤(验证、准备、解析等)。注意这个阶段会将类型的class文件加载到方法区,并不一定会调用类的构造器为对象分配内存(这种情况是需要调用类的构造器的
static User user = new User();
)。- new 实例化对象、类变量被调用或者赋值、类方法被调用、(常量
final+变量
在编译期间已经被赋值,加载到内存) - 使用java.class.reflect对类进行反射调用时。
- 初始化类时,发现父类没有初始化。(接口的初始化时,并不需要检查父类是否已经初始化,只有在真正使用到父类接口时,才会被初始化)
- JVM启动时,JVM需要先加载包含main方法的类到内存。
- 动态语言支持时,如果采用CGLIB动态代理模式,代理方式相当于基于原始类调用父类被代理的方法。
- new 实例化对象、类变量被调用或者赋值、类方法被调用、(常量
被动引用:(通过编译后的class文件说明)
- 通过子类引用父类的类变量发现并不会加载子类。
- 通过数组定义引用类,发现并不会引发此类的初始化。
- 常量在编译期间已经初始化,所以引用常量并不是导致常量所在类的初始化。
问题1:了解了constant_fieldref_info的结构,类的属性通常需要指定该属性所属于的类,所以在使用类的属性时,相应需要加载该类到JVM内存。在使用类变量(final修饰的属性值,在编译期间直接存储到常量池)时,需要将类加载带内存,但是子类在使用父类的类变量时,子类为什么没有加载到内存?
子类.父类变量
是如何得到值得?Son.str调用时,加载了父类Father,但是没有加载Son,为什么通过Son.str可以输出父类的属性值呢?(希望大神指点一二,有答案我会公布的)
public class Demo04 {
public static void main(String[] args) {
System.out.println(Son.str);
/**
* father static
* father's variable
*/
}
static class Son extends Father{
static{
System.out.println("Son static");
}
}
static class Father{
static{
System.out.println("father static");
}
static String str = "father's variable";
}
类的加载过程
加载:
加载是类加载的一个阶段,类加载包括加载、验证、准备、解析、初始化这几个阶段。类的加载过程主要完成的事情:
- 通过类的全限定名来获取此类的二进制字节流。
- 生成一个代表这个类的java.lang.class的对象,作为方法区这个类的各种数据访问的入口。
- 将字节流代表的静态存储结构转化为方法区的运行时数据结构。
途径:要加载的类型来源是未知的,可以从网络、ZIP等途径获取。
- 加载数组:
- 加载非数组:用户可以通过自定义的加载器将字节流加载到内存,也可以通过JVM提供的类对字节流进行加载。
- 数组类:数组类的组件类型是通过类加载加载的,而数组类本身是JVM直接创建的。如果数组类的组件类型是引用类型,那就采用递归去加载这个组件类型。如果数组的类型不是引用类型,那么数组的可见性默认为public。(这个数组类的加载我还不是很理解)
验证
- 作用:确保class文件中的字节流包含的信息符合当前虚拟机的要求,不会危害虚拟机的安全。
- 文件格式验证:验证class文件字节流格式的规范性。例如:魔数的开头、版本号、常量池中表的类型等。
- 元数据验证:对字节码描述的信息进行语义分析。例如:当前类是否有父类、是否继承了不能继承的类等。
- 字节码验证:通过数据流和控制流分析,确定程序的语义是合法的、符合逻辑的。对类的方法体进行校验,方法体中的类型转化是有效的、跳转指令时不会调整到方法体之外的。
- 符号引用验证:检测符号引用转化为直接引用的检测。例如根据符号引用是够能够找到对象的类。
准备:为类变量在方法区分配内存,设置初始值为0的阶段。
解析:将符号引用转化为直接引用的过程。
- 定义:符号引用是用一组符号来描述所引用的目标,它可以使用任何形式的字面量,只要能定义到目标即可。直接引用指向目标的指针或者句柄。虚拟机可以根据需要对常量池中的符号引用进行加载。
- 解析:对于一般引用来说(非动态技术),对符号引用的解析是被缓存的,在运行时常量池中将记录该符号引用的直接引用,并且将该符号引用记录为已解析状态。对于动态语言符号引用的解析,当JVM执行到动态调用点时,才会完成对符号引用的解析。
在重新理解下一章节时新的体会:解析是符号引用转化为直接引用的过程,转化过程包括:类或者接口的解析过程、字段解析过程、方法解析过程等。这个解析过程相当于类或者方法的加载(class文件被加载到内存),类中属性和方法的查找过程。但是解析过程并不包括类中引用其他类型的确认过程,这个过程是动态的,是运行时动态决定调用类型和类属性(类方法或者类的属性)的过程。看例子:
这个例子实际上就是一个策略模式,运行时动态的决定调用类的类型,这个会在下一章节介绍。
- 符号引用转化成直接引用是什么转化到什么的过程?
- 类或者接口解析:对类或者接口的解析相当于加载某个类或者接口的过程,类符号引用的表结构constant_class_info{tag;name_index},class文件中对应类中的引用类是用二进制形式的完全限定名定义的,对类或者接口的解析就是通过二进制形式的完全限定名对引用类进行加载的过程。
- 字段解析:类中字段的符号引用的表结构
constant_fieldref_info{tag;class_index;name_and_type_index},首先对类class_index符号引用进行解析,如果该类型(类或者接口就相当于一个类型)没有被加载,需要对类或者接口进行解析(该类或者接口定义为C),然后在C类中查找简单名称和字段描述符都相匹配的字段,如果在当前类中找到则返回直接引用(继承时的子类对父类的覆盖)。否则在继承类或者实现类中自下而上(子类、父类依次查找)查找该字段,不过这个地方会有一个问题,用代码说明下:
同等级的实现类中同时存在该属性b是存在问题的,在进行解释时,首先A类中不存在属性b,然后查找实现类中是否存在简单名称和字段描述都相匹配的字段,结果在两个实现类中都找到了该属性,在解析的过程中无法选择调用那个类的属性b,当然现在使用的编译器中,一旦出现这样的问题,编译器直接就发现了这个错误。 - 类方法解析:类中字段的符号引用的表结构
constant_methodref_info{tag;class_index;name_and_type_index}。需要对符号引用class_index进行解析和字段解析都是一样的(该类定义为C),但是在查找方法的过程中就存在差异了。name_and_type_index对应的表示字段和方法名称的符号引用,对应的符号引用表结构constant_NameAndType_info{tag;name_index;descriptor_index},当该符号引用形容字段属性时,表示的是属性名称和属性的类型。当该符号引用形容方法时,首先在C类中查找简单名称和字段描述都相匹配的字段,但是对于方法的加载过程存在重载的情况,也就是方法名称相同,参数不同的情况,这时需要根据constant_NameAndType_info表结构能够确定方法的名称、方法的参数类型和返回值类型。对于重载时,方法的调用版本可能不是唯一的,下一章节详细介绍调用重载方法的选择。 - 接口方法解析:解析过程和类方法解析基本相同可以参考。
- 符号引用转化成直接引用是什么转化到什么的过程?
- 初始化:类加载的最后一个阶段,这个阶段真正的主导者就是写者了,给类变量赋值一个符合类型的数据,在这个阶段就会初始化为该类型赋值。
- 类构造器和类的实例构造器:类构造是执行方法,类的实例构造器是执行方法。
- clinit方法:是由编译器收集所有类变量的赋值动作和静态语句块中语句合并产生的。
类加载器
- 类与类加载器:JVM的类加载通过类的二进制完全限定名去加载类的信息,。类加载器只是完成类的加载动作,但是同时需要我们保证安全性。对于同一个class文件加载到虚拟机内存需要同一个类加载器加载。类的java.lang.class对象是该文件方法区访问的入口,如果一个class文件被两个类加载器加载,那么该文件生成了两个方法区的访问入口。
- 双亲委派模型:双亲委派模型保证了对于class文件加载的过程中,同一个class文件由同一个类加载的。如果类加载器收到了加载请求,那么该加载器首先不会自己加载该类,会将加载的任务委派给父类,同样父类也是一样的操作,如果顶层的父类无法完成加载动作,那么该加载器会尝试加载次文件。
- OSGI:
- 特点:OSGI中的Bundle和jar的模块之间区别不大,bundle之间是平级关系。bundle模块中对于类库的控制很严格,只有被export的package才能被外界访问,其他的类库不能够被外界访问。基于OSGI的程序可以实现模块级别的热插拔功能,某个模块的升级与停用不影响其他模块的应用。
- 实现:OSGI功能的实现主要依靠灵活的类加载器架构,OSGI模块的加载器之间只有规则没有委派关系。如果某个bundle声明了一个他依赖的package,那么其他的模块涉及到该类的加载时,都由这个bundle模块加载。如果不涉及到某个具体的package和class时,各个bundle模块之间都是平级关系。如果某个模块bundle内的类库没有被export那么该类只能在该模块内找到。
- 加载规则:
- 以java,*开头的类,委派给父类加载器加载。
- 委派列表名单内的类,委派给父类加载器加载。
- Import列表中的类,委派给Export这个类的Bundle的类加载器加载。
- 查找当前bundle的classpath,使用自己的类加载器进行加载。
- 查找自己是否在自己的Frament bundle类中,如果是交给Fragment bundle的类加载器加载。
- 查找Dynamic bundle列表的Bundle,委派给对应的类加载器加载。
- 查找失败。