作者 青鸟

JVM线程

在Hotspot JVM中,每个线程与操作系统中的线程直接映射。当一个Java线程准备好执行后,此时操作系统的一个本地线程也同时创建。Java线程执行终止后,本地线程也会回收。
操作系统负责将所有线程安排调度到一个可用的CPU上。一旦本地线程初始化成功,它就会调用Java线程的run方法。
JVM中的线程分为守护线程和普通线程,当程序中最后一个非守护线程终止后,JVM也会进行终止
JVM的系统线程主要分为几种,其中GC线程、虚拟机线程、周期任务线程、编译线程、信号调度线程

Java虚拟机(JVM)线程是操作系统线程的一种抽象,用于支持Java程序的并发执行。JVM线程是Java多线程编程的基础,它允许Java应用程序在同一进程内同时执行多个独立的任务,实现并发和多线程处理。

以下是关于JVM线程的一些关键信息:

  1. 线程模型:JVM线程模型通常是基于内核线程(Kernel Thread)的。每个JVM线程都会映射到一个底层操作系统线程,这使得Java线程可以在多核处理器上并发执行。
  2. 线程生命周期:JVM线程具有生命周期,包括新建、就绪、运行、阻塞和终止等状态。线程可以通过创建Thread对象并调用start()方法来启动。
  3. 线程调度:JVM线程调度由JVM自动管理,线程调度器决定了哪个线程在某一时刻运行。线程可以让出CPU执行权,或者被操作系统挂起,等待I/O或其他事件完成。
  4. 线程同步:JVM提供了同步机制,如synchronized关键字和java.util.concurrent包中的工具,用于协调多个线程的访问共享资源,以避免数据竞争和并发错误。
  5. 线程安全性:线程安全性是确保多个线程可以同时访问共享数据而不会导致数据损坏或不一致的性质。线程安全的代码通常需要使用同步来保护共享资源。
  6. 线程池:线程池是一种重要的线程管理机制,它可以帮助有效地管理和重用线程,以减少线程的创建和销毁开销,提高性能。
  7. 线程问题:多线程编程可能引发各种问题,如竞态条件、死锁、饥饿等。程序员需要小心地设计和调试多线程代码,以避免这些问题。
  8. JVM参数:JVM提供了一些参数来配置线程相关的设置,如线程栈大小、线程池参数等。这些参数可以根据应用程序的需求进行调整。

Java多线程编程允许开发者充分利用多核处理器,提高应用程序的性能和响应能力。然而,它也需要谨慎地处理并发问题,以避免潜在的错误和性能问题。

JVM内存模型

共享数据区是所有线程所共享的,其中包括堆区域、方法区、直接内存

线程共享数据区

方法区:也被称为元数据区存储类信息。 主要用于存虚拟机加载的类信息,常量,静态变量
堆内存:存储所有对象的区域,是垃圾回收的主要工作区域
直接内存:非jvm内存的堆外内存,NIO操作时会用到,效率高

方法区:是线程共享的区域,整个运行时数据区中只有一份。垃圾回收主要针对的区域就是堆空间,其次是方法区

方法区

方法区(Method Area)是Java虚拟机(JVM)的一部分,用于存储类的元信息、静态变量、常量池以及编译后的字节码等数据。它在JVM规范中被定义为一种内存区域,通常随着JVM的启动而创建,用于存储在程序运行期间加载的类信息和字节码。

方法区的主要职责包括:

  1. 存储类的元信息:包括类的结构信息、方法和字段的描述符、访问标志等。
  2. 存储静态变量:所有类共享的静态变量被存储在方法区中。
  3. 存储常量池:常量池包含了类中的常量,如字符串字面值、静态常量等。
  4. 存储编译后的字节码:类的字节码被加载到方法区以供执行。

需要注意的是,方法区是Java虚拟机规范中的一个抽象概念,具体的实现可以因JVM的不同而异。在一些JVM实现中,方法区被实现为永久代(Permanent Generation),但在Java 8及以后的版本中,永久代已被元空间(Metaspace)所取代,元空间通常是使用本机内存来存储类的元信息,它的大小可以根据需要动态调整,不再受限于固定的永久代大小。

方法区的内存管理和垃圾回收是JVM的重要组成部分,因为它存储了许多与类加载和类的生命周期管理相关的数据。方法区的满溢或过度使用可以导致一些常见的错误,如OutOfMemoryError

堆内存

堆内存(Heap Memory)是Java虚拟机(JVM)运行时数据区域之一,用于存储对象实例和数组等动态分配的数据。堆内存是在JVM启动时创建的,并在程序执行过程中动态分配和回收内存,它是Java应用程序中最主要的内存区域之一。

堆内存的主要特点和职责包括:

  1. 存储对象实例:所有的Java对象,包括类的实例和数组,都在堆内存中分配内存空间。
  2. 动态分配和回收:堆内存的大小可以在JVM启动时预先设置,也可以根据应用程序的需求动态扩展。垃圾回收器负责自动回收不再被引用的对象,释放其占用的内存。
  3. 对象生命周期管理:堆内存中的对象生命周期由JVM自动管理。当一个对象不再被引用,它就成为可回收对象,垃圾回收器将其回收以释放内存。
  4. 堆内存的分代结构:堆内存通常分为年轻代(Young Generation)、老年代(Old Generation)和永久代(在Java 7以及之前的版本中,后来被元空间(Metaspace)取代)等不同的区域,以优化垃圾回收性能。新创建的对象首先被分配在年轻代,经过多次回收后,存活下来的对象逐渐晋升到老年代。

堆内存的大小和分代结构可以通过JVM参数进行调整,以满足不同应用程序的需求。过小的堆内存可能导致频繁的垃圾回收,而过大的堆内存可能导致长时间的垃圾回收停顿。因此,堆内存的配置需要根据具体应用的内存需求和性能要求来进行调优。

直接内存

直接内存(Direct Memory)是一种在Java中用于管理堆外内存(Off-Heap Memory)的机制。它并不是Java虚拟机的一部分,而是通过Java NIO(New I/O)库提供的一种方式,允许Java应用程序直接操作堆外内存,而无需将数据从堆内存复制到堆外内存或反之。

直接内存的主要特点和使用情况包括:

  1. 堆外内存:直接内存是在堆外分配的内存,不受Java虚拟机的堆内存管理机制所限制。这意味着它可以脱离Java堆的大小限制,适用于需要大量内存的操作,如高性能网络通信和文件I/O。

  2. 使用ByteBuffer:Java NIO库中的ByteBuffer类通常用于操作直接内存。ByteBuffer提供了一种灵活的方式来读取和写入二进制数据,而无需将数据从堆内存复制到直接内存。

  3. 零拷贝(Zero-Copy):直接内存的一个主要优势是它支持零拷贝操作。在网络传输或文件I/O中,数据可以直接从直接内存传输到网络套接字或文件,而无需额外的数据复制操作,从而提高了性能。

  4. 注意管理:直接内存不受Java垃圾回收的管理,因此需要程序员自行管理分配和释放内存。这可以通过ByteBuffer的allocateDirect()方法来分配直接内存,并通过显式调用ByteBuffer的release()方法来释放内存。

  5. 内存泄漏风险:由于直接内存的管理需要程序员自行负责,存在内存泄漏风险。必须谨慎地分配和释放直接内存,以确保不会出现内存泄漏问题。

总之,直接内存是Java中处理堆外内存的一种有效方式,特别适用于需要高性能和大内存操作的场景。然而,它需要程序员更加谨慎地管理内存,以避免潜在的内存泄漏和错误。

线程私有区域

程序计数器

程序计数器(Program Counter),通常简称为 PC 寄存器,是计算机体系结构中的一个关键部分,也是 Java 虚拟机(JVM)的一部分。在 JVM 中,每个线程都有自己独立的程序计数器,它用于存储当前线程正在执行的指令地址或下一条即将执行的指令地址。

程序计数器的主要作用和特点包括:

  1. 指令地址存储:程序计数器存储了当前线程执行的指令地址,它是一个指向方法区中的字节码指令的指针。当线程执行方法时,程序计数器会不断更新,以跟踪执行的进度。
  2. 线程独立性:每个线程都有自己独立的程序计数器,因此不同线程之间不会相互干扰。这有助于实现多线程并发执行。
  3. 指令跳转和方法调用:程序计数器在方法调用和返回时非常重要。它记录了方法调用的返回地址,以便线程能够回到正确的执行点。在循环和条件分支中,程序计数器也用于跟踪程序执行的流程。
  4. 线程切换:在多线程环境下,当一个线程被切换出去,JVM 会保存其程序计数器的值,以便稍后恢复执行。
  5. 线程启动:当一个新线程启动时,它的程序计数器通常初始化为线程的起始点,即线程的入口方法。

需要注意的是,程序计数器是 JVM 中唯一一个不会出现 OutOfMemoryError 的内存区域,因为它只存储线程执行的指令地址,并且不会分配堆内存或栈内存。程序计数器的大小通常取决于具体的 JVM 实现,一般不会太大,因为它只需要存储指令地址。如果程序计数器发生错误,通常会导致线程执行异常或进入无限循环。

虚拟机栈

首先我们应该知道的是Java中的几个栈的定义

  1. 虚拟机栈: 每个线程运行时所需的内存;
  2. 栈帧: 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存;
  3. 活动栈帧: 每个线程只有一个活动栈帧,对应正在执行的方法。活动栈帧就是位于栈最顶部的一个方法,方法的入栈顺序是由调用顺序来决定的,但执行顺序是从最后一个入栈的方法开始的,每执行一个就出栈一个。

Java虚拟机栈是Java虚拟机(JVM)中的一部分,用于管理线程的方法调用和局部变量。每个Java线程都有其自己的虚拟机栈,用于存储方法的调用帧。每当一个方法被调用,都会创建一个新的方法调用帧,其中包含方法的参数和局部变量。虚拟机栈是运行时的单位,栈是解决程序的运行问题,即程序如何执行。

每个线程在创建时都会创建一个虚拟机栈,线程运行时每调用一个方法都会生成一个对应的栈帧压入栈,方法执行结束(正常return或内部抛出异常)则出栈,并将结果返回给前一个栈帧,使前一个栈帧成为当前活动栈帧,直到所有栈帧全部出栈,则线程执行结束,栈也同时销毁。虚拟机栈是线程私有的,生命周期和线程一致。

虚拟机栈的主要职责包括:

  1. 存储方法的局部变量:局部变量是在方法中定义的临时变量,它们在方法的执行期间被使用。
  2. 跟踪方法的调用和返回:虚拟机栈维护方法调用的顺序,以确保正确的方法调用和返回。
  3. 检测栈溢出:如果虚拟机栈的深度超过了其容量限制,就会发生栈溢出异常,通常是StackOverflowError

虚拟机栈的大小是有限的,通常在JVM启动时由参数指定,不同的JVM实现可能有不同的默认值。虚拟机栈的大小会影响到程序的递归深度和方法调用的最大深度。

需要注意的是,虚拟机栈和堆是两个不同的内存区域。虚拟机栈用于存储方法调用信息,而堆用于存储对象实例和数组等动态分配的数据。

栈帧的内部结构

栈帧(Stack Frame)是在程序执行期间由虚拟机(如Java虚拟机)创建和管理的一种数据结构,用于表示方法的调用和执行状态。每个方法调用都会在虚拟机栈中创建一个对应的栈帧。

需要注意的是,栈帧中并不存储方法的代码指令,方法的字节码指令是存储在方法区对应类的类信息的对应方法信息中(具体就是在方法Code属性表的code属性值中),是所有线程共享的,栈帧中存储的是执行构成方法的一组指令所需的信息。

栈帧的内部结构是严格定义的,它是Java虚拟机执行方法时的关键数据结构之一。每个栈帧与一个方法调用相关联,当方法调用完成时,栈帧会被销毁,但其状态和结果可能会影响到调用者的栈帧。栈帧的创建和销毁是虚拟机管理的,程序员通常无需直接操作栈帧的内部结构,而是通过编写Java代码来实现方法的功能。

栈帧的内部结构包括以下重要部分:

  1. 局部变量表(Local Variable Table):局部变量表用于存储方法中的局部变量,包括方法参数和在方法内部定义的局部变量。局部变量表中的变量通过索引来访问,索引从0开始。不同的数据类型需要不同的局部变量表槽位来存储。局部变量表定义为一个数组,基本存储单元称为局部变量槽(Slot),用于存放 方法参数和方法内定义的局部变量方法参数和方法内定义的局部变量 法参数和方法内定义的局部变量,数据类型包括8类基本数据类型、对象引用和returnAddress类型(指向了一条字节码指令的地址),其中64位长度的long和double类型数据占用两个变量槽,其他类型占用一个。如果当前帧是由构造方法或者实例方法创建的,那么其局部变量表索引为0的slot处会存放其对象的this引用,其余参数按顺序继续排列。

局部变量表的大小在编译期就确定下来了,存储在方法Code属性表的max_locals属性中,当进入一个方法时,其栈帧的局部变量表所分配的内存空间是完全确定的,运行过程中也不会改变其大小。

  1. 操作数栈(Operand Stack):操作数栈用于执行方法的操作。它是一个后进先出(LIFO)的数据结构,用于临时存储操作数、中间结果和方法返回值。方法中的各种操作都在操作数栈上执行。

我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈就是指的操作数栈。操作数栈是数组实现的栈结构,用于解释引擎执行方法字节码指令过程中数据的临时存放和取出。

如下方法的字节码指令,push指令(前面的符号表示数据类型)用于将数据压入栈中,store指令用于出栈一个数据并将其存储到局部变量表指定索引位置(命令后面的数字),load指令用于将局部变量表指定索引位置数据加载到栈中,add命令用于从栈中出栈两个数据相加后将结果压入栈中。如果当前方法有返回值,则方法执行结束后将返回值压入到前一个栈帧的操作数栈中。

和局部变量表一样,操作数栈的大小也是在编译期确定的,存储在方法Code属性表的max_stack属性值中,为方法执行过程中栈的最大深度,如上图中方法操作数栈最大深度为2。

注意,局部变量表和操作数栈都是在栈帧创建时就分配了固定的空间,但是方法运行前其中都是没有数据的(this引用和参数除外),而是随着方法指令一步一步执行填充数据的。
此外,可以看到字节码指令的地址编号不是连续的,这是因为不同指令的长度不同,一个字节为一个长度,操作码本身占一个字节(所以Java指令集最多有256个操作码),操作数再占额外的字节,如上带参指令bipush占两个字节,无参指令istore占一个字节。

  1. 动态链接(Dynamic Linking):动态链接部分包括一个指向当前方法的运行时常量池(Runtime Constant Pool)的引用,以及一个指向该栈帧所属类的运行时常量池的引用。这些引用用于支持方法调用时的动态链接。每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。我们知道Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接。

  2. 返回地址(Return Address):返回地址指示了方法调用完成后应该返回到哪个指令继续执行。在栈帧中,通常有一个指向返回地址的引用,以便方法可以正确地返回到调用它的地方。存放调用该方法的指令的下一条指令的地址。本质上,方法的退出就是当前栈帧出栈的过程,此时,需要恢复上层方法的局部变量表和操作数栈、将返回值压入调用者栈帧的操作数栈中、设置PC寄存器的值等,让调用者方法继续执行下去。

方法中catch的异常,会存储在方法的异常处理表中,如果方法不是正常退出,即在方法执行过程中遇到了异常,并且这个异常没有在方法中进行处理,即异常处理表中没有匹配的项,则方法会异常退出,将异常抛给上层方法进行处理。

  1. 一些辅助信息:栈帧可能包含一些其他信息,如异常处理表(Exception Handling Table)、方法的访问标志(Access Flags)等,这些信息有助于支持方法的异常处理和其他操作。《Java虚拟机规范》允许虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试、 性能收集相关的信息,这部分信息完全取决于具体的虚拟机实现

本地方法栈

本地方法栈(Native Method Stack)是 Java 虚拟机(JVM)的一部分,与虚拟机栈类似,但主要用于执行本地方法(Native Methods)或本地代码,即用其他编程语言(如C、C++)编写的代码。本地方法栈的主要作用是管理和执行本地方法的调用。

以下是本地方法栈的关键特点和作用:

  1. 本地方法调用:本地方法栈用于管理本地方法的调用。本地方法是用其他编程语言编写的代码,通常是与底层操作系统或硬件相关的代码,无法由 Java 代码直接执行。
  2. 与虚拟机栈的关系:虚拟机栈(Java 虚拟机栈)用于管理 Java 方法的调用,而本地方法栈则用于管理本地方法的调用。每个线程都有自己的虚拟机栈和本地方法栈。
  3. 本地方法接口:Java 提供了本地方法接口(JNI,Java Native Interface),允许 Java 代码与本地方法进行交互。JNI 允许 Java 代码调用本地方法,并通过本地方法栈执行本地代码。
  4. 内存管理:与虚拟机栈类似,本地方法栈的大小也是有限的,通常在 JVM 启动时由参数指定。本地方法栈的大小限制了本地方法调用的深度,过小的栈容量可能导致栈溢出错误。

需要注意的是,本地方法栈通常在 Java 虚拟机的规范中并没有明确定义,因此具体的实现可以因 JVM 的不同而异。不同的 JVM 实现可能采用不同的方式来支持本地方法调用,但它们都有一个共同的目标,即提供一种机制来执行与 Java 代码交互的本地代码。在使用本地方法时,需要特别小心,因为本地方法可能会绕过 Java 的类型检查和内存管理,引发安全和稳定性问题。

参考文章:

JVM笔记(2)—— 运行时数据区概述及线程

Java虚拟机之程序计数器、虚拟机栈和本地方法栈。