本篇是对《深入理解Java虚拟机----JVM高级特性与最佳实践》周志明作的第二版对应内容的一个读书笔记。
一、什么是虚拟机类加载
JVM虚拟机类的加载机制是说:
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。类加载说的就是下图中的:加载+验证+准备+解析+初始化,这5个阶段。
简而言之一句话:Class文件怎么变成内存中可用的一个类/接口的。
二、类的整个生命周期
类从被加载到虚拟机内存中,到最后从内存中卸载,一共有7个阶段:
1.加载Loading
加载阶段,虚拟机需要完成一下3件事:
1.通过一个类的全限定名来获取定义此类的二进制字节流
2.将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
2.验证Verification
大致会完成4中验证:
1.文件格式验证
2.元数据验证
3.字节码验证
4.符号引用验证
3.准备Preparation
该阶段做的事就是:
1.为类变量(即是static修饰的变量)分配内存,这些static变量使用的内存在方法区中分配。
2.设置类变量初始值。这里设置的初始值一般情况是零值。
比如 public static int value = 55; 这里只是把value设置为0.
特殊情况则是final修饰的情况
比如 public static final int value = 55; 这里就会把value设置为55
基础数据类型对应的零值
数据类型 | 零值 | 数据类型 | 零值 |
int | 0 | boolean | false |
long | 0L | float | 0.0f |
short | (short)0 | double | 0.0d |
char | '\u0000' | reference | null |
byte | (btye)0 |
4.解析Resolution
注意:在Java运行时绑定(一个接口有多个实现,运行时绑定到底用哪个实现)的情况下,解析可能在初始化之后。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
解析动作主要针对:
1.类或接口
2.字段
3.类方法
4.接口方法
5.方法类型
6.方法句柄
7.调用点限定符
5.初始化Initialization
初始化阶段主要做的事就是执行类构造器<clinit>
通俗的说就是给已经被分配内存、被设置零值的static变量赋值!
1.<clinit>()方法是什么:
它是编译器自动收集类中的所有类变量(static变量)的赋值动作和静态语句块(static{}块)中的语句合并而成。
编译器收集的顺序是由语句在源文件中出现的顺序所决定的。
强调,它不是构造函数!这里说的是类加载!不是实例化对象!
2.<clinit>()方法不需要显示调用,虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕。所以在虚拟机中第一个被执行的<clinit>()方法的肯定是java.lang.Object。
但是执行接口的<clinit>()方法时,不需要先执行其父接口的<clinit>()方法。
执行接口的实现类的初始化时,也不会执行该接口的<clinit>()方法。
3.<clinit>()方法对于类或接口来说不是必须的,如果一个类中没有静态语句块(static{}块),也没有对static变量赋值操作,那么编译器可以不为这个类生成<clinit>()方法
对一个类进行主动引用,会触发初始化。
主动引用有且只有以下5中场景,除此以外都是被动引用,被动引用不会触发初始化方法。
1.遇到new、getstatic、putstatic或者invokestatic这4条字节码指令时,如果没有进行过初始化则需要先对其初始化。
这4个指令常见场景:new实例化对象,读取、设置一个类的静态static字段(被final修饰的静态static字段除外,它在编译器就被放到常量池中了),调用一个静态方法。
2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果没有进行过初始化则需要先对其初始化。
3.当初始化一个类的时候,其父类还没有初始化,则需要先初始化其父类。但是,当初始化一个接口的时候,并不要求其父接口已经初始化。
4.执行主类(有main方法那个类)。
5.当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先对其初始化。
三、类加载器
1.什么是类加载器,它干嘛的?
刚不是说了,在加载阶段要做的事,第一件事就是:通过一个类的全限定名来获取定义此类的二进制字节流。
虚拟机把这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何获取所需要的类。
而这个动作的代码模块就是类加载器。
2.常见问题
2.1类是否相等的判断
确定一个类在Java虚拟机中的唯一性:这个类本身+加载这个类的类加载器
比较两个类是否相等,只有在这两个类是由同一个类加载器加载出来的前提下,比较才有意义。
两个类即使来源于同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不一同,那这两个类就不相等。
这里的“相等”包括equals()方法、isAssignableFrom()方法、inInstance()方法,也包括使用instanceof关键字做对象所属关系判定。
类加载器会首先代理给父类加载器来尝试加载某个类,这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。
真正完成类的加载工作是通过调用defineClass来实现的;而启动类的加载过程是通过调用loadClass来实现的。
前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。
在Java虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。
两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。
方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常;
方法 defineClass()抛出的是 java.lang.NoClassDefFoundError异常。
类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。
下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。
也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。
2.2在代码中使用Class.forName(String name)默认会使用调用类的类加载器来进行类加载
2.3.当自定义类加载器没有指定父类加载器的情况下,默认的父类加载器即为Application ClassLoader
2.4如何在运行时判断系统类加载器能加载哪些路径下的类?
<1>直接调用ClassLoader.getSystemClassLoader()或者其他方式获取到application classloader(application classloader和扩展类加载器本身都派生自URLClassLoader),调用URLClassLoader中的getURLs()方法可以获取到。
<2>直接通过获取系统属性java.class.path来查看当前类路径上的条目信息 :System.getProperty(“java.class.path”)。
3.JVM虚拟机的角度下,类加载器的分类
1.Bootstrap ClassLoader启动类加载器,这个类加载器使用C++语言实现,是虚拟机自身的一部分
2.其他又Java语言实现的类加载器,独立于虚拟机外部,继承自抽象类Java.lang.ClassLoader
4.Java开发人员的角度下,类加载器的分类
1.Bootstrap ClassLoader启动类加载器。
2.Extension ClassLoader扩展类加载器。
3.Application ClassLoader应用程序类加载器,它是ClassLoader中的getSystemClassLoader方法的返回值,也称为系统类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下它就是程序中默认的类加载器。
名字 | 加载哪个目录下的类到虚拟机内存中 | 加载哪个参数指定的路径下的类 | 能否被开发人员直接使用 |
Bootstrap ClassLoader 启动类加载器 | <JAVA_HOME>\lib
虚拟机出于安全等因素考虑,不会加载<JAVA_HOME>/lib目录下存在的陌生类,换句话说,虚拟机只加载<JAVA_HOME>/lib目录下它可以识别的类。因此,开发者通过将要加载的非JDK自身的类放置到此目录下期待启动类加载器加载是不可能的。 | -Xbootclasspath | 不能 |
Extension ClassLoader 扩展类加载器 | <JAVA_HOME>\lib\ext | java.ext.dirs系统变量指定路径 | 能 |
Application ClassLoader 应用程序类加载器 | 用户路径ClassPath | 能 |
5.双亲委派模型
双亲委派模型的工作过程:
1.每个类加载器收到了类加载请求,会先检查该类是否已经被加载过了。
2.如果该类没有被加载过,它会把请求委派给父类加载器(调用父类加载器的loadClass()方法)去完成。
如果父类加载器为空,默认使用启动类加载器作为父类加载器。
3.只有父类加载器无法加载这个类(它的搜索范围中没有找到所需的类),子类加载器才会尝试自己加载该类。
双亲委派模型的优势:
Java类随着它的类加载器一起具备了一种带有优先级的层级关系。
例如类java.lang.Object,它存放在rt.jar包中,无论哪一个类加载器要加载这个类,最终都会委派给处于模型最顶端的启动类加载器来加载。
因此保证了Object类在程序的各种类加载器环境中始终都是同一个类。