JVM内存与GC(1)- 运行时数据区域

最后更新:2020-01-01

1. 运行时数据区域

JDK1.7和1.8的运行时数据区域有所不同

JDK1.7:

JDK1.8:

其中线程私有的是:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

线程共享的是:

  • 方法区
  • 直接内存(非运行时数据区的一部分)

2. 程序计数器

在代码执行的过程中,程序计数器是必不可少的,它的作用不仅仅只是计数,而且是执行下一条命令的地址指向。

  • 作用:是用来存储指向下一条指令的地址,也是即将要执行的指令代码。由执行引擎读取下一条指令。
  • 特点线程私有,不会存在内存溢出,唯一一个在Java虚拟机规范中没有OOM的区域。
  • 注意:在物理实现程序计数器是在寄存器实现的,整个CPU中最快的一个执行单元。
  • 解释:每一个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的字节码,由执行引擎读取下一条指令,是一个非常的小的内存空间,几乎可以忽略不计。

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。

在多线程中,就会存在线程上下文切换,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

public int test() {
    int x = 0;
    int y = 1;
    return x + y;
}

java 文件被翻译为字节码的时候,字节码大概类似于下面的样子

  public int test();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: iconst_1
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: ireturn
      LineNumberTable:
        line 6: 0
        line 7: 2
        line 8: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       8     0  this   Lcom/taobao/resttest/Append;
            2       6     1     x   I
            4       4     2     y   I

上面左边的 0、1、2、3 ,就是类似于字节码的行号(实际是指令的偏移地址),程序计数器中保存中的值,就是它们;字节码解释器,就是根据它们,来执行程序的 ;

从上面的介绍中我们知道程序计数器主要有两个作用:

  • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  • 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

如果线程执行 Java 方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址。如果执行 Navtive 方法,程序计数器值则为空(Undefined)。因为 Navtive 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口从而调用到 C/C++ 方法。由于该方法是通过 C/C++ 而不是 Java 进行实现。那么自然无法产生相应的字节码,并且 C/C++ 执行时的内存分配是由自己语言决定的,而不是由 JVM 决定的。

由于是线程私有的,程序计数器的生命周期随着线程,线程启动而产生,线程结束而消亡。

程序计数器保存的是当前执行的字节码的偏移地址,当执行到下一条指令的时候,改变的只是程序计数器中保存的地址,并不需要申请新的内存来保存新的指令地址;因此,永远都不可能内存溢出的;因此在jvm虚拟机规范中,程序计数器是唯一一个没有规定 OutOfMemoryError 异常 的区域。

3. Java 虚拟机栈

与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同,每一个运行在Java虚拟机里的线程都拥有自己的线程栈。这个线程栈包含了这个线程调用的方法当前执行点相关的信息。一个线程仅能访问自己的线程栈。一个线程创建的本地变量对其它线程不可见,仅自己可见。即使两个线程执行同样的代码,这两个线程任然在在自己的线程栈中的代码来创建本地变量。因此,每个线程拥有每个本地变量的独有版本。

所有原始类型的本地变量都存放在线程栈上,因此对其它线程不可见。一个线程可能向另一个线程传递一个原始类型变量的拷贝,但是它不能共享这个原始类型变量自身。

Java 内存可以粗糙的区分为堆内存(Heap)和栈内存(Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。(实际上,Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。)

3.1. 栈帧

栈帧(Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接 (Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。

每当一个方法被调用时,一个新的帧就会被创建。无论方法调用是正常完成还是因为未捕获异常而骤然完成,帧都会在方法调用完成时被销毁。帧是从创建它的线程的栈中分配的。每一个帧都有它自己的局部变量数组(也称为局部变量表)、操作数栈以及一个指向当前方法所在类运行时常量池的引用。

在JVM实现中可以给帧扩展一些特定的信息,例如:调试信息。

局部变量表和操作数栈的大小都是在编译时确定的,并且和与帧相关的方法代码一起被提供。因此帧数据结构的大小仅取决于具体的JVM实现方式,这些结构的内存可以在方法调用的同时分配。

在任何一个给定线程的栈帧中,只有一个帧(执行方法的帧)是活动的,称为当前帧(current frame),它所执行的方法称为当前方法(current method)。定义当前方法的类称为当前类(current class)。对局部变量和操作数栈的操作通常引用当前帧。

当一个帧的当前方法调用了另一个方法,或者当前方法完成了,那么这个帧就不再是当前帧。每当一个方法被调用时,就会创建一个新的帧并在控制转移到新方法时成为当前帧。在方法返回时,当前帧将其方法调用的结果(如果有的话)传回给前一帧。当前一帧变成当前帧时,原来的当前帧将被丢弃。

注意:一个线程创建的帧是该线程的局部帧,不能被任何其它线程引用。

3.2. 局部变量表

局部变量数组的容量是在编译时确定的,并且和与该帧相关的方法代码一起被提供。

局部变量表主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。

堆上包含在Java程序中创建的所有对象。如果一个对象被创建然后赋值给一个局部变量,或者用来作为另一个对象的成员变量,这个对象任然是存放在堆上。

下面这张图演示了调用栈和本地变量存放在线程栈上,对象存放在堆上。

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

局部变量表被组织为以一个字长为单位、从0开始计数的数组,类型为short、byte和char的值在存入数组前要被转换成int值,而long和double在数组中占据连续的两项,在访问局部变量中的long或double时,只需取出连续两项的第一项的索引值即可,如某个long值在局部变量区中占据的索引时3、4项,取值时,指令只需取索引为3的long值即可。

一个局部变量可以保存以下类型的值:boolean、byte、char、short、int、float、reference或returnAddress。一对本地变量可以一个保存float或者double类型的值。

long或double类型的值占用两个连续的局部变量数组位置,这样的值只能通过较小的那个索引值来处理。例如,一个double类型的值存储在索引为n的局部变量数组位置上,它其实占用n和n+1两个局部变量位置,但是不能从索引n+1处加载局部变量。可以向索引n+1处存储局部变量,这样会使局部变量n的内容无效。JVM实现者可以使用两个局部变量位置自由决定long和double类型值的表示方式。

JVM规范没有明确指出一个局部变量位置有多少位存储空间,但根据一个局部变量能存储的值的类型可以推断其存储空间在8到32位之间。

public static int runClassMethod(int i,long l,float f,double d,Object o,byte b) { 
   return 0;   
}

public int runInstanceMethod(char c,double d,short s,boolean b) { 
       return 0;   
}

局部变量表的作用?

JVM使用局部变量在方法调用时传递参数。在类方法调用中,所有参数都以从局部变量0开始的连续局部变量的形式传递。在实例方法调用上,局部变量0传递的是实例方法被调用的对象的引用(Java编程语言中的this),随后,所有参数都在从局部变量1开始的连续局部变量中传递。

3.3. 操作数栈

栈最典型的一个应用就是用来对表达式求值。想想一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。

每一个帧都包含一个后进先出(LIFO)栈,称为操作数栈。一个帧的操作数栈最大深度是在编译确定的,并和与该帧相关的方法代码一起被提供。

当帧被创建时,它所包含的操作数栈是空的。JVM提供了指令,用于从局部变量或字段中将常量或值加载到操作数栈。JVM也提供了另外一些指令,用于从操作数栈中获取操作数,对它们进行操作,然后将结果推回到操作数栈。操作数栈也用于准备传递给方法的参数以及接收方法调用的结果。

和局部变量表一样,操作数栈也被组织成一个以字长为单位的数组。但和前者不同的是,它不是通过索引来访问的,而是通过入栈和出栈来访问的。可把操作数栈理解为存储计算时,临时数据的存储区域。下面我们通过一段简短的程序片段外加一幅图片来了解下操作数栈的作用。

int a = 100;
int b = 98;
int c = a+b;

从图中可以得出:操作数栈其实就是个临时数据存储区域,它是通过入栈和出栈来进行操作的。

操作数栈中的每个条目都可以保存任意JVM数据类型的值,包括long或double类型的值。必须以适合其类型的方式对操作数栈中的值进行操作。例如,不可能将两个整型值压入栈中,却又要把它们当做long类型值处理。也不可能将两个float值压入栈中,却想用iadd指令将它们相加。有一小部分JVM指令(dup和swap)在操作运行时数据区域时不需要考虑值的具体类型,这些指令被定义为不能用于修改或分解单个值。以上这些对操作数栈操作的限制是通过类文件验证强制执行的。

在任何时间点,操作数栈都有一个深度,long和double类型的值为深度贡献两个单位,其它类型的值为深度贡献一个单位。

3.4. 动态链接

每个栈帧都包含一个指向当前方法所在类运行时常量池的引用,从而支持方法代码的动态链接。在class文件代码中,方法通过符号引用来引用要调用的其它方法和要访问的变量。动态链接的过程就是将这些符号引用转换为具体引用。JVM会按需加载必要的类来解析符号,将变量/方法访问转换为变量/方法运行时位置相关的存储结构中的适当偏移量。

这种变量与方法的后期绑定使其它类中的修改不太可能对调用者代码造成破坏。举个例子:类A中有一个方法a,通过符号$B.b调用类B的方法b,无论B如何修改,JVM都是在运行时动态确认b的具体引用。修改B不会导致A也要跟着修改,通过一个引用符号以及运行时动态绑定实现了编码上的解耦。

符号引用和直接引用有什么区别呢?

比如我们定义了String name=”jack”, 其中jack是一个字面量,会在字符串常量池(String Pool)中保存一份。

如果我们存储的时候,存的是name,那么这个就是符号引用。如果我们存储的是jack在字符串常量池中地址,那么这个就是直接引用。

从上面的介绍我们可以知道,为了实现最终的程序正常运行,所有的符号引用都需要转换成为直接引用才能正常执行。

而这个转换的过程,就叫做动态链接。

3.5. 方法出口信息

当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。由于每个线程正在执行的方法可能不同,Java 虚拟机栈也是线程私有的,每个线程都有各自的Java虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。

当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。

Java方法有两种返回方式:

  • return 语句。
  • 抛出异常。

不管哪种返回方式都会导致栈帧被弹出。

return 语句

如果调动没有发生异常(JVM直接抛出的异常或显式执行抛出异常语句而抛出的异常),这次调用将正常完成。在方法调用正常完成时,被调用方法可能会通过执行返回指令向调用者方法返回一个值(如果有返回值的话),该指令的选择必须与返回值的类型相匹配。被调动方法的当前帧用来恢复调用者方法的状态,状态包括局部变量和操作数栈,返回值会被压入调用者方法帧中的操作数栈。对于发起方法调用的当前线程,它的程序计数器会被适当地增加,跳过方法调用指令,指向下一个要执行的指令,在调用者方法的帧中继续正常执行。

抛出异常

如果JVM指令的执行导致JVM抛出了一个异常且方法没有对这个异常进行处理,那么方法的调用就会骤然结束。athrow指令的执行也会引起异常并且异常会被显式抛出,若该异常没有被方法捕获,则方法调用骤然结束。由于未捕获异常而骤然结束的方法调用将不会把返回值传回给调用者。

3.6 栈的整个结构

在前面就描述过:栈是由栈帧组成,每当线程调用一个java方法时,JVM就会在该线程对应的栈中压入一个帧,而帧是由局部变量区、操作数栈和帧数据区组成。那在一个代码块中,栈到底是什么形式呢?下面是我从《深入JVM》中摘抄的一个例子:

public class Main{    
	public static void addAndPrint(){      
    	double result = addTwoTypes(1,88.88);    
        System.out.println(result);    
    }   
         
    public static double addTwoTypes(int i,double d){  
    	return i + d;  
    }
}

上面所给的图,只想说明两件事情:

  1. 只有在调用一个方法时,才为当前栈分配一个帧,然后将该帧压入栈
  2. 帧中存储了对应方法的局部数据,方法执行完,对应的帧则从栈中弹出,并把返回结果存储在调用 方法的帧的操作数栈中

Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。

  • StackOverFlowError: 若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。
  • OutOfMemoryError: 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。

4. 本地方法栈

本地方法栈和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。

方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。

5. 堆

对于大多数应用来说,堆是java虚拟机管理内存最大的一块内存区域,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。

java虚拟机规范对这块的描述是:所有对象实例及数组都要在堆上分配内存,但随着JIT编译器的发展和逃逸分析技术的成熟,这个说法也不是那么绝对,但是大多数情况都是这样的。

即时编译器:可以把把Java的字节码(包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序)

逃逸分析:通过逃逸分析来决定某些实例或者变量是否要在堆中进行分配,如果开启了逃逸分析,即可将这些变量直接在栈上进行分配,而非堆上进行分配。这些变量的指针可以被全局所引用,或者其其它线程所引用。

堆可以处于物理上不连续的空间,只要逻辑上是连续的即可。

Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap).由于现在收集器基本都采用分代垃圾收集算法,所以Java堆被分为了三个部分:

  • 年轻代 : 常常又被划分为Eden区和Survivor(又分为From Survivor和To Survivor两部分)区,默认空间分配比例是8:1:1
  • 老年代
  • 永久代

    • 在Java 6中,永久代在非堆内存中
    • 在Java 7中,永久代的静态变量运行时常量池合并到堆
    • 在Java 8中,永久代被元空间取代

JDK1.7

JDK1.8

大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden区->Survivor 区后对象的初始年龄变为1),当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置。

虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制

注意 PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效,Parallel Scavenge收集器不认识这个参数,Parallel Scavenge收集器一般并不需要设置。如果遇到必须使用此参数的场合,可以考虑ParNew加CMS的收集器组合。

参数

  • -XX:InitialSurvivorRatio 新生代Eden/Survivor空间的初始比例,默认8
  • -XX:NewRatio Old区/Young区的内存比例,默认2
  • -XX:MaxTenuringThreshold 一个对象从新生代晋升到老年代的阈值。默认值是15

6. 方法区

方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与 Java 堆区分开来。

方法区主要用来存放已被虚拟机加载的类相关信息。Class文件中除了有类的版本、字段、方法、接口、父类等描述信息外,还有一项信息是常量池( Constant pool table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入运行时常量池中存放。运行时常量池相对于class文件常量池的另外一个特性是具备动态性,java语言并不要求常量一定只有编译器才产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。

JVM在执行某个类的时候,必须经过加载、连接(验证、准备、解析)、初始化

  • 加载类时,JVM会先加载class文件,在class文件除了有类的版本、字段、方法、接口等描述信息外,还有常量池
    • 常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用
    • 字面量:字符串(String a=”b”),基本类型的常量(final修饰)
    • 符号引用:类和方法的全限定名、字段的名称和描述符、方法的名称和描述符
  • 当类加载到内存中后,JVM会将class文件常量池中的内容存放到运行时常量池中
  • 在解析阶段,JVM会把符号引用替换为直接引用(对象的索引值)

运行时常量池是全局共享的,多个类共用一个运行时常量池,class文件中的常量池多个相同的字符串在运行时常量池只会存在一份

方法区 != 永久代

《Java虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。方法区和永久代的关系很像Java中接口和类的关系,类实现了接口,而永久代就是HotSpot虚拟机对虚拟机规范中方法区的一种实现方式。也就是说,永久代是HotSpot的概念,方法区是Java虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。

JDK 1.8 之前通常通过下面这些参数来调节方法区大小

  • -XX:PermSize=N 方法区(永久代)初始大小
  • -XX:MaxPermSize=N 方法区(永久代)最大大小,超过这个值将会抛出OutOfMemoryError异常:java.lang.OutOfMemoryError: PermGen

相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。

JDK 1.8 彻底移除了永久代,取而代之是元空间,元空间使用的是直接内存。

  • -XX:MetaspaceSize=N 设置Metaspace的初始(和最小大小)
  • -XX:MaxMetaspaceSize=N 设置Metaspace的最大大小,超过此值就会触发Full GC,此值默认没有限制,但应取决于系统内存的大小。JVM会动态地改变此值。
  • -XX:MinMetaspaceFreeRatio 在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
  • -XX:MaxMetaspaceFreeRatio 在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

与永久代很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。

永久代或元空间内并没有保存类实例的具体信息(及类对象),也没有反射对象(例如方法对象),这些内容都保持在常规的堆空间内,永久代和元空间保持的信息只对编译器或者JVM的运行时有用,这部分信息也称为“类的元数据”。

为什么要将永久代(PermGen)替换为元空间(MetaSpace)呢?

整个永久代有一个 JVM 本身设置固定大小上线,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到java.lang.OutOfMemoryError。你可以使用-XX:MaxMetaspaceSize标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。

7. 运行时常量池

运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)

既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。

JDK1.8的运行时常量池位于元空间

8. 字符串常量池

字符串常量池独立于运行时常量池,它实际是一种由C++实现的Map,结构上类似于Hashtable,区别在于其无法自动扩容。

String的String Pool是一个固定大小的Hashtable,默认值大小长度是1009,如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降(因为要一个一个找)。

在 jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。在jdk7中,StringTable的长度可以通过一个参数指定:-XX:StringTableSize=99991

字符串常量池是全局的,JVM 中独此一份,因此也称为全局字符串常量池。运行时常量池中的字符串字面量若是成员的,则在类的加载初始化阶段就使用到了字符串常量池;若是本地的,则在使用到的时候(执行此代码时)才会使用到字符串常量池。其实,“使用常量池”对应的字节码是一个 ldc 指令,在给 String 类型的引用赋值的时候会先执行这个指令,看常量池中是否存在这个字符串对象的引用,若有就直接返回这个引用,若没有,就在堆里创建这个字符串对象并在字符串常量池中记录下这个引用(jdk1.7)。String 类的 intern() 方法还可在运行期间把字符串放到字符串常量池中。JVM 中除了字符串常量池,8种基本数据类型中除了两种浮点类型剩余的6种基本数据类型的包装类,都使用了缓冲池技术,但是 Byte、Short、Integer、Long、Character 这5种整型的包装类也只是在对应值在 [-128,127] 时才会使用缓冲(Character是[0,127],Integer可以通过-XX:AutoBoxCacheMax=<size>修改),超出此范围仍然会去创建新的对象。其中:

  • 在JDK1.6及更早版本中字符串常量池位于方法区
  • 在JDK1.7及以后的版本中中字符串常量池位于堆

9. 直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是JVM规范中定义的内存区域。但这部分内存也被频繁的使用,而且也可能导致OutOfMemoryError异常出现。

JDK1.4中新引入了NIO机制,它是一种基于通道与缓冲区的新I/O方式,可以直接从操作系统中分配直接内存,即直接堆外分配内存,这样能在一些场景中提高性能,因为避免了在Java堆和Native堆中来回复制数据。

10. 内存溢出

内存泄露(Memory Leak):程序在申请内存后,对象没有被GC所回收,它始终占用内存,内存泄漏的堆积最终会造成内存溢出。

内存溢出(Memory Overflow):程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于老年代或永久代垃圾回收后,仍然无内存空间容纳新的Java对象的情况。通常都是由于内存泄露导致堆栈内存不断增大,从而引发内存溢出。

运行时区域大概了解后,我们在来总结一下:

运行时区域 异常 主要原因
虚拟机栈和本地方法栈 StackOverflowError、OutOfMemoryError StackOverflowError:线程请求的栈深度大于虚拟机所允许的最大深度;OutOfMemoryError:虚拟机在扩展栈时无法申请足够的内存空间
程序计数器
OutOfMemoryError 对象数量到达最大堆的容量,内存泄漏、内存溢出
方法区和运行时常量池 OutOfMemoryError 反射,动态代理:CGLib、JSP、OSGI等

11. 参考资料

https://juejin.im/post/5d4e2aa7f265da03d15540b9

http://zhongmingmao.me/2019/09/07/java-performance-jvm-memory-model/

https://www.kancloud.cn/imnotdown1019/java_core_full/1012266#23__81

https://mp.weixin.qq.com/s/tDrmeV7foUeXBYdF_FDv2w

Edgar

Edgar
一个略懂Java的小菜比