JVM虚拟机基础知识
Java Virtrual Machine 运行java文件 JVM虚拟机
jdk:java development Kit java开发工具包 包含jre
jre:java Runtime Envirement java运行环境 包含 JVM
运行时数据区
什么是运行时数据区(就是我们java运行时的东西是放在那里的)
红色为线程私有数据区
绿色为线程共享数据区
1. 程序计数器(Program Counter Register)
- 作用:记录当前线程所执行的字节码的地址。可以看作是执行当前指令的指针。
- 特点:线程私有,每个线程有独立的程序计数器,因为Java的多线程通过线程轮流切换来实现。
- 内容:如果执行的是本地方法,程序计数器为空(
undefined
)。
2. Java虚拟机栈(Java Virtual Machine Stack)
- 作用:存储方法调用和执行过程中所需要的局部变量、操作数栈、动态链接和方法返回地址等。
- 特点:线程私有,生命周期与线程相同。
- 栈帧:每个方法执行时,会创建一个栈帧,用于存储局部变量表、操作数栈、方法返回地址等信息。
3. 本地方法栈(Native Method Stack)
- 作用:为JVM执行本地方法(Native Method)服务,与Java虚拟机栈类似,只不过它是用来执行Native方法的。
- 特点:线程私有,负责调用本地代码(通常是用C/C++编写的)。
4. 堆(Heap)
- 作用:用于存储所有Java对象和数组,所有线程共享的内存区域。
- 特点:堆是垃圾回收机制(GC)的主要管理区域。根据对象的生命周期,堆可以进一步划分为“年轻代”和“老年代”,以提高GC的效率。
- 分代回收:堆被分为年轻代(Young Generation)和老年代(Old Generation),年轻代主要存储短命的对象,老年代存储长期存活的对象。
5. 方法区(Method Area)
- 作用:存储类的结构信息、常量、静态变量、即时编译器(JIT)编译后的代码等。
- 特点:也是线程共享的区域,常被称为“永久代”(PermGen),在JDK 8之后被称为“元空间”(Metaspace)。
- 元空间:Metaspace使用的是本地内存,而不是堆内存。
Java内存结构
直接内存(Direct Memory)
- 作用:直接由操作系统内存分配和管理,不属于JVM运行时数据区的一部分,但也常用,主要用于NIO(New Input/Output)操作。
- 特点:高效,避免了Java堆与操作系统内存之间的二次拷贝。
直接内存与堆内存的区别:
直接内存申请空间耗费很高的性能,堆内存申请空间耗费比较低
直接内存的IO读写的性能要优于堆内存,在多次读写操作的情况相差非常明显
类的加载过程
三个主要阶段:加载、链接、初始化
1. 加载(Loading)
这是类加载的第一个阶段,负责将类的字节码读入内存并创建 Class
对象。这个阶段的具体流程如下:
- 通过类加载器(如启动类加载器、应用类加载器、自定义类加载器等)找到类的
.class
文件。 - 读取该类文件,并将它的字节码内容加载到 JVM 内存中。
- 为该类在内存中创建一个对应的
Class
对象,用来封装类的所有信息。
2. 链接(Linking)
链接阶段将类的字节码合并到 JVM 中,它包括三个子阶段:
-
验证(Verification):
- 检查类的字节码是否符合 JVM 的规范,保证类文件没有被破坏或篡改,确保它的安全性和正确性。
-
准备(Preparation):
- 为类的静态变量分配内存并设置默认初始值。这里注意,静态变量仅分配内存,并未赋值(如果有赋初值是在初始化阶段进行的)。
-
解析(Resolution):
- 将常量池中的符号引用转换为直接引用。例如,将类、字段、方法等符号引用解析为实际内存地址或指针。
3. 初始化(Initialization)
这是类加载的最后阶段,初始化阶段会执行类中的静态初始化块和静态变量的赋值操作。此时 JVM 开始执行类的 <clinit>
方法(由编译器自动生成,包含静态初始化代码),并完成所有静态变量的初始化。
类加载器
- 启动(Bootstrap)类加载器
- 扩展(Extension)类加载器
- 系统类加载器
- 自定义加载器
双亲委派机制
双亲委派机制是指,类加载器在尝试加载某个类时,会先将请求交给父类加载器处理,逐层向上,直到委派给最顶层的启动类加载器。如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。
为什么要有双亲委派机制?
-
安全性: 双亲委派机制确保了核心类库不会被篡改或覆盖。例如,如果没有双亲委派机制,某个自定义类加载器可以自己加载
java.lang.String
类,那么这可能导致核心类被恶意替换,破坏 JVM 的运行。 -
避免重复加载: 如果类加载器不遵循双亲委派机制,那么同一个类可能会被多个类加载器重复加载,导致类的定义不一致,产生
ClassCastException
等问题。
垃圾回收机制
JVM的垃圾回收机制(Garbage Collection,GC)负责自动管理内存,回收不再使用的对象,避免内存泄漏。GC通过跟踪对象的生命周期,释放不再需要的内存资源。主要的垃圾回收机制涉及对象分代管理和回收算法。主要用于Java堆的管理
分代垃圾回收(Generational Garbage Collection)
JVM根据对象的生命周期将堆内存分为不同的区域,以提高GC的效率。通常分为年轻代和老年代。
年轻代(Young Generation)
- 特点:大多数对象在年轻代创建,生命周期较短。年轻代又分为三个部分:
- Eden区:新对象首先分配在Eden区。
- Survivor区(S0和S1):当对象在Eden区存活后,进入Survivor区,两者会在GC过程中互相切换。
- Minor GC(年轻代GC):当Eden区满了时触发,回收年轻代的内存。存活下来的对象会移动到Survivor区或老年代。
老年代(Old Generation)
-
特点:存放长生命周期的对象。当对象在年轻代多次经历GC后,仍然存活,就会被移动到老年代。
-
Major GC(老年代GC)或Full GC(全堆GC):发生在老年代满了时,会回收老年代和年轻代的内存。这通常比Minor GC耗时更多。
垃圾回收算法
JVM采用多种算法来处理不同区域的对象。常用的GC算法包括:
标记-清除算法(Mark-Sweep)
-
过程:
- 标记阶段:从GC Roots出发,标记所有可达的对象。
- 清除阶段:回收所有未标记的对象。
-
优点:简单,直接清理无用对象。
-
缺点:容易产生大量不连续的内存碎片,导致后续内存分配效率低。
标记-整理算法(Mark-Compact)
-
过程:
- 标记阶段:与标记-清除算法相同,标记可达对象。
- 整理阶段:将所有存活对象移动到内存的一端,整理出连续的内存空间。
-
优点:避免内存碎片问题。
-
缺点:对象移动需要额外的开销,处理速度比标记-清除略慢。
复制算法(Copying)
-
过程:
- 将内存分成两个等大小的区域,先在其中一个区域分配对象。
- 当该区域满时,将存活的对象复制到另一个区域,清空旧区域。
-
优点:回收效率高,简单,无内存碎片问题。
-
缺点:需要双倍内存,浪费空间。适用于年轻代,因为年轻代对象存活率低,复制开销较小。
分代收集算法(Generational Collection)
- JVM结合了上述几种算法的优点,按对象的生命周期进行分代管理:
- 年轻代:使用复制算法,效率高。
- 老年代:使用标记-整理或标记-清除算法,减少碎片。
垃圾回收器
JVM中有多个GC收集器,不同的GC收集器适用于不同的应用场景。
Serial GC
Serial 收集器:新生代。发展历史最悠久的收集器。它是一个单线程收集器,它只会使用一个 CPU 或者线程去完成垃圾收集工作,而且在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
特点:
- 新生代收集器,使用复制算法收集新生代垃圾。
- 单线程的收集器,GC工作时,其它所有线程都将停止工作。
- 简单高效,适合单 CPU 环境。单线程没有线程交互的开销,因此拥有最高的单线程收集效率
Parallel GC(吞吐量优先)
- 多线程收集器,年轻代使用复制算法,老年代使用标记-清除或标记-整理。
- 优点:适合并发执行,提高吞吐量。
- 缺点:GC暂停时间可能较长。
CMS GC(Concurrent Mark-Sweep,低停顿优先)
- 主要针对老年代,使用标记-清除算法,目标是减少GC停顿时间。
- 优点:并发收集,停顿时间短,适合需要低延迟的应用。
- 缺点:容易产生内存碎片,需要更多的CPU资源。
G1 GC(Garbage-First)
- 适用于大内存、多核CPU的应用,将堆划分为多个区域,优先回收垃圾最多的区域。
- 优点:兼顾吞吐量和低停顿时间,适合大规模应用。
- 缺点:相对复杂,调优较难。
ZGC
- 低停顿的垃圾收集器,设计用于处理超大堆内存(TB级别)。
- 优点:几乎所有GC工作都是并发进行,停顿时间极短(低于10ms)。
- 缺点:仍然较新,可能需要更高的硬件支持。