前言
之前去面试的时候面试官问了我关于关于 JVM 性能调优的问题, 由于自己之前公司的项目里自己没有接触到 JVM 性能调优的相关问题(感觉这些都是公司架构师考虑的问题), 所有面试官问的时候自己一脸懵逼, 所有最后的结果当然是凉凉.., 于是, 为了查漏补缺, 就去学习了一下 JVM 的相关知识, 希望能帮助到大家.
正文
在学习任何一项新的知识之前, 我都会先列出一份学习大纲, 然后按照这个学习大纲一步一步的来学习了解, 所以学习 JVM 这个新的技术, 我也分为了 3 个板块来学习: JVM 类加载器, JVM 内存结构, JVM 垃圾回收这三个板块来学习, 今天这篇文章讲的是 JVM 类加载器.
1, 什么是 JVM
既然是学习关于 JVM 的相关理论知识, 我们当然得知道什么是 JVM.JVM 是 Java Virtual Machine(Java 虚拟机)的缩写. 既然说到虚拟机, 可能又会有人问什么是虚拟机了, 我这里把虚拟机得相关概念放在这里:
虚拟机: 就是一台虚拟的计算机, 他是一款软件; 用来执行一系列计算机指令. 虚拟机可以分为系统虚拟机和程序虚拟机.
系统虚拟机: 比如 VMware, 他们完全是对物理计算机的仿真, 提供了一个可运行完整操作系统的软件平台.
程序虚拟机: 比如 Java 虚拟机, 它专门为执行单个计算机程序而设计. 在 Java 虚拟机中执行的 指令我们称为 Java 字节码指令.(JVM 是运行在操作系统之上的, 它与硬件没有直接的交互)
所以根据定义, 我们可以得知 JVM 是程序虚拟机. 那么 JVM 在哪里呢, 其实, 我们在最开始学习 Java 得时候, 都必须按照 Java 得运行环境, 从网上下载 JDK 安装包, 安装完成之后, 在安装路径下会有两个文件夹, 一个叫 Jdk, 一个叫 jre, 而 java 虚拟机就在 jre 的文件夹里面.
存在即有他存在的道理, 那么 JVM 的存在有什么用呢? 他是用来干嘛的呢? 学过 JAVA 的都知道, java 程序要想运行, Java 源程序 (.java) 要先编译成与平台无关的字节码文件(.class), 然后字节码文件再解释成机器码运行. 而解释得这个过程就是通过 Java 虚拟机来执行的(可以参考下面这张图理解).java 虚拟机是来解释字节码文件的, 而解释得这个过程其实是一个很复杂得过程, 所以这就到了我们今天要讲得主题了.
2, 类加载(classLoading)
我们先来了解一下类加载得整个过程. 从下图可以看到类的生命周期一共分为 5 个阶段, 加载, 连接(包括验证, 准备和解析), 初始化, 使用(类得实例化), 卸载(垃圾回收).
在 Java 代码中, 我们都知道类 (指的是类本身 Class, 比如, Interface,Enum) 的加载, 连接, 初始化过程都是在程序运行期间完成的. 下面我们就先讲一下类得加载, 连接和初始化.
类的加载: 最常见的一种情况是将已存在的类的 Class 文件 (也就是字节码文件) 从磁盘上面加载到内存里面, 将其放在运行时数据区的方法区中, 然后在内存中创建一个 java.lang.Class 对象用来封装类在方法区中的数据结构
类的连接(又细分了三个阶段):
1, 验证: 确保被加载类的正确性
2, 准备: 为类的静态变量 (也可以称为类变量) 分配内存, 并将其初始化为默认值(比如 int 的默认值就是 0)
3, 解析: 将类中的符号引用转换为直接引用
类的初始化: 为类的静态变量进行赋值(从代码从上到下执行)
Java 程序对类的使用方式可分为两种:
主动使用
被动使用
所有的 Java 虚拟机实现, 在每个类或接口被 Java 程序 "首次主动使用" 时才初始化他们, 一定要记住, 是首次并且还是主动使用得时候才会初始化类.
如果对其类或者接口主动使用导致初始化了 (此时的初始化就说明加载, 连接(连接的三个步骤, 注意, 此时的连接只完成类的静态变量分配内存, 并将其初始化为默认值) 已经完成了)
我这里总结了 7 种主动使用:
-- 创建类的实例
-- 访问某个类或接口的静态变量, 或者对该静态变量赋值
-- 调用类的静态方法
-- 反射(如 class.forName())
-- 初始化一个类的子类
--Java 虚拟机启动时被表明为启动类的类
--JDK1.7 开始提高的动态语言支持;
除了以上 7 种情况, 其他使用 Java 类的方式都被看做是对类的被动使用, 都不会导致类的初始化.
3, 类的加载连接初始化详细讲解
其实我们知道类的加载的最终产品是位于内存中的 Class 对象, Class 对象封装了类在方法区内的数据结构, 并且向 Java 程序员提供了访问方法区内的数据结构的接口.
根据以上的总结, 我们知道类的连接其实就是当类被加载后, 就进入连接阶段. 连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行环境中去. 那么类的验证的内容有哪些呢?
类文件的结构检查
语义检查
字节码验证
二进制兼容性的验证
4, 类加载器
类的加载其实是类加载器去完成的, 我们可以把类加载器想象成一个小人, 帮助 JVM 干活的. 那么类加载器的定义是什么呢, 这里按照我个人的理解总结了一下:
类加载器(classLoader): 类加载器是用来把类加载到 Java 虚拟机的内存空间中(加载类的工具, 类一定是由类加载器去加载). 从 JDK1.2 版本开始, 类的加载过程采用双亲委托机制. 这种机制能更好的保证 Java 平台的安全. 在此委托机制中, 除了 Java 虚拟机自带的根类加载器之外(因为根类加载器本身是没有父加载器的), 其余的类加载器都有且只有一个父加载器. 当 Java 程序请求加载器 loader1 加载 Sample 类时, loader1 首先委托自己的父加载器去加载 Sample 类, 若父加载器能加载, 则由父加载器完成加载任务, 否则才有加载器 loader1 本身加载 Sample 类.
类加载器分为两种类型:
1,Java 虚拟机自带的加载器
根类加载器(BootstrapClassLoader), 也称启动类加载器
扩展类加载器(ExtensionClassLoader)
系统 (应用) 类加载器(SystemClassLoader 或者 AppClassLoader)
2, 用户自定义的类加载器
java.lang.ClassLoader 的子类(所有用户自定义的类加载器都应该继承抽象类 ClassLoader 类)
用户可以定制类的加载方式
类加载器并不需要等到某个类被 "首次主动使用" 时再加载它
5, 类加载器双亲委托机制详解
这一小节我们来详细了解一下类加载器的双亲委托机制. 父亲委托机制也称为双亲委托机制(我个人得理解实际上应该叫做父亲委托机制, 因为在源码里面是 parent 而不是 parents): 在父亲委托机制中, 各个加载器按照父子关系形成了熟悉结构(逻辑上的, 比如下图), 除了启动类加载器之外, 其余的类加载器都有且只有一个父加载器.
以下几种加载器从表面看是继承关系, 实际上是包含关系哦
我举例来看看父亲委托机制的实际执行:
对上图执行流程我详细得解释一下类加载器父亲委托机制具体是怎么执行得: 首先 loader1 和 loader2 是我们自定义的加载器, loader1 尝试去加载 Sample 类, 根据父亲委托机制, 其实并不是由 loader1 去直接加载 Sample 类到虚拟机当中, 相反, 它是把这个加载任务转交给系统类加载器去完成, 系统类加载器再把这个加载任务转交给扩展类加载器, 然后扩展类加载器再转交给根类加载器去完成, 由于根类加载器已经是类加载器体系层次的最顶层, 所以根类加载器会尝试去 Sample 类到虚拟机当中(然后根类加载器不能加载, 因为他是从特定的几个目录去加载), 既然根类加载器无法完成加载, 他就会把这个任务返回给扩展类加载器(同理, 原则上也不能加载), 再让系统类加载器去加载(一般是可以加载成功). 最终再把这个流程返回给 loader1, 就宣告类加载过程结束.
6, 获取类加载器的几种途径
既然我们了解了类加载器的种类, 那我们也需要了解通过什么方式可以获取到类加载器, 获取类加载器的方式我这里总结了 4 种方式:
第一种: 获得当前类的 ClassLoader:
clazz.getClassLoder()
具体实现如下所示:
- Class<?> clazz1 = Class.forName("java.lang.String");
- System.out.println(clazz1.getClassLoader());
第二种: 获得当前线程上下文的 ClassLoader:
Thread.currentThread().getContextClassLoader();
具体实现如下所示:
- ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
- System.out.println(contextClassLoader);
第三种: 获得系统 ClassLoader:
ClassLoader.getSystemClassLoader();
第四种: 获得调用者的 ClassLoader
DriverManager.getCallerLoader()
我们还需要知道其实数组并不是由类加载器加载创建的的, 而是当被需要时, 被 jvm 运行时自动创建的, 对于数组来说, 他的类加载器是和他元素的类型的类加载一样的, 如果元素类型是基本类型, 则数组没有类加载器
ClassLoader 类本身默认是并行加载的的(parallel capable), 如果子类想支持并行加载, 是需要自己注册的, 用户自定义加载器若需要并行加载, 需要自行配置, 通过调用 registerAsParallelCapable()
7, 总结
通过以上得相关总结, 我们其实可以发现, JVM 学习并不是像 spring,springcloud 都是应用框架, 是可以马上做东西的, 立竿见影, 可以马上看到效果, JVM 不是这样的, 涉及到了很多理论. 很多同学可能觉得不重要, 感觉学了也没有, 其实不然, 就像练武一样, 只有你的内功修炼好了, 再去练其他的招式就会很容易, 才会精益求精, 而 JVM 就相当于内功, 所以可想而知, 对于 JVM 的学习, 显然是很重要的. 以上就是我对 JVM 类加载器相关总结, 下一篇文章应该是推出关于结合 java 源码理解类加载器得相关内容, 当然后续也会推出 JVM 其他板块相关知识得相关总结.
公众号: 良许 Linux
有收获? 希望老铁们来个三连击, 给更多的人看到这篇文章
来源: https://www.cnblogs.com/yychuyu/p/13362695.html