目录
类的加载
加载流程
类的加载器
类的链接
类的检验阶段
类的准备阶段
类的解析阶段
类的初始化
static与final的搭配问题
()的线程安全性
类的初始化情况:主动使用vs被动使用
类的使用
类的卸载
类、类的加载器、类的实例之间的引用关系
类的生命周期
类的卸载
方法区的垃圾回收
按照Java虚拟机规范,从class文件到加载到内存中的类,到类卸载出内存为止,它的整个生命周期包括如下7个阶段:
类的加载(装载)、链接(验证、准备、解析)、初始化、使用、卸载。
在Java中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚拟机预先定义,不需要进行类的加载,引用数据类型则需要进行类的加载。
当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过类的加载、类的链接、类的初始化这三个步骤来对类进行初始化。如果不出现意外,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载或者初始化。
类的加载
加载流程
类的加载指的是通过类的加载器将类的.class文件中的数据以二进制的形式读取到内存中,存放在运行时数据区的方法区中,并创建一个大的Java.lang.Class对象,用来封装方法区内的数据结构,在加载类时,Java虚拟机必须完成以下3件事情:
-
通过类的全名,获取类的二进制数据流
-
解析类的二进制数据流为方法区中的数据结构(也称为Java类模型、类模板对象)
-
创建java.lang.Class类的实例(存放在堆中),表示该类型,每一个类都有一个对应的实例对象。作为方法区这个类的各种数据的访问入口,在程序中new 出来的这个类变量就指向堆中的这个实例对象。
所谓类模板对象,其实就是Java类在JVM内存中的一个快照,JVM将从字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中,这样JVM在运行期便能通过类模板而获取Java类中的任意信息,能够对Java类的成员变量进行遍历,也能进行Java方法的调用
类的加载器
类加载器(ClassLoader)的作用
ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例。然后交给Java虚拟机进行链接、初始化等操作、因此,ClassLoader在整个装载(加载)阶段,只能影响到类的加载,而无法通过ClassLoader去改变类的链接和初始化行为。至于它是否可以运行,则由执行引擎(Execution Engine)决定
JVM支持两种类型的类加载器 。分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。
class文件的显式加载与隐式加载的方式是指JVM加载class文件到内存的方式(在日常开发以上两种方式一般会混合使用)
显式加载:指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象
隐式加载:指的是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。比如 new User()
启动类加载器(引导类加载器,Bootstrap ClassLoader)
-
这个类加载使用C/C++语言实现的,嵌套在JVM内部。
-
它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
-
并不继承自ava.lang.ClassLoader,没有父加载器。
-
加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
-
出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
扩展类加载器(Extension ClassLoader)(java9之后发生了改变)
-
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() 方法可以获取到该类加载器
//获取系统类加载器ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();System.out.println(systemClassLoader);//jdk.internal.loader.ClassLoaders$AppClassLoader@1f89ab83//获取系统类加载器的上层:平台类加载器 PlatformClassLoaderClassLoader extClassLoader = systemClassLoader.getParent();System.out.println(extClassLoader);//jdk.internal.loader.ClassLoaders$PlatformClassLoader@b379bc6//获取其上层,找不到引导类加载器ClassLoader classLoader = extClassLoader.getParent();System.out.println(classLoader);//null//String类型使用引导类加载器进行加载--> java的核心类库都是使用引导类加载器进行加载的ClassLoader classLoader1 = String.class.getClassLoader();System.out.println(classLoader1);//null//用户自定义的类使用系统类加载器进行加载ClassLoader classLoader2 = People.class.getClassLoader();System.out.println(classLoader2);//jdk.internal.loader.ClassLoaders$AppClassLoader@1f89ab83
双亲委派模型
如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回。只有父类加载器无法完成此加载任务时,才自己去加载。
其本质就是规定了类加载的顺序是:引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由系统类加载器或自定义的类加载器进行加载。
沙箱安全机制
沙箱安全机制
-
保证程序安全
-
保护Java原生的JDK代码
JDK1.6时期,引入了域(Domain)的概念
虚拟机会把所有代码加载到不同的系统域和应用域。系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示。
沙箱安全机制的体现:
如图,虽然我们自定义了一个java.lang包下的String尝试覆盖核心类库中的String,但是由于双亲委派机制,启动加载器会加载java核心类库的String类(BootStrap启动类加载器只加载包名为java、javax、sun等开头的类),而核心类库中的String并没有main方法,所以就会报错。
我们自定义了一个String类,但是在加载String类的时候会率先使用引导类加载器加载,而引导类加载器在加载过程中会先加载jdk自带的文件,报错信息说没有main方法就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
Java9新特性
为了保证兼容性,JDK9没有从根本上改变三层类加载器架构和双亲委派模型,但为了模块化系统的顺利运行,仍然发生了一些值得被注意的变动。
-
扩展机制被移除,扩展类加载器由于向后兼容性的原因被保留,不过被重命名为平台类加载器(platform class loader)。可以通过classLoader的新方法getPlatformClassLoader()来获取。 JDK9基于模块化进行构建(原来的rt.jar和tools.jar被拆分成数十个JMOD文件),其中的Java类库就已天然地满足了可扩展的需求,那自然无须再保留<JAVA_HOME>\lib\ext目录,此前使用这个目录或者java.ext.dirs系统变量来扩展JDK功能的机制已经没有继续存在的价值了。
-
平台类加载器和应用程序类加载器都不再继承自java.net.URLClassLoader。现在启动类加载器、平台类加载器、应用程序类加载器全都继承于jdk.internal.loader.BuiltinClassLoader。
-
在Java9中,类加载器有了名称。该名称在构造方法中指定,可以通过getName()方法来获取。平台类加载器的名称是platform,应用类加载器的名称是app。类加载器的名称在调试与类加载器相关的问题时会非常有用。
-
启动类加载器现在是在jvm内部和java类库共同协作实现的类加载器(以前是C++实现),但为了与之前代码兼容,在获取启动类加载器的场景中仍然会返回null,而不会得到BootClassLoader实例。
-
类加载的委派关系也发生了变动。当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。
类的链接
类的链接分为类的检验阶段、类的准备阶段、类的解析阶段。
类的检验阶段
类的检验阶段就是要确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性。
主要包括以下四种:
其中格式验证会和加载阶段一起执行。验证通过之后,类加载器才会成功将类的二进制数据信息加载到方法区中。
格式验证之外的其他验证操作将会在方法区中进行。
具体说明:
-
格式验证:是否以魔数0XCAFEBABE开头,主版本和副版本号是否在当前Java虚拟机的支持范围内,数据中每一个项是否都拥有正确的长度等.
-
语义检查:Java虚拟机会进行字节码的语义检查,但凡在语义上不符合规范的,虚拟机也不会给予验证通过。比如:
-
是否所有的类都有父类的存在(在Java里,除了object外,其他类都应该有父类)
-
是否一些被定义为final的方法或者类被重写或继承了
-
非抽象类是否实现了所有抽象方法或者接口方法
-
-
字节码验证:Java虚拟机还会进行字节码验证,它试图通过对字节码流的分析,判断字节码是否可以被正确地执行。比如:
-
字节码的执行过程中,是否会跳转到一条不存在的指令
-
函数的调用是否传递了正确类型的参数
-
变量的赋值是不是给了正确的数据类型等
-
-
符号引用的验证:校验器还将进符号引用的验证。Class文件在其常量池会通过字符串记录自己将要使用的其他类或者方法。因此,在验证阶段,虚拟机就会检验这些类或方法是否是真实存在的,并且当前类是否有权限访问这些数据,如果一个需要使用类无法在系统中找到,则会抛出NoClassDefFoundError,如果一个方法无法被找到,则会抛NoSuchMethodError。此阶段在解析环节才会执行。
类的准备阶段
类的准备阶段简言之就是为类的类变量(静态变量)分配内存空间,并且将其初始化为默认值。
当一个类验证通过时,虚拟机就会进入准备阶段。在这个阶段,虚拟机就会为这个类分配相应的内存空间,并设置默认初始值。
Java并不支持boolean类型,对于boolean类型,内部实现是int,由于int的默认值是0,故对应的,boolean的默认值就是false。
注意:对于基本数据类型使用final修饰的static变量,在编译阶段就已经初始化了,在准备阶段进行显示赋值操作。
// 一般情况:static final修饰的基本数据类型、字符串类型字面量会在准备阶段赋值
private static final String str = "Hello world";
// 特殊情况:static final修饰的引用类型不会在准备阶段赋值,而是在初始化阶段赋值
private static final String str = new String("Hello world");
注意这里不会为实例变量(不使用static修饰的)分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
在这个阶段并不会像初始化阶段中那样会有代码被执行。
类的解析阶段
在准备阶段完成后,就进入了解析阶段。解析阶段(Resolution),简言之,将类、接口、字段和方法的符号引用转为直接引用。
具体描述:符号引用就是一些字面量的引用,和虚拟机的内部数据结构和和内存布局无关。比较容易理解的就是在Class类文件中,通过常量池进行了大量的符号引用。但是在程序实际运行时,只有符号引用是不够的,比如当一个方法被调用时,系统需要明确知道该方法的位置。
虚拟机在加载Class文件时才会进行动态链接,也就是说,Class文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行起来时,需要从常量池中获得对应的符号引用,再在类加载过程中(初始化阶段)将其替换直接引用,并翻译到具体的内存地址中。
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。
直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。
不过Java虚拟机规范并没有明确要求解析阶段一定要按照顺序执行。在HotSpot VM中,加载、验证、准备和初始化会按照顺序有条不紊地执行,但链接阶段中的解析操作往往会伴随着JVM在执行完初始化之后再执行。
符号引号有:类和接口的权限定名、字段的名称和描述符、方法的名称和描述符。
类的初始化
类的初始化阶段为类变量进行显示赋值。
初始化阶段就是执行类构造器方法< clinit >()的过程。此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码快中的语句合并而来。
static与final的搭配问题
使用static+ final修饰的字段的显式赋值的操作,到底是在哪个阶段进行的赋值?
-
情况1:在链接阶段的准备环节赋值
-
情况2:在初始化阶段<clinit>()中赋值
在链接阶段的准备环节赋值的情况:
-
对于基本数据类型的字段来说,如果使用static final修饰,则显式赋值(直接赋值常量,而非调用方法)通常是在链接阶段的准备环节进行
-
对于String来说,如果使用字面量的方式赋值,使用static final修饰的话,则显式赋值通常是在链接阶段的准备环节进行
-
在初始化阶段<clinit>()中赋值的情况: 排除上述的在准备环节赋值的情况之外的情况。
结论:使用static+final修饰,且显式赋值中不涉及到方法或构造器调用的基本数据类到或String类型的显式赋值,是在链接阶段的准备环节进行。
public static final int INT_CONSTANT = 10;// 在链接阶段的准备环节赋值
public static final int NUM1 = new Random().nextInt(10);// 在初始化阶段clinit>()中赋值
public static int a = 1;// 在初始化阶段<clinit>()中赋值public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100);//在初始化阶段<clinit>()中赋值
public static Integer INTEGER_CONSTANT2 = Integer.valueOf(100);//在初始化阶段<clinit>()中概值public static final String s0 = "helloworld0";// 在链接阶段的准备环节赋值
public static final String s1 = new String("helloworld1");// 在初始化阶段<clinit>()中赋值
public static String s2 = "hellowrold2";// 在初始化阶段<clinit>()中赋值
<Clinit>()的线程安全性
对于<clinit>()方法的调用,也就是类的初始化,虚拟机会在内部确保其多线程环境中的安全性。
虚拟机会保证一个类的 <clinit> ()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。
正是因为<clinit>是带锁线程安全的,因此,如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个线程阻塞,引发死锁。并且这种死锁是很难发现的,因为看起来它们并没有可用的锁信息。
如果之前的线程成功加载了类,则等在队列中的线程就没有机会再执行<clinit>()方法了。那么,当需要使用这个类时,虚拟机会直接返回给它已经准备好的信息。
类的初始化情况:主动使用vs被动使用
Java程序对类的使用分为两种:主动使用和被动使用。
主动使用
Java虚拟机不会无条件地装载Class类型。Java虚拟机规定,一个类或接口在初次使用前,必须要进行初始化。这里指的“使用”,是指主动使用,主动使用只有下列几种情况:(即:如果出现如下的情况,则会对类进行初始化操作。而初始化操作之前的加载、验证、准备已经完成。)
-
当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化
-
访问某个类或接口的静态变量,或者对该静态变量赋值
-
调用类的静态方法
-
反射(比如:Class.forName(“com.xiaozhi.Test”))
-
初始化一个子类(当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化)
-
当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
-
JDK7开始提供的动态语言支持(涉及解析REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄对应的类)
-
如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化
被动使用
除了以上的情况属于主动使用,其他的情况均属于被动使用。被动使用不会引起类的初始化。意味着没有<clinit>()的调用。
-
调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化
-
当访问一个静态字段时,只有真正声明这个字段的类才会被初始化。
-
当通过子类引用父类的静态变量,不会导致子类初始化
-
引用常量不会触发此类或接口的初始化。因为常量在链接阶段就已经被显式赋值了
-
通过数组定义类引用,不会触发此类的初始化
类的使用
任何一个类型在使用之前都必须经历过完整的加载、链接和初始化3个类加载步骤。一旦一个类型成功经历过这3个步骤之后,便“厉事俱备只欠东风”,就等着开发者使用了。
开发人员可以在程序中访问和调用它的静态类成员信息(比如:静态字段、静态方法),或者使用new关键字为其创建对象实例。
类的卸载
类、类的加载器、类的实例之间的引用关系
在类加载器的内部实现中,用一个Java集合来存放所加载的类的引用。一个Class对象总是会引用它的类加载器,调用Class对象的getClassLoader()方法,就能获得它的类加载器。由此可见,代表某个类的Class实例与其类的加载器之间为双向关联关系。
一个类的实例总是引用代表这个类的Class对象。在Object类中定义了getClass()方法,这个方法返回代表对象所属类的Class对象的引用。此外,所有的java类都有一个静态属性class,它引用代表这个类的Class对象。
类的生命周期
当类被加载、链接和初始化后,它的生命周期就开始了。当代表类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,类在方法区内的数据也会被卸载,从而结束类的生命周期。
一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。
loader1变量和obj变量间接引用代表Sample类的Class对象,而objClass变量则直接引用它。
如果程序运行过程中,将上图左侧三个引用变量都置为null,此时Sample对象结束生命周期,MyClassLoader对象结束生命周期,代表Sample类的Class对象也结束生命周期,Sample类在方法区内的二进制数据被卸载。
当再次有需要时,会检查Sample类的Class对象是否存在,如果存在会直接使用,不再重新加载;如果不存在Sample类会被重新加载,在Java虚拟机的堆区会生成一个新的代表Sample类的Class实例(可以通过哈希码查看是否是同一个实例)
类的卸载
启动类加载器加载的类型在整个运行期间是不可能被卸载的(jvm和jls规范)
被系统类加载器和扩展类加载器加载的类型在运行期间不太可能被卸载,因为系统类加载器实例或者扩展类的实例基本上在整个运行期间总能直接或者间接的访问的到,其达到unreachable的可能性极小。
被开发者自定义的类加载器实例加载的类型只有在很简单的上下文环境中才能被卸载,而且一般还要借助于强制调用虚拟机的垃圾收集功能才可以做到。可以预想,稍微复杂点的应用场景中(比如:很多时候用户在开发自定义类加载器实例的时候采用缓存的策略以提高系统性能),被加载的类型在运行期间也是几乎不太可能被卸载的(至少卸载的时间是不确定的)。
综合以上三点,一个已经加载的类型被卸载的几率很小至少被卸载的时间是不确定的。同时我们可以看的出来,开发者在开发代码时候,不应该对虚拟机的类型卸载做任何假设的前提下,来实现系统中的特定功能。
方法区的垃圾回收
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。
HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。
判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:
-
该类所有的实例已经被回收。也就是Java堆中不存在该类及其任何派生子类的实例。
-
加载该类的类加载器已经被回收。这个条件除非是经过精心设计的可替换类加载器的场景,否则是很难达成的。
-
该类对应的Java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。