前言:学习JVM是一个漫长的课程,在你学的过程中,JVM的学习过程中是枯燥的乏味的,不要想着一口吃一个胖子,需要沉下心,一步一步来。
JVM是一个令人望而却步的领域,因为它博大精深,涉及到的内容与知识点非常之多。虽然Java开发者每天都在使用JVM,但对其有所研究并且研究深入的人却少之又少。然而,JVM的重要性却又是不言而喻的。基于JVM的各种动态与静态语言生态圈已经异常繁荣了,对JVM的运行机制有一定的了解不但可以提升我们的竞争力,还可以让我们在面对问题时能够沉着应对,加速问题的解决速度;同时还能够增强我们的自信心,让我们更加游刃有余。
本人也是刚学JVM,想要把JVM学好,最起码能够应付面试和让自己对程序的理解更上一个台阶。我这次打算从jvm的以下方面进行学习:
- 类加载的五个阶段
这些东西在深入理解Java虚拟机上面其实是都有用的,但是我不会按照上面罗列的来介绍。我会按照我自己的理解来完善我这个系列博客的。
JVM的架构图(不想自己画了,从网上偷一个吧):
1类加载子系统的作用:
1.类加载子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识
2.classLocader只负责class文件的加载,至于它是否可以运行,则有执行引擎Execution Engine决定的
3.加载的类信息存放于一块成为方法区的内存空间。除了类信息之外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
1.1类加载器ClassLoader角色:
1.class file存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中来
根据这个文件实例,画出n个一模一样的实例
2.class file 加载到jvm中,被称为DNA元数据模板,放在方法区
3.在.class文件 -> JVM -> 最终称为元数据模板,此过程就要一个运输工具(类装载器 ClassLoader),扮演一个快递员的角色
2.类加载
在java的代码中,类型的加载,连接,初始化的过程都是在程序运行期间完成的
2.1加载:查找并加载二进制数据
1.通过一个类的全限定名获取定义此类的二进制字节流
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
2.2连接:
验证:1.目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,
不会危害虚拟机自身安 全。
2.主要包括四种验证,文件格式验证,源数据验证,字节码验证,符号引用验证。
准备:1.为类变量分配内存并且设置该类变量的默认初始值,即零值
2.这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化
3.这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量会随着对象一起分配到java堆中
解析:1.将常量池内的符号引用转换为直接引用的过程
2.事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行
3.符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的Class
文件格式中。直接引用就是直接指向目标的指针,相对偏移量或一个间接定位到目标的句柄
4.解析动作主要针对类或接口,字段,类方法,接口方法,方法类型等。对应常量池CONSTANT_Class_info等。
初始化:1.初始化阶段就是执行类构造器方法<clinit>()的过程
2.此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块的语句合并而来的.
我们注意到,如果没有静态变量,那么字节码文件就不会有clinit方法
3.构造器方法中指令按语句在源文件中出现的顺序执行
改变下number的位置就不一样了
4.clinit()不同于类的构造器(构造器是虚拟机视角下的<init>)
5.若类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕
6.虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁
类加载的五个阶段:
java程序对类的使用方式有两种,主动使用和被动使用
所有的java虚拟机实现必须在每个类或者接口被Java程序“首次主动使用”时才初始化他们
何为主动使用(七种方式):
1.创建类的实例
2.访问某个类或者接口的静态变量,或者对该静态变量赋值
3.调用类的静态方法
4.反射
5.初始化一个类的子类
6.java虚拟机启动被标明启动类的类
7.jdk1.7开始提供的动态语言支持(了解)
除了这七种情况,其他使用java类的方式都被看做是对类的被动使用,都不会导致类的初始化
举一个例子来分析下啥是类的主动和被动加载
下面我们可以把子类的静态变量改为静态常量
我们可以看出来,这一次并没有执行父类的静态构造,这是为什么呢???
原因:在编译阶段,把final定义的常量放到了调用这个常量的方法所在类的常量池中(这里就放在了MyTest的常量池当中)
我们下面把代码稍微的改东下,我们再看下输出的结果
原因:当一个常量的值并非编译期间能够确定的,那么其值就不会放到调用类的常量池中,这是程序在运行的时候,会导致主动使用这个常量所在的类,会导致这个类被初始化
下面我们来分析下父子类接口的加载关系:
首先我们要先把结论说出来:当一个接口被初始化的时候,并不要求他的父类完成初始化,这一点是和类是不一样的
为什么呢:这个案例其实不是很好举的,你在接口的定义的变量其实都是被final修饰的,他们都会被放到调用类的常量池里面,从而不会加载父类接口。
我们下面来分析一下准备阶段和初始化顺序的问题(面试的时候,笔试题可能会有这种类型的题,很坑哦)
我们来根据类的准备和初始化阶段来分析下这段代码的执行顺序
这一切的结果应该和很多人的想法一致,那么我们现在把,Single类里面的b的定义放在构造方法下面,我们再来分析一下这个结果和执行情况
类加载器的分类
JVM支持两种类型的加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)
从概念上讲,自定义加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器
无论加载器的类型如何划分,在程序中我们最常见的类加载器始终只有三个,如下图所示:
自定义类与核心类库的加载器
对于用户自定义类来说:使用系统类加载器AppClassLoader进行加载
java核心类库都是使用引导类加载器BootStrapClassLoader
public class ClassLoaderTest {
public static void main(String[] args) {
// 获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println("systemClassLoader:" + systemClassLoader); //sun.misc.Launcher$AppClassLoader@18b4aac2
// 获得上层 扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println("extClassLoader:" + extClassLoader); // extClassLoader:sun.misc.Launcher$ExtClassLoader@54bedef2
// 获得上层 获取不到引导类加载器
ClassLoader bootStrapClassLoader = extClassLoader.getParent();
System.out.println("bootStrapClassLoader:" + bootStrapClassLoader); // null
// 对于用户自定义的类来说:使用系统类加载器进行加载
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println("classLoader:"+classLoader); //classLoader:sun.misc.Launcher$AppClassLoader@18b4aac2
//String 类使用引导类加载器进行加载的 -->java核心类库都是使用引导类加载器加载的
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1); // null
}
}
虚拟机自带的加载器
启动类加载器(引导类加载器 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/lib/ext子目录(扩展目录)下加载类库。如果用户创建的jar放在此目录,也会有扩展类加载器自动加载
应用程序类加载器(系统类加载器,AppClassLoader)
- java语言编写,由sun.misc.Launcher$AppClassLoader实现
- 派生于ClassLoader类
- 类加载器为拓展类加载器
- 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
- 该类加载器是程序中默认的类加载器,一般来说,java应用的类都是由它来完成加载的
- 通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器
public class ClassLoaderTest1 {
public static void main(String[] args) {
System.out.println("********启动类加载器*********");
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL urL : urLs) {
System.out.println(urL.toExternalForm());
}
System.out.println("********拓展类加载器********");
String extDirs = System.getProperty("java.ext.dirs");
for (String path : extDirs.split(";")){
System.out.println(path);
}
}
}
用户自定义类加载器
为什么
- 隔离加载类
- 修改类加载的方式
- 扩展加载源
- 防止源码泄露
ClassLoader的常用方法及获取方法
ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)
ClassLoader继承关系
获取ClassLoader的途径
双亲委派机制
java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将她的class文件加载到内存生成class文件。而且加载某个类的class文件时,java虚拟机采用的是双亲微拍模式,即把请求交由父类处理,它是一种任务委派模式
双亲委派机制工作原理
例
如图,虽然我们自定义了一个java.lang包下的String尝试覆盖核心类库中的String,但是由于双亲委派机制,启动加载器会加载java核心类库的String类(BootStrap启动类加载器只加载包名为java、javax、sun等开头的类),而核心类库中的String并没有main方法
双亲委派机制的优势:
- 避免类的重复加载
- 保护程序安全,防止核心API被随意篡改
- 自定义类:java.lang.String
- 自定义类:java.lang.MeDsh(java.lang包需要访问权限,阻止我们用包名自定义类)