jvm学习-ClassLoader(二)
修订历史
2018-06-19 20:22 创建
2020-01-08 改 最近在看java字节码技术 重新复习一下 并重新整理
ClassLoader作用
- 负责将 Class 加载到 JVM 中
- 审查每个类由谁加载(父优先的等级加载机制)
- 将 Class 字节码重新解析成 JVM 统一要求的对象格式
类加载时机与过程
类从被加载到虚拟机内存中开始,直到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)。
加载
加载阶段主要做一下三件事情
- 通过一个类的名称获取类的字节流(可以从classpath加载远程加载)
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构(我的理解就是存储了包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等)
- 在java堆生成一个class对象
验证
为了确保class 字节流信息要求(文件格式,元数据,字节码,符号引用)
文件格式
是要验证字节流是否符合Class文件格式的规范
- 是否以魔数0xCAFEBABE开头(主要作用是判断当前文件是不是一个class文件 java文件通过编译器编译后都会以0xCAFEBABE开头)
- 主次版本是否在当前虚拟机处理范围之内(当在jdk1.8运行过得代码放到1.7再运行不知道大家是否遇到过这个错误Unsupported major.minor version 主要为了解决这种1.8编译后的代码再低版本不兼容问题 )
- 是否有不支持的常量类型(我的理解就是 public final static ddd i=1 可能会说那不是编译不过,因为我们类加载不一定是编译的class文件 只要是字节流就行)
...............
这一步验证通过才会将class信息转成能够存储到方法区的数据格式 后续操作都是操作方法区存储的数据
元数据验证
- 验证这个类是否有父类,除了Object外所有类都应该有父类(我们开发中定义没有父类的类 编译后都继承Object)
- 这个类是否继承了final修饰的类
- 如果这个类不是抽象类,是否实现了其父类或接口中要求实现的方法。
- 类中的字段方法是否与父类产生矛盾(如:覆盖了父类的final字段,出现了不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)
这一步主要是保证对元数据的语义进行校验,避免出现不符合java规范的元数据
字节码验证
主要是对字节码的语义进行验证,确保语义是符合逻辑的
- 保证跳转指令不会跳转到方法体以外的字节码指令上。详见<1>
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现在操作栈放置一个int类型的数据,使用时却按long类型来加载入本地变量表中。(如:int i=1L)
- 证证方法体中的类型转换时有效的,例如把子类对象赋值给父类数据类型是安全的,但是把父类对象赋值给子类是不合法的 如(Student stu=new Person();
........
<1>
go: public void test(){ do{ if(i>0){ break go; } }while (true); }
符号引用验证
- 符号引用中通过字符串描述的全限定名是否能找到对应的类
- 在指定类中是否存在符合方法字段的描述符及简单名称所描述的方法和字段
- 符号引用中的类、字段、方法的访问性(private,protected,public,default)是否可以被当前类访问。
如果符号引用验证不通过会报
java.lang.IncompatibleClassChangError异常的子类如java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。
什么是符号引用
public void test(){ Person person=new Person(); System.out.println(person.getName()); }
编译的时候生成的class文件并不知道person的引用地址,所以先保存一个符号标识 这里主要就是检查这个符号标识对应的类是否存在 以及操作是否符合规范
准备
为类的变量(static修饰的变量) 在方法区进行分配内存空间 但是并没有初始化 如 static int i=3 分配内存空间后 int i=0; object o=new object(); 我个人理解 会设置null 在栈分配一个能够存放指针大小的空间
注意:final修饰的staticf是直接赋值 而不是赋值默认值
解析
解析不一定会按上面先解析再初始化的顺序 也可能在初始化之后
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
初始化之前
因为final在准备的时候就直接赋值了
final static Person person=new Person(); public void test(){ System.out.println(person.getName()); }
准备阶段已经在栈开辟了一个能够存放指针的空间 由编译的符号引用改为栈空间直接引用
初始化之后
非final在初始化阶段才进行初始化
运行时绑定
static Person person=new Person(); public void test(){ System.out.println(person.getName()); }
初始化
从上到下解析执行静态变量的初始化包含静态代码块(注意是顺序哦)
public class Test2 { public Test2(){ System.out.println("我是test2"); } } public class Test1 { public static Test2 test2=new Test2(); static{ System.out.println("我是静态代码块"); } public static int i=5; public static void run(){ System.out.println("run:"+i); } } @SpringBootTest class LogbackTestApplicationTests { @Test void testInfo() { Test1.run(); } }
输出
我是test2
我是静态代码块
run:5
我们改一下顺序
public class Test1 { static{ System.out.println("我是静态代码块"); } public static Test2 test2=new Test2(); public static int i=5; public static void run(){ System.out.println("run:"+i); } }
输出
我是静态代码块
我是test2
run:5
分析题目1
class SingleTon { private static SingleTon singleTon = new SingleTon(); public static int count1; public static int count2 = 0; private SingleTon() { count1++; count2++; } public static SingleTon getInstance() { return singleTon; } } public class Test { public static void main(String[] args) { SingleTon singleTon = SingleTon.getInstance(); System.out.println("count1=" + singleTon.count1); System.out.println("count2=" + singleTon.count2); } }
通过以上理解猜猜输出啥
错误答案 count1=1 count2=1 正确答案 count1=1 count2=0
1.SingleTon.getInstance() 触发类加载
2.准备阶段为静态变量分配内存空间 singleTon=null,count1=0,count2=0;
3.初始化阶段 根据从上到下的顺序先初始化singleTon=new SingleTon();触发构造函数 count1++ count2++ count=1 count2=1;
4.开始初始化count1;因为没有赋值操作 所以什么都不做 count1还是1
5.开始执行count2的初始化 count2=0;
分析题目2
例子1
public class Test1 { public static int i=5; static{ i=7; } public static void run(){ System.out.println("run:"+i); } }
调用Test1.run方法输出啥
例子2
public class Test1 { static{ i=7; } public static int i=5; public static void run(){ System.out.println("run:"+i); } }
调用Test1.run输出啥
例子1输出7 例子2输出5
什么时候会触发类的初始化
主动引用
- 创建类的实例 Person person=new Person();
- 访问类的静态变量(除开final修饰)
- 调用类的静态方法
- 使用java.lang.reflenct包的方法对类进行放射调用,如果没有进行初始化,则需要触发其初始化
- 当一个类初始化的时候,如果其父类还没有初始化,则需要先对其父类进行初始化。
- 当虚拟机启动时,用户需要指定一个执行的主类,虚拟机会首先初始化这个主类
主动引用都会触发类的初始化
被动引用
主动引用之外的引用情况都称之为被动引用,这些引用不会进行初始化。
1.通过子类引用父类的静态字段,不会导致子类初始化
class SupperTest { public static int superIndex=1; static { System.out.println("supperTest输出"); } } class Test extends SupperTest{ static{ System.out.println("我是Test"); } } class Init4 { public static void main(String[] args) { int value = Test.superIndex; } }
打印
supperTest输出
2.通过数组定义来引用,不会触发此类的初始化
class SupperTest { public static int superIndex=1; static { System.out.println("supperTest输出"); } } class Test extends SupperTest{ static{ System.out.println("我是Test"); } } class Init4 { public static void main(String[] args) { Test[] tests=new Test[10]; } }
空白输出
3.常量在编译阶段会存入调用类的常量池中,本质没有直接引用到定义的常量类中,因此不会触发定义的常量类初始化
class SupperTest { public static final int superIndex=1; static { System.out.println("supperTest输出"); } } class Init4 { public static void main(String[] args) { int i=SupperTest.superIndex; } }
java的类加载器实现
Bootstrap ClassLoader
是虚拟机的一部分,由c++实现, <JAVA_HOME>/lib
路径下的核心类库或-Xbootclasspath
参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)
Extension ClassLoader
主要负责加载 <JAVA_HOME>/lib/ext
目录下或者由系统变量-Djava.ext.dir指定位路径中的类库
private static File[] getExtDirs() { //Launcher$Extension 获得目录 String var0 = System.getProperty("java.ext.dirs"); File[] var1; if (var0 != null) { StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator); int var3 = var2.countTokens(); var1 = new File[var3]; for(int var4 = 0; var4 < var3; ++var4) { var1[var4] = new File(var2.nextToken()); } } else { var1 = new File[0]; } return var1; }
AppClassLoader
它负责加载系统类路径java -classpath
或-D java.class.path
指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()
方法可以获取到该类加载器。
在应用程序 我们自定义类就是由它加载
class Test{ } class Init4 { public static void main(String[] args) { System.out.println(Test.class.getClassLoader()); } } 输出:sun.misc.Launcher$AppClassLoader@18b4aac2
什么是双亲委托
双亲委托下 除了顶层加载器,其他加载器都必须有一个父类加载器,不是继承关系 而是组合关系。
当一个类加载器获得加载的请求时,会判断自己是否有加载如果没有加载则委托给他的parent 如果父类能够完成加载则加载返回如果不能则继续向上委托。所有父类都不能完成才自己加载
双亲委托的优势
类不会重复加载.jdk核心api并不会直接替换 比如加载一个网络传输的java.lang.Integer类。传递到BootStrap 加载器 发现已经加载过了 直接返回加载过得java.lang.Integer
如果加载不是核心api里面的类如:java.lang.SingleInterge 因为包是核心api 包 这样也是不允许加载会报错
java.lang.SecurityException: Prohibited package name: java.lang
类图
ClassLoader
最顶的classLoader是一个抽象类 所有类加载器都需要继承这个类
loadClass
由下面逻辑可以看到双亲委托的逻辑
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded //从当前类加载器判断是否有加载过此类 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { //父加载器不为空 if (parent != null) { //交给父加载器加载 c = parent.loadClass(name, false); } else { //如果没有父加载器交给BootStrap加载器加载 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } //父加载器没有加载到 if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); //调用findClass 是一个空实现 自定义加载可以重写此方法 实现自己的加载逻辑 c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) {//是否需要加载时进行解析 resolveClass(c); } return c; } }
findClass
可以看出是个空实现
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
Class.forName的作用
使用调用者的class对象的ClassLoader进行加载 也提供一个重载指定classLoader
双亲委托打破
Tomcat中的web 容器类加载器也是破坏了双亲委托模式的,自定义的WebApplicationClassLoader除了核心类库外,都是优先加载自己路径下的Class;
class何时被回收
参考:https://www.cnblogs.com/LQBlog/p/9205580.html#autoid-2-0-0