1、JVM 的位置
2、JVM 的体系结构
JVM(Java虚拟机)是Java程序的运行环境,它对于Java平台的运行和跨平台特性的实现有着重要的作用。JVM的体系结构有以下几个部分:
-
类加载器(ClassLoader):负责将 .class字节码文件加载到内存中,并生成对应的Java类。
-
运行时数据区(Runtime Data Area):包括了方法区、堆、虚拟机栈、本地方法栈和程序计数器等五个部分。
- 方法区:用于存储类的信息,如静态变量、成员变量、常量池等。在Java 8以后,方法区被元空间(Metaspace)所代替。
- 堆:用于存储对象实例,包括数组和其他对象实例。堆被所有线程共享,同时也是垃圾回收器的重点关注对象。
- 虚拟机栈(Java Stack):用于存储线程执行方法的栈帧(Frame)。每个栈帧对应一个Java方法的执行,栈帧存储局部变量、操作数栈、动态连接和方法出口等信息。
- 本地方法栈(Native Stack):本地方法栈类似于虚拟机栈,不过它是为了执行Native方法服务,此处Native方法指的是使用非Java语言编写的方法。
- 程序计数器(Program Counter Register):当前线程所执行的字节码所在的位置指示器,在多线程时每个线程都有自己独立的程序计数器。
-
执行引擎(Execution Engine):负责解释字节码,并将字节码翻译成机器码,使程序能够在本地机器上运行
-
本地方法接口(Native Interface):JVM提供了本地方法接口,允许Java程序调用其他语言写成的程序。
总的来说,JVM是一个在本地机器上运行的进程,并且它是Java跨平台特性的核心实现。JVM的体系架构在使用Java程序开发的过程中是至关重要的,了解其内部结构有助于更好地理解Java程序是如何运行的。
3、类加载器
1️⃣ 什么是类加载器?
类加载器(Class Loader)是Java虚拟机(JVM)中的一个子系统,它的功能是将编译好的Java代码(.class文件)加载到内存中,在运行时动态地生成类,并将其连接到JVM运行状态之中
在Java中,程序需要在运行时动态地加载一些类,然而这些类在编写程序的时候可能并不确定,或者需要从网络或其他地方加载,因此有了类加载器作为动态加载类的一个重要工具
Java中的类加载器采用了一种双亲委派模式的机制。当一个类需要被加载到JVM中时,首先由Bootstrap类加载器(根类加载器)尝试加载该类,如果找不到该类,就将请求传递给Extension类加载器(扩展类加载器),如果Extension类加载器还是找不到该类,则将请求传递给Application类加载器(应用程序类加载器)进行加载。如果Application类加载器仍然无法找到该类,则该类将被认为是不存在,JVM会报ClassNotFoundException
2️⃣ 具体来说,类加载器的工作流程如下:
- 装载:查找并加载指定的**.class字节码文件,生成一个对应的Class**对象。
- 连接:将静态的字节码文件中的符号引用替换成直接引用,包括验证、准备、解析三个阶段。
- 初始化:对类进行初始化,执行类构造器函数()。
需要注意的是,类加载器除了加载类文件并生成Class对象之外,还有一些其他的职责,例如缓存已经加载的Class对象、保证类加载器的隔离性以及保护Java代码的安全性等
3️⃣ Class 对象是抽象的,使用同一个类的是相同的模板,但实例对象是具体的
package com.kuangshen;
/**
* @author Bonbons
* @version 1.0
* 查看实例对象、Class文件、ClassLoader的关系
*/
public class Car {
public static void main(String[] args) {
Car car1 = new Car();
Car car2 = new Car();
Car car3 = new Car();
// 输出他们的 HashCode
System.out.println(car1.hashCode()); // 325040804
System.out.println(car2.hashCode()); // 1173230247
System.out.println(car3.hashCode()); // 856419764
// 获取他们的字节码文件,证明属于一个模板
Class<? extends Car> aClass1 = car1.getClass();
Class<? extends Car> aClass2 = car2.getClass();
Class<? extends Car> aClass3 = car3.getClass();
System.out.println(aClass1.hashCode()); // 1836019240
System.out.println(aClass2.hashCode()); // 1836019240
System.out.println(aClass3.hashCode()); // 1836019240
}
}
4、双亲委派机制
1️⃣ 什么是双亲委派机制?
双亲委派机制(Parent Delegation Model)是Java类加载器的一种工作机制。它是一种由下至上的查找类的过程,通过一层层的委托,最终在顶层的类加载器中寻找类的定义
具体来说,当一个类加载器需要加载一个类时,它首先会判断该类是否已经被加载过了。如果已经被加载过了,则直接返回已加载过的类;否则,该类加载器就会委托父类加载器去加载该类。 如果父类加载器仍无法找到该类,那么就依次递归委托其父类加载器,直至到达最顶层的启动类加载器。如果启动类加载器仍没能加载该类,就会抛出 ClassNotFoundException
2️⃣ 这种双亲委派机制的工作原理有以下优点:
- 避免重复加载:由于每个类加载器都能够独立地加载类,如果没有双亲委派机制的限制,不同的类加载器可能会加载同一个类的不同版本,造成类的重复定义。而有了双亲委派机制,可以避免这种情况的出现。
- 确保安全性:由于Java中的安全模型是基于类加载器的,不同的类加载器可以用于隔离不同的代码源。双亲委派机制可以保证不同的代码源不会相互干扰,从而确保Java应用的安全性
- 简化开发:由于子类加载器会委派父类加载器来加载类,开发者可以很少直接涉及类加载器的工作,从而简化了Java应用的开发和部署的过程
需要注意的是,双亲委派只是一个规范,并不是Java虚拟机的强制规定。在特定的场景下,开发者也可以自定义类加载器,并打破双亲委派机制的限制。
3️⃣ 案例演示:
package com.kuangshen;
/**
* @author Bonbons
* @version 1.0
* 演示双亲委派机制:
* 尽管我们定义了String类
*/
public class String {
@Override
public java.lang.String toString() {
return "Hello";
}
public static void main(String[] args) {
String string = new String();
System.out.println(string.getClass().getClassLoader());
System.out.println(string);
}
}
因为当前String类没有被加载过,所以类加载器向上委托查找,在引导类加载器中发现有String类,但是因为那个String类中没有main方法,所以就会抛出下面的错误
4️⃣ 解释一下出现上面这个现象的原因:
当Java程序需要加载一个类时,当前线程的类加载器不会自己直接加载该类,而是首先将加载任务委托给其父类加载器来尝试加载该类
这个“从下到上,从左到右”的顺序是指类加载器之间的层次结构,从下向上表示类加载器的继承关系,从左到右表示同一级别的类加载器之间的加载顺序。例如,在默认情况下,系统类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器则是引导类加载器。因此,当系统类加载器需要加载一个类时,它会首先将加载任务委托给扩展类加载器,如果扩展类加载器不能加载该类,那么它会继续将加载任务委托给引导类加载器。如果所有的父类加载器都无法加载该类,当前线程的类加载器将会尝试自己加载该类。
5、沙箱安全机制
沙箱安全机制(Sandbox Security Mechanism)常常被用于Java应用程序的安全性控制中,其目的在于限制不可信代码的行为,保护Java应用程序不受恶意代码的攻击
1️⃣ 什么是沙箱安全机制?
沙箱安全机制是一种限制不可信代码行为的技术,它可以将不可信代码隔离在一个安全执行环境中,保护系统或应用程序的安全。在Java中,沙箱安全机制的实现是通过类加载器、安全管理器和访问控制器等技术来实现的。
2️⃣ 如何实现沙箱安全机制?
首先,Java应用程序通过类加载器将可信代码加载到JVM中,其中可信代码包括Java核心类库和用户编写的代码
然后,安全管理器负责检查应用程序的所有行为,如果遇到需要访问受限资源的行为,就会调用访问控制器来控制这些行为是否可以被执行
最后,JVM会根据安全管理器和访问控制器的规则,限制不可信代码的行为,保证系统或应用程序的安全
3️⃣ 沙箱安全机制的工作原理
(1) 类加载器
类加载器是Java中实现沙箱安全机制的重要组成部分,它的主要作用是根据指定的类名或者.class文件的路径,将类文件装载到JVM中,并生成对应的Class对象。在实现沙箱安全机制时,可以利用类加载器将可信代码和不可信代码分开加载,从而实现代码隔离
(2) 安全管理器
安全管理器是Java中实现沙箱安全机制的核心,它负责检查Java应用程序的所有行为,如文件访问、网络访问、系统属性的读写等,判断这些行为是否合法,并控制其执行
如果Java应用程序需要访问受限资源,如读取文件或者建立网络连接等,安全管理器将对其进行审查,如果该行为是被允许的,就会继续执行该行为,否则就会阻止该行为的执行。
(3) 访问控制器
访问控制器是安全管理器的一个重要附属部分,它负责控制访问受限资源的行为
在Java中,访问控制器通过访问控制列表(Access Control List,ACL)来管理受限资源的访问权限
ACL中记录了哪些用户、程序或者代码段可以访问特定的资源,以及如何访问这些资源
当Java程序要访问一个受限资源时,访问控制器会检查ACL中是否包含了该程序或代码段的访问权限,如果有,访问控制器就会允许这个程序或代码段访问该受限资源。
总之,沙箱安全机制是Java应用程序安全的重要防护措施。通过类加载器、安全管理器和访问控制器的协同工作,可以有效地限制不可信代码的行为,保护Java应用程序的安全。
4️⃣ 所有的Java应用程序都可以指定沙箱、安全策略,接下来介绍一下发展历程
Java 安全模型的核心就是 Java 沙箱
(1)JDK1.0 安全模型
再 Java 中执行程序分为本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信任的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的 java 实现中,安全依赖于沙箱机制
(2)JDK1.1安全模型
但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。所以在 1.1 版本中对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限
(3)JDK1.2安全模型
在 Java1.2 版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。
(4)JDK1.6安全模型
引入了域的概念,虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分负责与关键资源进行交互,而各个应用域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域,对应不一样的权限。存在于不同域中的类文件就具有有了当前域的全部权限
6、Native
1️⃣ 了解一下什么是 Native 关键字
在编程中,Native 通常用作关键字来表示某些代码是用其他语言编写的并且已被编译成机器码,以便在当前语言中使用
具体来说,Native 关键字表示一个外部的、非当前语言的函数或方法,通过Native关键字,程序能够直接访问外部函数或方法的实现,并将其结果集成到当前程序中
2️⃣ 我们查看一下 Native 在哪用过:
我们创建线程时,调用的 start 方法中,里面有个 start0 方法,这个方法就是被 native 关键字修饰的
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
3️⃣ 总结:
(1)使用了 native 关键字的,说明 java 的作用范围达不到了,会去调用底层 C 语言的库
(2)会进入本地方法栈,进而调用本地方法的本地接口 JNI(Java Native Interface)
(3)JNI作用:扩展 Java 的使用,融合不同的编程语言为 Java 所用
7、PC寄存器
什么是程序计数器?
程序计数器是计算机处理器中的一种特殊寄存器,用于保存正在执行的指令的地址或指针
在一次程序执行期间,处理器会按照程序设计者编写的指令序列来执行程序。处理器从程序代码的第一条指令开始执行,并且按照指令的顺序一条一条地执行下去。程序计数器的作用就是指示处理器下一条要执行的指令所在的内存地址
当处理器执行完一条指令时,程序计数器的值会自动增加,指向下一条指令的地址。这样处理器就可以继续执行下一条指令。如果程序发生了跳转,比如执行了一个函数调用或错误处理,程序计数器的值也会发生改变
程序计数器在不同的架构中可能有着不同的命名和实现方式。在大多数现代计算机体系结构中,程序计数器是一种常规寄存器,大小通常是与寻址空间相关,例如在32位架构中,程序计数器通常是32位,而在64位架构中,它通常是64位。
8、方法区
1️⃣ 方法区指的是什么?
Java虚拟机(JVM)中的方法区(Method Area)是一个存储类信息的区域。它存储了类的信息、常量、静态变量、即时编译器编译过的代码等。方法区在JVM的运行时数据区域中属于共享区域,被所有线程所共享
静态变量、常量、类信息(构造方法、接口定义)、运行时常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
方法区的内存空间是 JVM 中内存分配最大的一块,它的大小可以通过 -XX:MaxMetaspaceSize
参数设置,此参数在JDK 8及以后版本替代了 -XX:PermSize
和 -XX:MaxPermSize
参数。
2️⃣ 方法区的结构主要包括以下几个部分:
- 运行时常量池:是方法区的一部分,用于存储编译时产生的各种字面量和符号引用。字面量包括字符串、数字常量、类、方法等字面量,符号引用包括类和接口的全限定名、方法和字段的名称和描述符等。
- 类信息:包括类的全限定名、类的访问修饰符、父类、接口实现等信息。
- 静态变量:静态变量是在类加载时初始化的,其大小是固定的。
- 即时编译器(JIT)编译后的代码:JVM中提供的即时编译器是负责将 Java 代码编译成本地代码。JIT 编译器将 Java 字节码翻译成本地机器码后存放在方法区中。
在运行Java应用程序时,随着类和对象的不断加载和卸载,方法区会发生各种内存溢出和内存泄漏的问题。比如,如果应用程序中定义了大量的字符串常量,且这些字符串常量的生命周期比较长,就有可能导致方法区的内存溢出问题。解决这些问题需要在程序开发和应用部署时加以注意和规划方法区的内存使用。
9、栈
Java虚拟机中的方法栈(Method Stack)是一种为方法调用和返回而分配的内存区域
每个线程在调用方法时都会创建一个对应的方法栈,用于存放方法的局部变量、方法参数、方法返回值和部分调用时的数据
方法栈是基于栈的数据结构,它具有“先进后出”的特性。每次方法调用时,JVM都会在当前线程的方法栈中创建一个新的栈帧(Stack Frame),用于存储该方法的信息,包括方法本身的信息(方法名、访问修饰符、参数列表等)和该方法在调用状态下的局部变量、操作数栈等信息 【当前正在运行的方法一定在栈的顶部】
方法栈的大小也可以通过 JVM 参数进行设置,其大小受到限制。如果某个线程的方法栈空间已经被填满,就会抛出 StackOverflowError 异常;如果方法栈的大小超出了 JVM 所允许的最大值,就会抛出 OutOfMemoryError 异常【内存泄漏】
在方法调用过程中,每个线程的方法栈都是独立的,互不干扰。当一个线程完成这个方法调用后,它所使用的方法栈会被销毁,从而为该线程的其他方法调用释放空间 【不需要通过GC回收】
要注意的是,方法栈与堆内存中的对象是不同的概念,方法栈主要存储方法调用的相关信息,而不是对象本身。在Java虚拟机中,对象本身存放在堆内存中
10、三种VM
Java 虚拟机(JVM)是运行Java程序的关键组件,它提供了Java程序在不同硬件平台上独立运行的能力
-
Java 虚拟机有以下三种常见实现:
-
Oracle HotSpot VM:是目前最常用的Java虚拟机,在Java SE 6之后成为Java标准版(Java SE)中默认的虚拟机。它是Oracle公司提供的一款性能优秀、功能完备的JVM,包括了即时编译器(JIT)、垃圾回收、调试等多种功能
-
OpenJDK:是由Oracle公司开源的Java开发工具包(JDK)的一个开源实现。OpenJDK包括了Java虚拟机、类库和Java编译器等组件,也可以作为开发Java代码的工具。OpenJDK提供了跨平台的环境,可以在不同操作系统上运行。OpenJDK是许多其他Java虚拟机实现的基础
-
IBM J9 VM:是IBM公司提供的一款Java虚拟机。相比于Oracle HotSpot VM,IBM J9 VM 的特点在于对硬件的优化,以及更好的内存管理、GC 算法、动态编译器等。IBM J9 VM 通常被用于企业级应用中,因其更高的稳定性、扩展性以及对Java EE等规范的良好支持
-
除了上述三种常见的Java虚拟机外,还有一些其他的实现,如Azul Zing VM、BEA JRockit VM、Excelsior JET等等,这些虚拟机都具备不同的特点和适用场景
11、堆
1️⃣ 介绍一下 JVM 中的堆的作用
Java虚拟机(JVM)中的堆(Heap)是一个运行时数据区,用于存储Java程序中的对象
所有对象都在堆中分配内存,而且堆中的内存是在JVM启动时分配的
堆在JVM中是一个可扩展的内存池,它可以根据需要动态增长或缩小,但是堆的大小也受到一定的限制
在堆中创建对象时,虚拟机会根据对象的类型和大小分配一块连续的内存空间。如果内存空间不足,即使是最简单的对象也可能抛出 OutOfMemoryError 异常。堆中的内存空间可以通过 JVM 参数进行设置,例如 -Xmx 和 -Xms 参数可以分别设置堆的最大和初始大小
2️⃣ Java虚拟机的堆分为新生代和老年代两部分
-
新生代是存放新建立的对象的区域
- 它主要由一个较大的Eden(伊甸园)空间和两个较小的Survivor(幸存者)空间组成
- 如果某个对象存活时间比较长,就会被移动到老年代中
-
老年代是存放较旧的对象的区域,它的内存空间比新生代要大
- 同时老年代的垃圾回收也比较少,因此需要较多的内存空间来存储
- 同时老年代的垃圾回收也比较少,因此需要较多的内存空间来存储
堆还有其他一些内部结构,例如线程私有的本地分配缓冲区、永久代或元数据空间(在JDK 8之后,被元数据空间(Metaspace)所替代) 【Java8以后永久存储区改名为元空间】
- 堆的大小和使用情况可以通过许多工具进行监测和优化,例如 jstat、jmap、jconsole 等
- GC垃圾回收主要在新生区和养老区 【轻GC、重GC】
12、新生区、老年区
Java虚拟机(JVM)主要区分为三个代:新生代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation,JDK8及以后被Metaspace所替代),其中新生代和老年代可以影响JVM的性能和垃圾回收机制
新生区是JVM划分的内存区域之一,主要用于存储新创建的对象
- 新生区又分为Eden区(伊 ? 园区)和两个Survivor区(通常为S0和S1)(幸存者区),默认比例为8:1:1
- 当一个对象被创建时,它会被分配到Eden区
- 当Eden区满了时,JVM将执行Minor GC(新生代收集),清除不再使用的对象并将现有对象移动到一个 Survivor 区
- 在移动过程中,对象年龄加1,也就是经过Minor GC后还存活且在Survivor区中的对象,年龄会增加
- 当一个对象在Survivor区中的年龄达到一定的阈值(例如15岁),它会被移动到老年区
- 老年区主要用于存储生命周期长、经常使用的对象,例如全局缓存或应用中的静态资源
- 因为老年区的内存分配比较稳定,所以JVM对其垃圾回收的处理非常慢
- 但是在一定情况下需要执行
Full GC
(完全垃圾回收),清除不再使用的对象,以保证JVM的垃圾收集器能够及时回收内存
总的来说,新生区和老年区的划分和管理是JVM内存管理和垃圾回收的核心机制之一,也是JVM性能和应用稳定性的重要因素之一
13、永久区
Java虚拟机(JVM)的永久区(Permanent Generation,也称元数据区Metadata Space)是一个特殊的内存区域,主要用于存储JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等内容
JVM在启动时会预设大小和最大值,初始大小和最大值可以通过命令行参数进行设置 【这个区域不存在垃圾回收,关闭VM虚拟机就会释放这个区域的内存】
- jdk1.6 之前:永久代,常量池在方法区
- jdk 1.7 : 永久代,但是慢慢的退化了,去永久代,常量池在堆中
- jdk 1.8 之后:无永久代,常量池在元空间
14、堆内存调优
以下是一些常用的堆内存调优技巧:
- 设置堆的大小:堆的大小可以通过JVM参数进行设置,例如 -Xmx 和 -Xms 参数分别设置最大堆和初始堆的大小。适当设置堆的大小,可以避免OOM(Out of Memory)异常。
- 对象池:可重用的Java对象可以放入对象池中,而不是每次创建新对象时使用新的内存空间。对象池可以提高性能和减少GC时间。
- Dump文件:Dump文件可以记录当前JVM中的对象和内存使用情况,有助于诊断和解决内存泄漏或性能问题。
- 垃圾回收器:Java虚拟机有许多垃圾回收器可供选择,例如CMS、G1、Serial等,每个垃圾回收器都有不同的优点和适用场景。可以根据应用程序的特点选择合适的垃圾回收器,并根据JVM日志进行优化。
- 内存分析工具:内存分析工具可以帮助发现内存泄漏和优化内存使用。例如,Eclipse Memory Analyzer(MAT)是一个Java堆-Dump分析器和计算机内存调试工具,可以快速分析Dump文件。【还可以使用JProfiler】
- 避免创建过多的临时对象:在循环和递归中避免使用过多的临时对象,可以提高Java程序的性能
堆内存调优是一项复杂的任务,需要我们仔细分析应用程序的特点,了解JVM的特性和限制,以及通过不断实践和调整来改进应用程序的性能和稳定性。
15、GC
1️⃣ 什么是垃圾回收技术?
GC (Garbage Collection) 是指垃圾回收技术,是现代编程语言中的一个重要特性。垃圾回收是一种自动的内存管理技术,用于自动回收不再使用的内存,使得程序员可以更加专注于算法和业务逻辑的实现,而不必过于担心内存泄漏和关注内存管理方面的细节
在一个计算机程序中,内存是分配给程序使用的资源之一。程序会创建新的对象(实例),并使用它们执行其任务。在经过一段时间后,这些对象可能会变成无用对象(即垃圾),如未及时的回收,它们将占用宝贵的内存资源,导致系统变慢甚至崩溃
因此,垃圾回收技术是一种很重要的技术,它提供了一种自动内存管理机制,通过找出和清除不再被使用的(即垃圾)对象,以释放出内存。
2️⃣ 常见的垃圾回收机制
- 引用计数是一种简单的垃圾回收算法
- 跟踪每个对象的被引用次数,当一个对象的引用计数为0时,就将其释放
- 但它存在循环引用问题,即两个或多个对象互相强引用,导致它们的引用计数永远不为0,从而无法释放
- 标记-清除是一种使用标记的垃圾回收算法
- 它使用标记-清除两个阶段
- 标记阶段会遍历整个堆(heap)并标记所有存活的对象,而清除阶段会清理所有未被标记的对象
- 这种方法可以解决循环引用的问题,但会产生内存碎片问题
- 复制算法是一种常见的垃圾回收算法
- 它将堆分为两个区域,每次只使用其中一个区域,当一个区域的对象占满之后,就将标记的存活对象复制到另一个区域中,并清理掉原来的区域
- 这种算法可以保证堆中的内存连续,但空间利用率比较低
- 标记-整理算法则是在标记-清除算法的基础上改进而来的
- 它在清除未标记对象时,保证存活对象在内存中连续排列,从而减少了内存碎片问题
综上所述,不同类型的垃圾回收算法适用于不同的场景和应用。在开发软件时,程序员需要考虑编程语言采用什么样的垃圾回收算法以及如何最优化地使用内存。
3️⃣ 总结:
- 内存效率:复制算法 > 标记清除算法 > 标记压缩算法(时间效率)
- 内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法
- 内存利用率:标记压缩算法 = 标记清除算法 > 复制算法
16、JMM
JMM(Java内存模型)是描述Java程序中多线程并发访问内存的规范。Java程序在运行时会创建线程,这些线程会在共享内存中进行操作,并且可以访问同一个对象。JMM定义了Java程序中多线程之间内存访问的规则,以保证多线程并发访问内存时的正确性和可靠性
JMM主要定义了以下内容:
- 内存模型:
- JMM使用内存模型来描述Java程序中多线程并发访问共享内存的行为
- 在内存模型中,内存会被划分为工作内存和主内存,线程只能直接访问工作内存,而不能直接操作主内存
- 所有的操作都必须通过主内存来完成。
- 原子性、可见性和有序性:
- JMM为Java中的共享变量提供了原子性、可见性和有序性的保证
- 原子性指的是对于基本数据类型的操作是具有原子性的,例如int类型的自增操作是原子性的
- 可见性指的是一个线程修改了变量的值,其他线程能够立即看到这个变化
- 有序性指的是JMM保证程序执行的指令顺序与Java代码编写的顺序相同,如果要保证顺序,需要使用happens-before关系来确保顺序性
- happens-before关系:
- happens-before关系定义了两个操作之间的顺序关系
- 如果操作A happens-before操作B,那么操作B看到操作A的结果
- happens-before关系是JMM的一个重要概念,它可以用来确保Java程序中的指令执行顺序
总之,JMM是Java多线程编程的基础,了解JMM对于编写高质量的多线程程序是非常重要的