首页 > 编程语言 >【Java虚拟机探究】9.类装载器(上)

【Java虚拟机探究】9.类装载器(上)

时间:2023-04-03 16:01:32浏览次数:42  
标签:Java 虚拟机 class public 探究 static Son ClassLoader 加载


在JVM类要通过类装载器(ClassLoader)进行装载后,才能进行执行。本篇总结了类装载器的一些知识。


一、class装载验证流程

在第一篇总结中介绍了JVM的内存结构:

【Java虚拟机探究】9.类装载器(上)_jvm


可以看到class文件首先要通过“类加载器子系统”,才能被加载到内存中处理。那么class文件是怎么通过类加载器加载至内存中的呢?

下面是class装载验证的流程:

加载--->连接--->初始化

其中链接又可以分为“验证”、“准备”和“解析”:

【Java虚拟机探究】9.类装载器(上)_classloader_02


下面逐一分析:

1.加载

加载操作是装载类的第一个阶段,此时会取得类的二进制流,然后将该二进制流转为方法区的数据结构,最后在Java堆中生成对应的java.lang.Class对象。

2.链接
(1)验证

验证的目的就是保证Class流的格式是正确的。那么如果校验Class流格式呢?一般验证以下三点:

● 文件格式的验证

    - 是否以0xCAFEBABE开头(Cafebabe咖啡宝贝)

    - 版本号是否合理(JDK6不可能生成JDK7的版本)

● 元数据验证

    - 是否有父类

    - 是否继承了final

● 字节码验证(很复杂)

    - 运行检查

    - 栈数据类型和操作码数据参数吻合(例如操作码数量超过了设置的栈空间)

    - 跳转指令到合理的位置(字节码的偏移量位置错误,跳到不存在的地方)

● 符号引用验证

    - 常量池描述类是否存在

    - 访问的方法或者字段是否存在且有足够的权限(尝试访问权限不够的方法或字段)

(2)准备

在准备阶段,会为该class分配内存,并且为类设置初始值(方法区中)。

例如下面的代码:

public static int a=1;

在准备阶段,a会被初始化为0(int类型默认值为1),而在初始化的<clinit>中才会被设置为1。

对于static final类型:

public static final int a=1;

在准备阶段就会被赋上正确的值。

(3)解析

此阶段会将符号引用替换为直接引用。

符号引用简单来说就是一个字符串,例如超类(没有继承的类),在class常量池有一个字符串,字符串的内容为“java.lang.Object”,此时“java.lang.Object”就是一个符号引用,而符号引用并不能直接被使用,它只是一种使用方式,如果需要使用的话,需要将符号引用改为直接引用,所谓的直接引用就是我要知道这个对象在内存的哪里,及是指针或者地址偏移量。

具体的解析分为以下几类

1)类或接口的解析

一般类或者接口中都有各种变量,该解析就是判断这些变量是基本数据类型,还是普通的对象类型,进而通过不同的方式解析为直接引用。

2)字段解析

对类的目标字段进行解析时,首先会在本类中查找是否有与目标名称和字段描述相同的字段,如果有,则查找结束;如果没有,则会从下到上递归搜索该类实现的各个接口以及它们的父接口;如果还没有找到,则下到上递归搜索该类继承的各个父类以及它们父类的父类。

例如:

public class FieldParsingTest {
	public static void main(String[] args) {
		System.out.println(C.a); 
	}
}

class A{  
    public static int a = 1;  
}  

class B extends A{  
    public static int a = 2;  
}  

class C extends B{ 
	
}

结果:

【Java虚拟机探究】9.类装载器(上)_App ClassLoader_03


说明加载器先是去C本身找a字段,没找到,然后去找他的父类B,然后在B里面找到了,直接返回了a的值,不再去A里寻找a字段了。而此时如果我们在B和C中都不定义a,这个时候就会输出A中的a字段的值。


3)类方法解析

解析该类的方法,与字段解析,但是先搜索父类,再搜索接口。

4)接口方法解析

解析该类的接口方法,递归向上搜索父接口。


3.初始化

该阶段类构造器<clinit>为static变量赋值,执行static{}语句。

类的初始化称之为“class init”,而执行类构造器就是<clinit>,为英文的缩写。执行类构造器<clinit>,会将static变量变量赋值,并执行static{}语句。

这里子类的<clinit>调用前保证父类的<clinit>被调用,例如先执行了父类的static静态语句块,再执行子类的,例如:

public class ClinitTest {
	public static void main(String[] args) {
		Son son = new Son();
	}
}

class Parent{
	static{
		System.out.println("I'am Parent");
	}
}

class Son extends Parent{
	static{
		System.out.println("I'am Son");
	}
}

运行结果:

【Java虚拟机探究】9.类装载器(上)_BootStrap_04


重点注意!!!面试中经常将static{}语句的加载和字段解析混在一起,这里我们记住两点,一个是字段解析时从下向上寻找的,而static{}语句是从上向下执行的,且如果在仅仅调用静态字段而不去new类的话,static{}语句会执行到找到静态字段的那个类而停止向下执行:

public class ClinitTest {
	public static void main(String[] args) {
		//Son son = new Son();
		System.out.println(Son.a);
	}
}

class GrandParent{
	public static int a = 1;  
	static{
		System.out.println("I'am GrandParent");
	}
}

class Parent extends GrandParent{
	public static int a = 2;  
	static{
		System.out.println("I'am Parent");
	}
}

class Son extends Parent{
	static{
		System.out.println("I'am Son");
	}
}

运行结果:

【Java虚拟机探究】9.类装载器(上)_BootStrap_05


这里可以看到,寻找a字段是从下向上的,而static{}语句是从上向下执行的。这里因为a不会与子类Son关联在一起,因此并不会触发子类Son的初始化,所以不会执行它的static{}语句。


<clinit>是线程安全的,一个线程进去之后,其它线程就会等待。


问题:Java.lang.NosuchFieldError错误可能在什么阶段抛出?

答:NoSuchFieldError表示没有对应的字段,即可能是class中的属性名拼写错误,此时应该是类加载器进行链接阶段时,进行“符号引用验证”来判断常量池描述类是否存时报出的异常,当然,如果一个需要使用的类无法在系统中找到,此时会抛出NoClassDefFoundError的异常,,一个方法无法找到,会抛出NoSuchMethodError异常。


二、什么是类装载器ClassLoader
1.ClassLoader的特点

(1)CLassLoader是一个抽象类

(2)CLassLoader的实例将读入Java字节码将类装载到JVM中

(3)CLassLoader可以定制,满足不同的字节码流获取方式。

(4)CLassLoader负责类装载过程总的加载阶段。


2.CLassLoader的重要方法

(1)public Class<?> loadClass(String name) throws ClassNotFoundException

载入并返回一个Class


(2)protected final Class<?> defineClass(byte[] b, int off, int len)

定义一个类,不公开调用


(3)protected Class<?> findClass(String name) throws ClassNotFoundException

loadClass回调该方法,自定义ClassLoader的推荐做法


(4)protected final Class<?> findLoadedClass(String name)

寻找已经加载的类



三、JDK中ClassLoader默认设计模式

与应用程序相关的ClassLoader有以下几种:

(1)BootStrap ClassLoader (启动ClassLoader)

(2)Extension ClassLoader (扩展ClassLoader)

(3)App ClassLoader (应用ClassLoader/系统ClassLoader)

(4)Custom ClassLoader(自定义ClassLoader)


这里每个ClassLoader都有一个Parent作为父亲( BootStrap除外)。

下面是ClassLoader的结构图:

【Java虚拟机探究】9.类装载器(上)_jvm_06


在该图中说明了类加载的ClassLoader的协同工作步骤:

1)自底向上检查类是否已经加载

从Custom ClassLoader/App ClassLoader向上开始检查类是否加载,如果有,则返回;如果没有,就向上面的ClassLoader请求检查该类是否加载,如果全部的ClassLoader都检查没有加载该类的话,就要尝试进行加载。

2)自顶向下尝试加载类

先由BootStrap ClassLoader做加载,如果没有加载成功,则向下面的ClassLoader请求加载。

3)类加载范围

BootStrap ClassLoader加载的核心jar包就是JVM中的rt.jar,系统的核心类都在这个jar里面,而BootStrap ClassLoader会在JVM启动的时候加载这个jar中的类。我们也可以在启动参数中设置“-Xbootclasspath”,即启动类的路径,该路径下的类也会被BootStrap ClassLoader加载。

Extension ClassLoader加载的是%JAVA_HOME%/lib/ext/下的所有jar包。

App ClassLoader加载的是ClassPath下的所有类。

Custom ClassLoader为自定义加载器,它会根据开发人员的编写逻辑加载指定的类。



四、双亲委派模型

下面是:ClassLoader中loadClass的源代码:

【Java虚拟机探究】9.类装载器(上)_BootStrap_07


在加载类的时候,先执行findLoadedClass看一下该加载器是否已经加载了该类,如果该类已经加载则返回该类,如果没有进行加载,则看一下是否存在父加载器,如果存在,就去请求父加载器进行加载,如果不存在父加载器,则直接请求启动加载器BootStrap ClassLoader进行加载。也即是递归加载,直到顶部的启动加载器BootStrap ClassLoader。

(1)模型解读

上面的加载机制就叫做双亲委派模式,双亲委派模型要求除顶层启动类加载器外其余类加载器都应该有自己的父类加载器;类加载器之间通过复用关系来复用父加载器的代码。


(2)模型作用

任意一个类,都需要由加载它的类加载器和这个类本身共同确立其在Java虚拟机中的唯一性,而JVM中两个类是否“相等”(Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定),必须是由同一个类加载器加载的才可以判定为相等。

该模式的好处是:

1)Java类随着它的类加载器一起具备了一种带有优先级的层次关系
2)防止内存中出现多份同样的字节码

层级关系可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。

3)防止自定义类与系统核心类重名,导致程序混乱

jdk等核心类库中的类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统的核心类库提供的类,自己仿写的jdk核心类根本没有机会得到加载。

倘若没有双亲委派模型,而是由各个类加载器自行加载的话,如果开发者尝试编写一个与rt.jar类库中重名的Java类(如java.lang.Object类),虽然可以正常编译,但是永远无法被加载运行(因为内存中有两个一模一样,但逻辑完全不同的类)。

所以,在双亲委派模式下,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载。

但有些时候可能我们需要破坏双亲委派模型,例如基础类会调用户的代码(JNDI)服务。下一篇我们来介绍如何解除双亲委派模式,让顶层的类加载器能加载底层的类。

 

标签:Java,虚拟机,class,public,探究,static,Son,ClassLoader,加载
From: https://blog.51cto.com/u_16012040/6166630

相关文章

  • 【FastDFS分布式文件系统】6.FastDFS客户端启动与Java连接
    上一篇我们讲解了如何配置和启动FastDFS客户端,以及客户端上传下载的一些常用命令。那么,在许多需要进行分布式文件上传与下载的系统中,就不能像执行Linux命令一样去上传和下载文件,它们需要使用开发系统的语言去操作客户端使用其命令与服务端进行交互,此时FastDFS......
  • windows 10 系统 和 VMware Workstation 虚拟机网络互通设置
    windows10系统和VMwareWorkstation虚拟机网络互通设置 1,虚拟机设置网卡地址网关地址子网掩码2,VMwareWorkstation的编辑-虚拟网络编辑器,单击进入配置,为NAT类型。3,本地笔记本电脑的虚拟网卡配置地址网关掩码4,本地笔记本电脑使用secureCRT和winscp测试,连接和上传文件都OK......
  • 性能工具之JMeter两个Java API Demo
    概述本文演示两个通过JavaAPI执行JMeter脚本的示例主要功能在线生成jmx脚本(demo1)加载本地已有jmx脚本(demo2)运行多个Sampler将生成的TestPlan存储为.jmx文件执行单机压测将测试执行结果存储为.jtlor.csv文件示例Maven配置为了开始使用JMeterAPI,我们首先需要将它添加到......
  • java稀疏数组实现实例
    没有原理讲解,仅记录一个实现代码,作为参考和笔记使用如题,稀疏数组仅在原始数组有效数据较少的情况下起压缩空间的作用实现过程:首先为了方便查看和确认,封装一个打印二维数组的方法publicstaticvoidprintArray(int[][]arrays){for(int[]array:arrays){......
  • 114.二叉树展开为链表 Java
    114.二叉树展开为链表给你二叉树的根结点root,请你将它展开为一个单链表:展开后的单链表应该同样使用TreeNode,其中right子指针指向链表中下一个结点,而左子指针始终为null。展开后的单链表应该与二叉树先序遍历顺序相同。示例1:输入:root=[1,2,5,3,4,null,6]输出......
  • 详细解析Java异步线程处理队列任务工具类以及实战
    场景待入快速理解小场景描述:【一群人】来到【一个大厅】办理业务,大厅中有【多个窗口】给我们办理业务。每个人都有自己要办事情,处理过程需要消耗时间。大厅根据人群多少,开始窗口梳理。如果把“一群人”理解成一群待处理的n个【任务】,把这群人排成一个长队就形成了一个【任......
  • 【】Java Error: Port 9095 was already in use
    问题描述JavaError:Port9095wasalreadyinuse问题原因端口被占用导致解决方案Windsow系统netstat-ano|findstr9090查询到占用9090端口的进程PID为9784。tasklist|findstr9784查询到PID为0=7984的进程打开【任务管理器】->【服务】,将对应应用关闭Lin......
  • Java 缺失的特性:扩展方法
    作者:周密(之叶)什么是扩展方法扩展方法,就是能够向现有类型直接“添加”方法,而无需创建新的派生类型、重新编译或以其他方式修改现有类型。调用扩展方法的时候,与调用在类型中实际定义的方法相比没有明显的差异。为什么需要扩展方法考虑要实现这样的功能:从Redis取出包含多个商......
  • Java 缺失的特性:扩展方法
    作者:周密(之叶)什么是扩展方法扩展方法,就是能够向现有类型直接“添加”方法,而无需创建新的派生类型、重新编译或以其他方式修改现有类型。调用扩展方法的时候,与调用在类型中实际定义的方法相比没有明显的差异。为什么需要扩展方法考虑要实现这样的功能:从Redis取出包含多个商品ID......
  • java vue获取当月第一天和最后一天,当前周一和周日
    1,vue前端,通过moment获取当月第一天和最后一天,当前周一和周日letcurrDate=moment(newDate(),"YYYY-MM-DD");varfirstDay=moment(currDate.startOf("month").valueOf()).format('YYYY-MM-DD');//获取该月份第一天的时间戳varendDay=moment(cur......