聊聊程序员需掌握的JVM

大家好,今天我们一起聊聊Java开发所需要掌握的JVM。

什么是JVM

JVM是Java Virtual Machine(Java虚拟机)的缩写,它是一种能够让Java程序在任何计算机硬件上运行的系统。JVM是一个虚构出来的计算机,它是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

JVM的主要功能是解释和执行Java字节码,即Java源代码编译后的代码。通过JVM,Java程序可以在不同的平台上运行,实现了“一次编写,到处运行”的目标。JVM是Java语言跨平台的关键部分,也是Java“编写一次,到处运行”承诺的基础。

目前流行的JVM主要包括Oracle的HotSpot虚拟机、BEA System的JRockit虚拟机、IBM公司的J9虚拟机等。其中,HotSpot是Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机。JRockit是BEA Systems公司开发的JVM,具有高性能和低内存占用等优点,后被Oracle公司收购并合并于Hotspot。J9是IBM公司开发的JVM,主要用在IBM的产品中。

此外,还有一些开源的JVM项目,如Azul Zing JVM、Excelsior JET JVM等。其中,Azul Zing JVM专注于高性能、低延迟和可预测性等方面的优化,而Excelsior JET JVM可以将Java程序编译成本地机器代码,提高程序的执行效率。

JVM如何运行

JVM(Java Virtual Machine,Java虚拟机)运行Java程序的过程可以分为以下几个步骤:

编译:Java源代码首先被编译成字节码(即.class文件)。Java编译器将Java源代码转换成字节码,这是一种与平台无关的中间代码。

类加载:在运行时,JVM通过类加载器(ClassLoader)将字节码加载到内存中。类加载器根据类的全限定名(包名+类名)来获取对应的字节码文件,并创建对应的Class对象。

字节码解释执行:JVM通过解释器(Interpreter)来执行字节码。解释器逐条读取字节码并执行,将字节码转换为特定平台的机器码执行。

即时编译(JIT编译):为了提高程序的执行效率,JVM会使用即时编译器(JIT编译器)将热点代码(被频繁执行的代码)编译成本地机器码。编译后的代码会存储在方法区的永久代(PermGen)中。

垃圾回收:JVM的垃圾回收器(Garbage Collector)会自动回收不再使用的对象所占用的内存,以释放内存空间。垃圾回收器会定期执行,以保持内存的合理使用。

异常处理:在运行过程中,如果遇到异常或错误,JVM会根据异常类型和异常信息进行处理,例如抛出异常、异常链回溯等。

这些步骤共同协作,使得Java程序能够在不同的平台上运行,并且能够实现跨平台兼容性。JVM通过抽象掉底层硬件和操作系统的细节,为Java程序提供了一个统一的运行环境,使得Java程序能够达到“一次编写,到处运行”的目标。

聊聊程序员需掌握的JVM

JVM的结构

聊聊程序员需掌握的JVM

JVM包含三大子系统:类加载子系统、运行时数据区和执行引擎。

类加载子系统:负责加载Java动态类,当一个类需要被引用时,就会被加载和连接并初始化该类。它包括启动类加载器、扩展类加载器和应用程序类加载器,使用双亲委派机制进行加载。

运行时数据区:JVM内存结构模型,用于存储运行时的各种数据。它包括方法区、堆、栈、程序计数器、本地方法栈等部分。其中,堆被线程共享,存储对象实例;方法区被线程共享,存储已被虚拟机加载的类信息、常量、静态变量等数据;栈被线程独享,存储方法执行的每个阶段的执行状态。

执行引擎:负责执行class文件中包含的字节码。它包括解释器(Interpreter)即时编译器(JIT),解释器逐条读取字节码并执行,而JIT编译器会将热点代码编译成与本地平台相关的机器码,提高执行效率。

这三个子系统相互协作,使得Java程序能够在不同的平台上运行,并实现跨平台兼容性。

聊聊程序员需掌握的JVM

下面我们通过一幅图,看下class文件通过JVM的运行过程:

聊聊程序员需掌握的JVM

JVM的方法区

方法区(Method Area),也被称为元空间,是Java虚拟机(JVM)中的一部分,主要用于存储已被JVM加载的运行时常量池、类信息、字段信息、方法信息、类加载器的引用、对应class实例的引用、即时编译器编译后的代码等。它是JVM中除堆外的重要内存区域之一,与堆一样,是各个线程共享的内存区域。

方法区的物理内存空间可以有选择地和堆进行分离,它与堆一样,可以不连续,并且大小也可以选择固定或者可扩展。在JVM启动时,方法区就被创建。

聊聊程序员需掌握的JVM

JVM的堆

JVM中的堆是用于存储对象实例的内存区域,它是JVM中最大的一块内存。堆是所有线程共享的,在虚拟机启动时创建。

堆是垃圾回收器的主要管理区域,当我们在程序中创建一个对象时,该对象就会在堆上分配内存。堆是由所有线程共享的,因此,堆的大小和内存分配策略会影响到整个应用程序的性能。

堆内存的分配是通过垃圾回收器自动进行的,垃圾回收器会自动回收不再使用的对象所占用的内存,以释放堆空间。这种自动垃圾回收机制可以避免内存泄漏问题,并且能够自动调整堆的大小,以满足应用程序的需求。

堆内存的分配方式有两种:对象创建时分配和内存池分配。对象创建时分配是指在每次创建对象时,直接在堆上为该对象分配内存。这种方式虽然简单,但可能会导致频繁的内存分配和垃圾回收操作,影响程序的性能。内存池分配则是指预先分配一块内存区域,并在该区域内为对象分配内存。这种方式可以减少内存分配和垃圾回收的次数,提高程序的性能。

Java中基本类型的包装类:Byte、Short、Integer、Long、Float、Double、Boolean、Character类型的数据是存储在堆中的。

堆分为年轻代和老年代。而年轻代被分为1个Eden区和2个Survivor区。默认的内存分配,年轻代和老年代的内存大小比例为1 : 2,年轻代中的1个Eden区和2个Survivor区的内存大小比例为:8 : 1 : 1。

聊聊程序员需掌握的JVM

JVM的栈

JVM的栈是线程私有的,每个线程都会创建一个栈,因此也被称为线程栈。栈由一系列帧组成,每个帧对应一个方法的执行状态。因此,栈也被叫做“帧栈”。

线程栈用于存储Java方法的执行状态,包括局部变量、操作数栈、动态链接和方法出口信息等。每个方法从开始执行到执行完成都会对应一个栈帧在栈内存中。当一个方法被调用时,JVM会为其创建一个新的栈帧,并将其压入Java栈中。这个新的栈帧包含了该方法的局部变量、操作数栈、动态链接和方法出口信息等。当方法执行完成时,其对应的栈帧会被弹出Java栈。

聊聊程序员需掌握的JVM

在Java中,所有的基本数据类型(byte、short、int、long、float、double、boolean、char)和引用变量(对象引用)都是在栈中的。一般情况下,线程退出或者方法退出时,栈中的数据会被自动清除。

聊聊程序员需掌握的JVM

这里说的是这些引用所指向的具体对象一般都会在堆中开辟单独的地址空间进行存储,也有可能存储在直接内存中。

在JVM中,如果开启了逃逸分析和标量替换,则可能不会再在堆上创建对象,可能会将对象直接分配到栈上,也可能不再创建对象,而是进一步分解对象中的成员变量,将其直接在栈上分配空间并赋值。

JVM的本地方法栈

本地方法栈(Native Method Stack)是JVM中的另一个重要组件,它与Java栈类似,但主要用于支持native方法的执行。

在Java中,有些方法可以使用native关键字声明,表示这些方法是用非Java语言编写的,例如C或C++。这些native方法在执行时需要使用本地方法栈来存储和跟踪方法的执行状态。

本地方法栈的工作原理与Java栈类似,也是由一系列栈帧组成,每个栈帧对应一个native方法的执行状态。当一个native方法被调用时,JVM会为其创建一个新的栈帧,并将其压入本地方法栈中。这个新的栈帧包含了该方法的参数、返回值和异常处理等信息。当方法执行完成时,其对应的栈帧会被弹出本地方法栈。

本地方法栈与Java栈的主要区别在于,Java栈主要用于存储Java方法的执行状态,而本地方法栈主要用于存储native方法的执行状态。此外,本地方法栈的实现可能与Java栈有所不同,因为native方法的执行涉及到与本地代码的交互和调用。

JVM的程序计数器

程序计数器(Program Counter)是JVM中的一个小型寄存器,用于存储下一条要执行的字节码指令的地址。程序计数器是线程私有的,每个线程都会创建一个程序计数器,并且每个线程的程序计数器都是独立的。

在程序执行过程中,JVM通过程序计数器来依次执行字节码指令。当一个线程正在执行一个方法时,程序计数器会指向该方法的字节码指令地址。当该方法执行完成时,程序计数器会被重置为该方法的起始地址,以便下一次调用时能够正确地执行字节码指令。

程序计数器的主要作用是提供了一种方式来依次执行字节码指令,并且能够根据需要更新指令地址。它保证了JVM能够按照正确的顺序执行字节码指令,并且能够处理分支、循环、跳转等指令。

JVM调优

JVM调优,主要是对堆(新生代)、方法区和栈进行性能调优。

聊聊程序员需掌握的JVM

-Xms:用于设置 JVM 初始堆大小,在JVM启动时分配。

-Xmx:用于设置 JVM 最大堆大小,在JVM启动时分配。

-Xmn:用于设置新生代(Young Generation)的大小,在JVM启动时分配。

如果设置的初始堆大小过大或过小,可能会对应用程序的性能产生负面影响。如果初始堆大小设置得过大,可能会导致内存资源的浪费;如果初始堆大小设置得过小,可能会导致频繁的垃圾回收和内存不足的问题。

如果设置的 -Xmx 值过大或过小,可能会对应用程序的性能产生负面影响。如果最大堆大小设置得过大,可能会导致内存资源的浪费;如果最大堆大小设置得过小,可能会导致 JVM 在运行时无法分配足够的内存,从而导致 OutOfMemoryError 错误。
JVM 的堆内存被划分为新生代和老年代两个部分。新生代是堆内存中存放新创建的对象和短期存活对象的区域,老年代是堆内存中存放长期存活对象和不再使用的对象的区域。通过设置 -Xmn 参数,可以控制新生代的大小。

通过调整新生代的大小,可以对 JVM 的垃圾回收性能和应用程序的性能产生影响。如果新生代设置得过大,可能会导致老年代空间不足,频繁触发 Full GC;如果新生代设置得过小,可能会导致Minor GC频繁发生,影响应用程序性能。根据应用程序的实际需求进行合理的调整可以获得更好的性能和可靠性。

-Xss:用于设置线程的堆栈大小,JVM启动时分配。

每个 Java 线程在 JVM 中都有一个独立的堆栈,用于存储线程执行过程中的方法调用、局部变量等信息。堆栈大小决定了线程可以使用的内存量,进而影响线程的执行效率和稳定性。如果堆栈大小设置得较小,可能会导致 StackOverflowError 错误,因为线程没有足够的空间来存储方法调用和局部变量等信息。如果堆栈大小设置得较大,可能会增加内存消耗,导致内存不足的问题。

方法区(元空间)

-XX:MetaspaceSize : 设置元空间的初始容量。这个参数用于指定元空间在启动时的初始大小,默认设置为21M。元空间(Metaspace)达到 -XX:MetaspaceSize 设置的值时,将会触发 Metaspace 的垃圾回收(GC),清理不再使用的类元数据,以释放空间。这个过程类似于堆内存的垃圾回收,但是作用于元空间。Metaspace 是用于存储类元数据的空间,随着应用程序的运行,不断有类被加载和卸载,因此 Metaspace 的大小会动态变化。如果 Metaspace 频繁地触发垃圾回收,或者垃圾回收后仍然很快达到 -XX:MetaspaceSize 的值,这可能表明应用程序存在类加载问题或者内存泄漏。在这种情况下,需要进一步分析应用程序的代码和运行时行为,以找到问题的根源并进行修复。
-XX:MaxMetaspaceSize:设置元空间的最大容量。这个参数用于指定元空间的最大可用大小,默认值为-1,不受堆内存大小限制,此时,只会受限于本地内存大小。

为了尽量不让JVM动态调整方法区(元空间)的大小造成频繁的GC,一般将-XX:MetaspaceSize和-XX:MaxMetaspaceSize设置成一样的值。例如,物理内存8G,可以将这两个值设置为256M

总结

JVM的结构和数据存储是理解其性能调优的基础。通过对JVM的堆、栈和元空间进行合理的调整,以及对垃圾回收器和JIT编译器的优化,可以提高Java应用程序的性能和稳定性。在进行JVM调优时,需要充分了解应用程序的特点和需求,并进行充分的测试和验证。

存放在方法区和堆内的数据,在线程之间是共享的;存放在栈和本地方法栈中的数据,是线程独占的。所以在进行多线程编程时,应针对存放在方法区和堆内的数据,做详细分析,避免出现线程不安全的情况。

点击这里给我留言吧

原文始发于微信公众号(扬哥手记):聊聊程序员需掌握的JVM

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/239566.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!