jvm基础

发布于:2024-10-13 ⋅ 阅读:(50) ⋅ 点赞:(0)

概述

Java虚拟机(JVM)是一种提供运行环境来驱动Java应用程序的引擎。它将Java字节码转换为机器语言,使Java程序能够在任何具有兼容JVM的设备或操作系统上运行。这一过程被称为Java的“编写一次,到处运行”的能力。

JVM运行时数据区

1.程序计数器寄存器(PC寄存器):包含当前正在执行的Java虚拟机指令的地址。

概述

用于存储当前正在执行的字节码指令的地址,确保字节码能够被正确地解释和执行。

  1. 指令跟踪

    • 程序计数器保存当前线程正在执行的字节码指令的地址。
    • 在JVM执行指令时,程序计数器的值会根据指令的执行情况进行更新,以指向下一条将要执行的指令。
  2. 多线程支持

    • 在JVM中,每个线程都有独立的程序计数器。
    • 这是因为每个线程都需要独立地执行字节码指令,独立的程序计数器保证了线程之间的执行互不干扰。
  3. 方法调用与返回

    • 当方法调用时,程序计数器会保存调用方法的返回地址。
    • 方法返回后,程序计数器会恢复到调用方法的地址,继续执行后续指令。

程序计数器是java虚拟机规范中唯一一个不存在内存溢出的区。

2.虚拟机:存储帧,保存局部变量和部分结果,并在方法调用和返回时发挥作用。

2.1概述

负责管理方法调用和执行。每个线程在JVM中都有自己的JVM栈,它随着线程的创建而创建,随着线程的结束而销毁。JVM栈中的每个元素被称为“栈帧”。

2.2栈的运行
方法调用和执行过程:

程序计数器恢复到调用方法的指令位置,继续执行。

  1. 方法调用:

    • 当一个方法被调用时,JVM创建一个新的栈帧(Stack Frame),并将其推入当前线程的JVM栈顶。
    • 栈帧中包含该方法所需的局部变量表、操作数栈、动态链接、方法返回地址和附加信息。
  2. 局部变量表的初始化:

    • 局部变量表初始化并存储方法的参数和局部变量。
    • 例如,对于方法 void foo(int a, int b),局部变量表中会有两个条目用于存储参数 ab
  3. 操作数栈的使用:

    • 操作数栈用于执行字节码指令,存储临时计算结果和操作数。
    • 例如,字节码指令 iadd 从操作数栈中弹出两个整数,将它们相加,并将结果推回栈中。
  4. 动态链接:

    • 动态链接部分包含指向运行时常量池的方法和字段引用。
    • 在方法调用过程中,字节码指令会通过动态链接解析实际的内存地址。
  5. 方法执行:

    • JVM解释或编译方法字节码并在栈帧中执行。
    • 栈帧中的操作数栈和局部变量表在执行过程中不断变化,以反映当前的计算状态。
  6. 方法返回

    • 方法执行完毕后,栈帧从栈顶弹出。
    • 返回值(如果有)会被推入调用者的操作数栈中。
2.3栈帧(Stack Frame)的组成部分:
  1. 局部变量表(Local Variable Array)

    • 存储方法参数和局部变量。
    • 局部变量表的大小在编译时确定,并且在方法执行期间保持不变。
    • 包含基本数据类型(int、float等)、对象引用和returnAddress类型(指向操作数栈的某个字节码指令地址)。
  2. 操作数栈(Operand Stack)

    • 用于存储中间计算结果和操作数。
    • 通过字节码指令执行入栈(push)和出栈(pop)操作。
    • 大小在编译时确定。
  3. 动态链接(Dynamic Linking)

    • 包含指向运行时常量池的方法引用。
    • 支持方法调用过程中的动态链接,使得方法调用可以在运行时绑定到实际的内存地址。
  4. 方法返回地址(Return Address)

    • 存储调用方法返回后,程序计数器需要继续执行的字节码指令地址。
  5. 附加信息

    • 可能包含一些实现相关的信息,如调试信息。

3.本地方法栈:包含应用程序中使用的所有本地方法信息。

主要特点和作用:
  • 线程私有:每个线程独立拥有自己的本地方法栈。
  • 调用本地方法:存储调用本地方法时所需的信息,包括本地变量、参数和返回地址。
  • 执行环境:为本地方法提供运行时环境,管理内存和上下文切换。
  • 与Java方法栈的交互:通过Java本地接口(JNI)调用本地方法,并在本地方法中操作Java对象和方法。

4.堆:运行时数据区,从中为所有类实例和数组分配内存。

特点
  • 全局共享:堆是JVM中的全局共享区域,所有线程都可以访问堆中的对象。
  • 自动内存管理:通过垃圾回收机制自动管理内存,回收不再被引用的对象。
  • 动态内存分配:支持动态分配和释放内存,根据程序的运行时需求进行调整。
  • 分代收集:通常分为年轻代和老年代,优化垃圾回收性能。年轻代用于存储短生命周期的对象,老年代用于存储长生命周期的对象。
  • 线程不安全:堆本身是线程不安全的,对堆进行操作时需要进行同步控制。
内存泄漏(Memory Leak)

​ 内存泄漏是指程序中存在未被释放的对象引用,导致这些对象无法被垃圾回收器回收。内存泄漏会导致堆内存不断增加,最终可能导致堆内存溢出。

  • 典型例子:长期持有不再使用的对象引用,如静态集合类(例如HashMap)中不断添加元素,且未清理过期元素。
堆内存诊断
  1. jps工具
    • 查看当前系统中有哪些Java进程
  2. jmap工具
    • 查看堆内存占用情况 jmap-heap 进程id
  3. jconsole工具
    • 图形界面的,多功能检测工具,可以连续监测

5.方法区:存储类结构,如元数据、运行时常量池和方法代码。

5.1JDK8和之前的区别
  • JDK 7:使用**永久代(PermGen)**来实现方法区。永久代用于存储类的元数据、常量池、静态变量等。永久代的大小是固定的,并且可能导致OutOfMemoryError: PermGen space错误。

img

  • JDK 8:引入了**元空间(Metaspace)**来替代永久代。元空间将类的元数据存储在本地内存中,而不是堆内存,这使得类的元数据不再受到堆内存大小的限制。JDK 8 中的元空间动态调整其大小,通过-XX:MaxMetaspaceSize设置最大内存大小。

img

5.2常量池
  • 类文件常量池:类文件常量池位于每个类文件的开头部分,是编译器生成的,包含了类或接口中使用的各种常量,包括字面量(字符串、整数、浮点数等)和符号引用(类名、字段名、方法名等)。

  • 运行时常量池:运行时常量池是方法区的一部分,是类文件常量池在运行时的表示。每个类加载时,其类文件常量池会被载入到运行时常量池中。

  • 类文件常量池:类文件常量池位于每个类文件的开头部分,是编译器生成的,包含了类或接口中使用的各种常量,包括字面量(字符串、整数、浮点数等)和符号引用(类名、字段名、方法名等)。

  • 运行时常量池:运行时常量池是方法区的一部分,是类文件常量池在运行时的表示。每个类加载时,其类文件常量池会被载入到运行时常量池中。

运行时常量池不仅可以存储编译期的常量,还可以在运行期间将新的常量放入池中,如通过String.intern()方法将字符串添加到常量池。