前言
之前去面试的时候面试官问了我关于关于JVM性能调优的问题,由于自己之前公司的项目里自己没有接触到JVM性能调优的相关问题(感觉这些都是公司架构师考虑的问题),所有面试官问的时候自己一脸懵逼,所有最后的结果当然是凉凉。。,于是,为了查漏补缺,就去学习了一下JVM的相关知识,希望能帮助到大家。
正文
在学习任何一项新的知识之前,我都会先列出一份学习大纲,然后按照这个学习大纲一步一步的来学习了解,所以学习JVM这个新的技术,我也分为了3个板块来学习:JVM类加载器,JVM内存结构,JVM垃圾回收这三个板块来学习,今天这篇文章讲的是JVM类加载器。
一、什么是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虚拟机是来解释字节码文件的,而解释得这个过程其实是一个很复杂得过程,所以这就到了我们今天要讲得主题了。
二、类加载(classLoading)
我们先来了解一下类加载得整个过程。从下图可以看到类的生命周期一共分为5个阶段,加载、连接(包括验证、准备和解析)、初始化、使用(类得实例化)、卸载(垃圾回收)。
在Java代码中,我们都知道类(指的是类本身Class,比如,Interface,Enum)的加载、连接、初始化过程都是在程序运行期间完成的。下面我们就先讲一下类得加载、连接和初始化。
类的加载:最常见的一种情况是将已存在的类的Class文件(也就是字节码文件)从磁盘上面加载到内存里面,将其放在运行时数据区的方法区中,然后在内存中创建一个java.lang.Class对象用来封装类在方法区中的数据结构
类的连接(又细分了三个阶段):
- 验证:确保被加载类的正确性
- 准备:为类的静态变量(也可以称为类变量)分配内存,并将其初始化为默认值(比如int 的默认值就是0)
- 解析:将类中的符号引用转换为直接引用
类的初始化:为类的静态变量进行赋值(从代码从上到下执行)
Java程序对类的使用方式可分为两种:
- 主动使用
- 被动使用
所有的Java虚拟机实现,在每个类或接口被Java程序"首次主动使用"时才初始化他们,一定要记住,是首次并且还是主动使用得时候才会初始化类。
如果对其类或者接口主动使用导致初始化了(此时的初始化就说明加载、连接(连接的三个步骤,注意,此时的连接只完成类的静态变量分配内存,并将其初始化为默认值)已经完成了)
我这里总结了7种主动使用:
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(如class.forName())
- 初始化一个类的子类
- Java虚拟机启动时被表明为启动类的类
- JDK1.7开始提高的动态语言支持;
除了以上7种情况,其他使用Java类的方式都被看做是对类的被动使用,都不会导致类的初始化。
三、类的加载连接初始化详细讲解
其实我们知道类的加载的最终产品是位于内存中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
根据以上的总结,我们知道类的连接其实就是当类被加载后,就进入连接阶段。连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行环境中去。那么类的验证的内容有哪些呢?
- 类文件的结构检查
- 语义检查
- 字节码验证
- 二进制兼容性的验证
四、类加载器
类的加载其实是类加载器去完成的,我们可以把类加载器想象成一个小人,帮助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类)
- 用户可以定制类的加载方式
类加载器并不需要等到某个类被”首次主动使用“时再加载它
五、类加载器双亲委托机制详解
这一小节我们来详细了解一下类加载器的双亲委托机制。父亲委托机制也称为双亲委托机制(我个人得理解实际上应该叫做父亲委托机制,因为在源码里面是parent而不是parents):在父亲委托机制中,各个加载器按照父子关系形成了熟悉结构(逻辑上的,比如下图),除了启动类加载器之外,其余的类加载器都有且只有一个父加载器。
以下几种加载器从表面看是继承关系,实际上是包含关系哦
我举例来看看父亲委托机制的实际执行:
对上图执行流程我详细得解释一下类加载器父亲委托机制具体是怎么执行得:首先loader1和loader2是我们自定义的加载器,loader1尝试去加载Sample类,根据父亲委托机制,其实并不是由loader1去直接加载Sample类到虚拟机当中,相反,它是把这个加载任务转交给系统类加载器去完成,系统类加载器再把这个加载任务转交给扩展类加载器,然后扩展类加载器再转交给根类加载器去完成,由于根类加载器已经是类加载器体系层次的最顶层,所以根类加载器会尝试去Sample类到虚拟机当中(然后根类加载器不能加载,因为他是从特定的几个目录去加载),既然根类加载器无法完成加载,他就会把这个任务返回给扩展类加载器(同理,原则上也不能加载),再让系统类加载器去加载(一般是可以加载成功)。最终再把这个流程返回给loader1,就宣告类加载过程结束。多
六、获取类加载器的几种途径
既然我们了解了类加载器的种类,那我们也需要了解通过什么方式可以获取到类加载器,获取类加载器的方式我这里总结了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()
七、总结
通过以上得相关总结,我们其实可以发现,JVM学习并不是像spring,springcloud都是应用框架,是可以马上做东西的,立竿见影,可以马上看到效果,JVM不是这样的,涉及到了很多理论。很多同学可能觉得不重要,感觉学了也没有,其实不然,就像练武一样,只有你的内功修炼好了,再去练其他的招式就会很容易,才会精益求精,而JVM就相当于内功,所以可想而知,对于JVM的学习,显然是很重要的。以上就是我对JVM类加载器相关总结