Java运行时数据区分为线程共享区域和线程私有区域。线程共享区域随着虚拟机进程生命周期启动和消亡,线程私有区域依赖用户线程的启动和结束而建立和销毁。线程共享区域包括:方法区和堆;线程私有区域包括:栈、本地方法区和程序计数器。如下图所示:
程序计数器是一块较小的内存空间,可以看做当前线程所执行的字节码的行号指示器,通过改变计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
在任何时间,一个处理器(多核处理器情况指一个内核)只会执行一条线程中的指令,各线程相互不影响,所以每条线程都需要独立的程序计数器来存储。程序计算器是线程私有区域。
如果线程正在执行Java方法,程序计数器中的值为正在执行的虚拟机字节码指令的地址,如果是本地方法(Native)值为空(Undefined)。
程序计数器是《Java虚拟机规范》中唯一不会出现OutOfMemoryError的区域。
例如,下图为2个线程t1和t2被用一个CPU(内核)交替执行,当执行到t1的位置1的时候,切换到t2执行;当t2执行到位置3的时候,又切回t1,此时需要有个计数器记录之前t1的下一次执行位置。记录执行位置的内存空间称之为程序计数器,每个线程都需要有自己的程序计数器,它记录的是下一次执行的位置数。
字节码中的程序计数器展示如下:
与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期与线程相同。
Java虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
当一个方法开始执行后,只有两种方式退出这个方法。
无论采用何种退出方式,在方法退出之后,都必须返回到最初方法被调用时的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。 方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表 和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指 令后面的一条指令等。
例如:
public class Demo1 { public int m1(){ int i = 10; int b = 20; int c = i + b; return c; } }
对应的字节码如下
public com.dvomu.jvm.Demo1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 // 从局部变量0中装载引用类型值 0: aload_0 // 根据编译时类型来调用实例方法 1: invokespecial #1 // Method java/lang/Object."<init>":()V // 从方法中返回int类型的数据 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/dvomu/jvm/Demo1; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=4, args_size=1 // 将一个8位带符号整数压入栈 0: bipush 10 // 将int类型值存入局部变量1 2: istore_1 // 将一个8位带符号整数20压入栈 3: bipush 20 // 将int类型值存入局部变量2 5: istore_2 // 从局部变量1中装载int类型值 6: iload_1 // 从局部变量2中装载int类型值 7: iload_2 // 执行int类型的加法 8: iadd // 将int类型值存入局部变量3 9: istore_3 // 从类中获取静态字段 10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; // 从局部变量3中装载int类型值 13: iload_3 // 调度对象的虚方法方法 14: invokevirtual #3 // Method java/io/PrintStream.println:(I)V // 方法返回 17: return LineNumberTable: line 5: 0 line 6: 3 line 7: 6 line 9: 10 line 11: 17 LocalVariableTable: Start Length Slot Name Signature 0 18 0 args [Ljava/lang/String; 3 15 1 i I 6 12 2 b I 10 8 3 c I
上面的过程用图文简单描述前几步流程:
注意:局部变量表index从1开始
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行 Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
与虚拟机栈一样,本地方法栈也会在栈深度移除或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java 世界里“几乎”所有的对象实例都在这里分配内存。
需要注意的是,《Java虚拟机规范》并没有对堆进行细致的划分,所以对于堆的讲解要基于具体的虚拟机,我们以 使用最多的HotSpot虚拟机为例进行讲解。
Java堆是垃圾收集器管理的内存区域,因此它也被称作“GC堆”,这就是我们做JVM调优的重点区域部分。
如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展,Java虚拟机将会抛出OutOfMemoryError异常。
以下说明以jdk1.8内存模型为例,不再讨论jdk1.7的perm永久代。
Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中,Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用,在Eden区间变满的时候, GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到OldGen区。
老年区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在年轻代复制转移一定的次数以后,对象就会被转移到老年区,一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间。
元数据空间所占用的内存空间不是在虚拟机内部,而是在本地内存空间中,这也是与1.7的永 久代最大的区别所在。
由上图可以看出,jdk1.8的内存模型是由2部分组成,年轻代 + 年老代。
年轻代:Eden + 2 * Survivor
年老代:OldGen
官方说明:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gc-ergonomics.html
关于元空间需要注意的是,元空间会自动扩容,默认情况下不受限制,在实际中,经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常。
为什么要废弃永久代?
官网给出了解释:http://openjdk.java.net/jeps/122
This is part of the JRockit and Hotspot convergence effort. JRockit customers do not
need to configure the permanent generation (since JRockit does not have a permanent
generation) and are accustomed to not configuring the permanent generation.
移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、 常量、静态变量、即时编译器编译后的代码缓存等数据。
JDK8之前将HotSpot虚拟机把收集器的分代设计扩展至方法区,所以可以将永久代看做是方法区,JDK8之后废弃永久代,用元空间来代替。
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表,用于存放编译器生成的各种字面量与符号应用,这部分内容将在类加载后存放到方法区的运行时常量池。
运行时常量池具备动态性,运行期间也可以将新的常量池放入池中,比如通过String类的intern()方法。
例如:
当创建String对象是,会在堆里创建对象。如下图
当调用String的interrn()方法时编译器会将字符串添加到常量池中,并返回该常量池的引用。如下图
当使用String str1 = "xxx"
时,会先去常量池找"aaa"是否存在相同的常量,如果存在将栈中的引用直接指向该字符串;若不存在,则在常量池中生成一个字符串,再将栈中的引用指向该字符串。
参考
《Java Virtual Machine Specification》
《深入理解Java虚拟机》第3版