目录
1,jvm内存结构的布局
看看jvm的内存布局,我们的java程序经过编译器编译为字节码文件之后,这些字节码文件描述的数据信息是需要被加载到虚拟机中才可以被运行使用,而上面图上的类加载子系统就是负责把我们的字节码文件加载到虚拟机的固定区域。在java语言中类的加载,连接和初始化都是在程序的运行期间完成的,java可以动态扩展的语言 的特性就是依赖运行时期动态加载和动态链接这个特点实现的。
- 在看来加载子系统之前,我们先来看看jvm的全貌,这有利于加深我们对jvm的理解。
- 下面这张全局图对上面进行翻译:
- 加载字节码文件经过三个步骤:加载--->链接(验证---准备---解析)--->初始化,严格来说,一个字节码文件从被加载到虚拟机中开始直到被卸载出内存为止,完整的声明周期要经过:加载---链接(验证---准备---解析)---初始化---使用---卸载等过程。
- 方法区只有hotspot虚拟机有,另外两大商业虚拟机没有。
2,类加载子系统的作用
作用:
-
类加载子系统负责从本地文件或者网络文件中加载class文件,class文件开头有特定的标识符。
- classloader负责class文件的加载,至于他是否可以运行,由执行引擎决定execution engine。
- 加载的类信息存放在一块称为方法区的内存空间(也可以说是堆内存空间),除了类的信息外,方法区还会存放运行时常量池信息,可能还包括字符串常量和数字常量(这一部分常量信息是class文件中常量池部分的内存映射)。
- 加载完成后,java虚拟机外部的二进制字节流就会按照虚拟机所设定的格式存储在方法区之中,方法区的数据格式存储完全是由具体的虚拟机实现而确定的。然后会在java的堆内存中生成一个Class对象,这个对下行作为程序访问方法区的类型数据的外部入口。
类加载的过程:
3,类加载器(class loader)
-
class file存储于本地磁盘上面,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候要加载到jvm当中,根据这个模板实例化n多个一模一样的实例。
- class file加载到jvm当中,b被称为DNA元素的模板,存储在方法区。
- 在.class--->jvm---->最终成为元数据模板,此过程只要一个运输工具,类装载器(class loader),扮演者一个快递员的角色。
- 其中class文件加载到内存中是以二进制流的方式进行加载,一个对象通过getclass()方法还可以获取是哪一个类的对象。
4,类的加载过程
4.1,类的加载阶段(狭义上的加载)
加载阶段(Loading)
- 通过一个类的权限定名,获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区(jdk7前叫做永久代,之后叫做元数据空间,都是方法区的落地实现)的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据结构的访问入口。
4.2,类的链接
4.2.1,验证阶段(Verify)
- 为什么需要验证阶段?
为什么要进行验证,因为从java语言的角度来看,写好的程序经过编译之后的字节码文件应该是没有什么问题的,但是从jvm角度来看,读取到的class文件可以从任意地方,这就导致可能读入危害jvm的字节码文件,所以需要进行验证阶段,检查字节码文件是否是安全的。
目的在于确保.class文件的字节流中包含的信息符合当前的虚拟机的要求,保证被加载的正确性,不会危害虚拟机自身的安全。
- 主要包括四种验证方式
- 文件格式验证,不同文件文件头不一样。(主要验证字节流是否符合class文件格式的规范,也就是保证数据可以被存储到数据区里面,后面三部分验证都是基于这个验证之上的)。
- 元数据验证。(也就是语义的检验,要求语义符合java语言的规范)
- 字节码验证。(通过数据流分析和控制流分析,确定语义是合法的,符合逻辑)
- 符号引用验证(也就是将符号引用转换为直接引用)
4.2.2,准备阶段(prepare)
- 为类变量(static修饰的变量)设置内存和并且设置该类变量的初始值,即0。注意:这里的初始值是8中基本数据类型和一种引用类型的基本初始值,此时还是在jvm层面的初始化,还没有执行java代码的任何构造函数,所以是从虚拟机角度对变量进行初始化。
- 这里不包含用final修饰的static(也就是常量),因为final修饰的变量在编译阶段就已经分配空间了,也就是已经放入方法区的常量池之中,准备阶段会显示初始化。
- 这里不会为实例变量分配初始化(此时还没有创建对象),类变量会分配在方法区中,而实例变量会随着对象一起被分配到java的堆中。
4.2.3,解析阶段(Resolve)
- 将常量池中的符号引用转换为直接引用的过程。
- 解析操作往往会伴随着jvm在执行完初始化之后再执行。
- 符号引用就是一组符号来描述所引用的目标,符号引用的字面量形式明确定义java的class文件中,直接引用就是直接指向目标的指针,相对偏移量或者一个间接定位到目标的句柄。
- 解析动作主要针对类或者接口,字段,类方法,接口方法,方法类型等,对应常量池中的constant_class_info,constant_fieldref_info,constant_Methodref_info等。
4.3,初始化阶段
-
初始化方法就是执行类构造器方法clinit()过程。(注意这里是执行类构造器clinit()的过程)
- 此方法不需要自己定义,是javac编译器中自动收集类中所有的类变量的赋值动作和静态代码块中的语句合并而来的。如果没有这种操作,也就没有clinit()方法,
我们注意到如果没有静态变量c,那么字节码文件中就不会有clinit方法
。
- 构造方法中的指令按照源文件中出现的顺序执行。(也就是所有变量的赋值操作会按照文件中赋值顺序依次赋值,后面的赋值会覆盖前面的赋值)。
- clinit()方法不同于类的构造器,构造器是虚拟机视角下的init()方法。
- 如果该类具有父类,jvm会保证子类的clinit()方法执行前,父类的clinit()方法已经执行完毕,所以父类中定义的静态代码块一定要早于子类变量的赋值操作,这点在写程序时需要小心。
- 虚拟机必须保证一个类的clinit()方法在多线程下被同步加锁。
- 如果类或者方法中没有给变量赋值或者静态代码块,那么就没有调用此方法,对每一个类进行反编译都会产生一个init()方法,init()方法对应的就是类的构造器的方法。
- 可以从另一个角度去理解初始化阶段:初始化过程就是执行类构造器clinit()方法的过程,此方法并不是程序员写的,而是编译器自动生成的,此方法与类的构造函数不同,也就是init()方法,clinit()方法不需要显示的斯奥用父类的构造器,但是构造方法必须调用父类的构造器。
- clinit()方法对于一个类来说并不是必须的,如果一个类中没有静态代码块或者类变量,也就没有此方法,接口中不能有静态代码块,但是仍然有变量初始化的赋值操作,因此接口和类一样会生成clinit()方法,但是和类不同的是,执行接口的clinit()方法前不需要先执行其父类的clinit()方法,因为只有父类接口中的变量被使用的时候,才会被初始化,另外,接口的实现类在初始化的时候也不一定要执行接口的clinit()方法。
5,类的加载器
5.1,加载器的分类
java支持两种类型的类加载器:
- 引导型类加载器(bootstrap classloader)
- 自定义类加载器(user-define-classloader)
- 自定义类加载器是所有派生于抽象类classloader(也就是应用类型加载器)的类加载器。
- 系统中类加载器的组织架构:
extension class loader和system class loader都属于用户自定义加载器,应为他们都是继承自class loader,也就是只要是继承class loader的加载器,都是用户自定义加载器。
对于用户自定义类来说:使用系统类加载器AppClassLoader进行加载
java核心类库都是使用引导类加载器BootStrapClassLoader加载的
- 系统类加载器的演示
public class ClassLoaderTest {
public static void main(String[] args) {
//获取系统类加载器
ClassLoader classLoader=ClassLoader.getSystemClassLoader();
//打印系统类加载器对象的引用地址:sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(classLoader);
//获取其父类加载器,即扩展类加载器
ClassLoader parentClassLoader=classLoader.getParent();
System.out.println(parentClassLoader);
//获取bootstraploader加载器
ClassLoader parent = parentClassLoader.getParent();
System.out.println(parent);
//对用于自定义类来说,是有哪一个加载器加载的呢?
ClassLoader classLoader1 = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader1);
//查看String有那个加载器加载
ClassLoader classLoader2 = String.class.getClassLoader();
System.out.println(classLoader2);
}
}
sun.misc.Launcher$AppClassLoader@18b4aac2//系统类加载器,也叫作应用类加载器
sun.misc.Launcher$ExtClassLoader@4554617c//扩展类加载器
null
sun.misc.Launcher$AppClassLoader@18b4aac2//用户自定义类有系统加载器加载
null//String类目前的加载器为null,所以string也是有获取bootstraploader加载器类加载器进行加载,系统核心的类库全部是由获取bootstraploader核心加载器进行加载
//获取加载器是null的话,都是由引导类加载器加载的
5.2,加载器的介绍
sun.misc.Launcher是java虚拟机的一个入口
-
启动类加载器(引导类加载器 Bootstrap ClassLoader)
- 这个类加载器是由c&c++语言实现的,嵌套在java虚拟机内部。
- 此加载器用来加载java的核心库(JAVA_HOME/jre/lib/rt.jar)也即是负责加载lib文件夹下面的内容,用于提供jvm自身需要的类,
- 此加载器并不继承自java.lang.classloader,没有父类加载器。
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
- 处于安全考虑,bootstrap加载器仅仅加载包名为java,javax,sun等开头的类。
-
扩展类加载器(extention classloader)
- java语言编写的加载器,由sun.misc.launcher$EXTclassloader实现。
- 派生于classloader抽象类。
- 父类加载器是启动类加载器。
- 从java.ext.dirs系统目录下加载类库,或者从jdk的安装目录jre/lib/ext/子目录下加载类库,如果用户创建的jar包放在此目录下面,也会自动有扩展类加载器进行加载。
-
应用程序类加载器(系统类加载器appclassloader)
- java语言编写的加载器,由sun.misc.launcher$Appclassloader实现。
- 派生于classloader抽象类。
-
父类加载器为扩展类加载器。
- 他负责加载环境变量为classpath或者系统属性java.class.path指定路径下的类库。
-
该类加载器是系统中默认的加载器,一般来说java应用程序的类都是由系统类加载器完成加载的。
- 通过classloader#getsystemclassloader方法可以获取到该类加载器。
-
用户自定义类加载器
-
为什么要自定义类加载器?
-
隔离加载类
- 修改类加载方式。
- 扩展加载源。
- 防止源码泄露。
-
-
自定义类加载器的步骤:
- 开发人员可以通过继承java.lang.classloader的方式,实现自己的类加载器。
- 在jdk1.2之前,在自定义类加载器时候,总会去继承classloader类并且重写loaderClass()方法,从而实现自定义类加载器,在jdk1.2之后,已不再建议用户去覆盖loaderClass()方法,而是把自定义类的加载逻辑写在findclass()方法中。
- 在编写自定义类加载器时候,如果没有太过复杂的要求,可以直接继承URLclassloader类,这样可以避免自己去写findclass()方法,及其获取字节码流的方式,使自定义类加载器更加方便。
-
- 代码演示
/**
* 虚拟机自带加载器
*/
public class ClassLoaderTest1 {
public static void main(String[] args) {
System.out.println("********启动类加载器*********");
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
//获取BootStrapClassLoader能够加载的api路径
for (URL e:urls){
System.out.println(e.toExternalForm());
}
//从上面的路径中随意选择一个类 看看他的类加载器是什么
//Provider位于 /jdk1.8.0_171.jdk/Contents/Home/jre/lib/jsse.jar 下,引导类加载器加载它
ClassLoader classLoader = Provider.class.getClassLoader();
System.out.println(classLoader);//null
System.out.println("********拓展类加载器********");
String extDirs = System.getProperty("java.ext.dirs");
for (String path : extDirs.split(";")){
System.out.println(path);
}
//从上面的路径中随意选择一个类 看看他的类加载器是什么:拓展类加载器
ClassLoader classLoader1 = CurveDB.class.getClassLoader();
System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@4dc63996
}
}
5.3,获取ClassLoader的途径
- 代码演示
public class TestClassLoader {
public static void main(String[] args) throws ClassNotFoundException {
// 获取类加载器的第一种方式
// 加载string到内存只是获取内存中一个大的Class对象,也就是String类结构信息
// 系统类使用启动类加载器,所以返回结果是null
ClassLoader classLoader=Class.forName("java.lang.String").getClassLoader();
System.out.println(classLoader);
// 第二种方式,获取当前线程上下文的加载器
ClassLoader classLoader1=Thread.currentThread().getContextClassLoader();
System.out.println(classLoader1);
// 第三种方式
ClassLoader classLoader2=ClassLoader.getSystemClassLoader().getParent();
System.out.println(classLoader2);
}
}
6,双亲委派机制
- 双亲委派机制原理图
- java虚拟机对class字节码文件采用的是按需加载的方式,也就是说当需要该类的时候才会把该类的字节码文件加载到内存生成class对象,而且加载某一个类的class对象的时候,java虚拟机采用的是双亲委派机制,即把请求交给父类处理,它是一种任务委派模式。(如果父类处理不了请求,那么就在逐层向下,直到类加载为止)。
- 什么是双亲委派模型?
- 如果一个类加载器收到类的加载请求,他并不会自己先去加载,而是把这个请求先委托给自己的父类去执行。
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终会到达最顶层的启动类加载器。
- 如果父类加载器可以完成加载任务,就成功返回,倘若父类加载器无法完成此类的加载任务,子加载器才会尝试自己去加载,这就是双亲委派模型。
- 比如说自己定义的类,本身由系统类加载器(app类加载器)进行加载,但是系统类加载器不会直接加载,先向上传递给扩展类加载器,扩展类加载器在向上传递给启动类加载器,因为启动类加载器没有父类加载器,所以他就尝试自己加载,但是发现自己不能加载,然后他就向下传递给扩展类加载器,但是扩展类加载器发现自己也不能加载,就在向下传递,最终由系统类加载器进行加载。(扩展类加载器和启动类加载器有自己的类的加载目录,不在此目录中,这两个加载器就不会加载)。
- 类加载器这种上下层关系不是继承的一种关系,而是通过一种组合的关系复用父加载器的方式。
- 为什么要使用双亲委派机制进行类的加载?
- 使用这种方式使得java中的类随着他的类加载器一起具备了一种带有优先级的层次关系,会保证系统的类不会受到恶意的攻击。
- 双亲委派机制的优势
- 避免类的重复加载
- 保护程序安全,防止核心API被随意篡改(就像上面修改string类一样,不被允许)
- 自定义类:java.lang.String
- 自定义类:java.lang.MeDsh(java.lang包需要访问权限,阻止我们用包名自定义类)
6.1,jdbc加载举例
某一个程序要用到spi接口,那么spi中一些核心的jar包由引导类加载器进行加载,而核心的jar包是一些接口,要具体加载一些实现类,并且是第三方的实现类,不属于核心的api可以利用反向委派机制,由系统类加载器进行加载第三方的一些类,这里的系统加载器实际上是线程的上下文类加载器。线程上下文加载器实际上是一些系统加载器。
6.2,双亲委派机制的优势及作用
- 避免类的重复加载。(保证每一个类只有一个类加载器进行加载)
- 保护程序安全,防止核心api被篡改。(也就是说使用和系统一样的包名的话,如果在这个包下定义一个类,会报错,系统包名下的类是启动类加载器加载,但是系统类加载器在加载时会去找系统包名下的这个类,发现没有就会报错,也就是防止核心api被任意修改)
6.3,修改核心类例子
如图,虽然我们自定义了一个java.lang包下的String尝试覆盖核心类库中的String,但是由于双亲委派机制,启动加载器会加载java核心类库的String类(BootStrap启动类加载器只加载包名为java、javax、sun等开头的类),而核心类库中的String并没有main方法
7,沙箱安全机制
自定义string类,但是在加载自定义string类的时候会率先使用引导类型加载器进行加载,而引导类型加载器在加载的过程中会率先加载jdk自带的文件,(rt.jar包中的java/lang/String.jar),报错提示没有main方法,这就是应为加载的是(rt.jar包中的java/lang/String.jar)下的string类,这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
8,其他概念
- 在jvm中标示两个class对象(说的是内存中大的class对象,也就是类结构)是否为同一个类存在两个必要的条件:
- 类的完整类名(也就是权限定名称)必须完全一致,包括包名。
- 加载这个类的classloader也必须相同。(指的是classloader的实例对象)。
- 换句话说,在jvm中,即使两个类对象(class对象),来源于同一个class文件,被同一个虚拟机加载,但是只要加载他们的classloader实例对象不同,那么这两个对象也不相等。
- jvm必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的,如果一个类型是由用户类加载器进行加载,那么jvm会将这个类的加载器的一个引用作为类型信息的一部分保存在方法区中,当解析一个类型到另一个类型的引用的时候,jvm需要保证这两个类型的类加载器是相同的。
- java程序对类的使用方式分为:主动使用和被动使用
- 主动使用:七中情况,会导致类的初始化操作。
- 创建类的实例。
- 访问某一个类或接口的静态变量,或者对该静态变量赋值。
-
调用类的静态方法。
-
反射机制(Class.forname(com.rzf.Test))
-
初始化一个类的子类。
-
java虚拟机启动时被标明为启动类的类。
-
jdk7开始提供的动态语言支持:
-
java.lang.invoke.MethodHandle实例的解析结果。
- 除了以上七中情况,其他使用java类的方式都被看作是类的被动使用,都不会导致类的初始化。
- 主动使用:七中情况,会导致类的初始化操作。