一、简介
1.介绍
Java虚拟机(Java Virtual Machine,JVM)是Java平台的核心组成部分之一,它是一个在计算机上运行Java字节码的虚拟机。JVM 充当了 Java 应用程序和底层操作系统之间的中间层,提供了跨平台的特性,使得 Java 程序可以在不同的操作系统和硬件上运行。
2.JVM的主要功能和特点
(1)主要功能
执行字节码:JVM能够解释和执行Java编译后生成的字节码class文件。字节码是一种中间语言,它类似于机器码但不直接依赖于具体的硬件和操作系统,而是依赖于JVM来执行。
内存管理:JVM负责动态分配和管理Java程序的内存。它提供了垃圾回收机制(Garbage Collection)来自动释放不再使用的内存。
即时编译:JVM使用即时编译器(Just-In-Time Compiler,JIT)来优化字节码的执行速度。即时编译器将热点代码(即被频繁执行的代码)编译为本地机器码,提高执行效率。
异常处理:JVM提供了异常处理机制。此机制可捕获和处理Java程序中的异常。通过异常表和异常处理器来管理异常的传播和处理流程,保证程序的稳定性和可靠性。
类加载和运行时环境:JVM负责加载、验证、链接和初始化Java类。它使用类加载器(Class Loader)将类文件加载到内存中,并在运行时创建和管理类的实例。JVM还提供了一些系统级的库和API,用于支持Java程序的运行。
(2)特点
跨平台:跨平台性是JVM的是Java语言的重要优势之一。例如经常使用的Linux系统,MacOS系统,Win系统,一次编译,处处运行就是这样理解的。
字节码:字节码文件即JVM可以识别并执行的二进制文件,不同的编程语言经过编译器编译处理之后,转换成统一的字节码规范文件,这样JVM就可以执行。
跨语言:随着JVM不断的发展和优化,很多语言都使用了JVM的能力。
总的来说,JVM提供了Java程序运行的环境和基础设施,为开发者提供了一种跨平台、高效、安全的开发和运行环境。
二、java虚拟机
1.JVM整体构成
本地方法栈:非java写的方法(即native方法)执行时,存放于此;
虚拟机栈:每执行一个方法,都会在这里创建一个栈帧,存放操作数栈、常量表、返回值地址信息;
方法区:类加载子系统将class加载到内存中,即放在方法区,字节码指令存放在这里
程序计数器:线程间的切换原因,线程每次执行后都要记录该线程执行的地址,以便下次执行是从上次执行位置向下执行,即记录线程下次要执行的地址;
堆:存放java对象
类加载子系统:将编译后的java代码(class)文件从磁盘中加载到内存中;
解释器:用来执行方法区中存放的字节码指令;
JIT 编译器:即时编译,解释器执行字节码指令时,有些指令是多次重复执行,针对这些热点指令,翻译一次后直接缓存,不用每次都翻译,提高执行效率,因为字节码指令会由解释器翻译为机器指令,每次执行都会翻译;缓存后每次执行这个指令,就会从缓存中获取这些字节码指令的机器指令;
垃圾回收器:回收垃圾对象;
本地方法:其他语言写的方法,即native方法
1.1 类加载子系统
1.1.1 类加载子系统介绍
类加载子系统是将class文件加载到方法区的内存中,然后对class文件进行验证,class文件是否正确,是否被修改过,格式是否正确;之后进行准备操作,准备操作有两个不走,一是为static变量分配内存,二是给变量赋值为零值,例如 int a = 10,那么准备工作会将变量赋值为 int a = 0,在初始化步骤中将a 再赋值为10;
解析: 解析(Resolution)是将符号引用解析为直接引用的过程。为了理解这个过程,我们首先需要了解符号引用和直接引用的概念。
符号引用(Symbolic Reference):是一种编译时期的引用,他使用符号来表示所引用的目标。比如类、方法、字段等,符号引用并不直接指向内存中的位置,而是通过符号的形式描述所引用的目标;
直接引用(Direct Reference):指向内存中的实际地址的引用。java代码再运行时,需要将符号引用(类、方法、字段等)解析为直接引用,以便操作内存中的实际对象。
解析步骤:
-
定位目标:首先,JVM会根据符号引用的信息定位到目标对象,比如类、方法、字段等。这个定位过程可以通过查找类加载器的加载信息、运行时常量池等进行。
-
解析符号引用:一旦目标对象被定位,JVM会对符号引用进行解析。解析的过程就是将符号引用转化为直接引用的过程。在解析时,JVM会验证符号引用的正确性,并尝试将其转化为直接引用。
-
创建直接引用:一旦符号引用被解析为直接引用,JVM会根据直接引用创建相应的对象,比如类的实例、方法的直接引用等。
通过解析过程,JVM可以将在编译阶段生成的符号引用转化为运行时需要的直接引用,从而实现对类、方法、字段等的访问和操作。
总结起来,解析是将符号引用转化为直接引用的过程,让JVM能够准确地定位并操作内存中的实际对象。这个过程是类加载子系统中的重要环节,保证了Java程序的正确运行。
初始化:将类中的static属性和其他成员变量赋值操作(当然这些属性,有给默认值才执行赋值操作)。
1.1.2 类加载分类
JVM 加载class文件是通过类加载器去加载的。
类加载器分为两大类,引导类加载器(BootstrapClassLoader)、自定义类加载器
引导类加载器(BootstrapClassLoader):非java代码写的代码,JVM 默认的类加载器,加载jre 下的 lib下面的jar包;
自定义类加载器:是使用java代码写的类加载器,继承ClassLoader。jvm中默认的自定义类加载器,扩展类加载器(ExtClassLoader)、应用类加载器(AppClassLoader)。ExtClassLoader加载的是 jre下的lib下的ext下面的 jar 包,AppClassLoader加载的是当前应用classpath下的类。
1.1.3 双亲委派
作用:
- 避免类的重复加载
- 防止核心API被篡改
1.2 运行时数据区
1.2.1 程序计数器
PC Register, 程序计数寄出去,简称程序计数器
(1)物理寄存器的抽象实现;
(2)记录待执行的下一条指令的地址;
(3)是程序控制流指示器,循环、if else、异常处理、线程恢复等都依赖于程序计数器实现;
(4)解释器工作时就是通过它来获取下一条需要执行的字节码的指令;
(5)程序计数器是JVM中唯一一个没有规定任何 OutOfMemoryError 情况的区域。
1.2.2 虚拟机栈(java方法栈)
1.2.2.1 虚拟机机构
每个线程再执行的时候,都会生成一个虚拟机栈,虚拟机栈保存一个个栈帧,每个栈帧对应一个方法。
每执行一个方法,都创建一个栈帧1,如果这个方法调用了另一个方法,那么会在虚拟机栈创建一个栈帧2,并且放入到虚拟机栈中,同理,再执行方法3和方法4都会生成栈帧3和栈帧4,并且存入虚拟机栈中。如果方法4执行完成,那么对应的栈帧就会出栈。依次执行方法3,2,1 都执行完成后,虚拟机栈就会消失。
虚拟机栈特点:
1. 虚拟机栈是线程私有的;
2. 每个方法执行都会创建一个栈帧存入虚拟机栈,方法执行完成,栈帧出栈。因此,虚拟机栈不需要垃圾回收;
3. 虚拟机栈存在OutOfMemoryError和StackOverflowError问题;
4. 线程太多,就可能出现OutOfMemoryError,线程创建时,没有足够内存去创建虚拟机栈了;
5. 方法调用层数太多,可能会出现StackOverflowError错误,虚拟机栈内存不足;
6. 可以通过 -Xss 来设置虚拟机栈的大小。
1.2.2.2 栈帧结构
每个栈帧由局部变量表、操作数栈、返回值地址、动态链接、附加信息组成。
1.2.2.2.1 局部变量表
局部变量表有一个个Slot组成,一个栈帧相当于一个方法,在这个方法中我们通常会定义一些局部变量,存放再栈帧中的局部变量表当中,那么这个局部变量就是Slot
1.2.2.2.2 操作数栈
说明:
1. java代码:定义了变量a、b、c,其中a = 10,b = 20,c = a + b;
2. 字节码指令:java代码解析为字节码后执行的指令
3. 局部变量表:存放字节码中变量 a,b,c 的表
执行过程说明:
上图用第一个指令做了一个示例,下面将详细介绍执行流程
1.执行第一行字节码指令(bipush)将10压入到操作数栈中;
2.执行第二行字节码指令(istore_1)将10从操作数栈中拿出,存入到局部变量表索引为1的位置;
3.执行第三行字节码指令(bipush)将20压入到操作数栈中;
4.执行第四行字节码指令(istore_2)将10从操作数栈中拿出,存入到局部变量表索引为 2 的位置;
5.执行iload_1指令,将局部变量表下标为1的数据读出来,压入操作数栈;
6.执行iload_2指令,将局部变量表下标为2的数据读出来,压入操作数栈;
7.执行idd ,将操作数栈中的10 和20 进行加法操作,得到结果30;
8.执行istore_3,将30 从操作数栈中取出,存入局部变量表索引为3的位置;
9.执行返回return 指令,将局部变量表中的30 返回,并栈帧消失。
1.2.2.2.3 返回值地址
每个方法执行时都会创建一个栈帧,当方法结束后,将继续往下执行,那么这个栈帧中的返回值地址就是要执行下一条指令的地址。
1.2.2.2.4 动态链接
动态链接的主要目的是支持方法之间的调用和返回。为了实现动态链接,每个栈帧都包含了一个指向其调用者栈帧的引用。这个引用通常被称为"帧指针"或"前一帧指针"。通过帧指针,可以在运行时建立和维护方法调用链。当一个方法执行完成后,JVM会将其栈帧出栈,恢复到调用者方法的栈帧,并继续执行调用者方法的剩余部分。这种递归的栈帧结构和动态链接机制使得Java能够支持方法的嵌套调用和返回。
总结一下,动态链接是通过在栈帧中维护帧指针来实现的,它用于建立和维护方法之间的调用关系,支持方法的嵌套调用和返回。
1.2.3 本地方法栈
本地方法:navive method,在java中定义的方法,但是是由其他语言实现;
本地方法栈存放的是本地方法调用的栈帧;
线程私有,也可能出现OOM(OutOfMemeryError)和SOF(StackOverflowError)。
1.2.4 堆
JVM 运行时最重要的一块区域。JVM 规范中所有的对象和数组都存放在该区域,在执行字节码指令时,会将创建的对象放入堆中,对象引用的地址放入虚拟机栈的栈帧中,当方法执行完成后,引用的对象不会立即被回收,而是需要等待守护线程GC后,才能被回收。
-Xms:ms(memory start),指定堆初始化内存大小,等价于:-XX:InitialHeapSize
-Xmx:mx(memory max),指定堆最大内存大小,等价于:-XX:MaxHeapSize
一半会把 -Xms和 -Xmx 设置为一样大小,原因是JVM在GC后不需要去调整内存大小,提高效率。
默认情况下,初始化内存大小=物理内存大小 / 64,最大内存大小=物理内存 / 4
可以通过 -XX:NewRatio 参数来设置新生代和老年代的比例,默认是2,表示新生代占1,老年代占2,也就是新生代内存占堆区总大小的 1/3; 一半是不需要调整的,只有明确知道存活时间比较长的对象偏多,就需要调大 NewRatio来调整老年代占比。
Eden:伊甸园区,新的对象会存放到Eden区(除非对象大小超过了Eden区,那就只能直接进入老老年代);S0(Servivor0)、S1(Servivor1)区,也可以叫from区和to区,用来存放MinorGC(YGC)后存活的对象
默认情况下(Eden : S0 : S1 内存比例为 8 : 1 : 1), 可以通过 -XX:ServivorRatio 来调整
Yong GC / Minor GC(YGC):负责对新生代进行垃圾回收;
Old GC / Major GC:负责对老年代进行垃圾回收,目前只有CMS垃圾回收器单独对老年代进行GC,其他垃圾回收器是对整堆进行GC,同时对老年代进行GC;
Full GC:整堆回收,包括方法区。
创建新对象和回收过程详细说明:
1. 图1中,创建多个对象,有写对象在执行完成后不在引用(灰色)即垃圾对象,有的继续引用(粉色),当Eden区域满了之后会进行一次MinorGC(YGC),存活的对象会进入到S0区,并且给存活的对象标记上数字1,表示经历过一次MinorGC,如图2所示;
2. 再次当Eden区对象满了之后,再次进行MinorGC,会将垃圾对象清理,如下图3。将Eden存活的对象放入S1区,并且给对象标记上数字1,表示经历过一次MinorGC;将S0区存活的对象也放入到S1区,并且给对象标记上数字2,表示又经历过一次MinorGC,如下图4所示。
3. 继续创建新对象,并放入Eden区,当Eden区再次满了,会再次进行MinorGC,会将垃圾对象清理,如下图5。将Eden存活的对象放入S0区,并且给对象标记上数字1,表示经历过一次MinorGC;将S0区存活的对象也放入到S0区,并且给对象标记上数字3,表示又经历过一次MinorGC,如下图6所示。
4. 不断重复这些过程,有些对象一直存活,经历过15次MinorGC,如下图7所示;最后一直存活的对象,将会存放再老年代中,如下图8所示
5、如果Eden有个大对象,导致Eden直接满了,执行MinorGC,由于S0和S1区内存不足于存放该大对象,则直接会放到老年代中,如图9、图10所示;
6.如果堆中创建了一个超大对象,Eden区、S0区、S1区都放不下,则直接会放入到老年代,如图11、图12所示;
1.2.5 方法区
方法区和堆一样,是一个被其他线程共享的区域。主要存储解析器解析后的机器指令、类信息、方法信息、常量池、静态变量等。
JDK 1.8之前,称之为永久代,实际上在1.7版本的时候已经有一部分不是永久代了,比如常量池,静态变量已经移出永久代,到了1.8则完全使用元空间(本地内存)替代。
`
1.3 垃圾回收
1.3.1 为什么要有垃圾回收
在JVM中,没有任何引用指向的对象称之为垃圾。垃圾不清理,就会一直占用着内存,内存中的对象越来越多,最终会导致OOM。
1.3.2 垃圾回收算法
1.3.2.1 引用计数法
每个对象都记录一个引用计数器属性,用来记录被引用的次数。
优点:实现简单,只要引用计数器为0,就是垃圾
缺点:
- 无法解决循环引用问题
- 需要额外空间存储
- 需要额外时间维护
1.3.2.2 可达性分析
可达性分析是一GC root 为起点,一层一层地找到所引用的对象,被找到的就是存活的对象,其他不可达的对象称为垃圾对象。
GC Roots 是一组引用,包括:
- 虚拟机栈中正在执行的方法对应的参数,局部变量表中对象的引用;
- 本地方法栈中正在执行的方法对应的参数,局部变量表中对象的引用;
- 方法区中保存的类中的static属性所对应的对象引;
- 方法区中保存的类中的常量属性所对应的对象引用等等;
1.3.3 回收算法
1.3.3.1 标记-清除
常用的垃圾回收算法,当新生代或者老年代内存不足,则会暂停所有用户线程,即STW(Stop The World),执行算法进行垃圾回收。
A.标记阶段:从GC roots 开始遍历,找到可达对象,并在对象头中进行记录;
B.清除阶段:堆内存空间进行线性遍历,如果发现对象头中没有记录此对象是可达的对象,则进行回收。
标记-清除算法优缺点:
优:思路简单
缺:
A.效率不高;
B.内存碎片;由图可以看出来,内存是分散的,不是连续的,会导致有些对象没法存入。
1.3.3.2 复制
将内存分为A、B两块,每次只使用一块,在垃圾回收的时候,将可达对象存入到另一块空的内存中,然后再清除当前内存块中不可达对象。
复制算法优缺点:
优:
1.没有标记-清除阶段,直接将可达对象复制,不需要修改对象头,效率高
2.不会出现内存碎片
缺:
1.需要更多的内存,总有一块内存没有被使用
2.对象复制后,内存地址发生改变,需要修改栈帧中对象引用的地址
3.如果可达对象比较多,那么复制效率就很低,所以适合垃圾对象多的情况(通常用在新生代)
1.3.3.3 标记-整理
分为三个阶段:
1. 第一阶段,从GC Roots开始遍历,找到可达对象,并在对象头中记录;
2. 第二阶段,将可达对象移动到内存的另一端;
3. 第三阶段,清理边界外所有空间。
优缺点:
优:
1. 不会产生内存碎片
2. 不需要额外的内存空间
缺:
1.效率低下
2.需要修改栈帧中的内存地址
三个算法比较,如下图:
1.3.4 常见垃圾收集器