Loading... ## 一、JVM定义 Java Virtual Machine(Java[虚拟机](https://baike.baidu.com/item/%E8%99%9A%E6%8B%9F%E6%9C%BA?fromModule=lemma_inlink)),它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java[虚拟机](https://baike.baidu.com/item/%E8%99%9A%E6%8B%9F%E6%9C%BA?fromModule=lemma_inlink)有自己完善的[硬件](https://baike.baidu.com/item/%E7%A1%AC%E4%BB%B6?fromModule=lemma_inlink)架构,如处理器、[堆栈](https://baike.baidu.com/item/%E5%A0%86%E6%A0%88?fromModule=lemma_inlink)、寄存器等,还具有相应的[指令系统](https://baike.baidu.com/item/%E6%8C%87%E4%BB%A4%E7%B3%BB%E7%BB%9F?fromModule=lemma_inlink)。JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java[虚拟机](https://baike.baidu.com/item/%E8%99%9A%E6%8B%9F%E6%9C%BA?fromModule=lemma_inlink)上运行的[目标代码](https://baike.baidu.com/item/%E7%9B%AE%E6%A0%87%E4%BB%A3%E7%A0%81?fromModule=lemma_inlink)([字节码](https://baike.baidu.com/item/%E5%AD%97%E8%8A%82%E7%A0%81?fromModule=lemma_inlink)),就可以在多种平台上不加修改地运行。Java[虚拟机](https://baike.baidu.com/item/%E8%99%9A%E6%8B%9F%E6%9C%BA?fromModule=lemma_inlink)在执行[字节码](https://baike.baidu.com/item/%E5%AD%97%E8%8A%82%E7%A0%81?fromModule=lemma_inlink)时,实际上最终还是把字节码解释成具体平台上的[机器指令](https://baike.baidu.com/item/%E6%9C%BA%E5%99%A8%E6%8C%87%E4%BB%A4?fromModule=lemma_inlink)执行。 我们一开始接触Java时第一课所下的JDK中,就包含了**JVM**和**屏蔽操作系统差异的组件**。其中JVM在各个操作系统中是一致的,而屏蔽操作系统差异的组件在不同系统上各不相同(类比与不同系统需要下载不同JDK)  ## 二、类的生命周期 类的生命周期包括:**类的加载→连接→初始化→使用→卸载** **1、类的加载**:查找并加载类的二进制数据(.class文件),即把硬盘上的.class文件加载到JVM内存中 **2、连接**:确定类与类之间的关系(如 `student.setAddress(address)`) (1)验证:校验.class文件正确性,防止损坏或被篡改 (2)准备:给static静态变量分配内存,并赋予**初始化默认值**(如 `static int num = 10`在准备阶段num会赋予Integer的默认值0,在初始化阶段再将0修改成10)。**在准备阶段,JVM只有类没有对象**。 ```java Public class Student { static int age;// 准备阶段初始化为0 String name; } ``` (3)**解析**:把类中的**符号引用**转为**直接引用** 前期阶段,还不知道类的具体内存地址,只能用 `cn.tangsong.Student`来代替 `Student`类,这就是符号引用;而在解析阶段,JVM就可以将 `cn.tangsong.Student`映射成实际内存地址,这种使用内存地址来代替类方法就称为直接引用 **3、初始化**:给static变量赋予正确的值 **4、使用**:对象的初始化、对象的垃圾回收、对象的销毁 **5、卸载**:即正常结束 JVM结束生命周期时机:正常结束、异常结束/错误、`System.exist()`、操作系统异常 ## 三、JVM内存模型(Java Memory Model) JMM用于定义所有线程的**共享变量**(即方法外的全局变量,不能是局部变量)的访问规则。 JMM将内存划分为主内存区和工作内存区: * **主内存区**:真实存放变量 * **工作内存区**:主内存中变量的副本,供各个线程使用 **细节点:** 1、**工作内存区是私有的,主内存区是公有的。** 各个线程只能访问自己的私有工作内存区,不能访问其他线程的工作内存,也不能访问主内存 2、不同线程之间,可以通过主内存间接的访问其他线程的工作内存   1. `Lock`:将主内存中的变量,标识为一条线程的独占状态 2. `Read`:将主内存中的变量,读取到工作内存中 3. `Load`:将2中读取的变量拷贝到工作内存对应的变量副本中(i放到i,而不是i1、i2) 4. `Use`:把工作内存中的变量副本,传递给线程去使用 5. `Assign`:把线程正在使用的变量,传递给工作内存中的变量副本中 6. `Store`:将工作内存中变量副本的值,传递到主内存中 7. `Write`:将变量副本作为一个主内存中的变量进行存储 8. `Unlock`:解决线程的独占状态 3、**JMM并不是真正存在,是一种规则规定了程序中各个变量的访问方式。** CPU中包括了CPU缓存、CPU寄存器、CPU主内存(RAM也叫主存)。线程映射到到操作系统上就是内核线程,工作内存和主内存映射到硬件上可以是CPU缓存、CPU寄存器、CPU主内存,是交叉关系没有固定内容。所以工作内存和主内存的数据流转在CPU层面来看是没有什么方向性的。  从读取速度来说CPU寄存器>CPU缓存>主内存,所以,当一个CPU需要访问主内存时,它会将主内存的一部分读入其CPU缓存。它甚至可能将缓存的一部分读入其内部的CPU寄存器,然后对其进行操作。当CPU需要将结果写回主存时,它将把数值从其内部CPU寄存器冲到CPU缓存中,并在某个时间将数值冲回主存。 4、**Java线程与操作系统的关系**:在Window和Linux中,Java线程是基于一对一模型实现的,使用线程的时候在Java虚拟机内部是调用当前操作系统的**内核线程**来完成当前任务的。每个线程映射一个内核线程,通过线程能够调用内核线程,再由操作系统内核将任务映射到各个处理器上。 **内核线程**是由操作系统内核支持的线程,由操作系统内核完成线程的切换,内核操作调度器对线程执行调度,并将线程映射到各个处理器上,每个内核线程都可以一视为内核的一个分身,是操作系统可以处理多个任务的原因。 **并发**操作的原因是线程映射到操作系统的内核线程,然后内核操作调度器对内核线程进行调度并映射到多处理器上。通常我们所说的并发指的是时间片轮转法的**假象的并发**。 > 线程的处理瓶颈主要在于操作系统内核线程的线程数。每个操作系统都有一个限制,即最大可创建的线程数。 > 如果Java线程的数量低于操作系统内核线程的线程数,那么可能会导致产能过剩。因为Java线程无法充分利用操作系统提供的多线程处理能力,部分处理器资源可能没有得到有效利用。 > 多个内核线程可以使用多个处理器来并行执行任务,这将允许更多的线程同时运行,提高整体的处理能力。当系统具有多个处理器时,操作系统会将任务分配给不同的处理器来并行处理。 > 然而,**如果系统只有一个处理器,那么多个内核线程将共用这一个处理器的计算资源。这种情况下,多个线程之间会进行时间片轮转,通过操作系统的调度算法来分配处理器时间。这就是所谓的并发执行,不同线程在时间上交替执行,但实际上每个时刻只有一个线程在执行。** **线程安全**:就是存在共享资源,多个线程对共享资源进行操作。线程将变量从主内存中拷贝到工作内存中,对变量的副本进行修改,同时另一个线程拷贝了变量的副本进行了修改,这个时候,操作完成后向主内存中刷新数据这时候就出现问题,引起线程的一致性问题、可见性问题。 ## Volatile **概念:** JVM提供的一个轻量级的同步机制 **作用:** 1. 防止JVM对long/double等64位的非原子性协议进行的误操作(读/写半个数据) 2. 可以使变量对所有的线程立即可见(某一个线程如果修改了工作内存中的变量副本,那么加上volatile 之后,该变量就会立刻同步到其他线程的工作内存中)volatile会迅速同步到主内存再到各个工作内存中 3. 禁止指令的**重排序**优化 > 计算机内存的基本单位是字节(Byte),每个字节都有一个唯一的地址,并且可以单独进行读写操作。 > 线程在读写操作时,通常是以字(Word)为单位进行的。一个字通常是4个字节或8个字节,具体取决于计算机架构和操作系统,是通过处理器提供的原子性读写指令来实现的。处理器能够保证对内存中一个字的读写操作是原子的,这样就避免了读/写半个数据的情况。 > > 然而,`volatile`关键字与原子性并不直接相关。`volatile`关键字主要用于确保变量的可见性和禁止指令重排,并不能直接提供原子性保证。所以,说 `volatile`关键字可以防止JVM对64位非原子操作的误操作并不准确。 > 对于长整型(`long`)和双精度浮点型(`double`)这样的64位数据类型,在某些平台上,对它们的读写操作可能会被拆分成多个32位的操作,这样就可能出现读/写半个数据的情况。使用 `volatile`关键字可以确保对 `long`和 `double`类型的变量进行读写操作时的可见性,但并不能解决拆分操作导致的非原子性问题。 原子性:`num = 10;` 非原子性:`int num = 10;` -> `int num; num =10;` **重排序:** 排序的对象就是原子性操作,目的是JVM为了提高执行效率,自我优化 ```java int a = 10; //1 int a ; a = 10 ; int b; //2 b = 20; //3 int c = a * b; //4 ``` 重排序**不会影响单线程的执行结果**,因此以上程序在经过重排序后,可能的执行结果:`1,2,3,4`、`2,3,1,4` ```java //2 3 1 4 int b; b = 20; int a = 10; int c = a * b; ``` 但是在多线程下,重排序就会出现问题: ```java //双重检查式的懒汉式单例模式 public class Singleton { private static Singleton instance = null ;//单例 private Singleton(){} public static Singleton getInstance(){ if(instance == null){ synchronized (Singleton.class){ if(instance == null){ instance = new Singleton() ;//不是一个原子性操作 } } } return instance ; } } ``` 以上代码可能会出现问题,因为 `instance = new Singleton() `不是一个原子性操作,会在执行时拆分成以下动作: 1. JVM会分配内存地址、内存空间 2. 使用构造方法实例化对象 3. instance = 第1步分配好的内存地址 根据重排序的知识,可知,以上3个动作在真正执行时 可能1、2、3,也可能是1、3、2。如果在多线程环境下,使用1、3、2可能出现问题:  解决方案: ```java private volatile static Singleton instance = null ;//单例 ``` volatile是通过**内存屏障**防止重排序问题: 1. 在volatile写操作前,插入StoreStore屏障 2. 在volatile写操作后,插入StoreLoad屏障 3. 在volatile读操作前,插入LoadLoad屏障 4. 在volatile读操作后,插入LoadStore屏障 **volatile是否能保证原子性、保证线程安全?** 都不能!volatile只能保证可见性,要想保证原子性/线程安全,可以使用原子包 `java.util.cocurrent.atomic`中的类,该类能够保证原子性的核心,是因为提供了 `compareAndSet()`方法,该方法提供了 CAS算法(无锁算法)。 如果要保证原子性的话, 同一时刻只能有一个线程或者CPU能够执行成功 ,底层是需要对硬件进行加锁的,只有某个CPU或者线程锁定了,享有独占的权限,它的操作才能是不被其它CPU或者线程打断的。 ```java import java.util.concurrent.atomic.AtomicInteger; public class TestVolatile { // static volatile int num = 0; static AtomicInteger num = new AtomicInteger(0); public static void main(Math[] args) throws InterruptedException { for (int i = 0; i < 100; i++) { // 每个线程将num累计3万次;100个线程在线程安全时,结果应该300万 new Thread(() -> { for (int j = 0; j < 30000; j++) { // num++;// 不是一个原子性操作 num.incrementAndGet(); /* num = num + 1 : ① num+1 ② num = ①的结果 2个线程同时执行 num +1 (假设此时num的值是10) 线程A: 10 +1 -> 11 线程B: 10 + 1 -> 11 */ } }).start();// lambda } Thread.sleep(1000); System.out.println(num); } } ``` **原子性操作**:基本类型的读取和变量赋值、引用reference的赋值操作、`java.util.cocurrent.atomic`包下的操作 **非原子性操作**:除上之外的,如自增 `i++`、对象实例化 `instance = new Instance()`、对象赋值 `y = x` **CAS算法**:CAS(Compare-and-Swap)算法是一种无锁算法,用于解决并发环境下的数据竞争问题。它通过比较内存中的值与期望值是否一致,如果一致则修改内存中的值,否则放弃修改。 CAS算法的底层实现依赖于硬件提供的原子操作指令。具体来说,它使用了处理器提供的原子性指令,例如 `cmpxchg`(在x86架构中)或 `ldrex/strex`(在ARM架构中),这些指令可以在一个原子操作中完成比较和交换两个值。 1.首先,获取内存位置的当前值。 2.然后,与期望值进行比较。如果相等,则表示内存位置的值没有被其他线程修改过。 3.如果相等,将新值写入内存位置,完成更新并返回成功。 4.如果不相等,则表示其他线程已经修改了内存位置的值,此时无法完成更新,需要重新尝试。 通过循环不断重试,CAS算法保证了原子性操作,避免了传统的锁机制所带来的开销和可能导致的死锁问题。 CAS算法的核心思想是利用硬件提供的原子操作指令,在硬件层面上保证了操作的原子性,从而避免了使用锁所带来的性能损失。它是一种乐观并发控制方法,通过检查共享数据是否发生变化来解决竞争问题,而不是使用锁来限制并发访问。 在高并发环境下,CAS算法具有较好的性能表现,但也存在一些问题,例如ABA问题。为了解决这些问题,通常需要结合版本号/时间戳、引入额外的标记位、使用带有回调函数的CAS来确保数据的一致性和正确性。 ## JVM运行时的内存区域 与上述工作内存、主内存等是维度不同的划分,本质是一个东西,分为五大部分:  ### 程序计数器 程序计数器,也叫行号指示器,指向当前线程所执行的字节码指令的地址。 可以理解为class文件中的行号。 1. 一般情况下,程序计数器是行号。但如果正在执行的方法是native本地方法(如Object类下调用操作系统),则程序计数器的值为 `undefined`。 2. 程序计数器是唯一一个 不会产生**内存溢出**的区域。(代码量不可能出现内存溢出1024^3*255 byte) 3. goto的本质就是改变的程序计数器的值(java中没有goto,goto在java中是唯一的保留字) ### 虚拟机栈 描述方法执行时的内存模型: * 方法在执行的同时,会在虚拟机栈中创建一个栈帧(先进后出) * 栈帧中包含:方法的局部变量表、操作数据栈、动态链接、方法出口信息等  当方法太多时,就可能发生栈溢出异常 `StackOverflowError`,或者内存溢出异常 `OutOfMemoryError`: ```java public static void main(String[] args) { main(new String[]{"abc","abc"}); } ``` ### 本地方法栈 原理和结构与虚拟机栈一致,不同点在于虚拟机栈中存放的 jdk或我们自己编写的方法,而本地方法栈调用的 操作系统底层的方法。 ### 堆 * 堆是Java虚拟机(JVM)管理的内存区域之一,用于存储对象实例(对象、数组,除了基础类型都是对象)。它在JVM启动时被创建,是JVM中最大的一块内存区域。 * GC(垃圾收集器)主要管理堆内存中的对象回收和内存释放。 * 堆本身是线程共享的,但可以在堆内部划分出多个线程私有的缓冲区,例如线程私有的分配指针(TLABs)。 * 堆内存的物理空间可以是不连续的,只要逻辑上是连续的即可,这是由于堆内存的动态分配特性。 * 堆内存可以被划分为新生代和老年代,其中新生代和老年代的大小比例通常是1:2。新生代又可以进一步划分为Eden区、Survivor 0区(S0)和Survivor 1区(S1),大小比例通常是8:1:1。 * 新生代主要用于存放生命周期较短、小型的对象,而较大(集合、数组、字符串)或生命周期较长的对象会被存放在老年代。 * 使用 `-XX:PretenureSizeThreshold`设置大对象直接进入老年代的阈值,默认为30MB。 使用 `-XX:MaxTenuringThreshold=20`设置对象在新生代和老年代中的晋升年龄的阈值,即生命周期,默认为15次Minor GC。 * 在新生代中的对象经过一次Minor GC后,如果仍然存活,会被转移到Survivor区(S区)。之后,如果Survivor区中的对象仍然存活,它们的年龄会增加。当年龄达到一定阈值时,对象会被晋升到老年代。 * 新生代的回收采用复制算法,目的是为了避免碎片产生。只有一个Survivor区可用,在垃圾回收期间,对象存活的部分会被移动到空闲的Survivor区,而不是被清理掉。 * 对老年代进行垃圾回收的操作通常被称为Major GC或Full GC。  **总结**:大部分对象都存在于新生代,新生代的空间小、回收频率高、效率高;而老生代的空间大、增长速度慢、回收频率低。 **意义**:可以根据项目中对象大小的数量,设置新生代或老生代的空间容量,从而提高GC性能。 **异常**:对象太多会导致内存异常。 `java.lang.OutOfMemoryError: Java heap space` ```java public class TestHeap { public static void main(String[] args) { List list = new ArrayList(); while(true) { list.add(new int[1024*1024]); } } } ``` 虚拟机常用参数: ```java -Xms128m :JVM启动时的大小,一般设置为与总大小一致 -Xmn32m:新生代大小 -Xmx128:总大小 jvm总大小= 新生代 + 老生代 (+ 永久代,也叫持久代在jdk8后废弃了,转移到了方法区,主要存放clss信息这类元数据的静态数据,默认64M) ``` ```java # java 启动参数, 请谨慎变更 CMD java -server -Xmx8g -Xms8g -Xmn3g -XX:+UseG1GC -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=512m -XX:+PrintGC -XX:+PrintGCDetails -XX:MaxGCPauseMillis=100 \ -XX:InitiatingHeapOccupancyPercent=35 -XX:MinMetaspaceFreeRatio=50 -XX:MaxMetaspaceFreeRatio=80 \ -XX:+PrintTenuringDistribution -XX:+PrintGCDateStamps -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses \ -Xloggc:/data/logs/${POD_NAME}-gc-%p-%t.log -XX:+PrintHeapAtGC \ -XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCApplicationStoppedTime -XX:PrintFLSStatistics=1 -XX:-OmitStackTraceInFastThrow \ -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/logs/${POD_NAME}-oom-%p-%t.hprof \ -javaagent:/data/html/transmittable-thread-local-2.12.6.jar \ -jar /data/html/${APP_NAME}.jar -Djava.security.egd=file:/dev/urandom \ --server.port=8080 --spring.profiles.active=prod --cli --server.address=0.0.0.0 ``` ### 方法区 **方法区存放**:类的元数据(描述类的信息)、常量池(存放编译期间产生的字面量(如"abc"字符串常量池)、符号引用)、方法信息(方法数据、方法代码) **GC在方法区一般回收**:类的元数据(描述类的信息)、常量池 **常量**:常量顾名思义就在常量池中,具体在调用这个常量的方法所在的常量池中。常量在编译时就会被解析并直接替换为其对应的值,而不需要在运行时进行初始化。这种优化称为"编译时常量折叠"(Compile-time Constant Folding),它可以提高执行效率和代码的可读性。 方法区中数据如果太多,也会抛异常OutOfMemory异常 PS:导致内存溢出的异常OutOfMemoryError,除了虚拟机中的4个区域以外,还可能是直接内存。在NIO技术中会使用到直接内存。  ## 类的使用方式 类的初始化:JVM只会在**首次主动使用**一个类/接口时,才会初始化它们 。 ### 主动使用 **1、new构造类的使用** ```java package init; public class Test1 { static { System.out.println("Test1..."); } public static void main(String[] args) { new Test1();// 首次主动使用才会打印 new Test1(); } } ``` **2、访问类/接口的静态成员(属性、方法)** ```java package init; class A { static int i = 10; static { System.out.println("A..."); } static void method() { System.out.println("A method..."); } } public class Test2 { public static void main(String[] args) { // A.i = 1 ;// 赋值静态属性,首次主动使用才会打印 // A.i = 1 ; // System.out.println(A.i);// 调用静态属性 A.method();// 调用静态方法 } } ``` PS: 1.main()本身也是一个静态方法,也此main()的所在类也会在执行被初始化 2.如果成员变量既是static,又是final ,即常量,则不会被初始化;而如果常量的值是一个随机值,则会被初始化 (为了安全,保障在第一次使用时才生成随机数,否则在程序加载出来时就预设了值,这时在字节码把class文件拿到就可以把随机数解析出来了) ```java class A { final static int i = 10; } public class Test { // 在Test中,保存了A中产生的常量 i=10 public static void main(String[] args) { System.out.println(A.i); } } // .class文件 public class Test { public Test() { } public static void main(String[] args) { System.out.println(10); } } ``` ```java class Father { public static int 1 = 10; static { System.out.plintln("father...");// i源自Father,打印 } } class Son extends Father { // public static int 1 = 100;// 若子类也有i,则子类初始化,打印;子类初始化父类必然初始化,先打印 static { System.out.plintln("Son...");// 不打印 } } public class Test { public static void main(String[] args) { System.out.plintln(Son.i); } } ``` **3、使用Class.forName("init.B")执行反射时使用的类(B类)** ```java package init; class B { static int i = 10; static { System.out.println("B..."); } } public class Test3 { public static void main(String[] args) { // 反射的三种方式 Class b = B.class;// 被动使用,不会打印 B b = new B();// 反射前已经初始化,无法验证 b.getClass();// 被动使用,不会打印 Class.forName("init.B");// 主动使用,会打印 } } ``` **4、初始化一个子类时,该子类的父类也会被初始化** ```java public class Father { static { System.out.println("Father..."); } } public class Son extends Father { public static void main(String[] args) { new Son(); } } ``` **5、动态语言在执行所涉及的类 也会被初始化(动态代理)** ### 被动使用 除主动使用之外全部为被动使用,如构造类数组 ```java package init; class BD { static { System.out.println("BD..."); } } public class BeiDong { public static void main(String[] args) { BD[] bds = new BD[3];// new构造数组 } } ``` Last modification:May 16, 2024 © Allow specification reprint Like 0 喵ฅฅ