虚拟机类加载机制

包机制:

使用package语句,每个源文件只能有一个package语句,属只于一个包。

一个包中可在底下包含其他包,用“.”的形式表示,在路径中代表”/”,如package lib.hello,说明lib包内包含hello包,其中lib在根目录下,要使用hello必须从根目录开始。

该包机制能够实施对访问权限的控制。

类文件结构(包访问机制就定义在其中的access_flags中)

.class描述的类文件结构中包含:头4个字节为魔数0XCAFEBABE,用来判断是否被虚拟机接受的class文件;而后是常量池,常量数量不固定,起初会放入常量池容量计数值,然后放入常量,包含字面量和符号引用;常量池结束后紧接着两个字节代表访问标识(access_flags),用于识别一些类或接口层次的访问信息,包括:该Class是类还是接口,是否定义为public,是否定义为abstract,若是类,是否被定义为final等;类索引、父类索引、接口索引等;字段表集合(field_info)包含字段作用域如public,是否为static,可变性(final),并发可见性(volatile),可否被序列化(transient修饰符),字段数据类型(基本类型、对象、数组等);方法表;属性表;归根到底一张图:


类加载的过程:

类从加载到虚拟机内存中开始到卸载出内存,整个生命周期包括:


加载、连接(验证、准备)、初始化、使用、卸载这五个阶段顺序是确定的,类加载过程按这种顺序开始执行,但解析阶段就不一定:某些情况下,解析可能会在初始化之后,以支持java的动态绑定(运行期绑定)。这里说的开始是说每个阶段开始的顺序,不一定是完成的先后顺序,一般来说,这些阶段都是交叉混合进行,在一个阶段运行过程中开始调用激活下一个阶段。

类初始化时机只要满足以下5个条件之一即可:




只有这四种会触发类初始化、称为对一个类进行主动引用,除此之外的所有引用类的的方式,都不会触发初始化,称为被动引用。三个栗子:

Ex1. 被动引用之一——调用子类中继承的父类静态变量


输出:


对静态字段,只有直接定义该字段的类才会被初始化。


Ex2. 被动引用之二——定义数组来引用类,不会触发类的初始化


输出:

空白

因为对数组引用类的情况,不会触发该类的初始化。


EX3.被动引用三——常量在编译阶段存入调用类的常量池,本质上没有直接引用到定义常量的类,不会触发定义常量的类的初始化。


结果:


hello world

没输出SuperClass init,因为虽在代码中引用了TestP中的静态常量HELLOWORLD,但在编译期该常量值已被存储到了Test类的常量池中,对常量TestP.HELLOWORLD的引用实际上都转为Test类对自身常量池的引用了。

接口加载与类加载有所不同,接口中不能使用static{}语句块,但也有<clinit>()类构造器,真正和类有区别的是前面所述的“有且仅有”需要开始初始化场景的第三种:当一个类在初始化时,要求父类全部都已经初始化过了,但是接口在初始化时,并不要其父类接口全部完成初始化,只有真正用到父类接口(如引用接口中定义的常量)才会初始化


类加载(在初始化之前)具体过程:


注意:“准备”部分有问题。“准备”中只是为类变量进行内存分配,而不是实例变量,且如

Public static int value = 123,经过准备阶段,其值仅为初始值0,而不是123,赋值会在“初始化(<clinit>(),这不是在准备阶段做的)”进行;但若该value为final,那么在准备阶段就会赋值,并带有ConstantValue属性


“初始化”阶段:

l  <clinit>()由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生,收集顺序与语句定义顺序有关,静态语句块只能访问定义在该语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问

l  <clinit>()方法与类的构造器不同或者实例构造器<init>()不同,它不需要显式调用父类构造器、虚拟机会保证在子类<clinit>()执行前、父类的<clinit>()已执行完毕,因此在虚拟机中第一个被执行的<clinit>()肯定是java.lang.Object

l  父类<clinit>()先执行,所以父类中定义的静态语句块要优先子类的变量赋值操作。

l  <clinit>()对于接口和类不是必须的,若类中没有静态语句块也没有对变量的赋值操作,那么可以不为该类生成该<clinit>()

l  接口中不能用static语句块,但仍有变量初始化操作,因为接口也会有该<clinit>(),但不同的是:执行接口的该方法时不需要先执行父接口的该方法。只有当父接口中定义的变量被使用时,父接口才会初始化。另外接口的实现类在初始化时也一样不会执行接口的<clinit>()。

l  虚拟机会保证一个类的<clinit>()在多线程环境下正确加锁和同步。

类加载器

执行上面这段类加载过程的是ClassLoader,在JVM中是通过ClassLoader和类本身共同判断两个Class是否相同,换句话说,不同的ClassLoader加载同名Class文件会被认为是不同的类。

 

类加载器之间的关系:

有时不会从Class文件加载流(如java applet是从网络中加载),那么这个ClassLoader和普通的实现逻辑就不同,需要使用不同的ClassLoader。但允许使用不同的classLoader引发新的问题,如果我也声明了一个java.lang.Object,但内部代码非常危险,这里就引入了双亲委派模式:


l  Bootstrap ClassLoader引导类加载器:负责Java核心类的加载,如String等,在JRE的lib下的rt.jar中

l  Extension ClassLoader扩展类加载器:加载JRE中ext目录下的jar包类

l  Application ClassLoader:负责在JVM启动时加载来自java命令的class文件(自定义的)ClassPath环境变量所指定的jar包和类路径。

双亲委派模型要求除顶层的启动类加载器外,其余的类加载器应该有父类加载器(一般不以继承方式,而是以组合方式复用父类加载器的代码),它在接到加载类的请求时优先委派给父类加载器完成。

其工作过程为:若一个类加载器收到了类加载请求,首先不会加载该类,而是交给父类加载器完成,只有当父类加载器反馈自己无法完成该加载请求,子加载器才会尝试自己加载。

其好处是:Java类随它的类加载器一起具备了优先级的层次关系,如java.lang.Object类,无论哪一个类加载器要加载该类,最终都是委派给处于最顶端的启动类加载器完成,因此Object类在程序的各种类加载器环境中都是相同的,相反若没有双亲委派模式,自己编写的类加载器去加载该Object类就会使代码异常混乱。(实现方式递归一直到最顶层的bootstrap ClassLoader来加载当前类,加载方式由上而下)。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值