JVM上篇内存与垃圾回收-类加载子系统
类加载子系统
作用
- 负责从文件系统或者网络中加载Class文件,Class文件开头有特定标识,魔术,咖啡杯壁
- Classloader只负责class文件的加载,至于是否可运行,则由执行引擎决定
- 加载的类信息存放于称为方法区的内存空间,除了类信息,方法区还会存放运行时常量池信息,还可能包括字符串字面量和数字常量
- 常量池运行时加载到内存中,即运行时常量池
类的加载过程
加载
- 加载刚好是加载过程的一个阶段,二者意思不能混淆
- 通过一个类的全限定名获取定义此类的二进制字节流
- 本地系统获取
- 网络获取,Web Applet
- zip压缩包获取,jar,war
- 运行时计算生成,动态代理
- 有其他文件生成,jsp
- 专有数据库提取.class文件,比较少见
- 加密文件中获取,防止Class文件被反编译的保护措施
- 将这个字节流所代表的的静态存储结果转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口
链接
验证(Verify):
- 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
- 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
准备(Prepare):
- 为类变量分配内存并且设置该类变量的默认初始值,即零值。
- 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
- 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
- 实例变量和类变量都是成员变量,前者不是static修饰,后者是。
解析(Resolve):
将常量池内的符号引用转换为直接引用的过程。
在编译的时候一个每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址,所以就用符号引用来代替,而在解析阶段就是为了把这个符号引用转化成真正的地址的阶段。
符号引用是指用字符串形式表示的类名、方法名和字段名等符号,而直接引用是指直接指向内存地址的引用。在Java程序运行时,常量池中存储的符号引用需要被转换为直接引用才能被虚拟机使用。
解析符号引用的过程包括以下几个步骤:
(1)找到类或接口的全限定名。
(2)根据类或接口的全限定名加载相应的类或接口。
(3)根据方法或字段的名称和描述符,找到相应的方法或字段。
(4)将方法或字段的符号引用转换为直接引用。
初始化
- 初始化阶段就是执行类构造器方法
()的过程。 - 如果该类的直接父类还没有被初始化,则先初始化其直接父类;如果类中有初始化语句,则系统依次执行这些初始化语句。
- 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
- 构造器方法中指令按语句在源文件中出现的顺序执行。
()不同于类的构造器。(关联:构造器是虚拟机视角下的 ()) - 若该类具有父类,JVM会保证子类的
()执行前,父类的 ()已经执行完毕。 - 虚拟机必须保证一个类的
()方法在多线程下被同步加锁。
补充说明:
- 加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的。
- 解析阶段不一定,在某些情况下可以在初始化阶段之后再开始,为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)
- Java虚拟机规范严格规定了,有且只有六种情况,必须立即对类进行初始化
- 遇到new,getstatic,putstatic或invokestatic这四条字节码指令时。
- 使用new关键字实例化对象
- 读取或设置一个类型的静态字段(final修饰已在编译期将结果放入常量池的静态字段除外)
- 调用一个类型的静态方法的时候
- 对类型进行反射调用,如果类型没有经过初始化,则需要触发初始化
- 初始化类的时候,发现父类没有初始化,则先触发父类初始化
- 虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会初始化这个主类
- 只用JDK7中新加入的动态语言支持,如果一个java.lang.invoke.MethodHandler实例最后的解析结果为REF_getStatic,REF_putStatic,REF_invokeStatic,REF_newInvokeSpecial四种类型的方法句柄,并且这个方法对应的类没有进行初始化,则先触发其初始化
- 当一个接口中定了JDK8新加入的默认方法时,如果这个接口的实现类发生了初始化,要先将接口进行初始化
- 遇到new,getstatic,putstatic或invokestatic这四条字节码指令时。
- 除了以上几种情况,其他使用类的方式被看做是对类的被动使用都不会导致类的初始化
类加载器分类
引导类加载器和自定义加载器
概念上,将所有派生于抽象类ClassLoader的类加载器都划分为自定义加载器
对于用户来说定义器来说,默认使用系统类加载器进行加载
Java的核心类库,使用引导类加载器进行加载
启动类加载器(引导类加载器,Bootstrap ClassLoader)
- 这个类加载使用C/C++语言实现的,嵌套在JVM内部。
- 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
- 并不继承自Java.lang.ClassLoader,没有父加载器。
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
- 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
扩展类加载器(Extension ClassLoader)
- Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
- 派生于ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/1ib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
应用程序类加载器(系统类加载器,AppClassLoader)
- java语言编写,由sun.misc.LaunchersAppClassLoader实现
- 派生于ClassLoader类
- 父类加载器为扩展类加载器
- 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
- 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
- 通过ClassLoader#getSystemclassLoader() 方法可以获取到该类加载器
用户自定义类加载器
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。 为什么要自定义类加载器?
- 隔离加载类
- 修改类加载的方式
- 扩展加载源
- 防止源码泄漏
用户自定义类加载器实现步骤:
- 开发人员可以通过继承抽象类ava.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
- 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass() 方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadclass() 方法,而是建议把自定义的类加载逻辑写在findClass()方法中
- 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass() 方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。
关于ClassLoader
它是一个抽象类,除了启动类加载器,其他类加载器都继承自他
双亲委派机制
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
工作原理
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
优势
- 避免类的重复加载
- 保护程序安全,防止核心API被篡改
举个例子,假设你在自己的程序中定义了一个和JDK核心类库中同名的类,比如java.lang.String。当你的程序运行时,如果没有双亲委派机制,那么你定义的这个类就会覆盖JDK核心类库中的String类。这样就会导致一些安全问题。
但是由于有了双亲委派机制,当你的程序运行时,类加载器会先把加载请求委派给父类加载器。最终请求会被委派到启动类加载器。启动类加载器会先去JDK核心类库中查找是否有这个类。由于JDK核心类库中已经有了一个String类,所以启动类加载器就会直接返回这个String类。这样就保证了JDK核心类库中的String不会被覆盖。
沙箱安全机制
沙箱机制就是将Java代码限定在虚拟机JVM特定的运行范围中,并且严格限制代码对本地资源的访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。
通俗来说就是虚拟机把代码加载到拥有不同权限的域里,然后代码就拥有了该域的所有权限。这样就能控制不同代码拥有不同调用操作系统和本地资源的权限
例如:
自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的string类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
其他
如何判断两个class对象是否相同
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
- 类的完整类名必须一致,包括包名。
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
换句话说,在JVM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。
对类加载器的引用
JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
类的主动使用和被动使用
Java程序对类的使用方式分为:主动使用和被动使用。
主动使用,又分为七种情况:
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(比如:Class.forName(”com.atguigu.Test”))
- 初始化一个类的子类
- Java虚拟机启动时被标明为启动类的类
- JDK 7 开始提供的动态语言支持: java.lang.invoke.MethodHandle实例的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的类没有初始化,则初始化
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。