• 极客专栏正式上线!欢迎访问 https://www.jikewenku.com/topic.html
  • 极客专栏正式上线!欢迎访问 https://www.jikewenku.com/topic.html

Java虚拟机JVM复习之类加载机制

极客笔记 巷子的童年 3个月前 (03-02) 143次浏览 已收录 0个评论 扫描二维码
Class文件是一组以8位字节为基础单位的二进制流,在Class文件中包含两种数据类型:无符号数和表。
1)无符号数,以u1、u2、u4、u8分别代表1、2、4、8个字节的无符号数。
2)表,以“_info”结尾,由多个无符号数或其它表构成的复合数据类型。
1. 魔数与版本号
每个Class文件的头4个字节称为魔数,它的唯一作用是确定这个文件的类型,紧跟魔数后的是Class文件的版本号。
2. 常量池
Class文件中第一个出现的表类型数据项。由于常量池中常量数量不固定,因此在常量池入口需要放置一项u2类型数据,代表常量池的容量,其计数是从1开始,第0项常量保留,这样做是为了满足后面某些指向常量池的索引值的数据在特定情况下表达“不引用任何一个常量池项目”,此时就可以将索引值置为0。
常量池中主要存放两大类常量:字面量和符号引用。
3. 访问标志
常量池结束之后有两个字节代表访问标志,用于识别一些类或接口层次的访问信息。如:这个class时接口还是类;是否为public等。
4. 类索引、父类索引、接口集合索引
类索引、父类索引都是一个u2类型的数据,而接口索引集合则是一组u2类型数据的集合,类索引用于确定这个类全限定名(指向常量池),同理得父类索引,因为接口可以实现多个,所以是个集合。
5. 字段表集合和方法表集合
字段表用于描述接口或者类中申明的变量(包括变量名、描述符等信息),同理方法表用于定义方法。
二. 类加载机制

1. 加载:根据查找路径找到相应的class文件,然后导入。
在加载阶段,虚拟机需要完成三件事:
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流中的静态存储结构转变为内存中方法区的运行时数据结构。
3)在内存中生成一个代表整个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口(对于HotSpot而言,Class类对象比较特殊,它虽然是对象,但是存放在方法区中)。
注:数组的加载较为特殊,数组本身不通过类加载器创建,而是jvm直接创建,但是数组元素类型对应的类则由对应的类加载加载(如果是引用类型,则由系统类加载器加载,如果是普通类型则由引导类加载器加载)。
2. 验证:验证Class文件的正确性。
1)文件格式验证
检验字节流是否符合class文件格式的规范,其目的是保证输入的字节流能正确地解析并存储在方法区中。
2)元数据验证
对类的元数据信息进行语义校验,保证其符合java规范。
3)字节码验证
对类的方法体进行校验,以保证方法体在运行时不会做出危害虚拟机安全的事情。
4)符号引用验证
对符号引用所指向的对象进行匹配性验证(即即全限定名能否匹配到对应的类、符号引用的字段是否允许被访问),以保证后续的解析正常进行。
验证阶段很重要,但不是必须的。
3. 准备:给类中的静态变量分配存储空间。
正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存将在方法区中进行分配(这里所说的初始值一般为数据类型对应的零值)。
4. 解析:将符号引用解析为直接引用。
将常量池内的符号引用替换直接引用。
符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时可以无歧义地定位到目标即可。
直接引用:直接指向目标的地址。
对于解析而言,虚拟机并没有规定解析开始时间,只需要在使用字符引用之前对其进行解析即可,同时对一个符号引用进行多次解析也是常见的事情,因此除了invokedynamic指令外,虚拟机实现可以对第一次解析的结果进行缓存,避免解析动作重复执行。
对于invokedynamic而言,每次解析出的直接引用地址都不一定相同,因此对invokedynamic的解析结果不能缓存。
5. 初始化:对类中的静态变量和静态代码块进行初始化,即执行类构造器<clinit>()方法的过程。
1)<clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块中语句按出现顺序合并产生。
2)虚拟机保证在子类 <clinit>()方法执行之前父类的初始化方法已经执行完了(不需显性调用父类初始化方法),因此Object的初始化方法第一个执行(因此也可知父类的静态变量初始化也先于子类)。
3)<clinit>()对于类或接口不是必须的,即如果没有静态变量或静态语句块则不会有初始化方法。
4)接口中不能使用静态语句块,但可以有变量初始化,执行初始化方法时不需要先初始化父接口的方法,只有当使用父接口的变量时才会初始化。接口的实现类也不会执行接口的<clinit>()方法(接口只在使用的时候类初始化而子类必须在父类初始化之后初始化)。
三、类加载器
简介:实现类加载的过程,同时让程序自己决定如何去获取所需的类。对于任何一个类,都必须由加载该类的类加载器以及这个类本身一起来唯一确定。类加载器可分为两类,一类是系统提供,一类是自定义。
类加载器ClassLoader它是一个抽象类,ClassLoader的具体实例负责把java字节码读取到JVM当中,ClassLoader还可以定制以满足不同字节码流的加载方式,比如从网络加载、从文件加载。ClassLoader的负责整个类装载流程中的“加载”阶段。
ClassLoader的重要方法:
public Class<?> loadClass(String name) throws ClassNotFoundException
载入并返回一个类。
protected final Class<?> defineClass(byte[] b, int off, int len)
定义一个类,该方法不公开被调用。
protected Class<?> findClass(String name) throws ClassNotFoundException
查找类,loadClass的回调方法
protected final Class<?> findLoadedClass(String name)
查找已经加载的类。
系统提供的类加载器有如下:
1)启动类加载器(bootstrap class loader):用来加载java核心库,负责加载存放在<JAVA_HOME>lib目录中的类。
注:由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的。
2)扩展类加载器(extensions class loader):用来加载 Java 的扩展库。
3)系统类加载器(system class loader):根据 Java 应用的类路径来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器。
除了引导类加载器外其余类加载器均有父类加载器,且均实现了ClassLoader接口,在ClassLoader接口中有一个getParent()方法,用于得到父类加载器。

双亲委派机制:所谓的双亲委派机制是指如果一个类加载器收到类加载的请求,它首先不会自己去加载这个类,而是把这个请求委托给父类加载器加载,每个层次的类加载器都是如此,只有当父加载器无法加载时子类加载器才会去加载。
这样做的目的是为了保证java核心内库的安全,如Object那个类,无论那个类加载器要加载这个类都委托给最顶端的启动类加载器进行加载,如果没有双委托机制,而是由各个自定义的类加载器进行加载,当用户自定义了Object之后,系统存在多个Object,就破环了java类型体系的一些基本功能。
类加载器实现过程:在ClassLoader的loadClass实现了双亲委托机制的逻辑,在这个函数中:首先检查jvm中是否已经加载了对应名称的类,使用findLoadedClass(String )函数;如果类未加载,则调用父类加载器的loadclass()方法来加载(双亲委托机制的实现是使用组合的方式来实现,即一个类加载器中组合进入一个父类加载器);如果父类加载器无法加载类,则会抛出ClassNotFoundException异常,子类加载器再通过findClass()来加载;如果没有父类加载器,则使用findBootstrapClassOrNull来调用Bootstrap来进行加载;如果上述均未加载对应类,则通过findClass()来自己加载,而如果要自定义类加载器则可以通过覆写此方法来实现自定义,具体的自定义实现过程为在某个地方将文件输入内存中获得其输入流,将其转为二进制输出流,随后调用defineClass方法即可完成类加载,并生成class对象。
注:defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象。
线程上下文类类加载器
Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。
这些 SPI 的接口由 Java 核心库来提供,因此一般来说是由引导类加载器加载,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径里,由系统类加载器加载。因此引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。
而线程上下文类加载器则破坏了“双亲委派模型”,类 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器,同时其默认使用的也是系统类加载器。
以jdbc为例,对于数据库的访问,java提供了一套访问接口,而具体实现则交由具体厂商实现,这就是典型的SPI方式,在数据访问时我们需要使用DriverManager来管理驱动程序,而这个DriverManager是java的核心类,它必须由引导类加载器加载,但是在加载DriverManger并进行初始化时,这个DriverManger会通过静态代码块的方式来调用loadInitialDrivers();,这个方法用于加载数据库驱动程序的实现类。

此时就会出现问题,即一个处于java核心类库的类需要使用java应用库中的类,但是由于双亲委托机制的存在使得加载核心类库的引导类加载器无法加载java应用,也不能通过使用系统类加载器来加载,此时的解决方法是使用线程上下文类加载器来加载,这个上下文加载器如果没有进行设置,默认使用系统类加载器加载,这样就能够加载驱动程序的实现类。
类加载器与web容器
对于运行在 Java EE容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。

丨极客文库, 版权所有丨如未注明 , 均为原创丨
本网站采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行授权
转载请注明原文链接:Java虚拟机JVM复习之类加载机制
喜欢 (0)
[247507792@qq.com]
分享 (0)

您必须 登录 才能发表评论!

  • 精品技术教程
  • 编程资源分享
  • 问答交流社区
  • 极客文库知识库

客服QQ


QQ:2248886839


工作时间:09:00-23:00