【八股学习】JVM——类加载、类加载器、双亲委派模型

1、类加载过程

类从被加载到虚拟机内存中开始到卸载出内存为止。
系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析

①加载

虚拟机加载Class文件的方式:
首先是第一步:1.全类名获取定义该类的二进制字节流 2.字节流代表的静态存储结构转换为方法区的运行时数据结构 3.内存中生成一个代表该类的 Class 对象,作为方法区数据的入口
主要通过类加载器完成,并通过双亲委派模型决定加载的具体类。

②验证

连接的第一步,确保 Class 文件中的字节流中的信息都符合虚拟机规范。
这一步骤会消耗很多资源,所以生产环境中可以使用 -Xverify:none 参数关闭大部分类验证措施。
文件格式验证是唯一一个对字节流进行读取和操作的验证阶段,为保证字节流可以解析并存储在方法区内。
符号引用验证是JVM将符号引用转化为直接引用,可能会抛出异常例如:NoSuchFieldError、NoSuchMethodError、IllegalAccessError

③准备

正式为类变量分配内存并设置类变量初始值的阶段
1、只对类变量,即静态变量分配内存
2、JDK7之前,类变量的内存都在方法区中分配。JDK7之后,字符串常量池和静态变量移入
3、初始值是默认的零值,如下图。
在这里插入图片描述

④解析

将常量池中的符号引用转换为直接引用。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。
1、符号引用:使用字面量,可以定位到目标,与不同虚拟机的实现布局无关,引用的目标不一定已被加载到虚拟机中。
2、直接引用:可以直接指向目标的指针等,可以定位到目标,和虚拟机内存布局勇冠,引用目标必须已经在虚拟机内存中。

⑤初始化

这一步JVM才开始执行类中的字节码。
只有主动使用类才会初始化类,例如如下情况:
1、new 一个类,读取静态字段(不是被final修饰的静态常量,这个会被放在运行时常量池),调用类的静态方法,给静态变量赋值。
2、对没有初始化的类进行反射
3、初始化一个类,对父类进行初始化
4、虚拟机初始化一个要执行的主类
5、MethodHandle 和 VarHandle
6、当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

⑥类卸载

TODO:待补充GC

2、类加载器

ClassLoader 是一个抽象类,给定二进制类名,通过定位或生成构成类的数据。策略:二进制转化为文件名,从文件系统中直接读取。
每个Java类都有一个引用指向加载它的 ClassLoader ,数组类是通过JVM自动创建的,因为其没有二进制字节流。数组类通过getClassLoader()方法获取 ClassLoader 的时候和该数组的元素类型的 ClassLoader 是一致的。

即将Java类的字节码 .class 文件加载到JVM中,在内存中生成一个Class对象。

①加载规则

按需加载,内存友好
已加载的类会存放在ClassLoader中,如果没被加载过才会加载。

②类加载器分类

JVM中内置了三个 ClassLoader:
1、BootstrapClassLoader(启动类加载器):C++实现,所以在Java中表示为null,最顶层的加载器,用来加载**%JAVA_HOME%/lib**下的jar包和类以及被 -Xbootclasspath参数指定的路径下的所有类。
2、ExtensionClassLoader(扩展类加载器):主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。
3、AppClassLoader(应用程序类加载器):面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。

除此之外,我们可以自定义类加载器,通过继承 ClassLoader 抽象类,有两个关键方法(建议重写第二个,因为第一个会打破双亲委派模型):
1、protected Class loadClass(String name, boolean resolve):加载指定二进制名的类name,resolve 为 true 则加载时调用 resolveClass 方法进行解析。
2、protected Class findClass(String name):根据类的二进制名称来查找类,默认实现是空方法。

3、双亲委派模型

简单来说:每个ClassLoader实例都有一个相关的父类加载器,需要查找类或资源时,其会在亲自查找之前将这个任务委托给父类加载器。

补充类加载器之间的父子关系通常是通过组合来复用父加载器的代码。

①执行流程

即**loadClass()**的执行流程
1、检查该类是否已经加载过,c不为null则直接返回

Class c = findLoadedClass(name);

2、如果c为null,并且父类加载器不为空,则执行父类的loadClass方法尝试加载

if (parent != null) { 
                    c = parent.loadClass(name, false);
                }

3、如果父类加载器为空,则调用启动类加载器来加载

c = findBootstrapClassOrNull(name);

4、如果父类加载器无法加载时,当前加载器才会使用**findClass()**方法来加载该类

②好处

补充:JVM判断两个类是否相同:类全名是否相同,并且均由同一个类加载器加载,才是相同的类。

所以,双亲委派模型可以保证避免类被重复加载,避免篡改Java的核心API。
例如自己编写的java.lang.Object类,程序在运行时由于最终会传递到 BootstrapClassLoader ,会直接返回Object类,避免了加载自己所编写的类的情况。

③打破双亲委派模型

此前已经说了,如果想要打破这个模型,需要重写loadClass() 方法,因为其主流程就是在这个方法中实现的。

④自定义类加载器的局限

有些情况下会遇到,高层的类加载器需要加载低层的加载器才能加载的类。
例如 SPI的接口和其实现 一般是要求由同一个类加载器加载,SPI可以由BootstrapClassLoader加载,但其实现是无法通过这个加载器找到的。
解决方法:线程上下文类加载器
通过在线程中设置低层的类加载器,可以让高层的类加载器获取到以加载业务相关的类。
在Tomcat、Spirng、Jetty中都有相关涉及。

cl = Thread.currentThread().getContextClassLoader();
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值