JVM :java虚拟机
概述
虚拟机:所谓虚拟机(Virtual Machine),就是一台虚拟的计算机。它是一款软件,用来执
行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机和程序虚拟机。它专门为
执行某个单个计算机程序而设计。在 java 虚拟机中执行的指令我们称为 java 字节码指令。
Java 技术的核心就是 java 虚拟机,因为所有的 java 程序都运行在 java 虚拟机内部。
JVM作用:
Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对
应平台上的机器码指令执行
特点:
1.一次编译到处运行
2.自动内存管理
3.自动垃圾回收功能
JVM的位置
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 个主要组成部分的职责与功能。
JVM 组成指的是运行时数据区(Runtime Data Area),因为通常需要程序员调试分析的区域就是“运行时数据区”,或者更具体的来说就是“运行时数据区”里面的 Heap(堆)模块。
jvm运行流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cfKkKcIq-1626868135368)(C:\Users\Administrator\Desktop\JVM运行流程.PNG)]
JVM 架构模型
Java 编译器输入的指令流基本上是一种基于栈的指令集架构,另一种指令集架构
是基于寄存器的指令集架构.
两种架构之间的区别:
基于栈式架构的特点
设计和实现更简单,适用于资源受限的系统.
使用零地址指令方式分配,其执行过程依赖于操作栈,指令集更小,编译器容易实现.
不需要硬件支持,可移植性好,更好实现跨平台.
基于寄存器式架构特点:
指令完全依赖于硬件,可移植性差.
性能优秀,执行更高效.
完成一项操作使用的指令更少.
使用 javap -v class文件可以将 class 文件反编译为指令集
由于跨平台的设计,Java 指令集都是根据栈来设计的,不同 CPU 架构不同,所以不能设计为基于寄存器的.
优点是跨平台,指令集小,编译器容易实现.
缺点是性能下降,实现同样功能需要更多的指令.
类加载
类加载过程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3LZ3CcPs-1626868135371)(C:\Users\Administrator\Desktop\类加载的过程.PNG)]
加载
- 通过类名(地址)获取此类的二进制字节流.
- 将这个字节流所代表的静态存储结构转换为方法区(元空间)的运行时结构.
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的
各种数据的访问入口.
链接
验证:检验被加载的类是否有正确的内部结构,并和其他类协调一致;
准备:准备阶段则负责为类的静态属性分配内存,并设置默认初始值;
不包含用 final 修饰的 static 实例变量,在编译时进行初始化.
不会为实例变量初始化
解析:将类的二进制数据中的符号引用替换成直接引用(符号引用是用一组符
号描述所引用的目标;直接引用是指向目标的指针).
初始化
类什么时候初始化?
1 )创建类的实例,也就是 new 一个对象
2)访问某个类或接口的静态变量,或者对该静态变量赋值
3)调用类的静态方法
4)反射(Class.forName(“”))
5)初始化一个类的子类(会首先初始化子类的父类)
垃圾回收阶段算法**
当成功区分出内存中存活对象和死亡对象后,GC 接下来的任务就是执行垃
圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对
象分配内存
类的初始化顺序
先对static修饰的变量或语句块进行复制
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
顺序是:父类 static –> 子类 static –> 父类构造方法- -> 子类构造方法
类加载器分类
JVM 支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)
和自定义类加载器(User-Defined ClassLoader).
引导类加载器
并不集成于 java.lang.ClassLoader没有父加载器
负责加载扩展类加载器和应用类加载器,并为他们指定父类加载器.
出于安全考虑,引用类加载器只加载包名为 java,javax,sun 等开头的类.
扩展类加载器
Java 语言编写的,由 sun.misc.Launcher$ExtClassLoader 实现.
派生于 ClassLoader 类.
上层类加载器为引用类加载器.
应用程序类加载器
上层类加载器为扩展类加载器.
加载我们自己定义的类
该类加载器是程序中默认的类加载器.
ClassLoader 类,它是一个抽象类,其后所有的类加载器都继承自 ClassLoader
(不包括启动类加载器)
双亲委派机制*
概述:Java 虚拟机对 class 文件采用的是按需加载的方式,也就是说当需要该类时才会将它的 class 文件加载到内存中生成 class 对象.而且加载某个类的 class 文件时,Java 虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HiaTXEdn-1626868135373)(C:\Users\Administrator\Desktop\双亲委派机制.PNG)]
工作原理:
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请
求委托给父类的加载器去执行.
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终
将到达顶层的启动类加载器.
- 如果父类加载器可以完成类的加载任务,就成功返回,倘若父类加载器无法完
成加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制.
如果均加载失败,就会抛出 ClassNotFoundException 异常。
双亲委派机制的优点*
1 安全,可避免用户自己编写的类动态替换 Java 的核心类,如 java.lang.String
2 避免全限定命名的类重复加载(使用了 findLoadClass()判断当前类是否已加载)
沙箱安全机制
作用:防止恶意代码污染 java 源代码
比如上面我们定义了一个类名为 String 所在包也命名为 java.lang,因为这个类
本来是属于 jdk 的,如果没有沙箱安全机制的话,这个类将会污染到系统中的
String,但是由于沙箱安全机制,所以就委托顶层的引导类加载器查找这个类,如
果没有的话就委托给扩展类加载器,再没有就委托到系统类加载器.但是由于
String 就是 jdk 的源代码,所以在引导类加载器那里就加载到了,先找到先使用,
所以就使用引导类加载器里面的 String,后面的一概不能使用,这就保证了不被
恶意代码污染.
面试题:在JVM中如何判断两个对象是属于同一个类
- 类的全类名(地址)完全一致.
- 类的加载器必须相同.
类的主动使用/被动使用
JVM规定:每个类或者接口被首次主动使用时才对其进行初始化
主动使用:
1.通过new关键字被导致类的初始化,这是大家经常使用的初始化一个类的方式,他肯定会导致类的加载并且初始化
2.访问类的静态变量,包括读取和更新
3.访问类的静态方法
4.对某个类进行反射操作,会导致类的初始化
5.初始化子类会导致父类的的初始化
6.执行该类的 main 函数
被动使用:
除了主动其余全是被动使用
主动使用和被动使用的区别在于类是否会被初始化.
JVM 运行时数据区
Java 虚拟机所管理的内存将会包括以下几个运行时数据区域
1.程序计数器:程序计数器(Program Counter Register)是一块较小的内存空间,它可以
看作是当前线程所执行的字节码的行号指示器。
作用:
程序计数器用来存储下一条指令的地址,也即将要执行的指令代码.由执行引擎读
取下一条指令.
面试:1. 使用程序计数器存储字节码指令地址有什么用?
为什么使用程序计数器记录当前线程的执行地址呢?
因为 CPU 需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪儿
开始继续执行. JVM 的字节码解释器就需要通过改变程序计数器的值来明确下一条应该执行什
么样的字节码指令.
2.程序计数器为什么被设定为线程私有的.
我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的
方法,CPU 会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无
差呢?
为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然
是为每一个线程都分配一个程序计数器,这样一来各个线程之间便可以独立计算,
从而不相互干扰.
2.Java虚拟机栈
描述的是 Java 方法执行的内存模型,每个方法在执行的同时都会创建一个
线帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出
口等信息,每个方法从调用直至执行完成的过程,都对应着一个线帧在虚拟
机栈中入栈到出栈的过程。
Java 虚拟机栈是线程私有的.
生命周期和线程一致.
作用:
主管 Java 程序的运行,它保存方法的局部变量(8 种基本数据类型,对象的引用地
址),部分结果,并参与方法的调用和返回。
特点:
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器.
JVM直接对 java 栈的操作只有两个:调用方法,进栈. 执行结束后出栈.对于栈来说不存在垃圾回收问题.
栈中出现的异常
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。
栈的运行原理
1.JVM 直接对 java 栈的操作只有两个,就是对栈帧的压栈和出栈,遵循”先进后
出”后进先出的原则.
线帧的内部结构
局部变量表
操作数栈
动态链接
方法返回地址
什么情况下会出现栈溢出(StackOverflowError)?
栈溢出就是方法执行时创建的栈帧超过了栈的深度。那么最有可能的就是方法递归调
用产生这种结果。
通过调整栈大小,就能保证不出现溢出吗?
不能
分配的栈内存越大越好吗?
并不是的,只能延缓这种现象的出现,可能会影响其他内存空间
垃圾回收机制是否会涉及到虚拟机栈?
不会
3.本地方法栈
与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地
方法栈是为虚拟机调用 Native 方法服务的。
4.Java 堆
是 Java 虚拟机中内存最大的一块,是被所有线程共享的,在虚拟机启动时
候创建,Java 堆唯一的目的就是存放对象实例,几乎所有的对象实例都在这
里分配内存,随着 JIT 编译器的发展和逃逸分析技术的逐渐成熟,栈上分配、
标量替换优化的技术将会导致一些微妙的变化,所有的对象都分配在堆上渐
渐变得不那么“绝对”了。
堆内存区域划分
Java8 及之后堆内存分为:新生区(新生代)+老年区(老年代)
新生区分为 Eden(伊甸园)区和 Survivor(幸存者)区
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G8RGLLBi-1626868135374)(C:\Users\Administrator\Desktop\堆内存区划分.PNG)]
为什么分区(代)?
将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫
描垃圾时间及 GC 频率。针对分类进行不同的垃圾回收算法,对算法扬长避短
对象创建内存分配过程
新生区与老年区配置比例
默认**-XX:NewRatio**=2,表示新生代占 1,老年代占 2,新生代占整个堆的 1/3
可以修改**-XX:NewRatio**=4,表示新生代占 1,老年代占 4,新生代占整个堆的 1/5
当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整老年代的大小,来
进行调优
新生区的对象默认生命周期超过 15 ,就会去养老区养老
TLAB 机制
为什么有 TLAB
堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据.
由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程
不安全的.
为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度.
什么是 TLAB?
TLAB 的全称是 Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线
程专用的内存分配区域。如果设置了虚拟机参数–XX:UseTLAB .在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在
自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。避免多线程冲突,提高了对象分配的效率。
字符串常量池
永久代的回收效率很低,在 Full GC 的时候才会执行永久代的垃圾回收,而 Full GC 是老年代的空间不足、永久代不足时才会触发。这就导致 StringTable 回收效率不高,而我们开发中会有大量的字符串被创建,回收效率低,
导致永久代内存不足。放到堆里,能及时回收内存。
5方法区(方法区看做是一块独立于 java 堆的内存空间.)
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等
数据。内存区域是很重要的系统资源,是硬盘和 CPU 的中间桥梁,承载着操作系统和应
用程序的实时运行.JVM 内存布局规定了 Java 在运行过程中内存申请,分配,管理
的策略,保证了 JVM 的高效稳定运行.不同的 JVM 对于内存的划分方式和管理机
制存在着部分差异
HotSpot JVM
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2q2cFQU8-1626868135375)(C:\Users\Administrator\Desktop\HotSpot虚拟机.PNG)]
方法区,栈,堆的交互关系
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pEkkMT56-1626868135375)(C:\Users\Administrator\Desktop\java方法区栈堆的关系.PNG)]
方法区大小设置
Java 方法区的大小不必是固定的,JVM 可以根据应用的需要动态调整.
方法区的垃圾回收
方法区的垃圾收集主要回收两部分内容:运行时常量池中废弃的常量和不再使用
的类型。
类卸载
判定一个类型是否属于“不再被使用的类”的条件
1.该类所有的实例都已经被回收,也就是 Java 堆中不存在该类及其任何派生子
类的实例。
2.加载该类的类加载器已经被回收
3.该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通
过反射访问该类的方法。
面试:
说一下 JVM 内存模型吧,有哪些区?分别干什么的?
Java内存模型是Java语言在多线程并发情况下对于共享变量读写(实际是共享变量对应的内存操作)的规范,主要是为了解决多线程可见性、原子性的问题,解决共享变量
标记清除算法
当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整
个程序(也被称为 stop the world),然后进行两项工作,第一项则是标记,第二项则是清除
标记:标记所有被引用的对象
标记-清除算法的优点:
非常基础和常见的垃圾收集算法容易理解
标记-清除算法的缺点:
标记清除算法的效率不算高
在进行 GC 的时候,需要停止整个应用程序,用户体验较差
这种方式清理出来的空闲内存是不连续的,产生内碎片,需要维护一个空闲
列表。(空闲列表-记录垃圾对象地址)。
注意:何为清除?
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的
地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,
就存放(也就是覆盖原有的地址)。
复制算法
为了解决标记-清除算法在垃圾收集效率方面的缺陷,它将可用内存按容量
划分为大小相等的两块,每次只使用其中的一块。在垃圾回收时将正在使用的内
存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所
有对象,交换两个内存的角色,最后完成垃圾回收。
优点
没有标记和清除过程,实现简单,运行高效
复制过去以后保证空间的连续性,不会出现“碎片”问题。
缺点:
需要两倍的内存空间 占用量大
标记-压缩算法
容易理解
标记-清除算法的缺点:
标记清除算法的效率不算高
在进行 GC 的时候,需要停止整个应用程序,用户体验较差
这种方式清理出来的空闲内存是不连续的,产生内碎片,需要维护一个空闲列表。(空闲列表-记录垃圾对象地址)。