- 博主简介:想进大厂的打工人
- 博主主页:@xyk:
- 所属专栏: JavaEE初阶
本文我们主要讲解一下面试中常见的问题,如果想深入了解,请看一下《Java虚拟机规范》这本书
目录
文章目录
一、JVM简介
二、JVM整体组成
2.1 运行时数据区组成
2.2 小结
三、JVM类加载
3.1 类加载过程
四、类加载什么时候会触发
五、双亲委派模型
一、JVM简介
JVM 是 Java Virtual Machine 的简称,意为 Java虚拟机。
虚拟机是指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统,JVM是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进行了裁剪。
二、JVM整体组成
JVM整体组成可分为四个部分:
1.类加载器(ClassLoader)
2.运行时数据区(Runtime Data Area)
3.执行引擎(Execution Engine)
4.本地库接口(Native Interface)
各个组成部分的用途:
程序在执行之前先要把java代码转换成字节码(.class文件),JVM首先需要把字节码通过一定的方式 类加载器(ClassLoader) 把文件加载到内存中 运行时数据区(Runtime Data Area) ,而字节码文件是JVM的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine) 将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这4个主要组成部分的职责与功能。
2.1 运行时数据区组成
jvm的运行时数据区,不同虚拟机实现可能略微有所不同,但都会遵从Java虚拟机规范,Java 8 虚拟机规范规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域:
1.程序计数器(Program Counter Register)
程序计数器是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器,记录当前线程执行到哪个指令。
特征:线程私有,由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,也就是一个处理器都只会执行一条线程中的指令。因此为了线程切换后能恢复到正确的执行位置,每个线程都由独立的程序计数器。
如果线程正在执行Java中的方法,程序计数器记录的就是正在执行虚拟机字节码指令的地址,如果是Native方法,这个计数器就为空(undefined),因此该内存区域是唯一一个在Java虚拟机规范中没有规定OutOfMemoryError的区域。
2.Java虚拟机栈(JVM Stacks)
Java虚拟机栈(Java Virtual Machine Stacks)描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法返回地址等信息,每个方法从调用直至执行完成的过程,都对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
特性:线程私有(不绝对,局部变量可以互相访问),它的生命周期和线程相同。
3.本地方法栈
本地方法栈(Native Method Stack)与虚拟机栈的作用是一样的,只不过虚拟机栈是服务Java方法的,而本地方法栈是为虚拟机调用Native方法服务的。
4.堆区(Heap)
Java堆(Java Heap)是Java虚拟机中内存最大的一块,是被所有线程共享的。我们常见的 JVM 参数设置 -Xms10m 最小启动内存是针对堆的,-Xmx10m 最大运行内存也是针对堆的
堆的作用:程序中创建的所有对象(new对象,类的成员变量)都在保存在堆中。
关于新生代和老生代,后面再讲~
5.方法区(元数据区)
方法区(Methed Area)用于存储已被虚拟机加载的类信息(类对象)、常量、静态变量、即时编译后的代码等数据。
在《Java虚拟机规范中》把此区域称之为“方法区”,而在 HotSpot 虚拟机的实现中,在 JDK 7 时此区域叫做永久代(PermGen),JDK 8 中叫做元空间(Metaspace)。
PS:永久代(PermGen)和元空间(Metaspace)是 HotSpot 中对《Java虚拟机规范》中方法区的实现,它们三者之间的关系就好比,对于一辆汽车来说它定义了一个部分叫做“动能提供装置”,但对于不同的汽车有不同的实现技术,比如对于燃油车来说,它的“动能提供装置”的实现技术就是汽油发动机(简称发动机),而对于电动汽车来说,它的“动能提供装置”的实现就是电动发动机(简称电机),发动机和电机就相当于永久代和元空间一样,它是对于“制动器”也就是方法区定义的实现。
运行时常量池是方法区的一部分,存放字面量与符号引用:
- 字面量 : 字符串(JDK 8 移动到堆中) 、final常量、基本数据类型的值。
- 符号引用 : 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。
2.2 小结
记住这几个原则:
- 局部变量在栈上
- 普通成员变量在堆上
- 静态成员变量,基本数据类型的值,final常量,在方法区/元数据区上
三、JVM类加载
3.1 类加载过程
对于一个类来说,它的生命周期是这样的:
主要分为这几个步骤:
1. 加载:找到.class文件,并且读文件内容到内存中
2. 连接:
2.1. 验证:检查.class文件格式是否正确
2.2. 准备:给类对象分配内存空间,内存空间中的数据全是0
2.3. 解析:初始化字符串常量,将符号引用转为直接引用
那么符号引用是什么?
符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,符号引用和虚拟机的布局无关。
3. 初始化:针对类对象进行初始化(调用构造方法,初始化静态成员,执行静态代码块,类要是有父类还需要加载父类)
四、类加载什么时候会触发
不是JVM一启动,就把所有的.class都加载了,整体是一个“懒加载“的策略,非必要,不加载~~
什么是叫做必要?
1.创建了这个类的实例
2.使用了这个类的静态方法/静态属性
3.使用子类,会触发父类的加载
一旦加载过后,后续不再重复加载~
五、双亲委派模型
双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最 终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无 法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
JVM中,加载类,需要用到一组特殊的模块,类加载器~
在JVM中,内置了三个 类加载器:
- BootStrap ClassLoader(java规范的标准库)
- Extension ClassLoader(JVM扩展库)
- Application ClassLoader(加载用户提供的第三方库/项目中的类)
加载一个类的时候,先从Applicaton ClassLoader开始加载,但是先把任务交给父亲Extension ClassLoader,Extension ClassLoader收到请求再交给自己的父亲BootStrap ClassLoader,由于BootStrap ClassLoader没有了父亲,此时就自己来搜索自己负责的区域,如果搜索到,直接进行后续加载步骤,如果没搜到,再交给自己孩子去处理,依此类推到Application ClassLoader,当都没有找到的时候,会抛出一个 ClassNotFound Exception.
为了避免bug,保证BootStrap ClassLoader先加载,Application ClassLoader后加载~
双亲委派模型优点:
1. 避免重复加载类:比如 A 类和 B 类都有一个父类 C 类,那么当 A 启动时就会将 C 类加载起来,那么在 B 类进行加载时就不需要在重复加载 C 类了。
2. 安全性:使用双亲委派模型也可以保证了 Java 的核心 API 不被篡改,如果没有使用双亲委派模
型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object
类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类,而有些 Object 类又是用户
自己提供的因此安全性就不能得到保证了。