Java基础知识整理(三)

来来来,我们继续好好学习。以下内容摘抄自《深入理解java虚拟机--JVM高级特性与最佳实践》一书。

10 类文件结构

class类文件,亦称字节码文件,是由虚拟机规范规定了其结构形式的文件。class文件是一组以8位为基础单位的二进制流,哥哥数据项目严格按照顺序紧凑排列在class文件中,中间没有任何分隔符,以保证整个class文件中存储的内容全部是程序运行的必要数据,没有空隙。当遇到需要占用8位字节以上的空间数据时,会按照高位在前的方式分割成若干上8位字节进行存储。

Class文件格式中只有两种数据类型:无符号数和表。无符号数属于基本的数据类型,以u1、u2、u4、u8分别代表1、2、4、8个字节的无符号数。无符号数可以用来描述数字、索引引用、数量值、或者按照UTF-8编码构成字符串值。表是多个无符号数或者其他表作为数据项构成的符合数据类型,所有表都习惯性地以"_info"结尾。无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,称这一系列连续的某一类型的数据为某一类型的集合。

虚拟机必须判定输入的文件是不是一个Class文件,其依据是文件的首4个字节的魔数(0xCAFEBABE)。虚拟机判断Class能够否被兼容,依据的是文件的第5~8个字节。


11 java的类加载机制?

虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被直接使用的Java类型,这就是虚拟机的类加载机制。

类的生命周期包括加载(Loading),验证(Verification),准备(Preparation),解析(Resolution),初始化(Initialization),使用(Using),卸载(Unloading)等7个阶段,其中验证、准备和解析三个部分统称为链接(Linking)。而类的加载指的是从加载到初始化这5个阶段。

这七个阶段的顺序除了解析阶段和使用阶段之外,其他几个阶段的开始顺序是确定的,必须按照这种顺序按部就班的开始,但不要求按这种顺序按部就班的完成。这些极端通常是相互交叉地混合式进行的,通常会在一个阶段执行的过程中调用或激活另外一个阶段。解析阶段在某些情况下可以在初始化阶段之后再开始,以支持Java的运行时绑定(RTTI),而使用阶段则是按类文件内容的定义的不同而在不同的阶段进行。

虚拟机规范对于何时进行加载这一阶段并没有强制约束,但对于初始化阶段,虚拟机规范是严格规定了有且只有四种情况必须立刻对类进行初始化:

1> 遇到new, getstatic, putstatic, 或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这四条指令的场景是:使用new关键字实例化对象;读取或设置一个类的静态字段;调用一个类的静态方法。当然,被final修饰并在编译期就把结果放入常量池的静态字段不属于这些场景,这类静态字段的值在编译期时就会被编译器优化而直接放入常量池,其引用直接指向其在常量池的入口。

2> 使用java.lang.reflect包的方法对类进行反射调用时,如果类没有进行过初始化,则需要先触发其初始化。

3> 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

4> 当虚拟机启动时,用户需要制定一个要执行的主类(包含main方法的类),虚拟机会先初始化这个主类。

以上四种场景中的行为称为对一个类进行主动引用,除此之外所有引用类的方式都不会触发初始化,称为被动引用。

接口的加载过程与类的加载过程最主要的区别在于第三点,即当初始化一个接口时,并不需要先初始化其父接口,而是只有真正使用到父接口中的字段的时候才会初始化。

类加载的各个阶段简单说明:

1> 加载阶段,虚拟机主要完成三件事:通过一个类的全限定名来获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,在Java堆中生成一个代表这个类的java.lang.class对象,作为方法区这些数据的访问入口。

2> 验证阶段,不同虚拟机会进行不同类验证的实现,但大致都会完成以下四个阶段的校验过程:文件格式验证(验证字节流是否符合class文件格式的规范,并能被当前版本的虚拟机处理);元数据验证(对字节码描述信息进行语义分析,保证其描述信息符合java语言规范);字节码验证(对类方法体进行数据流和控制流分析,保证类的方法在运行时不会做出危害虚拟机的行为);符号引用验证(发生在将符号引用转化为直接引用的时候,在解析阶段中发生)。

3> 准备阶段,正式为类成员变量(注意,不是实例成员变量,实例变量会在对象实例化时随着对象一起分配在java堆上),分配内存并设置类变量初始值(通常情况下是数据类型的零值,不进行赋值操作)的阶段,这些内存都将在方法区中进行分配。

4> 解析阶段,虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用与内存布局无关,而直接引用的目标必定已经在内存中存在。解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。

5> 初始化阶段,真正开始执行类中定义的java程序代码(字节码),是执行类构造器<clinit>()方法的过程。

<clinit>方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量。

<clinit>方法与类的构造函数(或者说实例构造器<init>方法)不同,它不需要显示的调用父类构造器,虚拟机会在子类的<clinit>方法执行之前完成父类<clinit>方法的执行,因此父类中定义的静态语句块要先于子类的变量赋值操作。

<clinit>方法对于类或接口来说不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,则编译器可以不为这个类生成<clinit>方法。

接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>方法,不同于类的地方是执行接口的<clinit>方法时不用先执行父类的<clinit>方法。

虚拟机会保证一个类的<clinit>方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,则只有一个线程去执行这个类的<clinit>方法,其他线程阻塞等待,直到活动线程执行<clinit>方法完毕。


12 类加载器的作用及类别?

类加载器只用于实现类的加载动作,即实现通过一个类的全限定名来获取此类的二进制字节流。但对于类来说,要判断两个类是否相等(instanceof, equal),其前提是两个类是由同一个类加载器所加载的,否则无论两个类是否来源于同一个class文件,这两个类都必定不等,即,对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机的唯一性。

在Java开发人员看来,类加载器可划分为以下三类系统提供的类加载器:

1> 启动类加载器(Boostrap ClassLoader),负责将存放在<JAVA_HOME>\lib目录中的类库加载到虚拟机内存中,其无法被java程序直接引用。

2> 扩展类加载器(Extension ClassLoader),由sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>\lib\ext目录中的类库,可被开发者直接使用。

3> 应用程序类加载器,由sun.misc.Launcher$AppClassLoader来实现,负责加载用户类路径(ClassPath)上指定的类库,可被开发者直接使用,且为默认的类加载器。

java中采用双亲委派模型(Parents Delegation Model)来实现类的加载模式。双亲委派模型除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围没有找到所需的类)时,子加载器才会自己去加载。如下图所示:



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值