谈到java类,那我们不得不扒一下类的加载和类的详细信息以及类的双亲加载模型。
类文件结构
类一般由魔数、Class文件版本,常量池,访问标志,类索引、父类索引、接口索引集合、字段表集合、方法表集合、属性表集合组成。
详细内容可参看这篇文章
类文件源码
package exceptiontest;
public class ClassName {
private int prvFiled = 1;
public static final int psfFild = 2;
protected String proMethod(String paras, Integer parsI) {
return paras + parsI;
}
}
我们可以用命令
javap –verbose classname
来查看编译后的类信息,部分图如下
1523847721(1).jpg
由于编译后的class文件较为复杂,我就不在这里进行分析。
类加载机制
说了类的文件结构,我们来说说类加载机制。
类加载器
类加载器是一个用来加载类文件的类。Java源代码通过javac编译器编译成类文件。然后JVM来执行类文件中的字节码来执行程序。类加载器负责 加载文件系统、网络或其他来源的类文件。
有三种默认使用的类加载器:Bootstrap类加载器、Extension类加载器和Application类加载器。每种类加载器都有设定好从哪里加载类。
Bootstrap类加载器负责加载rt.jar中的JDK类文件,它是所有类加载器的父加载器。Bootstrap类加载器没有任何父类加载 器,如果你调用String.class.getClassLoader(),会返回null,任何基于此的代码会抛出 NUllPointerException异常。Bootstrap加载器被称为初始类加载器。
Extension加载类的请求先委托给它的父加载器,也就是Bootstrap,如果没有成功加载的话,再从jre/lib/ext目录下 或者java.ext.dirs系统属性定义的目录下加载类。Extension加载器由 sun.misc.Launcher$ExtClassLoader实现。
Application类加载器它负责从classpath环境变量中加载某些应用相 关的类,classpath环境变量通常由-classpath或-cp命令行选项来定义,或者是JAR中的Manifest的classpath属性。 Application类加载器是Extension类加载器的子加载器。通过sun.misc.Launcher$AppClassLoader实现。
除了Bootstrap类加载器是大部分由C来写的,其他的类加载器都是通过java.lang.ClassLoader来实现的。
总结一下,下面是三种类加载器加载类文件的地方:
- Bootstrap类加载器 – JRE/lib/rt.jar
- Extension类加载器 – JRE/lib/ext或者java.ext.dirs指向的目录
- Application类加载器 – CLASSPATH环境变量, 由-classpath或-cp选项定义,或者是JAR中的Manifest的classpath属性定义.
类加载过程
类的加载要经过三步:装载(Load),链接(Link),初始化(Initializ)。其中链接又可分为校验(Verify),准备(Prepare),解析(Resolve)三步。
加载
ClassLoader就是用来装载的。通过指定的className,找到二进制码,生成Class实例,放到JVM中。
链接
链接就是把load进来的class合并到JVM的运行时状态中。可以把它分成三个主要阶段:
校验
对二进制字节码的格式进行校验,以确保格式正确、行为正确。这一阶段主要是为了确保Class文件的字节流中包含的信息复合当前虚拟机的要求,并且不会危害虚拟机自身的安全。主要验证过程包括:
1.文件格式验证:验证字节流文件是否符合Class文件格式的规范,并且能被当前虚拟机正确的处理。
2.元数据验证:是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言的规范。
3.字节码验证:主要是进行数据流和控制流的分析,保证被校验类的方法在运行时不会危害虚拟机。
4.符号引用验证:符号引用验证发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在解析阶段中发生。
准备:准备类中定义的字段、方法和实现接口所必需的数据结构。比如会为类中的静态变量赋默认值(int等:0, reference:null, char:’\u0000′)。
准备
正式为类变量分配内存并设置初始值。这里的初始值并不是初始化的值,而是数据类型的默认零值。这里提到的类变量是被static修饰的变量,而不是实例变量。关于准备阶段为类变量设置零值的唯一例外就是当这个类变量同时也被final修饰,那么在编译时,就会直接为这个常量赋上目标值。
如:
pirvate static int size = 12;
那么在这个阶段,size的值为0,而不是12。 final修饰的类变量将会赋值成真实的值。
解析
装入类所引用的其他所有类,虚拟机将常量池中的符号引用替换为直接引用。可以用许多方式引用类:超类、接口、字段、方法签名、方法中使用的本地变量。
初始化
在准备阶段,变量已经赋过一次系统要求的初始值,在初始化阶段,则是根据程序员通过程序的主观计划区初始化类变量和其他资源。
类初始化前,它的直接父类一定要先初始化(递归),但它实现的接口不需要先被初始化。类似的,接口在初始化前,父接口不需要先初始化。
多态原理
这里我们谈一谈多态原理
首先澄清几个概念,因为我在看这里的时候也特别容易混淆和迷惑。首先是符号引用。
符号引用
符号引用是一个字符串,它给出了被引用的内容的名字并且可能会包含一些其他关于这个被引用项的信息——这些信息必须足以唯一的识别一个类、字段、方法。这样,对于其他类的符号引用必须给出类的全名。对于其他类的字段,必须给出类名、字段名以及字段描述符。对于其他类的方法的引用必须给出类名、方法名以及方法的描述符。
方法签名
对于同名不同类、同类不同名的方法,方法签名的意义并不是很大,但是对于重载方法来说,方法签名的意义就十分巨大了。由于重载方法之间的方法名是相同的,那么我们势必要从构成方法的其他几个要素中找到另一个要素与方法名组成能够唯一标示方法的签名,方法体当然不予考虑。那么就是形参列表和返回值了,但是由于对于调用方法的人来说,方法的形参数据类型列表的重要程度要远远高于返回值,所以方法签名就由方法名+形参列表构成,也就是说,方法名和形参数据类型列表可以唯一的确定一个方法,与方法的返回值一点关系都没有,这是判断重载重要依据
在类加载过程中的解析阶段,会将常量池中符号引用替换为直接引用。但是只是替换了部分。我们看下面一段话:
(1) 所有私有方法、静态方法、构造器及初始化方法都是采用静态绑定机制。在编译器阶段就已经指明了调用方法在常量池中的符号引用,JVM运行的时候只需要进行一次常量池解析即可。
(2) 类对象方法的调用必须在运行过程中采用动态绑定机制。
首先,根据对象的声明类型(对象引用的类型)找到“合适”的方法。具体步骤如下:
① 如果能在声明类型中匹配到方法签名完全一样的方法,那么这个方法是最合适的。
② 在第①条不能满足的情况下,寻找可以“凑合”的方法。标准就是通过将参数类型进行自动转型之后再进行匹配。如果匹配到多个自动转型后的方法签名f(A)和f(B),则用下面的标准来确定合适的方法:传递给f(A)方法的参数都可以传递给f(B),则f(A)最合适。反之f(B)最合适 。
③ 如果仍然在声明类型中找不到“合适”的方法,则编译阶段就无法通过。
最后,根据在堆中创建对象的实际类型找到对应的方法表,从中确定具体的方法在内存中的位置。
网友评论