# 一、JVM 内存结构

我们以JDK8版本的JVM内存结构为准,将堆内的方法区替换成了堆外的元数据。

JVM

【堆空间】: JVM 初始分配的内存由 -Xms指定,默认是物理内存的 1/64;JVM最大分配的内存由 -Xmx指 定,默认是物理内存的1/4。默认空余堆内存小于 40%时,JVM就会增大堆直到 -Xmx的最大限制;空余堆内存大于 70%时,JVM会减少堆直到 -Xms的最小限制。因此服务器一般设置 -Xms-Xmx相等以避免在每次 GC 后调整堆的大小。可以利用 JVM提供的 -Xmn -Xms -Xmx等选项可进行堆内存设置,一般的要将 -Xms和 -Xmx选项设置为相同,而 -Xmn为1/4的 -Xmx值,建议堆的最大值设置为可用内存的最大值的 80%。初始化堆的大小是 JVM在启动时向系统申请的内存的大小。一般而言,这个参数不重要。但是有的应用程序在大负载的情况下会急剧地占用更多的内存,此时这个参数就是显得非常重要,如果 JVM启动时设置使用的内存比较小而在这种情况下有许多对象进行初始化,JVM 就必须重复地增加内存来满足使用。由于这种原因,我们一般把 -Xms和 -Xmx设为一样大,而堆的最大值受限于系统使用的物理内存。一般使用数据量较大的应用程序会使用持久对象,内存使用有可能迅速地增长。当应用程序需要的内存超出堆的最大值时 JVM就会提示内存溢出,并且导致应用服务崩溃。所以,如果 Xms超过了 Xmx值,或者堆最大值和非堆最大值的总和超过了物理内存或者操作系统的最大限制都会引起服务器启动不起来。

【栈】: 栈是线程私有的内存区域,每个方法执行的时候都会在栈创建一个栈帧,方法的调用过程就对应着栈的入栈和出栈的过程。每个栈帧的结构又包含局部变量表、操作数栈、动态连接、方法返回地址。局部变量表用于存储方法参数和局部变量。当第一个方法被调用的时候,他的参数会被传递至从0开始的连续的局部变量表中。操作数栈用于一些字节码指令从局部变量表中传递至操作数栈,也用来准备方法调用的参数以及接收方法返回结果。动态连接用于将符号引用表示的方法转换为实际方法的直接引用。

【元数据/方法区】: 也叫永久保存的区域,用于存放 Class和 Meta信息,Class在被 Load的时候被放入该区域。它和存放类实例(Instance)的 Heap区域不同,GC(Garbage Collection)不会在主程序运行期对 PermGen space 进行清理。JVM使用 -XX:PermSize 设置非堆内存初始值,默认是物理内存的 1/64;由 XX:MaxPermSize 设置最大非堆内存的大小,默认是物理内存的1/4。 GC不会对 PermGen space进行清理,所以如果你的APP会 Load很多 Class的话,就很可能出现 PermGen space错误。元数据与方法区的区别就是,元数据在堆外分配的内存,方法区在对内分配的内存。

【JVM内存限制】 (最大值):首先 JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就 32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说 Windows系统下为1.5G-2G,Linux系统 下为2G-3G),而 64bit以上的处理器就不会有限制了。

【本地方法栈】: 主要用于执行本地 native方法的区域。

【程序计数器】: 也是线程私有的区域,用于记录当前线程下虚拟机正在执行的字节码的指令地址。

内存是硬盘和CPU之间的桥梁,JVM内存布局规定了Java在运行过程中内存申请分配管理的策略,保证了JVM的高效稳定运行。不同的JVM对于内存的划分方式和管理机制存在着部分差异。下图是JVM整体架构,中间部分就是Java虚拟机定义的各种运行时数据区域。

JVM

Java虚拟机运行期间会使用到运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是随着线程的创建而创建,随着线程的销毁而销毁。
【1】线程私有:程序计数器、虚拟机栈、本地方法区
【2】线程共享:堆、方法区, 堆外内存(元空间、代码缓存)

# 二、堆空间

Java堆是Java虚拟机管理的内存中最大的一块,被所有线程共享。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数据都在这里分配内存。为了进行高效的垃圾回收,虚拟机把堆内存逻辑上划分成三块区域(分代的唯一理由就是优化 GC性能):
【1】新生带(年轻代):新对象和没达到一定年龄的对象都在新生代
【2】老年代(养老区):被长时间使用的对象,老年代的内存空间应该要比年轻代更大
【3】元空间:像一些方法中的操作临时对象等,JDK1.8之前是占用JVM内存,JDK1.8之后直接使用物理内存

JVM

Java虚拟机规范规定,Java堆可以是处于物理上不连续的内存空间中,只要逻辑上是连续的即可,像磁盘空间一样。实现时,既可以是固定大小,也可以是可扩展的,主流虚拟机都是可扩展的(通过-Xmx-Xms控制),如果堆中没有完成实例分配,并且堆无法再扩展时,就会抛出 OutOfMemoryError异常。

# 年轻代

年轻代是所有新对象创建的地方。当填充年轻代时,执行垃圾收集。这种垃圾收集称为Minor GC。年轻一代被分为三个部分——伊甸园(Eden Memory)和两个幸存区(Survivor Memory,被称为from/to或s0/s1),默认比例是8:1:1
【1】大多数新创建的对象都位于Eden内存空间中
【2】当Eden空间被对象填充时,执行Minor GC,并将所有幸存者对象移动到一个幸存者空间中
【3】Minor GC检查幸存者对象,并将它们移动到另一个幸存者空间。所以每次,一个幸存者空间总是空的
【4】经过多次GC循环后存活下来的对象被移动到老年代。通常,这是通过设置年轻一代对象的年龄阈值来实现的,然后他们才有资格提升到老一代

# 老年代

旧的一代内存包含那些经过许多轮小型GC后仍然存活的对象。通常,垃圾收集是在老年代内存满时执行的。老年代垃圾收集称为主GCMajor GC,通常需要更长的时间。

大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝

# 元空间

JDK8的元空间,可以看作是Java虚拟机规范中方法区的实现。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫Non-Heap(非堆),目的应该是与Java堆区分开。所以元空间放在后边的方法区再说。

# 三、虚拟机栈

每个线程在创建的时候都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次Java方法调用,是线程私有的,生命周期和线程一致。保存方法的局部变量、部分结果,并参与方法的调用和返回。

特点:
【1】栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
【2】JVM直接对虚拟机栈的操作只有两个:每个方法执行,伴随着入栈(进栈/压栈),方法执行结束出栈
【3】栈不存在垃圾回收问题

Java虚拟机规范允许Java虚拟机栈的大小是动态的或者是固定不变的
【1】如果采用固定大小的Java虚拟机栈,那每个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常
【2】如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常

可以通过参数-Xss来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。

# 栈运行原理

【1】JVM直接对Java栈的操作只有两个,对栈帧的压栈出栈,遵循“先进后出/后进先出”原则
【2】在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧栈顶栈帧是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)
【3】执行引擎运行的所有字节码指令只针对当前栈帧进行操作
【4】如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,称为新的当前栈帧
【5】不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧中引用另外一个线程的栈帧
【6】如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧
【7】Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令,另一种是抛出异常,不管用哪种方式,都会导致栈帧被弹出

IDEAdebug时候,可以在debug窗口看到Frames中各种方法的压栈和出栈情况

JVM

# 栈帧的内部结构

每个栈帧(Stack Frame)中存储着:
【1】局部变量表(Local Variables)
【2】操作数栈(Operand Stack)(或称为表达式栈)
【3】动态链接(Dynamic Linking):指向运行时常量池的方法引用
【4】方法返回地址(Return Address):方法正常退出或异常退出的地址
【5】一些附加信息

JVM

# 局部变量表

【1】局部变量表也被称为局部变量数组或者本地变量表
【2】是一组变量值存储空间,主要用于存储方法参数和定义在方法体内的局部变量,包括编译器可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此相关的位置)和returnAddress类型(指向了一条字节码指令的地址,已被异常表取代)
【3】由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
【4】局部变量表所需要的容量大小是编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的
【5】方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。
【6】局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束

# 槽 Slot

【1】局部变量表最基本的存储单元是 Slot(变量槽)
【2】在局部变量表中,32位以内的类型只占用一个 Slot(包括returnAddress类型),64 位的类型(long和double)占用两个连续的Slot

byte、short、char 在存储前被转换为int,boolean也被转换为int,0 表示 false,非 0 表示 true。long 和 double 则占据两个 Slot。

【3】JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值,索引值的范围从 0开始到局部变量表最大的Slot数量
【4】当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个Slot
【5】如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问 long 或 double 类型变量,不允许采用任何方式单独访问其中的某一个 Slot)
【6】如果当前帧是由构造方法或实例方法创建的,那么该对象引用this将会存放在index为0的Slot处,其余的参数按照参数表顺序继续排列(这里就引出一个问题:静态方法中为什么不可以引用 this,就是因为this 变量不存在于当前方法的局部变量表中)
【7】栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。(下图中,this、a、b、c 理论上应该有 4 个变量,c 复用了 b 的槽)

JVM

【8】在栈帧中,与性能调优关系最为密切的就是局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
【9】局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收

# 操作数栈

【1】每个独立的栈帧中除了包含局部变量表之外,还包含一个后进先出(Last-In-First-Out)的操作数栈,也可以称为表达式栈(Expression Stack)
【2】操作数栈,在方法执行过程中,根据字节码指令,往操作数栈中写入数据或提取数据,即入栈(push)、出栈(pop)
【3】某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈。比如,执行复制、交换、求和等操作
【4】操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
【5】操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,此时这个方法的操作数栈是空的
【6】每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性的max_stack数据项中
【7】操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问
【8】如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令
【9】Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈

# 栈顶缓存(Top-of-stack-Cashing)

HotSpot的执行引擎采用的并非是基于寄存器的架构,但这并不代表HotSpot VM的实现并没有间接利用到寄存器资源。寄存器是物理CPU中的组成部分之一,它同时也是CPU 中非常重要的**高速存储资源**。一般来说,<font color=red><b>寄存器的读/写速度非常迅速,甚至可以比内存的读/写速度快上几十倍不止,不过寄存器资源却非常有限,不同平台下的CPU寄存器数量是不同和不规律的。</b></font><font color=#665757>寄存器主要用于缓存本地机器指令、数值和下一条需要被执行的指令地址等数据。</b></font>基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数。由于操作数是存储在内存中的,因此频繁的执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM`设计者们提出了栈顶缓存技术将栈顶元素全部缓存在物理 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率

# 动态链接(指向运行时常量池的方法引用)

【1】每一个栈帧内部都包含一个指向运行时常量池中所属该栈帧方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。
【2】在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在Class文件的常量池中。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用

JVM

# JVM 是如何执行方法调用的

方法调用不同于方法执行,方法调用阶段的唯一任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。Class文件的编译过程中不包括传统编译器中的连接步骤,一切方法调用在Class文件里面存储的都是符号引用,而不是方法在实际运行时内存布局中的入口地址(直接引用)。也就是需要在类加载阶段,甚至到运行期才能确定目标方法的直接引用

JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制有关 静态链接: 当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接
动态链接: 如果被调用的方法在编译期无法被确定下来,也就是说,只能在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接

# 虚方法和非虚方法

【1】如果方法在编译器就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法,比如静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法
【2】其他方法称为虚方法

# 虚方法表

在面向对象编程中,会频繁的使用到动态分派,如果每次动态分派都要重新在类的方法元数据中搜索合适的目标有可能会影响到执行效率。为了提高性能,JVM采用在类的方法区建立一个虚方法表(virtual method table),使用索引表来代替查找。非虚方法不会出现在表中。

每个类中都有一个虚方法表,表中存放着各个方法的实际入口。

虚方法表会在类加载的连接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。

# 方法返回地址(return address)

用来存放调用该方法的PC寄存器的值。一个方法的结束,有两种方式:①、正常执行完成;②、出现未处理的异常,非正常退出;

无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定的,栈帧中一般不会保存这部分信息

当一个方法开始执行后,只有两种方式可以退出这个方法:
【1】执行引擎遇到任意一个方法返回的字节码指令,会有返回值传递给上层的方法调用者,简称正常完成出口。一个方法的正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定。在字节码指令中,返回指令包含ireturn(当返回值是 boolean、byte、char、short 和 int 类型时使用)、lreturnfreturndreturn以及areturn,另外还有一个return指令供声明为void的方法、实例初始化方法、类和接口的初始化方法使用。
【2】在方法执行的过程中遇到了异常,并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。简称异常完成出口。方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码。

本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。

正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值

# 四、方法区(元数据/永久代)

【1】方法区(Method Area)与 Java堆一样,是所有线程共享的内存区域。
【2】虽然 Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫 Non-Heap(非堆),目的应该是与Java堆区分开。
【3】运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本/字段/方法/接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将类在加载后进入方法区的运行时常量池中存放。运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的是String.intern()方法。受方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
【4】方法区的大小和堆空间一样,可以选择固定大小也可选择可扩展,方法区的大小决定了系统可以放多少个类,如果系统类太多,导致方法区溢出,虚拟机同样会抛出内存溢出错误
【5】JVM关闭后方法区即被释放
【6】方法区(method area)只是JVM规范中定义的一个概念,用于存储类信息、常量池、静态变量、JIT编译后的代码缓存等数据,并没有规定如何去实现它,不同的厂商有不同的实现。而永久代(PermGen)是Hotspot虚拟机特有的概念, Java8的时候又被元空间取代了,永久代和元空间都可以理解为方法区的落地实现。
【7】永久代物理上是堆的一部分,和新生代,老年代地址是连续的(受垃圾回收器管理),而元空间存在于本地内存(我们常说的堆外内存,不受垃圾回收器管理),这样就不受JVM 限制了,也比较难发生OOM(都会有溢出异常) Java7 中我们通过-XX:PermSize-xx:MaxPermSize来设置永久代参数,Java8之后,随着永久代的取消,这些参数也就随之失效了,改为通过-XX:MetaspaceSize-XX:MaxMetaspaceSize用来设置元空间参数
【8】存储内容不同,元空间存储类的元信息。 静态变量和常量池等并入堆中。相当于永久代的数据被分到了堆和元空间中
【9】-XX:MetaspaceSize默认值依赖于平台。Windows 下,-XX:MetaspaceSize21M-XX:MaxMetaspacaSize的值是-1,即没有限制
【10】与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据发生溢出,虚拟机一样会抛出异常OutOfMemoryError:Metaspace
【11】-XX:MetaspaceSize:设置初始的元空间大小。对于一个64位的服务器端JVM 来说,其默认的-XX:MetaspaceSize的值为20.75MB,这就是初始的高水位线,一旦触及这个水位线,Full GC 将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置,新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值
【12】如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次,通过垃圾回收的日志可观察到Full GC多次调用。为了避免频繁GC,建议将-XX:MetaspaceSize设置为一个相对较高的值。

WARNING

类元数据的本机内存分配将在大小足以容纳多个类元数据的块中完成。每个块将与一个类加载器相关联,并且该类加载器加载的所有类元数据将由 Hotspot 从该类加载器的块中分配。将根据需要为类加载器分配额外的块。块大小将根据应用程序的行为而变化。将选择大小以限制内部和外部碎片。释放类元数据的空间将在类加载器死亡时通过释放与类加载器关联的所有块来完成。类元数据在类的生命周期内不会被移动。

# 方法区内部结构

类型信息: 对每个加载的类型(类 class、接口 interface、枚举 enum、注解 annotation),JVM必须在方法区中存储以下类型信息
【1】这个类型的完整有效名称(全名=包名.类名)
【2】这个类型直接父类的完整有效名(对于 interface或是 java.lang.Object,都没有父类)
【3】这个类型的修饰符(public,abstract,final 的某个子集)
【4】这个类型直接接口的一个有序列表

域(Field)信息
【1】JVM 必须在方法区中保存类型的所有域的相关信息以及域的声明顺序
【2】域的相关信息包括:域名称、域类型、域修饰符(public、private、protected、static、final、volatile、transient 的某个子集)

方法(Method)信息JVM必须保存所有方法的
【1】方法名称
【2】方法的返回类型
【3】方法参数的数量和类型
【4】方法的修饰符(public,private,protected,static,final,synchronized,native,abstract 的一个子集)
【5】方法的字符码(bytecodes)、操作数栈、局部变量表及大小(abstract 和 native 方法除外)
【6】异常表(abstract 和 native 方法除外)每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
【7】栈、堆、方法区的交互关系

# 运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分,理解运行时常量池的话,我们先来说说字节码文件(Class 文件)中的常量池(常量池表)。在加载类和结构到虚拟机后,就会创建对应的运行时常量池。常量池表(Constant Pool Table)是 Class 文件的一部分,用于存储编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。运行时常量池中包含各种不同的常量,包括编译器就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。运行时常量池,相对于Class文件常量池的另一个重要特征是:动态性Java语言并不要求常量一定只有编译期间才能产生,运行期间也可以将新的常量放入池中,String类的intern()方法就是这样的。

常量池: 一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Pool Table),包含各种字面量和对类型、域和方法的符号引用。

为什么需要常量池: 一个Java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接的时候用到的就是运行时常量池。

如下,我们通过jclasslib查看一个只有Main方法的简单类,字节码中的#2指向的就是Constant Pool

JVM

常量池可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。

# 移除永久代原因

为永久代设置空间大小是很难确定的: 在某些场景下,如果动态加载类过多,容易产生Perm区的OOM。如果某个实际Web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现OOM。而元空间和永久代最大的区别在于,元空间不在虚拟机中,而是使用本地内存,所以默认情况下,元空间的大小仅受本地内存限制。再就是对永久代进行调优较困难。

# 方法区的垃圾回收

方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。

先来说说方法区内常量池之中主要存放的两大类常量:字面量和符号引用。字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。

判定一个类型是否属于“不再被使用的类”,需要同时满足三个条件:
【1】该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例
【2】加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGiJSP的重加载等,否则通常很难达成
【3】该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

Java虚拟机被允许堆满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,不使用了就必然会回收。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClassLoading-XX:+TraceClassUnLoading查看类加载和卸载信息。

在大量使用反射、动态代理、CGLibByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

# 五、本地方法栈

本地方法接口: 简单的讲,一个Native Method就是一个Java调用非Java代码的接口。我们知道的Unsafe类就有很多本地方法。

Java使用起来非常方便,然而有些层次的任务用Java实现起来也不容易,或者我们对程序的效率很在意时,问题就来了
【1】与Java环境外交互:有时Java应用需要与Java外面的环境交互,这就是本地方法存在的原因。
【2】与操作系统交互:JVM支持Java语言本身和运行时库,但是有时仍需要依赖一些底层系统的支持。通过本地方法,我们可以实现用Java与实现了jre的底层系统交互,JVM的一些部分就是C语言写的。
【3】Sun's JavaSun的解释器就是C实现的,这使得它能像一些普通的C一样与外部交互。jre大部分都是用Java实现的,它也通过一些本地方法与外界交互。比如,类java.lang.ThreadsetPriority()的方法是用Java实现的,但它实现调用的是该类的本地方法setPrioruty(),该方法是C实现的,并被植入JVM内部。

本地方法栈(Native Method Stack):
【1】Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用
【2】本地方法栈也是线程私有的
【3】允许线程固定或者可动态扩展的内存大小:如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常。如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个OutofMemoryError异常。
【4】本地方法是使用C语言实现的
【5】它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载本地方法库当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
【6】本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区,它甚至可以直接使用本地处理器中的寄存器,直接从本地内存的堆中分配任意数量的内存
【7】并不是所有JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈
【8】在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一

WARNING

栈是运行时的单位,而堆是存储的单位。

栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪

# 六、程序计数器

程序计数寄存器(Program Counter Register),Register的命名源于CPU的寄存器,寄存器存储指令相关的线程信息,CPU只有把数据装载到寄存器才能够运行。这里,并非是广义上所指的物理寄存器,叫程序计数器(或PC计数器或指令计数器)会更加贴切,并且也不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器
【1】它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域
【2】在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期一致
【3】任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。如果当前线程正在执行的是Java方法,程序计数器记录的是JVM字节码指令地址,如果是执行native方法,则是未指定值(undefined)
【4】它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
【5】字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
【6】它是唯一一个在JVM规范中没有规定任何OutOfMemoryError情况的区域

作用: PC寄存器用来存储指向下一条指令的地址,即将要执行的指令代码。由执行引擎读取下一条指令。

JVM

(分析:进入class文件所在目录,执行javap -v xx.class反解析(或者通过IDEA插件Jclasslib直接查看,上图),可以看到当前类对应的Code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等信息。)

使用PC寄存器存储字节码指令地址有什么用呢?为什么使用PC寄存器记录当前线程的执行地址呢?
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。

PC寄存器为什么会被设定为线程私有的?
多线程在一个特定的时间段内只会执行其中某一个线程方法,CPU会不停的做任务切换,这样必然会导致经常中断或恢复。为了能够准确的记录各个线程正在执行的当前字节码指令地址,所以为每个线程都分配了一个PC寄存器,每个线程都独立计算,不会互相影响。

(adsbygoogle = window.adsbygoogle || []).push({});