JVM 内存结构

JVM 是典型的基于栈的架构,不同于 Android 操作系统设计的 Dalvik 虚拟机采用的基于寄存器的架构,其在单运算的操作次数上会多于基于寄存器的架构。

Java 虚拟机会将内存分为几个不同的管理区,这些区域各自有各自的用途,根据不同的特点,承担不同的任务以及在垃圾回收时运用不同的算法。总体分为下面几个部分:

  • 堆(Heap):所有的对象实例和数组,根据虚拟机规范的规定,Java 堆可以是固定的大小也可以是按照需求动态扩展的,而且不需要保证是连续的。
  • 元空间(Metaspace):类的结构信息,如类的字段、方法、接口、构造函数,还有运行时常量池等。在 Java 8 之前,PermGen 是存放在堆中,在 Java 8 之后,PermGen 被 Metaspace 替代,并且 Metaspace 直接存放在原生内存,而不再是堆中。
  • 程序计数器(Program Counter Register):如果线程执行的是一个 Java 方法,那么寄存器里面记录的就是正在执行的虚拟机字节码指令的地址,如果线程执行的是一个 native 方法,那么寄存器记录的值为 undefined。程序计数寄存器也是虚拟机规范里面唯一一个没有规定任何 OutOfMemoryError 情况的区域。
  • 虚拟机栈(JVM Stacks):局部变量表、操作数栈、方法出口等信息,局部变量表存放了编译时期可知的各种基本数据类型、对象引用和指向了一条字节码指令的地址。
  • 本地方法栈(Native Method Stacks):局部变量表、操作数栈、方法出口等信息。

除此之外,还有 JVM 内存管理之外的一个内存区:直接内存。在 JDK1.4 中新加入类 NIO 类,引入了一种基于通道与缓冲区的 IO 方式,它可以使用 Native 函数库直接分配堆外内存,即我们所说的直接内存,这样在某些场景中会提高程序的性能

Java 内存模型规定所有的变量都是存在主存当中,每个线程都有自己的工作内存。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。每当创建一个新的线程时,JVM 会为该线程创建一个虚拟机栈,同时会为这个线程分配一个 PC 寄存器,并且这个 PC 寄存器会指向这个线程的第一行可执行代码。每当调用一个新方法时会在这个栈上创建一个新的栈帧数据结构,这个栈帧会保留这个方法的一些元信息,如在这个方法中定义的局部变量、一些用来支持常量池的解析、正常方法返回以及异常处理机制等等。

如果线程清求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果虚拟机栈可以动态扩展 (当前大部分的 Java 虚拟机都可动态扩展,只不过 Java 虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会拋出 OutOfMemoryError 异常。

Metaspace

方法区

方法区是所有线程共享的内存区域,用于存储已经被 JVM 加载的类信息、常量、静态变量等数据,一般来说,方法区属于持久代(关于持久代,会在 GC 部分详细介绍,除了持久代,还有新生代和旧生代),也难怪 Java 规范将方法区描述为堆的一个逻辑部分,但是它不是堆。方法区的垃圾回收比较棘手,就算是 Sun 的 HotSpot VM 在这方面也没有做得多么完美。此处引入方法区中一个重要的概念:运行时常量池。主要用于存放在编译过程中产生的字面量(字面量简单理解就是常量)和引用。一般情况,常量的内存分配在编译期间就能确定,但不一定全是,有一些可能就是运行时也可将常量放入常量池中,如 String 类中有个 Native 方法 intern()。

方法区(method area)只是 JVM 规范中定义的一个概念,用于存储类信息、常量池、静态变量、JIT 编译后的代码等数据,具体放在哪里,不同的实现可以放在不同的地方。而永久代是 Hotspot 虚拟机特有的概念,是方法区的一种实现,别的 JVM 都没有这个东西。

在 Java 6 中,方法区中包含的数据,除了 JIT 编译生成的代码存放在 native memory 的 CodeCache 区域,其他都存放在永久代; 在 Java 7 中,Symbol 的存储从 PermGen 移动到了 native memory,并且把静态变量从 instanceKlass 末尾(位于 PermGen 内)移动到了 java.lang.Class 对象的末尾(位于普通 Java heap 内); 在 Java 8 中,永久代被彻底移除,取而代之的是另一块与堆不相连的本地内存——元空间(Metaspace),‑XX:MaxPermSize 参数失去了意义,取而代之的是-XX:MaxMetaspaceSize。

方法区是一个线程共享的区域,它用于存储已被虚拟机加载的类信息、常量、静态变量。方法区是堆的逻辑组成部分,Hotspot 用永久代实现了方法区。方法 区还包含运行时常量池,用于存放编译时生成的各种字面量和符号引用,但是不要求常量一定是在编译时期产生的,运行期间也可以将新的常量放入池中,比如 String 的 intern()方法便是利用了这一特性。 方法区是所有线程共享的内存区域,用于存储已经被 JVM 加载的类信息、常量、静态变量等数据,一般来说,方法区属于持久代(关于持久代,会在 GC 部分详细介绍,除了持久代,还有新生代和旧生代),也难怪 Java 规范将方法区描述为堆的一个逻辑部分,但是它不是堆。方法区的垃圾回收比较棘手,就算是 Sun 的 HotSpot VM 在这方面也没有做得多么完美。此处引入方法区中一个重要的概念:运行时常量池。主要用于存放在编译过程中产生的字面量(字面量简单理解就是常量)和引用。一般情况,常量的内存分配在编译期间就能确定,但不一定全是,有一些可能就是运行时也可将常量放入常量池中,如 String 类中有个 Native 方法 intern()。

此处补充一个在 JVM 内存管理之外的一个内存区:直接内存。在 JDK1.4 中新加入类 NIO 类,引入了一种基于通道与缓冲区的 IO 方式,它可以使用 Native 函数库直接分配堆外内存,即我们所说的直接内存,这样在某些场景中会提高程序的性能。 所有线程共享同一个方法区,因此访问方法区数据的和动态链接的进程必须线程安全。如果两个线程试图访问一个还未加载的类的字段或方法,必须只加载一次,而且两个线程必须等它加载完毕才能继续执行。

Classloader 引用
运行时常量池
    数值型常量
    字段引用
    方法引用
    属性
字段数据
    针对每个字段的信息
        字段名
        类型
        修饰符
        属性(Attribute)
方法数据
    每个方法
        方法名
        返回值类型
        参数类型(按顺序)
        修饰符
        属性
方法代码
    每个方法
        字节码
        操作数栈大小
        局部变量大小
        局部变量表
        异常表
        每个异常处理器
        开始点
        结束点
        异常处理代码的程序计数器(PC)偏移量
        被捕获的异常类对应的常量池下标Copy to clipboardErrorCopied

运行时常量池

JVM 维护了一个按类型区分的常量池,一个类似于符号表的运行时数据结构。尽管它包含更多数据。Java 字节码需要数据。这个数据经常因为太大不能直接存储在字节码中,取而代之的是存储在常量池中,字节码包含这个常量池的引用。运行时常量池被用来上面介绍过的动态链接。

常量池中可以存储多种类型的数据:

  • 数字型
  • 字符串型
  • 类引用型
  • 域引用型
  • 方法引用

示例代码如下:

Object foo = new Object();Copy to clipboardErrorCopied

写成字节码将是下面这样:

0:     new #2             // Class java/lang/Object
1:    dup
2:    invokespecial #3    // Method java/ lang/Object "<init>"( ) VCopy to clipboardErrorCopied

new 操作码的后面紧跟着操作数 #2。这个操作数是常量池的一个索引,表示它指向常量池的第二个实体。第二个实体是一个类的引用,这个实体反过来引用了另一个在常量池中包含 UTF8 编码的字符串类名的实体(// Class java/lang/Object)。然后,这个符号引用被用来寻找 java.lang.Object 类。new 操作码创建一个类实例并初始化变量。新类实例的引用则被添加到操作数栈。dup 操作码创建一个操作数栈顶元素引用的额外拷贝。最后用 invokespecial 来调用第 2 行的实例初始化方法。操作码也包含一个指向常量池的引用。初始化方法把操作数栈出栈的顶部引用当做此方法的一个参数。最后这个新对象只有一个引用,这个对 象已经完成了创建及初始化。 如果你编译下面的类:

package org.jvminternals;
public class SimpleClass {
    public void sayHello() {
        System.out.println("Hello");
    }
}Copy to clipboardErrorCopied

生成的类文件常量池将是这个样子:

Constant pool:
   #1 = Methodref          #6.#17         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #18.#19        //  java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #20            //  "Hello"
   #4 = Methodref          #21.#22        //  java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #23            //  org/jvminternals/SimpleClass
   #6 = Class              #24            //  java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lorg/jvminternals/SimpleClass;
  #14 = Utf8               sayHello
  #15 = Utf8               SourceFile
  #16 = Utf8               SimpleClass.java
  #17 = NameAndType        #7:#8          //  "<init>":()V
  #18 = Class              #25            //  java/lang/System
  #19 = NameAndType        #26:#27        //  out:Ljava/io/PrintStream;
  #20 = Utf8               Hello
  #21 = Class              #28            //  java/io/PrintStream
  #22 = NameAndType        #29:#30        //  println:(Ljava/lang/String;)V
  #23 = Utf8               org/jvminternals/SimpleClass
  #24 = Utf8               java/lang/Object
  #25 = Utf8               java/lang/System
  #26 = Utf8               out
  #27 = Utf8               Ljava/io/PrintStream;
  #28 = Utf8               java/io/PrintStream
  #29 = Utf8               println
  #30 = Utf8               (Ljava/lang/String;)VCopy to clipboardErrorCopied

这个常量池包含了下面的类型:

方法信息

异常表

异常表像这样存储每个异常处理信息:

  • 起始点(Start point)
  • 结束点(End point)
  • 异常处理代码的 PC 偏移量
  • 被捕获异常的常量池索引

如果一个方法有定义 try-catch 或者 try-finally 异常处理器,那么就会创建一个异常表。它为每个异常处理器和 finally 代码块存储必要的信息,包括处理器覆盖的代码块区域和处理异常的类型。

当方法抛出异常时,JVM 会寻找匹配的异常处理器。如果没有找到,那么方法会立即结束并弹出当前栈帧,这个异常会被重新抛到调用这个方法的方法中(在新的栈帧中)。如果所有的栈帧都被弹出还没有找到匹配的异常处理器,那么这个线程就会终止。如果这个异常在最后一个非守护进程抛出(比如这个线程是主线程),那么也有会导致 JVM 进程终止。

Finally 异常处理器匹配所有的异常类型,且不管什么异常抛出 finally 代码块都会执行。在这种情况下,当没有异常抛出时,finally 代码块还是会在方法最后执行。这种靠在代码 return 之前跳转到 finally 代码块来实现。

符号表

除了按类型来分的运行时常量池,Hotspot JVM 在永久代还包含一个符号表。这个符号表是一个哈希表,保存了符号指针到符号的映射关系(也就是 Hashtable),它拥有指向所有符号(包括在每个类运行时常量池中的符号)的指针。 引用计数被用来控制一个符号从符号表从移除的过程。比如当一个类被卸载时,它拥有的在常量池中所有符号的引用计数将减少。当符号表中的符号引用计数 为 0 时,符号表会认为这个符号不再被引用,将从符号表中卸载。符号表和后面介绍的字符串表都被保存在一个规范化的结构中,以便提高效率并保证每个实例只出现一 次。

字符串表

Java 语言规范要求相同的(即包含相同序列的 Unicode 指针序列)字符串字面量必须指向相同的 String 实例。除此之外,在一个字符串实例上调用 String.intern() 方法的返回引用必须与字符串是字面量时的一样。因此,下面的代码返回 true:

("j" + "v" + "m").intern() == "jvm"Copy to clipboardErrorCopied

Hotspot JVM 中 interned 字符串保存在字符串表中。字符串表是一个哈希表,保存着对象指针到符号的映射关系(也就是Hashtable),它被保存到永久代中。符号表和字符串表的实体都以规范的格式保存,保证每个实体都只出现一次。 当类加载时,字符串字面量被编译器自动 intern 并加入到符号表。除此之外,String 类的实例可以调用 String.intern() 显式地 intern。当调用 String.intern() 方法时,如果符号表已经包含了这个字符串,那么就会返回符号表里的这个引用,如果不是,那么这个字符串就被加入到字符串表中同时返回这个引用。

在 Java 虚拟机中,堆是可供各个线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域。这块区域随着虚拟机的启动而创建,它的唯一使命就是存放对象实例,这块区域也是 GC 主要关注的地方。

堆内存是内存中最重要的一块,也是最有必要进行深究的一部分。因为 Java 性能的优化,主要就是针对这部分内存的。所有的对象实例及数组都是在堆上面分配的(随着 JIT 技术的逐渐成熟,这句话视乎有些绝对,不过至少目前还基本是这样的),可通过-Xmx 和-Xms 来控制堆的大小。JIT 技术的发展产生了新的技术,如栈上分配和标量替换,也许在不久的几年里,即时编译会诞生及成熟,那个时候,“所有的对象实例及数组都是在堆上面分配的”这句话就应该稍微改改了。堆内存是垃圾回收的主要区域,所以在下文垃圾回收板块会重点介绍,此处只做概念方面的解释。在 32 位系统上最大为 2G,64 位系统上无限制。可通过-Xms 和-Xmx 控制,-Xms 为 JVM 启动时申请的最小 Heap 内存,-Xmx 为 JVM 可申请的最大 Heap 内存。

Heap Usage:
PS Young Generation
Eden Space:
  capacity = 17301504 (16.5MB)
  used    = 2483088 (2.3680572509765625MB)
  free    = 14818416 (14.131942749023438MB)
  14.351862127130682% used
From Space:
  capacity = 2621440 (2.5MB)
  used    = 2615312 (2.4941558837890625MB)
  free    = 6128 (0.0058441162109375MB)
  99.7662353515625% used
To Space:
  capacity = 6291456 (6.0MB)
  used    = 0 (0.0MB)
  free    = 6291456 (6.0MB)
  0.0% used
PS Old Generation
  capacity = 44564480 (42.5MB)
  used    = 13316368 (12.699478149414062MB)
  free    = 31248112 (29.800521850585938MB)
  29.88112505744485% used
PS Perm Generation
  capacity = 22020096 (21.0MB)
  used    = 14907008 (14.2164306640625MB)
  free    = 7113088 (6.7835693359375MB)
  67.6972888764881% usedCopy to clipboardErrorCopied

新生代

JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即 90% )的新生代空间。 假如某个 Java 进程的 JVM 参数配置如下:-Xms1G -Xmx2G -Xmn500M -XX:MaxPermSize=64M -XX:+UseConcMarkSweepGC -XX:SurvivorRatio=3,其中-Xmn500M 表示年轻代大小是 500M,-XX:SurvivorRatio=3 表示 Eden 区与两个 Survivor 区的大小比值为 3:1:1,故 Eden 区的大小为 300M。

一个 JVM 栈由多个帧组成,当一个方法被调用的时候,会 push 一个帧到栈顶,当方法执行完毕时(正常返回或者抛出异常),一个帧会从栈顶弹出。每个帧由两部分组成:

  • 一个数组,用于存放本地变量,数组长度由编译器计算确定,一个局部变量可用于存储任意类型的值,long 和 double 值除外,它们需要两个局部变量;
  • 一个操作栈,用于存放中间值,可作为指令的操作数,或者作为方法调用的参数。

值得一提的是,局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的。在方法运行期间不会改变局部变量表的大小。

JVM 虚拟机栈

JVM 虚拟机栈就是我们常说的堆栈的栈(我们常常把内存粗略分为堆和栈),和程序计数器一样,也是线程私有的,生命周期和线程一样,每个方法被执行的时候会产生一个栈帧,用于存储局部变量表、动态链接、操作数、方法出口等信息。方法的执行过程就是栈帧在 JVM 中出栈和入栈的过程。局部变量表中存放的是各种基本数据类型,如 boolean、byte、char、等 8 种,及引用类型(存放的是指向各个对象的内存地址),因此,它有一个特点:内存空间可以在编译期间就确定,运行期不在改变。这个内存区域会有两种可能的 Java 异常:StackOverFlowError 和 OutOfMemoryError。

每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法的调用直到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。 与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型:每个方法被执 行的时候都会同时创建一个栈帧(Stack Frame0)用于存储局部变量表、操作栈、动态 链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在 虚拟机栈中从入栈到出栈的过程。

经常有人把 Java 内存区分为堆内存(Heap)和栈内存(Stack),这种分法比较粗 糙,Java 内存区域的划分实际上远比这复杂。这种划分方式的流行只能说明大多数程序 员最关注的、与对象内存分配关系最密切的内存区域是这两块。其中所指的“堆”在后 面会专门讲述,而所指的“栈”就是现在讲的虚拟机栈,或者说是虚拟机栈中的局部变 量表部分。

局部变 M 表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不等同于对象本身,根据不同的虚拟 机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或 者其他与此对象相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址)。

其中 64 位长度的 long 和 double 类型的数据会占用 2 个局部变姑空间(Slot),其余 的数据类型只占用 1 个。局部变量表所需的内存空间在编译期间完成分配,当进入一个 方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间 不会改变局部变量表的大小。

在 Java 虚拟机规范中,对这个区域规定了两种异常状况:如果线程清求的栈深度大 于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果虚拟机栈可以动态扩展 (当前大部分的 Java 虚拟机都可动态扩展,只不过 Java 虚拟机规范中也允许固定长度的 虚拟机栈),当扩展时无法申请到足够的内存时会拋出 OutOfMemoryError 异常。 虚拟机栈也是线程私有的内存区域。每个方法在执行的时候都会创建一个栈帧用于存储局部变量表、操作数栈、方法出口等信息,每一个方法从调用到执行完成就是一个栈帧入栈和出栈的过程 。 局部变量表存放了编译时期可知的各种基本数据类型、对象引用和指向了一条字节码指令的地址。 JVM 虚拟机栈就是我们常说的堆栈的栈(我们常常把内存粗略分为堆和栈),和程序计数器一样,也是线程私有的,生命周期和线程一样,每个方法被执行的时候会产生一个栈帧 ,用于存储局部变量表、动态链接、操作数、方法出口等信息。方法的执行过程就是栈帧在 JVM 中出栈和入栈的过程。局部变量表中存放的是各种基本数据类型,如 boolean、byte、char、等 8 种,及引用类型(存放的是指向各个对象的内存地址),因此,它有一个特点:内存空间可以在编译期间就确定,运行期不在改变。这个内存区域会有两种可能的 Java 异常:StackOverFlowError 和 OutOfMemoryError。

栈帧

每次方法调用都会新建一个新的栈帧并把它压栈到栈顶。当方法正常返回或者调用过程中抛出未捕获的异常时,栈帧将出栈。更多关于异常处理的细节,可以参考下面的异常信息表章节。每个栈帧包含:

  • 局部变量数组
  • 返回值
  • 操作数栈
  • 类当前方法的运行时常量池引用

局部变量数组

局部变量数组包含了方法执行过程中的所有变量,包括 this 引用、所有方法参数、其他局部变量。对于类方法(也就是静态方法),方法参数从下标 0 开始,对于对象方法,位置 0 保留为 this。有下面这些局部变量:

  • boolean
  • byte
  • char
  • long
  • short
  • int
  • float
  • double
  • reference
  • returnAddress

除了 long 和 double 类型以外,所有的变量类型都占用局部变量数组的一个位置。long 和 double 需要占用局部变量数组两个连续的位置,因为它们是 64 位双精度,其它类型都是 32 位单精度。

操作数栈

操作数栈在执行字节码指令过程中被用到,这种方式类似于原生 CPU 寄存器。大部分 JVM 字节码把时间花费在操作数栈的操作上:入栈、出栈、复制、交换、产生消费变量的操作。因此,局部变量数组和操作数栈之间的交换变量指令操作通过字节码频繁 执行。比如,一个简单的变量初始化语句将产生两条跟操作数栈交互的字节码。

int i;Copy to clipboardErrorCopied

被编译成下面的字节码:

0:    iconst_0    // Push 0 to top of the operand stack
1:    istore_1    // Pop value from top of operand stack and store as local variable 1Copy to clipboardErrorCopied

更多关于局部变量数组、操作数栈和运行时常量池之间交互的详细信息,可以在类文件结构部分找到。

动态链接与运行时常量池引用

每个栈帧都有一个运行时常量池的引用。这个引用指向栈帧当前运行方法所在类的常量池。通过这个引用支持动态链接(dynamic linking)。 C/C++ 代码一般被编译成对象文件,然后多个对象文件被链接到一起产生可执行文件或者 dll。在链接阶段,每个对象文件的符号引用被替换成了最终执行文件的相对偏移内存地址。在 Java 中,链接阶段是运行时动态完成的。 当 Java 类文件编译时,所有变量和方法的引用都被当做符号引用存储在这个类的常量池中。符号引用是一个逻辑引用,实际上并不指向物理内存地址。JVM 可以选择符号引用解析的时机,一种是当类文件加载并校验通过后,这种解析方式被称为饥饿方式。另外一种是符号引用在第一次使用的时候被解析,这种解析方式 称为惰性方式。无论如何,JVM 必须要在第一次使用符号引用时完成解析并抛出可能发生的解析错误。绑定是将对象域、方法、类的符号引用替换为直接引用的过程。绑定只会发生一次。一旦绑 定,符号引用会被完全替换。如果一个类的符号引用还没有被解析,那么就会载入这个类。每个直接引用都被存储为相对于存储结构(与运行时变量或方法的位置相 关联的)偏移量。

本地方法栈

和虚拟机栈类似,存储 Native 方法的相关信息。从名字即可看出,本地方法栈就是用来处理 Java 中的本地方法的,Java 类的祖先类 Object 中有众多 Native 方法,如 hashCode()、wait()等,他们的执行很多时候是借助于操作系统,但是 JVM 需要对他们做一些规范,来处理他们的执行过程。此区域,可以有不同的实现方法,向我们常用的 Sun 的 JVM 就是本地方法栈和 JVM 虚拟机栈是同一个。

从名字即可看出,本地方法栈就是用来处理 Java 中的本地方法的,Java 类的祖先类 Object 中有众多 Native 方法,如 hashCode()、wait()等,他们的执行很多时候是借助于操作系统,但是 JVM 需要对他们做一些规范,来处理他们的执行过程。此区域,可以有不同的实现方法,向我们常用的 Sun 的 JVM 就是本地方法栈和 JVM 虚拟机栈是同一个。

程序计数器

这是一块比较小的内存,不在 RAM 上,而是直接划分在 CPU 上的,程序员无法直接操作它,它的作用是:JVM 在解释字节码文件(.class)时,存储当前线程所执行的字节码的行号,只是一种概念模型,各种 JVM 所采用的方式不同,字节码解释器工作时,就是通过改变程序计数器的值来选取下一条要执行的指令,分支、循环、跳转、等基础功能都是依赖此技术区完成的。还有一种情况,就是我们常说的 Java 多线程方面的,多线程就是通过现程轮流切换而达到的,同一时刻,一个内核只能执行一个指令,所以,对于每一个程序来说,必须有一个计数器来记录程序的执行进度,这样,当线程恢复执行的时候,才能从正确的地方开始,所以,每个线程都必须有一个独立的程序计数器,这类计数器为线程私有的内存。如果一个线程正在执行一个 Java 方法,则计数器记录的是字节码的指令的地址,如果执行的一个 Native 方法,则计数器的记录为空,此内存区是唯一一个在 Java 规范中没有任何 OutOfMemoryError 情况的区域。

下一节:我们常说的垃圾回收算法可以分为两部分:对象的查找算法与真正的回收方法。不同回收器的实现细节各有不同,但总的来说基本所有的回收器都会关注如下两个方面:找出所有的存活对象以及清理掉所有的其它对象——也就是那些被认为是废弃或无用的对象。