提示:本文只是对JVM粗浅的一个总结。因为本人刚刚看完狂神的JVM的视频,所以想才薄智浅的总结一下。如有错误,麻烦各位大佬在评论区指出,谢谢~
前言
JVM 是可运行 Java 代码的假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收,堆 和 一个存储方法域。JVM 是运行在操作系统之上的,它与硬件没有直接的交互。
来个JVM的结构图。
JVM很难,要怎么样可以理解JVM呢?网上有一堆的文章写得都很好很详细,但是大部分的文章都是在告诉我什么是GC?什么是类加载器?什么是双亲委派机制?……对于我来说,是不太有用的,所以我就自己总结了。
首先学习JVM的第一个问题应该是:Java为什么会有JVM?
解答:
一般情况下,使用 C++ 开发的程序,编译成二进制文件后,就可以直接执行了,操作系统能够识别它;但是 Java 程序不一样,使用 javac 编译成 .class 文件之后,还需要使用 Java 命令去主动执行它,操作系统并不认识这些 .class 文件。因此这就需要JVM了。
我们现在知道 Java 源文件,通过编译器,能够生产相应的.Class 文件,也就是字节码文件,而字节码文件又通过 Java 虚拟机中的解释器,编译成特定机器上的机器码 。
每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是 Java 为什么能够跨平台的原因了 ,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。
Java 文件->编译器>字节码->JVM->机器码。
前期准备
众所周知,JVM只能接受.Class文件,而现在的文件是.java的,所以要使用javac命令编译成.Class文件。图片来源
编译后的.Class文件是这样的。图片来源
第一站:类的加载机制
现实中并不是说,把一个文件修改成 .class 后缀,就能够在 JVM 运行了。而实际上是当我们使用到哪个类的时候就先会通过类加载器把.Class文件(class字节码文件)中的类加载到jvm内存中,然后就在jvm内存中运行我们的代码。然而类加载器是如何把类加载到jvm内存中这个过程是非常复杂的,但其主要的过程是这样:加载、连接(验证、准备、解析)、初始化。图片来源
加载
加载的主要作用是将外部的 .class 文件,加载到 Java 的方法区内。加载阶段主要是找到并加载类的二进制数据,比如从 jar 包里或者 war 包里找到它们。
大概的过程:
- 通过全类名获取定义此类的二进制字节流。
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
- 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口。
加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。
验证
肯定不能任何 .class 文件都能加载,那样太不安全了,容易受到恶意代码的攻击。验证阶段在虚拟机整个类加载过程中占了很大一部分,不符合规范的将抛出 java.lang.VerifyError 错误。像一些低版本的 JVM,是无法加载一些高版本的类库的,就是在这个阶段完成的。
为了保证被加载类的正确性。其主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。图片来源
准备
加载后的类通过了验证阶段,就会进入到准备阶段。这个阶段是给类分配一定的内存空间。这些内存都将在方法区中分配。
- 为类变量( Class Variables ,即静态变量,被 static 关键字修饰的变量,只与类相关,因此被称为类变量)分配内存:因为这里的变量是由方法区分配内存的,所以仅包括类变量而不包括实例变量,后者将会在对象实例化时随着对象一起分配在Java堆中。
- 设置类变量初始值:通常情况下零值(如 0、0L、null、false 等)。
- 比如我们定义了
public static int a=1
,那么 a变量在准备阶段的初始值就是 0 而不是 1(初始化阶段才会赋值)。特殊情况:比如给 a变量加上了 final 关键字public static final int a=1
,那么准备阶段 a的值就被赋值为 1。
- 比如我们定义了
基本数据类型的零值 : (图片来自《深入理解 Java 虚拟机》第 3 版 7.33 )
解析
解析在类加载中是非常非常重要的一环,是将符号引用替换为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。
符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
那这个阶段都做了哪些工作呢?大体可以分为:
- 类或接口的解析
- 类方法解析
- 接口方法解析
- 字段解析
我们来看几个经常发生的异常,就与这个阶段有关。
- java.lang.NoSuchFieldError 根据继承关系从下往上,找不到相关字段时的报错。
- jjava.lang.IllegalAccessError 字段或者方法,访问权限不具备时的错误。
- jjava.lang.NoSuchMethodError 找不到相关方法时的错误。
解析过程保证了相互引用的完整性,把继承与组合推进到运行时。
初始化
如果前面的流程一切顺利的话,接下来该初始化成员变量了,到了这一步,JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。
类加载器
整个类加载过程任务非常繁重,虽然这活儿很累,但总得有人干。类加载器做的就是上面 5 个步骤的事。
以下内容来源文章:
首先我们就先弄明白一个问题,jvm是什么时候去加载类的呢?
其实答案很简单,就是我们什么时候使用到了这个类,它就去class字节码文件中去加载这个类。
而作为程序的入口,具有main方法的类,肯定是最开始的时候就加载到jvm中了。
对于加载类的时间点问题,其实就是这么简单。
类加载器和双亲委派机制
既然我们知道了类加载的时间点,那么jvm是通过什么方式对类进行加载的呢?就是类加载器。
那接下来我们就来聊聊jvm的类加载器。
jvm的类加载器总体上可以分成4层,我们一起看一下。
1.启动类加载器
首先就是jvm启动的第一道关口,启动类加载器Bootstrap ClassLoader,它主要是加载java的核心类。
相信大家都知道,无论是什么环节下运行java程序,都是要安装jvm虚拟机环境的,而在这个环境的目录中是有一个lib文件夹的,这个文件下就是java最核心的类库,支撑着java系统的运行。
所以一旦jvm启动,那么首先就会通过启动类加载器去加载lib文件夹下的核心类库。
2.扩展类加载器
然后我们就到了第二层,扩展类加载器Extension ClassLoader,这个类加载器其实与启动类加载器是类似的。
在我们的jvm虚拟机环境目录下,是有一个lib/ext的文件夹的,这里面的类就是java运行环境的一些扩展类,这些扩展类就是在jvm启动后,通过扩展类加载器进行加载的。
3.应用程序类加载器
加载完核心类库和扩展类,这时候就到了第三层,应用程序类加载器Application ClassLoader,这个类加载器你就可以理解成是加载我们写好的java代码的就可以了。
4.自定义类加载器
前面的三层就是基本的类加载器了,然后第四层是自定义类加载器,根据一些特殊的需求来自己定义类加载器加载我们的类。
5.双亲委派机制
整体上类加载器就是这么的4层结构。很多小伙伴可能都听说过双亲委派机制,那么什么是双亲委派机制呢,
其实很好理解,就是当我们的类加载器要加载一个类的时候,它首先会委派给它的父亲去加载,但是如果它的父亲没找到就会把这个事交给他的孩子自己去完成了。
这就是双亲委派机制。
举个例子,假如我们的应用程序类加载器要加载一个类A,那么首先它会先回家找它老爸扩展类加载器,问问“老爸,你那有这个类A吗?”
然后扩展类加载器接到这个请求之后,同样也懒得处理,再去找它爷爷启动类加载器。
它爷爷找了一圈没找到类A,很生气,就对扩展类加载器说,“我这没有,你自己找去!”
然后扩展类加载器就灰溜溜的自己找了一圈,同样也没找到,这时候就找到应用类加载器了,说:“我这哪有你这个类A,这明明是你自己应该干的活,爱上哪找上哪找去,我不管了”。
这时候应用类加载器就只能自己去处理了,找了一圈发现找到了类A,就把它加载到jvm内存中了。
相信大家看了这个例子应该很容易理解了吧。
所以假设我们自己创建了一个类java.lang.String,它是不会被应用类加载器加载到内存中的,因为父类中可以找到这个类,就直接给加载到内存中了。
总结
提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。