1. JVM介绍
Java虚拟机(Java Virtual Machine,简称JVM)是Java程序运行的核心。它是一个抽象的计算机,提供了一个独立于硬件和操作系统的运行环境。JVM的主要任务是加载、验证和执行Java字节码。
JVM的主要特点包括:
- 跨平台性:一次编写,到处运行
- 自动内存管理:垃圾回收机制
- 安全性:字节码验证,沙箱安全模型
- 优化执行:即时编译(JIT)
2. JVM的组成
JVM主要由以下几个部分组成:
- 类加载器(Class Loader)
- 运行时数据区(Runtime Data Area)
- 执行引擎(Execution Engine)
- 本地方法接口(Native Method Interface)
让我们用一个图表来直观地展示JVM的组成:
3. JVM各组成部分的作用
3.1 类加载器(Class Loader)
类加载器负责加载Java类的字节码到JVM中。它遵循双亲委派模型,主要包括以下三种:
- 启动类加载器(Bootstrap ClassLoader):加载Java核心类库
- 扩展类加载器(Extension ClassLoader):加载扩展类库
- 应用类加载器(Application ClassLoader):加载应用程序的类
类加载过程包括:加载、验证、准备、解析和初始化。
详细的过程可以参考文章:Java类加载机制深度解析:从类加载过程到双亲委派模型_java 类加载与双亲委派模型-优快云博客
3.2 运行时数据区(Runtime Data Area)
运行时数据区是JVM管理的内存区域,用于存储程序运行时的数据。我们将在后面详细介绍。
3.3 执行引擎(Execution Engine)
执行引擎负责执行字节码。它包括以下部分:
- 解释器:逐行解释执行字节码
- JIT编译器:将热点代码编译成本地机器码,提高执行效率
- 垃圾回收器:自动回收不再使用的内存
3.4 本地方法接口(Native Method Interface)
本地方法接口允许Java代码调用非Java语言(如C、C++)编写的方法。
4. 运行时数据区详解
运行时数据区是JVM管理的内存区域,它包括以下几个部分:
- 方法区(Method Area)
- 堆(Heap)
- Java栈(Java Stack)
- 本地方法栈(Native Method Stack)
- 程序计数器(Program Counter Register)
让我们用一个图表来展示运行时数据区的结构:
4.1 方法区(Method Area)
方法区存储了每个类的结构信息,包括:
- 运行时常量池(用于存储类或接口的各种常量信息)
- 成员字段和方法数据
- 构造函数和普通方法的字节码内容(源代码编译后生成的字节码)
在HotSpot虚拟机中,方法区也被称为"永久代"(Permanent Generation)。但在JDK 8及以后,永久代被元空间(Metaspace)取代:
- 在JDK 1.8之前,方法区的实现是永久代(PermGen),其大小是固定的,无法动态调整。参数
-XX:PermSize
(来设置永久代初始分配空间)和-XX:MaxPermSize
(设定永久代最大可分配空间)进行设置,超出最大值会抛出OutOfMemoryError
。 - 之后改为了元数据区,改进后能够根据应用的大小进行动态调整大小。
为什么要这样改?
- 永久代内存空间大小固定,在动态加载大量类信息时容易出现内存溢出,改进后能够动态调整,更为灵活
- GC效率低,永久代有垃圾回收机制,但是大多时候元数据区的数据都会一直处于使用状态,垃圾很少,因此垃圾回收的效率较低。而元数据使用了本地内存而不再是JVM的堆内存,相比就更为灵活了。
4.2 堆(Heap)
堆是JVM中最大的一块内存,被所有线程共享。它主要用于存储对象实例和数组。堆可以分为两个区域:
-
新生代(Young Generation)
- Eden区:大多数新创建的对象首先被分配在Eden区
- Survivor区:从Eden区幸存下来的对象会被复制到Survivor区
-
老年代(Old Generation):长期存活的对象会被移到老年代
4.3 Java栈(Java Stack)
Java栈是线程私有的,它的生命周期与线程相同。每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
为什么要分年轻代和老年代:
主要是依据对象生命周期特点来提高垃圾的回收效率:对于对象的生命周期而言,大多数的对象都是存活时间短(年轻代),只有少部分对象的存活时间较长(老年代),对于垃圾回收而言主要就集中在了年轻代(回收更为频繁),由此划分后两部分各自采用不同的回收算法来进一步提高回收效率,新生代通常采用复制算法,而老年代采用标记-整理算法或者标记-清除算法。
如果不了解回收算法可以先去:垃圾回收算法及回收器详解
为什么要将年轻代又细分为S0、S1、Eden区:
首先前面提到新生代的对象生命周期短且需要频繁回收,更适合复制算法(会频繁创建对象,大部分对象都会在一次GC后被回收,只有会有少量对象存活下来,需要频繁回收对象),然而复制算法的内存利用率仅有50%,因此将内存再次细分了,默认Eden区、S0、S1所占内存之比为8:1:1
两个S区(幸存者 区)会交替接收GC之后存活的对象,比如初始对象放到Eden区,当数量达到阈值后GC将存活对象经复制算法拷贝到S0区,下次GC后就会将Eden区存活对象及S0存活对象复制到S1区,依次交替进行接收。
4.4 本地方法栈(Native Method Stack)
本地方法栈与Java栈类似,但它是为JVM使用到的Native方法服务的。
4.5 程序计数器(Program Counter Register)
程序计数器是每个线程都有的一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
5. JVM内存模型JMM
Java内存模型(JMM)是一种规范,它定义了Java虚拟机(JVM)在计算机内存(RAM)中的工作方式。JMM是Java并发编程的基础,它描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。
另外,JMM的工作内存是一个抽象概念,并不真实存在,涵盖了缓存、写缓冲区、寄存器及其他硬件和软件的集合,注意与TLAB区分(TLAB是Jvm为每个线程单独分配的一块堆内存,线程创建对象时优先使用自己单独的堆空间,当单独的堆空间不足或者创建对象较大时会向堆内存申请新的堆内存)。
5.1. JMM的主要目标
- 定义程序中各个变量的访问规则
- 规定编译器和处理器对内存的优化操作
- 保证并发编程中的原子性、可见性和有序性
5.2. JMM的抽象结构模型
JMM定义了线程和主内存之间的抽象关系:
- 所有的变量都存储在主内存(Main Memory)中
- 每个线程都有自己的工作内存(Working Memory)
- 工作内存中保存了该线程使用到的变量的主内存副本拷贝
- 线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量
- 不同的线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成
5.3. JMM定义的内存操作
JMM定义了以下8种操作来完成主内存和工作内存的交互:
- lock(锁定):作用于主内存的变量
- unlock(解锁):作用于主内存的变量
- read(读取):作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中
- load(载入):作用于工作内存的变量,把read操作从主内存中得到的变量值放入工作内存的变量副本中
- use(使用):作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎
- assign(赋值):作用于工作内存的变量,把一个从执行引擎接收到的值赋给工作内存的变量
- store(存储):作用于工作内存的变量,把工作内存中一个变量的值传送到主内存中
- write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中
5.4. Happens-Before规则
Happens-Before是Java内存模型中保证多线程操作可见性的机制,它是一个偏序关系,指定了两个操作之间的执行顺序。如果操作A happens-before操作B,那么操作A的执行结果对操作B可见,且A的执行顺序排在B之前。
5.4.1 Happens-Before的规则
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
- start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
- join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
- 中断规则:一个线程调用另一个线程的interrupt()方法happens-before于被中断线程检测到中断事件的发生。
- 构造函数规则:一个对象的初始化完成先于它的finalize()方法的开始。
5.4.2 Happens-Before规则的重要性
- 可见性保证:确保一个线程的操作对其他线程可见。
- 有序性保证:防止指令重排导致的问题。
- 简化并发编程:开发者可以依赖这些规则,而不需要理解底层的内存模型细节。
5.4.3 使用Happens-Before规则的示例
class HappensBefore {
int a = 0;
volatile boolean flag = false;
public void writer() {
a = 1; // 1
flag = true; // 2
}
public void reader() {
if (flag) { // 3
int i = a; // 4
}
}
}
在这个例子中:
- 根据程序顺序规则,1 happens-before 2,3 happens-before 4
- 根据volatile变量规则,2 happens-before 3
- 根据传递性规则,我们可以得出 1 happens-before 4
这意味着,如果reader()方法读到flag为true,那么它一定能读到a的最新值1。
6. Jvm的命令参数
1. 堆内存设置
- -Xms: 设置JVM启动时的初始堆大小。
- -Xmx: 设置JVM允许的最大堆大小。
- -Xmn: 设置年轻代的大小,通常占堆的1/3到1/4。
2. 垃圾回收策略
- -XX:+UseG1GC: 启用G1垃圾收集器,适用于大内存应用。
- -XX:+UseParallelGC: 启用并行垃圾收集器,适合多核CPU。
- -XX:+UseConcMarkSweepGC: 启用CMS垃圾收集器,减少停顿时间。
3. 线程栈大小
- -Xss: 设置每个线程的栈大小,通常为256k到1M,具体取决于应用需求。
4. 日志记录参数
- -XX:+PrintGC: 打印垃圾回收的简要信息。
- -XX:+PrintGCDetails: 打印详细的垃圾回收信息。
- -Xloggc:gc.log: 将GC日志输出到指定文件。
5. 其他重要参数
- -XX:NewRatio: 设置年轻代和老年代的比例。
- -XX:SurvivorRatio: 设置Eden区和Survivor区的比例。
- -XX:MaxTenuringThreshold: 设置对象晋升到老年代的最大年龄。
在熟悉JVM此基础上,可以了解垃圾回收机制。垃圾回收详解