引言:
Java虚拟机(JVM)是Java平台的核心组件,它负责将Java字节码转换成平台特定的机器指令,并在相应的硬件和操作系统上执行。JVM的引入使得Java语言具有“一次编写,到处运行”的跨平台特性。本文将深入探讨JVM的基本结构、内存模型、关键技术以及性能优化等方面的内容。
一、JVM的基本结构
JVM主要由类加载器、运行时数据区、执行引擎、垃圾收集器和本地接口几部分组成。
- 类加载器:负责加载Java类的字节码到JVM中,并将其转换为可以被JVM执行的数据结构。
- 运行时数据区:包括方法区、堆、Java栈、程序计数器以及本地方法栈。这些区域在JVM进程启动时创建,并在JVM进程结束时销毁。
- 执行引擎:负责读取Java字节码,并对其进行解释或即时编译(JIT),生成本地机器代码并执行。
- 垃圾收集器:负责自动回收不再使用的内存空间,防止内存泄漏和溢出。
- 本地接口:负责与本地方法库进行交互,允许Java代码调用本地代码(如C、C++等)或被本地代码调用。
运行流程 :首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
二、JVM的内存模型
JVM(Java Virtual Machine)的内存模型是Java程序运行时管理内存的一套规则。它定义了Java程序在JVM中如何通过内存来交互和操作。以下是对JVM内存模型的详细描述:
- 堆(Heap):
- 堆是JVM中最大的一块内存区域,用于存储对象实例以及数组。几乎所有的Java对象实例都在堆上分配。
- java堆是垃圾收集器管理的主要区域,因此也被成为“GC堆”。堆内存可以进一步细分为新生代和老年代。新生代主要用于存储新创建的对象,而老年代则用于存储长时间存活的对象。新生代又可以分为Eden区、From Survivor区和To Survivor区。
- 根据Java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间中。当前主流的虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制)。如果堆中没有内存可以完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
- 方法区(Method Area)(也称为元空间(Metaspace)在JDK 8及以后版本):
- 方法区存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 在JDK 8之前,方法区被称为永久代(PermGen),但由于永久代的大小是固定的,容易引发OutOfMemoryError,因此在JDK 8中,方法区被元空间所取代,元空间使用的是直接内存,受本机总内存限制。
- 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现,所以我们放到这里一起讲解。我的理解就是直接内存是基于物理内存和Java虚拟机内存的中间内存
- 虚拟机栈(Java Stack)(也称为线程栈):
- 虚拟机栈是每个线程私有的内存区域,用于存储局部变量、操作数栈、动态链接和方法出口等信息。
- 当一个方法被调用时,JVM会为该方法的局部变量在栈上分配内存空间,并在方法执行结束后自动释放这些内存空间。
- 每个方法在执行的同时都会创建一个栈帧(StackFrame)。解析栈帧:
- 局部变量表:是用来存储我们临时8个基本数据类型、对象引用地址、returnAddress类型。(returnAddress中保存的是return后要执行的字节码的指令地址。)
- 操作数栈:操作数栈就是用来操作的,例如代码中有个 i = 6*6,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去。
- 动态链接:假如我方法中,有个 service.add()方法,要链接到别的方法中去,这就是动态链接,存储链接的地方。
- 方法出口:出口是什呢,出口正常的话就是return 不正常的话就是抛出异常。
- 本地方法栈(Native Method Stack):
- 与虚拟机栈类似,本地方法栈也是线程私有的内存区域,但它主要用于支持本地方法(Native Method)的执行。
- native关键字的方法是看不到的,必须要去oracle官网去下载才可以看的到。本地方法通常是由C或C++编写的,通过JNI(Java Native Interface)调用。
- 程序计数器(Program Counter Register):
- 程序计数器是一个较小的内存空间,用于存储当前线程所执行的字节码的行号指示器。
- 字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令。
- 由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。称之为“线程私有”的内存。程序计数器内存区域是虚拟机中唯一没有规定OutOfMemoryError情况的区域。
在JVM中,堆和方法区是共享区,可以被多个线程访问;而虚拟机栈、本地方法栈和程序计数器则是线程私有的,每个线程都有自己独立的内存空间。这样的设计有助于实现线程之间的数据隔离和并发控制。
三、JVM的关键技术
- 即时编译(JIT):为了提高Java程序的执行效率,JVM引入了即时编译技术。JIT编译器可以将热点代码(频繁执行的代码)编译成本地机器代码,从而提高执行速度。
- 垃圾收集器:JVM提供了多种垃圾收集器,如Serial、Parallel、CMS和G1等。不同的垃圾收集器适用于不同的应用场景,选择合适的垃圾收集器对于提高JVM性能至关重要。
- 热点探测:JVM通过热点探测技术识别出热点代码,以便JIT编译器进行编译优化。热点探测主要基于执行计数器和采样两种方式。
- 逃逸分析:逃逸分析是一种确定对象是否会在方法体外被引用的技术。通过逃逸分析,JVM可以优化对象的分配和回收策略,提高内存使用效率。
四、JVM性能优化
JVM(Java Virtual Machine)性能优化是确保Java应用程序高效运行的关键步骤。通过合理的配置和调优,可以显著提高JVM的吞吐量、减少垃圾收集(GC)停顿时间、提高响应速度等。以下是对JVM性能优化的详细描述:
-
堆内存设置:
- 设置合适的初始堆大小(-Xms)和最大堆大小(-Xmx),以避免频繁的GC和内存溢出。
- 根据应用程序的特性和需求,调整新生代(Young Generation)和老年代(Old Generation)的比例。
-
GC调优:
- 选择合适的垃圾收集器(GC)。不同的GC适用于不同的应用场景,如Parallel GC适用于吞吐量优先的场景,CMS GC适用于响应时间优先的场景。
- 调整GC的触发条件和参数,如GC线程数、GC暂停时间目标等,以平衡吞吐量和响应时间。
- 使用GC日志分析工具(如GCViewer、GC Easy等)监控和分析GC行为,以便及时发现问题并进行调优。
-
JIT(Just-In-Time)编译器调优:
- 调整JIT编译器的参数,如编译阈值、内联策略、代码缓存大小等,以提高编译效率和代码执行效率。
- 使用热点探测技术识别热点代码,以便JIT编译器进行针对性的优化。
-
代码优化:
- 编写高效、简洁的代码,避免不必要的内存分配和对象创建。
- 使用合适的数据结构和算法来降低时间复杂度和空间复杂度。
- 使用字符串连接池来重用字符串对象,减少内存分配和垃圾收集的开销。
-
线程调优:
- 调整线程池的大小和类型,以适应应用程序的并发需求。
- 设置线程的优先级,以确保关键任务能够得到及时处理。
- 使用线程同步和并发控制机制(如锁、信号量、CyclicBarrier等)来避免竞态条件和死锁等问题。
-
锁优化:
- 减少锁的持有时间,避免长时间持有锁导致其他线程阻塞。
- 使用轻量级锁和偏向锁等高级锁技术来减少锁的竞争和开销。
- 使用读写锁(ReadWriteLock)来允许多个线程同时读取共享资源,提高并发性能。
-
类加载优化:
- 优化类的加载过程,减少类的加载时间和内存占用。
- 使用自定义类加载器来加载和管理特定类型的类。
-
I/O优化:
- 使用NIO(New I/O)或AIO(Asynchronous I/O)来提高I/O操作的性能和效率。
- 合理地使用缓冲区和缓存来减少I/O操作的次数和开销。
-
监控和诊断工具:
- 使用JVM监控和诊断工具(如JConsole、JVisualVM、JProfiler等)来监控和分析JVM的运行状态和性能瓶颈。
- 根据监控结果调整JVM参数和代码实现,以提高性能。
总之,JVM性能优化是一个综合性的任务,需要从多个方面入手进行调优。通过合理的内存管理、并发性能优化、代码优化和监控诊断等手段,可以显著提高Java应用程序的性能和稳定性。
五、虚拟机类加载机制
- 类加载的机制及过程
程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。
1、加载
加载指的是将类的class文件读入到内存,并将这些静态数据转换成方法区中的运行时数据结构,并在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口,这个过程需要类加载器参与。Java类加载器由JVM提供,是所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。类加载器,可以从不同来源加载类的二进制数据,比如:本地Class文件、Jar包Class文件、网络Class文件等等等。类加载的最终产物就是位于堆中的Class对象(注意不是目标类对象),该对象封装了类在方法区中的数据结构,并且向用户提供了访问方法区数据结构的接口,即Java反射的接口。
2、链接过程
当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中(意思就是将java类的二进制代码合并到JVM的运行状态之中)。类连接又可分为如下3个阶段。
- 验证:确保加载的类信息符合JVM规范,没有安全方面的问题。主要验证是否符合Class文件格式规范,并且是否能被当前的虚拟机加载处理。
- 准备:正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。
- 解析:虚拟机常量池的符号引用替换为字节引用过程。
3、初始化
初始化阶段是执行类构造器 () 方法的过程。类构造器 ()方法是由编译器自动收藏类中的 所有类变量的赋值动作和静态语句块(static块)中的语句合并产生,代码从上往下执行。当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化虚拟机会保证一个类的 () 方法在多线程环境中被正确加锁和同步。
- 什么是类加载器,类加载器有哪些?
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。主要有一下四种类加载器:
- 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。
- 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
- 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通ClassLoader.getSystemClassLoader()来获取它。
- 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。
- 什么是双亲委派模型?
如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。
总结:
JVM作为Java平台的核心组件,对于Java程序的性能和稳定性具有重要影响。了解JVM的基本结构、内存模型、关键技术以及性能优化等方面的知识,有助于我们更好地编写高效、稳定的Java程序。