JVM
JVM
JVM分为五个区域:方法区、堆、虚拟机栈、本地方法栈、程序计数器
1. 程序计数器
一块线程私有的很小的内存空间(每一个线程都有自己的计数器),作为当前线程的行号指示器;
为什么需要程序计数器
我们知道对于一个处理器(如果是多核cpu那就是一核),在一个确定的时刻都只会执行一条线程中的指令,一条线程中有多个指令,为了线程切换可以恢复到正确执行位置,每个线程都需有独立的一个程序计数器,不同线程之间的程序计数器互不影响,独立存储。
注意:如果线程执行的是个java方法,那么计数器记录虚拟机字节码指令的地址。如果为native【底层方法】,那么计数器为空。
这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域(程序计数器是用来指向下一条要执行的指令的位置,所以他是一个可预见大小的值,没有必要outOfMemory)。
2. 虚拟机栈 JVM Stacks
俗称”栈“,线程私有,生命周期与线程相同。
栈帧(Frame)是什么?
栈帧是一种数据结构,用于虚拟机进行方法的调用和执行。
栈帧是虚拟机栈的栈元素,也就是入栈和出栈的一个单元。
栈帧在内存中的位置?
内存 -> 运行时数据区 -> 某个线程对应的虚拟机栈 -> 这里就是栈帧了
入栈与出栈代表什么?
每个方法的执行和结束对应着栈帧的入栈和出栈。
入栈表示被调用,出栈表示执行完毕或者返回异常。
一个虚拟机栈对应一个线程,当前CPU调度的那个线程叫做活动线程;一个栈帧对应一个方法,活动线程的虚拟机栈里最顶部的栈帧代表了当前正在执行的方法,而这个栈帧也被叫做‘当前栈帧’。
栈帧既然是个数据结构,都有哪些数据?
局部变量表、操作数栈、动态链接、方法返回地址、附加信息。
栈帧的大小是什么时候确定的?
编译程序代码的时候,就已经确定了局部变量表和操作数栈的大小,而且在方法表的Code属性中写好了。不会受到运行期数据的影响。
什么是局部变量表?
是一片逻辑连续的内存空间,最小单位是Slot,用来存放方法参数和方法内部定义的局部变量。我觉得可以想成Slot数组....
JVMS7:“any parameters are passed in consecutive local variables starting from local variable 0”
虚拟机没有明确指明一个Slot的内存空间大小。但是boolean、byte、char、short、int、float、reference、returnAddress类型的数据都可以用32位空间或更小的内存来存放。这些类型占用一个Slot。Java中的long和double类型是64位,占用两个Slot。(只有double和long是jvms里明确规定的64位数据类型)
reference类型:与基本类型不同的是它不等同本身,即使是String,内部也是char数组组成,它可能是指向一个对象起始位置指针,也可能指向一个代表对象的句柄或其他与该对象有关的位置。
returnAddress类型:指向一条字节码指令的地址
虚拟机如何调用这个局部变量表?
局部变量表是有索引的,就像数组一样。从0开始,到表的最大索引(Slot的数量-1)
要注意的是,方法参数的个数 + 局部变量的个数 ≠ Slot的数量。因为Slot的空间是可以复用的,当pc计数器的值已经超出了某个变量的作用域时,下一个变量不必使用新的Slot空间,可以去覆盖前面那个空间。(这部分内容在P183页)
特别地,JVMS7:
On instance method invocation, local variable 0 is always used to pass a reference to the object on which the instance method is being invoked (this in the Java programming language)
手动翻译:在一个实例方法的调用时,局部变量表的第0位是一个指向当前对象的引用,也就是Java里的this。
什么是操作数栈?
Each frame (§2.6) contains a last-in-first-out (LIFO) stack known as its operand stack.
翻译:每个栈帧都包含一个被叫做操作数栈的后进先出的栈。叫操作栈,或者操作数栈。
1.栈桢刚创建时,里面的操作数栈是空的。
2.Java虚拟机提供指令来让操作数栈对一些数据进行入栈操作,比如可以把局部变量表里的数据、实例的字段等数据入栈。
3.同时也有指令来支持出栈操作。
4.向其他方法传参的参数,也存在操作数栈中。
5.其他方法返回的结果,返回时存在操作数栈中。
操作数栈本身就是一个普通的栈吗?
其实就是普通的栈,再加上数据结构所支持的一些指令和操作。
同时加上一些对类型的区分:
操作数栈是区分类型的,操作数栈中严格区分类型,而且指令和类型也好严格匹配。
栈桢之间是完全独立的吗?
虚拟机做了一些优化:为了避免过多的 方法间参数的复制传递、方法返回值的复制传递 等一些操作,就让一部分数据进行栈桢间共享。
什么是动态链接?
一个方法调用另一个方法,或者一个类使用另一个类的成员变量时,总得知道被调用者的名字吧?(你可以不认识它本身,但调用它就需要知道他的名字)。符号引用就相当于名字,这些被调用者的名字就存放在Java字节码文件里。
名字是知道了,但是Java真正运行起来的时候,真的能靠这个名字(符号引用)就能找到相应的类和方法吗?
需要解析成相应的直接引用,利用直接引用来准确地找到。
举个例子,就相当于我在0X0300H这个地址存入了一个数526,为了方便编程,我把这个给这个地址起了个别名叫A, 以后我编程的时候(运行之前)可以用别名A来暗示访问这个空间的数据,但其实程序运行起来后,实质上还是去寻找0X0300H这片空间来获取526这个数据的。
这样的符号引用和直接引用在运行时进行解析和链接的过程,叫动态链接。
每一个栈帧内部都要包含一个指向运行时常量池的引用,来支持动态链接的实现。
需要注意的是,局部变量表所需要的内存空间在编译期完成分配,当进入一个方法时,这个方法在栈中需要分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小。
Java虚拟机栈可能出现两种类型的异常:
- 线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError。
- 虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常。
3. 本地方法栈
本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。
4. 堆 Heap
线程共享,占用内存最大,JVM启动时创建,存放一切对象实例和数组
此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。堆内存是所有线程共有的,可以分为两个部分:年轻代和老年代。
java虚拟机规范对这块的描述是:所有对象实例及数组都要在堆上分配内存,但随着JIT编译器的发展和逃逸分析技术的成熟,这个说法也不是那么绝对,但是大多数情况都是这样的(有逃逸分析)。
注意:它是所有线程共享的,它的目的是存放对象实例。同时它也是GC所管理的主要区域,因此常被称为GC堆。根据虚拟机规范,Java堆可以存在物理上不连续的内存空间,就像磁盘空间只要逻辑是连续的即可。它的内存大小可以设为固定大小,也可以扩展。当前主流的虚拟机如HotPot都能按扩展实现(通过设置
-Xmx
和-Xms
),如果堆中没有足够内存完成实例分配,而且堆无法扩展将报OOM错误(OutOfMemoryError)
字符串常量池(string pool / String table):
字符串常量池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)。string pool在每个HotSpot VM的实例只有一份,被所有的类共享。在jdk1.8后,将String常量池放到了堆中。
package com.example;
public class Test {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";//在编译时会做优化,直接进行String s3 = "ab";
String s4 = s1 + s2;//new StringBuilder().append("a“).append("b").toString
String s5 = "ab";
String s6 =s4.intern();
System.out.println(s3 == s4);
//false: toString()方法调用了new String(),只要有new String(),那么就会在堆中开辟一个空间生成新对象实例,同时会引用String Pool中的该字符串(没有的话就会在String Pool中创建)
System.out.println(s3 == s5);
//都是对String Pool中"ab"的引用
System.out.println(s3 == s6);
//intern()方法会去String Pool中寻找有没有该字符串,如果有则引用
}
}
GC后面再讲
5. 方法区(Method Area)
-
方法区(Method Area)与 Java 堆一样,是所有线程共享的内存区域。为了和堆区分,也有“非堆(Non-Heap)"的叫法;方法区在启动时创建,逻辑上是堆的一部分(在jvm厂商处实现方式不一样)。
-
java虚拟机对方法区比较宽松,除了跟堆一样可以不存在连续的内存空间,定义空间和可扩展空间,还可以选择不实现垃圾收集。
-
实现区域
永久代:存储包括类信息、常量、字符串常量、类静态变量、即时编译器编译后的代码等数据。可以通过-XX:PermSize
和-XX:MaxPermSize
来进行调节。当内存不足时,会导致 OutOfMemoryError 异常。JDK8 彻底将永久代移除出 HotSpot JVM,将其原有的数据迁移至 Java Heap 或 Native Heap(Metaspace),取代它的是另一个内存区域被称为元空间(Metaspace)。 -
元空间(Metaspace):元空间是方法区的在 HotSpot JVM 中的实现,方法区主要用于存储类信息、常量池、方法数据、方法代码、符号引用等。元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。,理论上取决于32位/64位系统内存大小,可以通过
-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
配置内存大小。 -
JVM 为每个加载的类和接口都创建一个
java.lang.Class
实例(JDK6 存储在方法区,JDK6 之后存储在 Java 堆),这个对象存储了所有这个字节码内存块的相关信息,如平时使用的this.getClass().getName()
this.getClass().getDeclaredMethods()
this.getClass().getDeclaredFields()
,可以获取类的各种信息,都是通过这个 Class 引用获取。 -
class文件常量池(class constant pool)
可以理解为 *.class文件的资源仓库
类基本信息:举例HelloWorld.class
C:\Users\55202\IdeaProjects\untitled1\out\production\untitled1>javap -v HelloWorld.class Classfile /C:/Users/55202/IdeaProjects/untitled1/out/production/untitled1/HelloWorld.class Last modified 2021-3-26; size 601 bytes MD5 checksum 5687fddf5f7080b11d876985f0a54b45 Compiled from "HelloWorld.java" public class HelloWorld minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER
类方法定义(包括虚拟机指令)
{ public HelloWorld(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 1: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this LHelloWorld; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 //从这里开始就是虚拟机指令 0: getstatic #2 //Fieldjava/lang/System.out:Ljava/io/PrintStream; //getstatic即是获取静态成员变量System.out,#2指的是引用常量池中的#2,去下面常量池找 3: ldc #3 // String HelloWorld 5: invokevirtual #4 //虚方法调用,去找java/io/PrintStream类中的println方法,参数类型是(Ljava/lang/String;)V 8: return LineNumberTable: line 3: 0 line 4: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String; }
Constant Pool 常量池:就是一张表,虚拟机从中找到要执行的类名、方法名、参数类型、字面量("HelloWorld"字符串,基本类型Integer、Boolean、Final字段的常量)型等信息:
Java 中基本类型的包装类的大部分都实现了常量池技术,这些类是 Byte、Short、Integer、Long、Character、Boolean,另外 Float 和 Double 类型的包装类则没有实现。另外 Byte、Short、Integer、Long、Character 这5种整型的包装类也只是在对应值在-128到127之间时才可使用对象池。
Constant pool: #1 = Methodref #6.#20 // java/lang/Object."<init>":()V #2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #23 // HelloWorld #4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #23 // HelloWorld #6 = Class #26 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 LHelloWorld; #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = Utf8 args #17 = Utf8 [Ljava/lang/String; #18 = Utf8 SourceFile #19 = Utf8 HelloWorld.java #20 = NameAndType #7:#8 // "<init>":()V #21 = Class #27 // 找到#27的字符串 java/lang/System #22 = NameAndType #28:#29 // out:Ljava/io/PrintStream; #23 = Utf8 HelloWorld #24 = Class #30 // java/io/PrintStream #25 = NameAndType #31:#32 // println:(Ljava/lang/String;)V #26 = Utf8 java/lang/Object #27 = Utf8 java/lang/System #28 = Utf8 out //成员变量名:out #29 = Utf8 Ljava/io/PrintStream; //类型:java/io/PrintStream #30 = Utf8 java/io/PrintStream #31 = Utf8 println #32 = Utf8 (Ljava/lang/String;)V
-
运行时常量池(runtime constant pool):常量池是*.class文件中的,当类被加载时,其常量池信息被加入到运行时常量池,把符号地址变成真实地址
常量池只有类文件在编译的时候才会产生,而且是存储在类文件*.class中的。而运行时常量池是在方法区,而且可在JVM运行期间动态向运行时常量池中写入数据,比如String的 intern() 。