粗谈Java虚拟机3_类加载机制

1. 前言

 加载类目的是为了使用,换作说使用类前,必须先加载该类。这点不难理解,一开始.class文件还静静的躺在磁盘里,而程序运行要在内存当中。读完上篇分析 class文件 的文章,照猫画虎手写 class字节流 还真能做到,真的是可以为所欲为,直接使用会带来很大的安全问题。所以由磁盘到内存只是第一步,到真真可以使用,还需要进行各种验证,准备等步骤,这一整套下来就是类加载机制,明白这一点,学习起来就容易得多了。类的生命周期:

在这里插入图片描述

java虚拟机规范中,明确规定在开始初始化之前,必须先连接类,完成加载、验证、准备步骤,解析不要求,原文:

Prior to initialization, a class or interface must be linked, that is, verified, prepared, and optionally resolved.

规范中提到以下 6 种情况才会触发类初始化:

  • JVM 执行遇到 new、getstatic、putstatic、invokestatic 4条指令时,如果该类还未初始化,则需要先对其进行初始化。
  • 使用动态语言时,如果第一次调用 java.lang.invoke.MethodHandle 解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial的方法句柄,并且整个方法句柄对应的类还未初始化,则需要先对其进行初始化。
  • 使用 java.lang.reflect 包下的反射功能。
  • 被动触发初始化,初始化某个类时如果父类还未初始化,父类则需要先初始化。
  • 接口中定义一个非抽象、非静态的方法,初始化实现类时,会先初始化该接口,如下代码所示,在接口当中定义一个 default void 描述符的方法,初始化接口 K ,会先初始化接口 I :
public class IntfInit {
	interface I {
		int i = IntfInit.out("I::i", 1);
		default void method() { } // causes initialization!
	}
	interface J extends I {
		int j = IntfInit.out("J::j", 2);
	}
	interface  K extends J {
		int k = IntfInit.out("K::k", 3);

	}
	public static void main(String[] args) {
		System.out.println(K.k);
	}

	static int out(String s, int i) {
		System.out.println(s + "=" + i);
		return i;
	}
}

>log:

I::i=1

K::k=3

3

  • 作为 JVM 的启动类(包含 main() 方法)。
2. 加载

  加载阶段,是整个类加载机制的第一步,也是唯一能够由程序所控的一步。Java 提供了通过字节流生成Class对象的函数,足以说明加载阶段的自由性,通过磁盘、及时生成等方式都是可以的。

protected final Class<?> defineClass(String name, byte[] b, int off, int len)throws ClassFormatError{

    return defineClass(name, b, off, len, null);
}

加载阶段,会在堆内存中创建 Class 对象
在很多地方都看到过这句话,不禁产生疑问,如果 class 字节流不经过验证,确认是否安全,就创建 class 对象,是否合理?

  将一个经过 javac 编译器编译后的 class 文件,随便删除一些代码,再通过自定义加载器区加载,报 ClassFormatError,获取 class 对象失败。

public class MyClassLoader extends ClassLoader{
	protected Class<?> findClass(String name) {
		try {
			String path = "C:\\Users\\xx\\workspace\\Demo\\build\\classes\\linked\\Transform2.class";
			FileInputStream in = new FileInputStream(path) ;
			ByteArrayOutputStream baos = new ByteArrayOutputStream() ;
			byte[] buf = new byte[1024] ;
			int len = -1 ;
			while((len = in.read(buf)) != -1){
				baos.write(buf , 0 , len);
			}
			in.close();
			byte[] classBytes = baos.toByteArray();
			return defineClass(classBytes , 0 , classBytes.length) ;
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null ;
	}

	public static void main(String[] args) throws ClassNotFoundException {
		MyClassLoader myClassLoader = new MyClassLoader();
		Class<?> loadClass = myClassLoader.loadClass("");
		System.out.println(loadClass);
	}
}

  猜测:类加载器在将 calss 字节流加载到内存后,至少会进行 文件格式校验 ,才会创建 class 对象。为什么说 至少会经行文件格式校验,分别做了 2 次测试,第一次删除魔数、第二次删除了一个字符串的 符号引用,均都报了 ClassFormatError 异常。但是我尝试自定义一个类并且继承被 final 修饰的父类,通过自定义加载器和 类名.class 两种方法均都可以创建 class 对象,并且可以访问该类中定义的常量,静态变量访问不到,创建对象报错(说明没有验证元数据)。
测试了如下代码:

public class StringTest extends String {

	public  static final String  type = "1";
	
	public  static  String  str = "abc";

}
public static void main(String[] args) {
	Class<?> class1 = StringTest.class;
	System.out.println("class>>>"+class1);

	Field type = class1.getField("type");
	String anInt = (String) type.get(class1);
	System.out.println("type>>>"+anInt);

	Field str = class1.getField("str");
	System.out.println("str>>>"+(String) str.get(str));
	
	Object newInstance = class1.newInstance();
	System.out.println("object>>>"+newInstance);
}

>log:

class>>>class linked.StringTest

type>>>1

str>>>null

Exception in thread "main" java.lang.Error: Unresolved compilation problem: 

  Java虚拟机规范中没有确切的讲,加载阶段会创建 class 对象。可以这样说,类加载器会创建返回 class 对象,可能这个过程会涉及到对字节码的验证。以上观点,纯属个人看法,以后有阅读源码的能力,再来论证此观点。

3. 验证

  验证的目的是为了校验字节码是否符合规范、安全。如果验证失败则会抛出 java.lang.VerifyError 异常。验证阶段太多枯燥,具体的可参考 java虚拟机规范

  • 文件格式验证
  • 元数据验证
  • 字节码指令验证
  • 符号引用验证
4. 准备

准备阶段为类变量(静态变量)分配空间,并初始化为数据类型的默认值。同时被 final 修饰的常量,会直接在准备阶段赋为定义的值。举个例子:

//准备阶段后,serialVersionUID = 0L;
private static long serialVersionUID = 1L;
//准备阶段后,serialVersionUID = 1L;
private static final long serialVersionUID = 1L;

为此,通过断点求证了这一事实:
在这里插入图片描述常量断点走不过去,可以走到静态变量(静态变量在初始化阶段赋值)。走到静态变量时,常量已经为定义的值,说明常量已经先行一步赋为定义的值。

5. 解析

 Jvm 执行遇到 anewarray, checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual, ldc, ldc_w, multianewarray, new, putfield, and putstatic 着 16 条指令时。都需要将指令后面跟着的符号引用,替换为直接引用。如下图所示,invokespecial 指令后跟着所要执行方法的符号引用,解析阶段则是将该符号引用,替换为方法位于内存中的位置。如果该类还未加载,则会触发该类加载 ( 如果遇到初始化命令则初始化该类 )。
在这里插入图片描述 需要值得注意的是 HotSpot虚拟机 并不会在类加载阶段去解析这些符号引用,在使用时才去解析。如果在类加载阶段就解析常量池中的符号引用,加载某个类时,势必会去解析引用的多个类,这样做划不来。以下面代码为例,创建类的实例时,并不会触发成员变量引用类的类加载。

public class Person{
    //......
	private Card card;
	//......
}

public static void main(String[] args) {
	System.out.println(new Person());
}

打印JVM加载类的日志 -XX:+TraceClassLoading,从日志并未查找到 Card 类。

类或接口解析

假如当前所处类 D,要将未解析的符号引用 C 解析为直接引用 N,需要执行以下步骤:

  • 加载类 D 的类加载器去加载符号引用 C,触发 C 的类加载机制。
  • 如果类 C 是一个数组类,并且元素类型是引用类型,则按照上面第一点的方法去加载元素类,再由虚拟机创建数组对象。如果是基础类型的数组,也会在堆中开辟内存。
  • 最后检查 D 是否有权访问 C,如果无法访问到 C,则会抛出 IllegalAccessError 异常,解析失败。

如果步骤 1 和步骤 2 成功但步骤 3 失败,C 任然有效且可用。但是 D 被禁止访问 C

为了加深大家对内存分配的印象,以下面代码为例,分析论证基础类型的数组在堆中分配。

public static void main(String[] args) throws Exception {
	
	byte[] arrays = new byte[1024*1024*256];
	arrays[0] = 0;
	arrays[1] = 1;
	arrays[2] = 2;
	arrays[3] = 3;
	System.out.println("阻塞...");
	System.in.read();
}

使用 JDK 自带的 VisualVM 工具调试如下:
在这里插入图片描述
可以看到内存已经直接就飙到 200M 多了,接着再 dump 分析下堆内存:
在这里插入图片描述
数组大小和元素值,和代码中的一样。

字段解析

解析字段时,如果字段所属的类或接口的符号引用还未解析,则需要先解析该类或接口的符号引用。
如果字段所属类的符号引用解析失败,都会导致字段符号引用解析失败。解析字段时,首先尝试从其父类向上递归查找,过程如下:

  • 如果字段直接声明在当前类,查找成功。
  • 否则,递归从接口查找。
  • 否则,递归从父类查找。
  • 否则,查找失败,抛出NoSuchFieldError
  • 如果字段解析成功,但是字段无法访问,抛出 IllegalAccessError
类方法解析

类方法解析,和字段解析的第一个步骤一样,都需要先解析方法所属类的符号引用,再寻找方法位于内存中的位置。以 C 类方法为例,分析执行以下步骤:

  1. 如果 C 是一个接口,则抛出 IncompatibleClassChangeError。
  2. 否则,先从 C 类和父类查找:
  • 如果该方法本身就是 C 类的方法(非实现继承),则查找成功
  • 否则,如果 C 具有超类,则递归从父类中查找。
  1. 否则,从 C 类的实现接口开始查找。
  • 如果实现接口的方法名称和方法引用的描述符相同,并且不是抽象方法,则查找成功。
  • 如果 C 类的的接口声明了和方法引用描述符相同的方法,并且该方法不是 private、static 方法,则查找成功。
  • 否则,查找失败,抛出 NoSuchMethodError 异常

如果方法查找成功,但是无法访问,则类方法解析抛出 IllegalAccessError 异常。

接口方法解析

接口方法解析和字段解析的第一个步骤一样,都需要先解析方法所属类的符号引用。

  1. 如果 C 类不是接口,则抛出 IncompatibleClassChangeError 异常。
  2. 如果该方法本身就是 C 接口的方法,则查找成功。
  3. 否则,递归从 C 接口的父接口开始查找,包括 java.lang.Object 类。
  4. 否则,查找失败,抛出 NoSuchMethodError 异常。

如果接口方法查找成功,但是引用的方法无法访问,则接口方法解析会抛出 IllegalAccessError 异常。

6. 初始化

 初始化只干一件事:为类变量和静态代码块分配内存空间并初始化为定义的值。如果某个类存在静态变量和静态代码块,则生成 <clinit> 方法 ,如果没有则不生成。成员变量呢?凉了? 其实不然,成员变量是属于对象的。在 <init> 方法中初始化。

有静态变量和静态代码块时,自动生成 clinit 方法初始化静态变量和静态代码块,创建对象时,调用 init 方法初始化成员变量。

双亲委派原则

 JVM提供了两种类加载器:bootstrap类加载器(其它的系统类加载器都继承该类)和自定义的加载器。不同的类加载器,加载同一个源class,会创建不同的 class 对象。
还是以上面的自定义类加载为例,只不过将 StringTest 类改为能编译通过的代码。

public static void main(String[] args) throws Exception {
	MyClassLoader myClassLoader = new MyClassLoader();
	Class<?> loadClass = myClassLoader.loadClass("");
	System.out.println( loadClass.newInstance() instanceof StringTest);
}

>log 

false

 类加载器在收到某个类的加载请求时,自己不会去加载这个类,而是将加载请求委派给父类去处理,每个层次的加载器都是如此。因此所有的类加载请求都会委派到 bootstrap类加载器 手中。只有当父加载器提出自己无法处理该请求时(超出规定的加载范围),子加载器才会尝试自己去加载。

 使用双亲委派模型来组织类加载器之前的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如java.lang.Object类,它存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都会委派给最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载环境中都是同一个类。相反,如果没有使用加载器进行加载,由各个类加载器自行去加载的话,加入用户自己编写了一个java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类。Java体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。

Python网络爬虫与推荐算法新闻推荐平台:网络爬虫:通过Python实现新浪新闻的爬取,可爬取新闻页面上的标题、文本、图片、视频链接(保留排版) 推荐算法:权重衰减+标签推荐+区域推荐+热点推荐.zip项目工程资源经过严格测试可直接运行成功且功能正常的情况才上传,可轻松复刻,拿到资料包后可轻松复现出一样的项目,本人系统开发经验充足(全领域),有任何使用问题欢迎随时与我联系,我会及时为您解惑,提供帮助。 【资源内容】:包含完整源码+工程文件+说明(如有)等。答辩评审平均分达到96分,放心下载使用!可轻松复现,设计报告也可借鉴此项目,该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的。 【提供帮助】:有任何使用问题欢迎随时与我联系,我会及时解答解惑,提供帮助 【附带帮助】:若还需要相关开发工具、学习资料等,我会提供帮助,提供资料,鼓励学习进步 【项目价值】:可用在相关项目设计中,皆可应用在项目、毕业设计、课程设计、期末/期中/大作业、工程实训、大创等学科竞赛比赛、初期项目立项、学习/练手等方面,可借鉴此优质项目实现复刻,设计报告也可借鉴此项目,也可基于此项目来扩展开发出更多功能 下载后请首先打开README文件(如有),项目工程可直接复现复刻,如果基础还行,也可在此程序基础上进行修改,以实现其它功能。供开源学习/技术交流/学习参考,勿用于商业用途。质量优质,放心下载使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值