Java篇| JVM模型
一 、前言
- 本文主要是对JVM的机制模型进行简单表述。理解JVM一些基本的运行原理。
- 主要以JDK8分析
二、 思考的问题
1. 你将会怎样设计一门语言。
作为开发人员,通常会发现编程语言的语法相通性与差异性。比如学过C的会发现和C++之间的相通性。C与GO之间的相通性,如果是面向对象性语言C++,OC,Java,C#等等,无外乎一些语法的使用方式差别。而终究的核心是对计算机资源(网络,磁盘,内存,CPU等)的调度使用。而设计一门语言就是为了怎么调度或者更好的方式使用这些资源。
2. 理解 程序=数据结构+算法。
假设:对程序语言设计的定义 = 对计算机资源的调度使用的设计。 对计算资源的调度使用 = 数据元素的组织,计算存储和管理(数据结构) 设计 = 如何设计处理数据元素步骤(算法 ) 而最终程序语言设计产物也是程序 那么:数据结构+算法=程序
而我将JVM的设计称为对内存的操作设计
三、JVM虚拟机
一种以软件方式模拟硬件功能的计算系统。是物理机通过软件程序的实现。运行在隔离的独立环境。所以编写的JAVA代码编译后能够在JVM的环境上运行,而不用担心在不同硬件环境或系统平台上运行不通过。
1. JVM体系结构
2. JVM运行时组成
简单上一张核心图,第四节会举例分析一下代码方法在JVM中的执行过程。
JVM运行时数据区包含以下几个部分:
1. 程序计数器(Program Counter Register)
- 用于记录下一条指令的地址,是线程私有的内存区域。
- 当线程执行本地方法时,程序计数器的值为Undefined。
2. Java栈(Java Virtual Machine Stacks)
- 用于存储Java方法执行的数据和指令,每个方法在执行时都会创建一个栈帧,栈帧在方法执行结束后被销毁。
- Java虚拟机栈也是线程私有的内存区域。
3. 本地方法栈(Native Method Stack)
用于执行Native方法(即使用非Java语言编写的方法),与Java虚拟机栈的作用类似,也是线程私有的内存区域。
4. Java堆(Java Heap)
- 用于存储对象实例和数组,是JVM中最大的一块内存区域,也是所有线程共享的内存区域。
- 通常JVM进行GC回收的主要也是这块区域。
5. 元空间
-
在JDK 8及以上版本的HotSpot虚拟机中,方法区不再使用永久代(PermGen)实现,而是引入了元空间(Metaspace)。也是所有线程共享的内存区域。
-
方法区和元空间并不完全相同。可以将方法区看作是元空间的一个子集。
-
可以动态调整大小,而且使用的内存不在虚拟机限制之内。Metaspace 的大小可以通过参数 -XX:MaxMetaspaceSize 进行设置。
-
方法区(Method Area):主要包括虚拟机加载的类信息、类加载器信息、运行时常量池、静态变量、方法代码、常量、异常信息等数据。它也是所有线程共享的内存区域。如此多不同类型的数据,可以看出他的职责非常的杂多。
1. 运行时常量池: 运行时常量池是每个类或接口中的常量池的运行时表示形式,与类共生死,包含类的常量池和静态变量池中的所有常量。
2. 类信息: 类信息用于描述类的结构信息,如类名、父类名、实现的接口、字段及方法等。其中字段信息包括字段名称、修饰符、类型以及初始值等;方法信息包括方法名称、修饰符、返回值类型、参数列表以及方法体等。
3. 静态变量: 静态变量是用 static 关键字声明的变量,它属于类而不是对象,只会被初始化一次。在方法区中存储着各类的静态变量,这些变量的值在类装载过程中被初始化,并可以被访问、修改。
4. 方法代码: 方法代码是指经过编译后的字节码指令,保存在方法区中,包括了方法体中的所有Java虚拟机指令。
5. 常量: 常量是指被 static final 修饰的基本类型和字符串变量,也包括了通过 final String 常量定义的字符串常量。
6. 异常信息: 异常信息用于描述类中涉及的异常类型信息。
7. 类加载器信息: 类加载器是负责从文件系统或网络等位置读取 Java 类文件,并把它们转换为 JVM 运行时数据结构 Class 对象的程序实体。方法区中存储着每个类加载器的信息,包括了类加载器的名称、父加载器、已经加载的类等。
8. 符号引用:在 Java 虚拟机规范中,符号引用的解析过程被称为链接(Linking),该过程包括了验证、准备、解析三个阶段。提供了一种间接访问目标的方式,而在运行时期,这些符号引用将被解析成为对应的直接引用。方法区中存储着所有的符号引用,它们的解析工作会在类被加载时完成。
6. 直接内存(Direct Memory)
- JVM也可以使用操作系统的内存来作为堆外内存,与Java堆不同,这部分内存不受JVM管理,不受 Java 堆大小限制,需要手动进行内存的分配和释放。也就是C里面的malloc() 和 free()。
- 它是 JVM 使用 NIO ,利用 Native 函数库直接分配堆外内存的一种方式,主要用于提高 IO 操作的性能。 需要注意的是
直接内存不是Java虚拟机规范中规定的运行时数据区域之一,但是它又通常被归类为运行时数据区的一部分,因为它与前面几种类型之间存在着密切的关系,同时也会影响JVM运行时内存的使用和调优策略.
总体看来,JVM运行时数据区的不同部分在内存大小、作用、生命周期等方面都有所不同,开发者需要了解和掌握这些区域的特点和使用方法,以便更好地理解Java程序的执行过程。
四、 简单Java程序JVM执行分析
栈区
每个栈可理解为一个线程,每个线程上的方法运行时都有拥有栈帧。每个栈帧上又有局部变量,操作数栈,方法出口等特征。
Fun程序Fun.java
public class Fun {
public static int FLAG = 100;
public static User user = new User();
public int number() {
int a = 1;
int b = 2;
int c = (a + b) * 5;
return c;
}
public static void main(String[] args) {
Fun fun = new Fun();
fun.number();
System.out.println("Fun!");
}
}
使用javac Fun.java
生成Fun.class
,然后javap -c Fun.class
反汇编,得到内容
Compiled from "Fun.java"
public class Fun {
public static int FLAG;
public static User user;
public Fun();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public int number();
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: iconst_5
8: imul
9: istore_3
10: iload_3
11: ireturn
public static void main(java.lang.String[]);
Code:
0: new #2 // class Fun
3: dup
4: invokespecial #3 // Method "":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method number:()I
12: pop
13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
16: ldc #6 // String Fun!
18: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
21: return
static {};
Code:
0: bipush 100
2: putstatic #8 // Field FLAG:I
5: new #9 // class User
8: dup
9: invokespecial #10 // Method User."":()V
12: putstatic #11 // Field user:LUser;
15: return
}
简要分析number函数在栈上的操作,其他以此类推
下图表示了调用函数过程中,局部变量和操作数栈的变化过程。
查阅指令文档The Java® Virtual Machine Specification (oracle.com)进行操作解释
在第 0 行,使用 iconst_1 指令将常量 1 压入操作数栈中。
在第 1 行,使用 istore_1 指令将操作数栈顶的数值存储到局部变量表的索引为 1 的位置中。此时,局部变量表中的第 1 个元素值为 1。
在第 2 行,使用 iconst_2 指令将常量 2 压入操作数栈中。
在第 3 行,使用 istore_2 指令将操作数栈顶的数值存储到局部变量表的索引为 2 的位置中。此时,局部变量表中的第 2 个元素值为 2。
在第 4 行,使用 iload_1 指令将局部变量表索引为 1 的元素值(即 1)加载到操作数栈中。
在第 5 行,使用 iload_2 指令将局部变量表索引为 2 的元素值(即 2)加载到操作数栈中。
在第 6 行,使用 iadd 指令将操作数栈顶的两个数值相加,结果为 3,将其压入操作数栈中。
在第 7 行,使用 iconst_5 指令将常量 5 压入操作数栈中。
在第 8 行,使用 imul 指令将操作数栈顶的两个数值相乘,即计算出 3 * 5 = 15,将其压入操作数栈中。
在第 9 行,使用 istore_3 指令将操作数栈顶的数值存储到局部变量表的索引为 3 的位置中。此时,局部变量表中的第 3 个元素值为 15。
在第 10 行,使用 iload_3 指令将局部变量表索引为 3 的元素值(即 15)加载到操作数栈中。
在第 11 行,使用 ireturn 指令将操作数栈顶的元素值作为方法的返回值。
程序计数器在过程中的作用
假设我们在上面线程main线程中运行到number()方法第5行,系统切换了其他线程,如果没有程序计数器记录当前线程的运行位置,那么再次回到main线程时候,程序就将不知道从什么地方继续执行。
五、 堆内存垃圾回收机制
1. 堆内存模型
新生代
- 新生代是对象创建后被分配的内存区域,通常包含 Eden 区、Survivor From 和 To 两个区域。其中,Eden 区是对象创建时分配的内存区,大部分新生对象都在 Eden 区中分配。
- 当 Eden 区没法容纳所有新生对象时,此时会触发一次垃圾回收,将存活的对象移动到 Survivor 区 From 区,并清空 Eden 区。之后,若再次触发垃圾回收,便将存活的对象从 Survivor 区 From 区移动到 Survivor 区 To 区,并清空 Survivor 区 From 区。经过多次垃圾回收,存活下来的对象会被移到老年代。
老年代
- 老年代用于存储存活时间较长的对象,通常包含较多的 Java 对象。
- 当 Eden 区和 Survivor 区无法容纳当前对象时,这些对象会被直接分配到老年代。老年代内的对象不太容易被回收,因此需要特殊的垃圾收集算法进行回收。
大小比例
- 通常情况下新生代和老年代的比例为1:2或1:3
- Eden:From : To 为 8:1:1
这些比例大小都是可以通过JVM参数进行调整。合适的大小和分配比例可以归类为JVM调优的范畴,主要在运行Java后台服务的机器上需求较多,而对于移动Android或客户端平台,调优需求比较少。不同的业务逻辑和硬件条件都可能会影响到触发GC的频率,不同的垃圾回收算法也会对应适用不同的场景。
2. Minor GC和 Full GC
Minor GC : 或者叫Young GC,只会清理新生代内存区域,比较频繁,将存活时间较长的对象移到老年代
Full GC : 或者Major GC,会清理老年代、新生代、甚至元空间。Full GC 的触发条件可以是多种多样的,比如老年代空间不足、永久代或者元空间满、System.gc() 方法显式调用等。
通常会有一些辅助工具如Java VisualVM等可以帮助我们更好的了解GC信息。
3. STW机制
Java 中的 STW,全称为 Stop-The-World,是垃圾回收器进行垃圾回收时暂停应用程序的一种机制。在这个过程中,JVM 会挂起所有的 Java 线程,包括垃圾回收线程和用户线程,直到垃圾回收完成。STW 机制可以保证垃圾回收的正确性和一致性,但也会对应用程序性能造成一定的影响。
- Minor GC(短)和Full GC(长)一般STW的时间不同
- 不同垃圾回收算法STW一般情况下时间也不相同
六、 结尾
- 通过本篇文章,或多或少对了解JVM机制和GC机制有所启发。
- 待补充介绍GC的几种算法。
转载自:https://juejin.cn/post/7236413212093694010