一. 背景
编写本次 JVM 章程, 因没有找到合适及透明的文章讲解JVM,零一 下定决心既然没人做 从零到一的过程, 那我来做,编学边分享本次学习的过程, 先和同学们说一下本次JVM章程几乎全是概念,其实大家背下来理解即可,希望会对各位学习的童鞋们带来帮助
学会 : 看+记忆+练习+复习+练习+背下来 = 学会
注意 : 本次 HotSpot VM 讲解基于 JDK8 讲解,期间会进行与JDK6版本进行对比
公众号 : 倔强小狮子(最新发布)
文章目录
三. 类加载器(第一章)
1. JVM 整体结构图
HostSpot VM 是目前市面上高性能的代表作之一
它采用的是编译器和解释器并存的架构
在今天,Java程序早已经脱胎换骨,已经不是当年的Java了,生态圈庞大
2. 虚拟机的生命周期
2.1 虚拟机的启动
- Java虚拟机的启动是通过引导类加载器(bootstrap class loader), 创建一个初始类(initial
class)来完成的,这个类是由虚拟机具体实现指定的
2.2虚拟机的执行
- 一个运行的Java虚拟机有着一个清晰的任务,Java程序
- 程序运行时他才真正的运行,程序结束时它就停止
- 执行一个所谓的Java程序的时候,其实是执行的一个真真正正的Java虚拟机进程
2.3 虚拟机的退出(有以下几种情况)
- 程序正常退出
- 程序在运行时遇到异常或者错误而终止的非正常退出
- 由操作系统错误导致虚拟机进行终止
- 某线程调用 Runtime类或者System的exit方法,或者 Runtime类中的halt方法,并且Java安全管理器也允许这次exit或者halt操作
- 除此之外,JNI规范中还描述了JNI Invocation API 来加载或者卸载 Java虚拟机时, Java虚拟机的退出情况
其他 虚拟机 : JRockit, J9 等不再做介绍,本次以 Host Spot讲解
3. 代码执行流程
4. 类加载子系统(步入正题)
- 类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识(后期会体现)
- ClassLoader 只负责class文件的加载,至于他是否可以运行,则由Execution Engine 决定.
- 加载类的信息存放与一块称为元空间(方法区)的内存空间中,除了类的信息为,元空间还会存放运行时常量信息,可能还保活字符串字面量和数字常量(这部分常量信息是由Class文件中常量池部分的内存映射)
5. 类加载器 ClassLoader 角色
- class file 存在本地硬盘上, 可以理解成是一个模板,而最终这个模板要运行的时候在加载到JVM当中来,这时候JVM会根据这个模板文件实例化出n个一摸一样的实例(万物皆对象)
- class file 加载到 JVM 中成为 DNA 元数据模板,存放在元空间(方法区)
- 在 class 文件 -> JVM -> 最终成为元数据模板,此过程就要一个运输工具,而这个工具则是 类加载器 Class loader
6. 类加载过程(重点 ☆☆☆☆☆☆)
6.1. 加载
- 通过一个类的全限定类名获取定义类的二进制字节流
- 将这个字节流所代表的静态存储结构转发为元空间的运行时数据结构
- 内存中生成一个代表这个类的 java.lang.Class对象, 作为元空间这个类的各种数据的访问入口
6.2 验证(Verify)
- 目的是在于确保Class文件的字节流中的信息符合当前虚拟机的要求,保证被加载的正确性,不会危害到虚拟机的自身安全
- 主要包括四种验证, 文件格式验证,元数据验证,字节码验证,符号引用验证.
6.3 准备(Perpare)(重点)
- 为类变量分配内存并且设置该类变量的
默认值
,即零值 (int 默认 0 ) - 这不包含用
final修饰的static
, 因为final在编译的时候就被分配了,准备阶段会显式初始化 - 这里不会为实例变量分配实例化,为类变量分配在元空间中,而实例变量会随着对象一起分配到Java堆中
6.4 解析(Resolve)(重点)
- 将常量池内的
符号引用转换为直接引用
的过程(难以理解吧
,例如
: 堆空间对象头中存在指针地址(标记对象位置), 直接引用就是指针地址,而符号引用则是虚拟机不知道对象的真实地址时(预编译期)代表对象地址的符号(唯一), 解析过程中直接将常量池中的真实地址将符号地址替换了) - 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行
- 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的Class文件格式中。
直接引用
就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄 - 解析动作
主要针对
类或接口、字段、类方法、接口方法、方法类型等,对应常量池中的CONSTANT_Class info, CONSTANT_Fieldref_info, CONSTANT_Methodref_info等
6.5 初始化
- 初始化阶段就是执行类的构造器方法< clinit> () 的过程
- 此方法不需要定义,是Javac编译器
自动收集类
中所有类变量的赋值动作和静态代码块中的语句合并而来 - 构造器中的方法指令按照语句在源文件中出现的顺序执行(字节码指令)
- < clinit>() 不同类的构造器(构造器是VM视角下的< init>())
- 若该类具有父类,JVM会保证子类的< clinit>()执行前,父类的先执行完毕
- 虚拟机保证一个类的< clinit>()方法在多线程下被同步加载(并发下单例对象,枚举破坏字节码文件)
7. 类加载器的分类
- JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)
代码加密是否可以使用加载器实现的(留下思考吧,哈哈)
- 从概念上来讲, 自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器
- 无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个
8. 虚拟机自带类加载器
8.1 启动类加载器,引导类加载器 Bootstrap ClassLoader
- 这个类加载使用 C/C++语言实现的,嵌套在虚拟机内部
- 它用来加载Java核心内库(JAVA_HOME/jre/lib/tr.jar, resource.jar或者 sun.boot.class.path路径下的内容), 用于提供JVM自身的类
- 并不继承 java.lang.ClassLoader, 没有父加载类
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器
- Bootstrap 启动类加载器只加载 java, javax, sun 等开头的类
9. VM 自带的加载器
9.1 扩展类加载器 Extension ClassLoader
- Java语言实现, 由sun.misc.Launcher$ExtClassLoader实现
- 派生与ClassLoader 类
- 父类加载器为启动类加载器
- 从java.ext.dirs 系统属性所指定的目录中加载类库,或从JDK的安装目录的 jre/lib/ext子目录下加载类库,如果用户创建的 jar 放在此目录下,也会自动有扩展类加载器加载
9.2 应用程序类加载器 AppClassLader(系统类加载器)
- Java语言实现, 由sun.misc.Launcher$AppClassLoader实现
- 派生与ClassLoader 类
- 父类加载器为扩展类加载器
- 它负责加载环境变量 classpath 或者 系统属性, Java.class.path 指定路径下的类库
- 该类加载是和
程序默认的类加载器
,一般来说,Java应用程序
的类由它来完成
加载 - 通过 ClassLoader#getSystemClassLoader()方法可以直接获得该类加载器
10. 用户自定义类加载器
- Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。
- 为什么要自定义类加载器?
2.1 隔离加载类
2.2 修改类加载的方式
2.3 扩展加载源
2.4 防止源码泄漏(加密代码,指定类加载器解密)
用户自定义类加载器实现步骤
- 开发人员可以通过继承抽象类java . lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
- ,在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadclass ()方法,从而实现自定义的类加载类,但是在
JDK1.2之后
已不再建议用户去覆盖loadclass ()方法,而是建议把自定义的类加载逻辑写在findClass ()方法中(很多还是讲解重写 loaderclass(), 注意建议而已)
- 在编写自定义类加载器时,如果没有太过于复杂的需求,可以
直接继承URLClassLoader类
,这样就可以避免自己去编写findClass ()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁
11. 关于ClassLoader
ClassLoader 类,是一个抽象类,其他所有加载器都继承自ClassLoader(不包含启动类加载器)
返回值 | 方法名 | 描述 |
---|---|---|
ClassLoader | getParent() | 返回父类加载器进行委派 |
Class< T> | loadClass(String name) | 加载名为 name ,返回名为 name 的 java.lang.Class实例 |
Class< T> | findClass(String name) | 查找名为 name 类,返回名为 name 的 java.lang.Class实例 |
Class< T> | findLoaderClass(String name) | 查找名为 name 已经被加载过的类,返回 java.lang.Class实例 |
Class< T> | defineClass(String name, byte[] b, int off, int len) | 将字节数组转换为类别 类的实例,返回 java.lang.Class实例 |
void | resolveClass(Class<?> c) | 链接指定的类 |
注意 : paren1 = null 思考为什么? (后面聊)
12. 获取当前类的ClassLoader
- classXx.getClassLoder() 获得当前类的加载器
- Thread.currentThread.getContextClassLoader() 获得上下文的类ClassLoader
- ClassLoader.getSystemClassLoader() 获得系统的ClassLoader
13. 双亲委派机制
- Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时, Java虚拟机采用的是
双亲委派模式
,即把请求交由父类处理,它是一种任务委派模式。
- 工作原理 : 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
- 上面 预留`注意 : paren1 = null 思考为什么? , 因为加载 AppClassLoader 是引导类加载器 Bootstrap ClassLoader加载, 引导类加载器 Bootstrap ClassLoader使用 C/C++设计,所以Java获得不到对象实例
13.1 双亲委派优势
- 避免类的重复加载
- 保证程序安全,防止核心类库被篡改 (自定义 String 类是否被加载)
13.2 沙箱安全机制
自定义string类,但是在加载自定义string类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\string.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的string类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制
。
14. JVM 确定一个类的必要条件
- 类的全限定类名必须一致(包括包名)
- 加载这个类的 ClassLoader (指 ClassLoader实例对象). 必须相同
- 反比 2, 在JVM中,即使两个类的对象(Class对象)来源同一个Class文件,被同一个虚拟机加载,但只要加载他们的ClassLoader实例对象不是同一个,那么这个类对象也是不相等的
15. 对类加载器的引用
- JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。
- 如果一个类型是由用户类加载器加载的,那么JVM会将这一个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候, JVM需要保证这两个类型的类加载器是相同的。
作者:专业于写这些入门到深层知识,提升我们的基本功,期待你的关注,和我一起学习
转载说明:未获得授权,禁止转载