类加载子系统分成三个重要的阶段:加载阶段、链接阶段、初始化阶段
类加载系统的作用:将物理硬盘、网络等一些环境将静态的字节码.calss文件加载到内存当中,并且在堆空间中生成与之对应的java.lang.Class对象,作为方法区这个类的各种数据的访问入口,ClassLoader只负责class文件的加载,至于其运行还得需要JVM中的执行引擎来执行。
加载类的信息存放在运行时数据区中的方法区的区域。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量何数字常量。
1.1.1、通过一个类的全限定名获取该类的二进制流
1.1.2、将代表该类的静态存储结构的二进制流加载到名为方法区的运行时数据区的位置
1.1.3、在堆内存当中生成一个与之对应的java.lang.Class对象,作为方法区内该类的各种数据的访问入口
常见的加载.class文件的方式 .从本地磁盘系统中直接加载 .通过网络获取,典型场景:web applet .从.zip压缩包中读取,jar,war的形式读取 .其他文件生成,例如:JSP应用1.2 链接-linking
1.2.1、验证(Verify)
目的在于确保Class文件的字节流中的信息满足JVM的要求,保证类加载的过程是安全的,不会危害虚拟机的自身安全。
主要包含四种验证:
1.2.1.1、文件格式验证:
.字节码文件是否是魔数(CA FE BA BE)开头(可以下载一个二进制文件查看工具查看.class文件) .主、次版本号是否在当前虚拟机的处理范围之内 .常量池中的常量是否有不被支持的常量类型 .指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量 .Class文件中各个部分及文件本身是否有被删除的或附加的其他信息(文件的完整性) 注:文件格式验证是基于二进制字节流进行的,只有通过了文件格式验证,字节流才会真正进入到内存的方法区中进行存储,后面的3个验证阶段都是基于方法区的存储结构进行的,不会再直接 *** 作字节流
1.2.1.2、元数据验证:
对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,这个阶段可能包括的验证点: .这个类是否有除了java.lang.Object之外的父类 .这个类的父类是否继承了被final修饰的类 .如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法 .类中的字段、方法是否与父类产生矛盾
1.2.1.3、字节码验证:
文件格式校验、元数据验证保证了文件的二进制流的合法性(满足java类信息的要求)、语义的合法性,字节码验证是为了保证方法在执行的过程中的安全性: .保证任意时刻 *** 作数栈的数据类型与指令代码序列都能配合工作 .保证跳转指令不会跳转到方法体以外的字节码指令上 .保证方法体中的类型转换是有效的
1.2.1.4、符号引用验证(解析阶段)
.符号引用中通过字符串描述的全限定名是否能找到对应的类。 .在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段 .符号引用中的类、字段、方法的访问性是否可被当前类访问
1.2.2、准备(Prepare)
作用:为类的静态变量(static修饰的变量)分配内存并将其初始化为默认值,这些内存都将在**方法区**中分配。 注意的知识点: 1、 实例变量(非静态变量)不在该阶段分配内存,实例变量在后面将要说到的初始化过程中分配;被final修饰的常量,在该阶段直接赋开发者定义的值。 2、这里的初始化赋值不是开发者显示定义的值,而是JVM在该阶段给不同类型的变量自动进行默认赋值: 基本数据类型的**类变量**(static)在该阶段会根据不同的类型进行默认赋值(boolean:false;char:'/uoooo'(null);byte:(byte)0;short:(short)0;int:0;long:0L;float:0.0f;double:0.0d),**局部变量**来说,在使用前必须显式地为其赋值,否则编译时不通过. 3、对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值 4、对于引用数据类型来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。 5、如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值
1.2.3、解析 (Resolve)
作用:将常量池中的符号引用解析为直接引用。 符号引用:一组符号来描述目标,可以是任何字面量; 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄 解析动作: 1、类或接口的解析 如果字节码文件代表的类A中在解析的过程中出现一个从未解析过的符号引用B,解析过程会出现下面几种情况: (1)、如果符号引用对应的不是一个数组类型,JVM会将这个符号引用B的全限定名传递给加载A这个类的类加载器去加载符号引用B对应的类:C; (2)、如果符号引用对应的是一个数组类型,且是一个对象数组,非基本数据类型的数组,会按照(1)的规则去加载数组里面对象对应的类型的类:C (3)、如果上面两种情况都没有发生异常,那么C在虚拟机中实际上已经成为一个有效的类或接口了(被JVM加载过的),但在解析完成之前还要进行符号引用验证,确认B是否具备对C的访问权限。 2、字段解析 要解析一个未被解析过的字段,首先要对这个字段所属的类或者接口进行解析(上面步骤)如果类或者接口解析完成才会开始对这个类对应的这个字段进行解析,解析过程: (1)、如果C类中本身就包含这个字段,直接就返回这个类中对应这个字段的直接引用即可。 (2)、如果C类中不包含这个字段,就往父类或者实现的接口从下往上去找,如果存在父类和接口中同时都定义了,就会出现Error异常,如果不出现该异常,找到了与目标匹配的字段,则直接返回其直接引用 (3)、如果上面两种方式都没找到,就会抛出java.lang.NoSuchFieldError异常 (4)、最后,如果查找过程成功返回了直接引用,就会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError异常。 3、类方法解析 和字段解析类似,要解析一个为被解析的方法,首先要对这个方法所属的类或者接口进行解析,如果类或者接口解析完成才会对这个类对应的这个方法进行解析,解析过程: (1)、类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现C是个接口,直接抛出异常(类方法表出现C是个接口,说明C这个接口没有任何实现,又属于类方法而不是属于接口方法这种情况是非法的)。 (2)、如果C不是接口,首先会到C中查找与目标匹配的方法,如果查找到了,直接返回该方法的直接引用,类方法解析阶段结束。 (3)、如果C本身不包含这个方法,则会往父类从下往上进行寻找匹配,如果匹配成功则直接返回其方法的直接引用,如果还没查找成功,往其接口、父接口......进行查找,如果匹配成功也会抛出异常,说明该方法没有任何实现 (4)、都没查找到,则宣告方法查找失败,抛出java.lang.NoSuchMethodError异常。 最后,如果查找过程成功返回了直接引用,就会对这个方法进行权限验证,如果发现不具备对此方法的访问权限,将抛出java.lang.IllegalAccessError异常。 4、接口方法解析 接口方法和上面类方法,字段解析一样,也要进行解析出接口方法所属的类或接口的符号引用,如果解析成功,依然用C表示这个接口,接下来虚拟机将会按照如下步骤进行后续的接口方法搜索,解析过程: (1)、如果在接口方法表中发现C是个类而不是接口,直接抛出异常(和上面类方法表类似)。 (2)、否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。 (3)、否则,在接口C的父接口中递归查找,直到java.lang.Object类为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。 (4)、否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError异常。 由于接口中的所有方法默认都是public的,所以不存在访问权限的问题。1.3 初始化-Initialization
初始化阶段:初始化类变量和静态代码块(非类变量即非静态变量和非静态代码块不是该阶段的职责)
具体过程就是执行clinit里面的内容(这块可以加载一个jclasslib 工具可以查看字节码.class文件对应的clinit的内容)
注: 1、这个clinit方法是不需要开发者去定义的,这个javac编译器会自动收集类的所有类变量的赋值动作和静态代码块的语句合并而来的。 2、这个方法的指令是按照在源文件中出现的顺序执行的 3、clinit不同于类的构造器init(init的执行时机是在类的实例化过程当中) 4、子类的clinit执行前,父类的clinit一定会先被执行,所以虚拟机执行的第一个类的clinit就是Object的clinit 5、虚拟机必须保证一个类的clinit在多线程下被同步加锁,也就一个类的clinit只会被执行一次
验证第5点:
public class ClinitTest { public static void main(String[] args) { Runnable r = () -> { System.out.println(Thread.currentThread().getName() + "开始"); DeadThread dead = new DeadThread(); System.out.println(Thread.currentThread().getName() + "结束"); }; Thread t1 = new Thread(r,"线程1"); Thread t2 = new Thread(r,"线程2"); t1.start(); t2.start(); } } class DeadThread{ static{ if(true){ System.out.println(Thread.currentThread().getName() + "初始化当前类"); while(true){ } } } }
执行结果: 线程1开始 线程1初始化当前类 线程2开始
类初始化时机:只有当对类主动使用的时候才会导致类的初始化,类的主动使用方式:
.创建类的实例,也就是new的方式; .访问某个类或接口的静态变量,或者对该静态变量赋值; .调用类的静态方法; .反射(如Class.forName("…")); .初始化某个类的子类,则其父类也会被初始化; .Java虚拟机启动时被标明为启动类的类,直接使用java.exe命令来运行某个主类。2、类加载器的分类
JVM的类加载器主要分两大类,一种是引导类加载器(启动类加载器),它是用非java语言开发的,而是用C/C++开发的,另外一种是自定义类加载器(java代码实现的,间接实现了ClassLoader类)。
除了引导类加载器没有父类加载器(注意这里的父类不是java语言所说的继承关系,而是一种上下包含关系,后面讲到的双亲委派机制也是依赖于这种关系实现的),其他的自定义加载器都会有父类加载器的。
2.1 引导类加载器 .该加载器是使用C/C++语言实现的,嵌套在JVM内部 .它用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar、resource.jar或sun.boot.class.path类路径下的内容),用于提供JVM自身需要的类 .不继承java.lang.ClassLoader,没有父加载器 .加载拓展类加载器和系统类加载器,并指定为他们的父类加载器 .处于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun开头的类 2.2 JVM自带的两种自定义加载器 2.2.1 拓展类加载器 .java语言开发的,由sun.misc.Launcher$ExtClassLoader实现 .派生于ClassLoader .父类加载器为启动类加载器 .从java.ext.dirs系统属性所指定的目录加载类库,或从JAVA_HOME/jre/ext子目录下加载类库。如果用户创建的JAR放在该目录下,也会自动被拓展类加载器加载 2.2.2 应用类加载器 .java语言编写,由sun.misc.Launcher$AppClassLoader实现 .派生于ClassLoader .父类加载器为拓展类加载器 .该类加载器是程序的默认的类加载器,一般来说,Java应用的类都是由它来完成加载的 .负责加载环境变量classpath或系统属性java.class.path指定路径下的类库 .通过ClassLoader#getSystemClassLoader()方法可以获取该类加载器 2.3 开发者自定义类加载器 实现步骤: .写一个自定义类加载器并且实现java.lang.ClassLoader .JDK1.2后(之前就不介绍),实现ClassLoader的findClass()方法
package com.example.algorithm; import java.io.*; public class MyClassLoader extends ClassLoader { @Override protected Class> findClass(String name) { Class> log = null; byte[] classData = getData(name); if (classData != null) { log = defineClass(name, classData, 0, classData.length); } return log; } private byte[] getData(String name) { File file = new File(name); if (file.exists()) { FileInputStream in = null; ByteArrayOutputStream out = null; try { in = new FileInputStream(file); out = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int size = 0; while ((size = in.read(buffer)) != -1) { out.write(buffer, 0, size); } } catch (IOException e) { e.printStackTrace(); } finally { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } return out.toByteArray(); } else { return null; } } }3、 双亲委派机制
java虚拟机对字节码文件.class是采用的是按需加载的(类初始化时机),也就是说当需要这个的时候才会将这个字节码文件加载到内存当中,并且在堆内存生成一个java.lang.Class对象与在方法区这块数据区进行对应。而且加载某个类的class的时候,JVM采用的是双亲委派模式,即先把请求交给自己的父类加载器进行处理。
工作原理: 1、如果一个类加载器收到一个类加载的请求,它并不会第一时间自己去加载该类,而是先把这个类交给自己的父类加载器处理。 2、如果这个类是属于这个父类加载管理的类库下的类,则直接交给这个父类加载器进行加载该类,否则继续查找该父类加载器的父类加载器,逐渐往上委托,如果有父类加载进行加载则加载完成,如果这个类都不属于所有父类加载器所管理的,子加载器才会尝试自己去加载,这就是双亲委派模式。
例子:儿子收到一个苹果,自己先不吃,给自己的父亲吃,父亲不吃,再给爷爷吃,爷爷不吃最终才会轮得到自己吃。
优势: .避免类的重复加载 .保护程序的安全性,防止核心类库中的类被随意篡改: 例如自定义一个:java.lang.String 会报错(沙箱安全机制)4、 沙箱机制
作用:防止恶意代码污染java源代码
举例:定义了一个类名为String所在包为java.lang,因为这个类本来是属于jdk的,如果没有沙箱安全机制的话,这个类将会污染到我所有的String,但是由于沙箱安全机制,按照双亲委派模型最终顶层的bootstrap类加载器查找这个类,所以最终加载的java.lang.String是启动类加载器所管理的类,而不是我们自定义的String这个类,这就保证了核心类库不被恶意代码污染
还可以参考一下别人的文章:
沙箱安全机制
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)