回顾
上一篇博客讲到了JVM的定义,和JRE,JDK的区别。重点介绍了JVM的整体体系结构。包括class文件,类加载器,JVM内存区域,执行引擎,本地方法接口,垃圾收集器等部分,对JVM的知识体系有了一个总体的认识。
这篇博客主要介绍了JVM内存区域,对内存区域的划分、各部分的作用和特点以及内存溢出等相关知识。
JVM内存区域总体划分
不多说,内存区域划分,也常叫运行时数据区域,分为程序计数器、本地方法区、虚拟机栈、方法区、堆五个部分。
程序计数器
定义:行号指示器,比如用于循环、跳转等,是下一个指令的行号
特点:没有OOM(out of memory error),因为它的大小不会随程序执行而变化,线程隔离的。
虚拟机栈
这是一个重点区域。在常看的培训视频中所谓的堆,栈中的栈就是指的这个虚拟机栈。
定义:在网上查阅资料发现,基本都没有直接对栈进行定义。找了下,发现强加定义也不好,我也姑且这样写吧。一般性的描述如下:虚拟机栈的元素叫做栈帧,当一个java方法调用时,对应的栈帧就入栈。栈帧中包括局部变量表,操作数栈,方法出口,动态链接等。
个人发现学习栈帧还是有很多疑问的,就从局部变量表和操作数栈两个最重要的数据结构开始理解吧,每个部分做什么?怎么做?各个部分结合起来对应的一个方法的执行过程是怎样的?
操作数栈
存放操作数。操作数是什么?在汇编中,指令由操作码和操作数组成,操作数一般存在寄存器等位置。JVM中也一样,指令中有操作数。更细一点,在一篇文章中看到,操作数可分为嵌入式操作数、栈内操作数和隐含操作数。(准确性有待考证,欢迎读者提意见)
这里有必要先弄清楚,字节文件、字节指令有什么区别?为什么有时候说java程序编译后是字节文件,有时候看到的是一个个的指令?
字节文件,是用javac编译后的.class文件,而看到的指令的形式的文件其实是为了让字节文件方便阅读,使用javap命令如javap -v可以将class文件输出成指令形式。下面是使用javap -v解析HelloWorld.class文件的部分截图。
弄清楚了字节文件和字节指令的区别后,解释三种操作数。嵌入式操作数在编译后就决定好了,处于class文件中,如putstatic指令的后两个字节就是嵌入式操作数,该指令用于给特定静态变量赋值,putstatic表示赋值动作,后面的嵌入式操作数表示赋值给哪个静态变量。因此是一个地址指针,指向运行时常量池中的该静态变量的符号引用。
栈内操作数就是我们熟悉的操作数了,我们常说执行引擎是面向操作数的(执行引擎在后面的学习笔记会复习到),指的就是这个操作数类型。还是举例上面的putstatic指令,有了操作,有了赋值的变量,总得有值啊。这个值就是此时在操作数栈的栈顶的操作数。栈内操作数又分为三种类型,比如赋值的变量为byte,short,int,char,boolean等,该操作数就为4字节的,而long,double,float等就为8字节的,若为引用类型,则也为对应的引用类型。
最后是隐式操作数。即没有明确写出来,如指令iconst_1,操作数隐含在指令里,没错,就是1,它表示将1放入操作数栈。
局部变量表
存储方法执行时的方法参数和定义的局部变量
局部变量表中一个个的slot组成的,32位的变量占1个slot,64位的占两个连续的slot。
局部变量表中的变量是不能直接拿来用的,必须通过指令将数据加载到操作数栈中才能使用。如下图:
这段代码经javac编译,javap -v 转换成字节指令形式后,如下图:
可以看出局部变量表的变量如果要使用,需要有iload指令将存在变量中的值加载到操作数栈中,然后才能进行运算。这个例子也能说明局部变量表,操作数栈的组合是怎样完成代码的逻辑的。在执行引擎的学习部分,还会详细学习这部分的内容。
特点
上面说了那么多,其实讲得是虚拟机栈的数据结构,下面是虚拟机栈的特点。
内存溢出:不仅有OOM,还有stackOverFlowerror。栈嘛,总会有stackoverflow的。。。那么什么情况下OOM,什么情况下stackoverflow呢?说来也巧,在之前做导师项目的时候我不仅遇到了OOM,还遇到了stackoverflow,当时都吓尿了,这什么玩意儿!没见过啊!stackoverflow最经典的触发情况就是无限递归或者递归太多了。在后面会单独写一篇博客对运行时区域的各部分进行OOM等内存问题的测试。本篇博客只是简单列举一下。
线程:线程隔离的部分。虚拟机栈属于线程私有的。
本地方法栈
不展开说了,对于本地方法,上一篇博客也介绍,至于数据结构和执行和虚拟机栈很类似。甚至有的JVM实现直接将二者结合了。本地方法栈同样有OOM和stackoverflow,同样是线程隔离的。
堆
又来了一个重点,堆是java中运行时数据区域的组成中及其重要的一个组成部分。
定义:存储大部分的对象实例以及数组。之所以说是大部分,当然是有特例了,比如我们比较熟悉的java.lang.Class类的对象实例在Hotspot虚拟机中就存储在方法区中,并没有存在堆中!
既然是存储对象实例的,那自然就涉及到对象实例的创建过程,对象实例在内存中的布局、各存储部分的作用和含义以及对象实例的使用方法。
对象实例的创建过程
1.查看该对象实例对应的类是否已经加载
2.对象实例内存分配。对象所需的内存空间在类加载完成后就已经确定了。分配内存空间的方法有指针碰撞法和空闲列表法。其中涉及到并发安全的问题。
3.对象空间的初始化,对象空间除对象头外全部置为零值。
4.设置对象头。对对象头中的GC分代,哈希码等进行设置。
5.执行对象的构造方法。将对象按照程序员的意愿进行初始化
指针碰撞法
即内存是泾渭分明的,一边是分配了的内存,另一边是没有分配的。中间用一个指针指向分割线,在给新对象分配内存空间时,只需要将指针向没有分配过的一边移动足够大的距离即可。
空闲空间列表法
内存不是泾渭分明的,分配了的和没有分配的空间是相互交错的,JVM维护一个没有分配的内存块的列表,给新对象分配内存空间时只需要从列表中选出一块合适的内存块即可。
并发问题解决
由于堆是线程共享的区域,因此需要考虑并发问题。在指针碰撞法中,当两个线程同时去操作那个分割指针,肯定是不安全的,这种情况下,JVM会采用CAS操作+失败重试保证操作的原子性。还有一种方法是设置分配缓冲区,每个线程都有一个TLAB(Thread local alocation buffer),分配空间时优先从这个TLAB中分配空间,如果TLAB不够了再通过同步机制分配新的TLAB。
对象的内存布局
包括对象头,实例数据,对齐填充三个部分
对象头在之后java多线程和JVM关系的博客中会详细写到,这里只做简单介绍。对象头分为“MARK WORD”和类型指针,MARK WORD存储哈希码、分代年龄、偏向锁等信息,类型指针用于指向该对象所属的类,不是必须的。另外,若堆中存的数组,则还要有记录数组长度的空间。
实例数据存储了对象中所有类型的字段信息。
对齐填充不是必须的,只是要求对象的空间大小为8字节的整数倍而已。
对象的访问
分为直接指针法和句柄池法。
直接上图,句柄池就是单独在堆中划分一块句柄池,存放实例数据指针和对象类型指针。直接指针就是类型指针作为对象内存中的一部分。前者句柄池地址不变,访问稳定,后者速度更快(只需要一次指针访问找到实例数据)
特点
OOM,线程共享。
方法区
定义:存放类加载器加载的类,即时编译器编译的类,常量,静态变量的地方。
特点:OOM,线程共享
如上面定义所说,在周志明的第2版《深入理解java虚拟机》中,原话是“方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据”。这里我阅读的时候有很大的疑惑:类信息中不包括常量和静态变量吗?加载的类不就是将class文件的内容放入方法区吗?难道class文件中还能不包含常量和静态变量吗?
我目前是直接忽略掉这个问题进行理解的。在书中的类加载机制这一章,加载阶段作者提到,方法区的数据存储格式有具体实现自己定义,虚拟机规范并没有作规定。所以,我觉得直接简单地理解就行,方法区包括一个运行时常量池和其他普通区域。
运行时常量池
在书中解释运行时常量池存储了class文件的常量池中的内容和运行时动态进入的常量,如String的intern方法。后者没啥解释的,记住就好,那么class文件的常量池又是哪些内容呢?
class文件常量池
这里简单记录一下, 具体内容在class文件结构篇再详细叙述。class文件常量池主要存放两大类常量:字面量和符号引用。说了等于没说。。。
字面量:接近java层面的常量概念,如文本字符串,声明为final的常量值等。
符号引用:更高大上了,包括三方面内容:类的全限定名,字段的名称和描述符,方法的名称和描述符。
读完一脸蒙蔽。。。什么鬼。下面用通俗的例子解释:
首先是字面量,这个还是比较好理解的
String str = “helloworld”;//helloworld就是字面量
private final int a=10;//10就是字面量
然后是符号引用:
java.lang.Object对应java/lang/Object;就是类的全限定名
int a对应int就是描述符,a就是变量名称
void sayHello():void就是描述符,sayHello就是方法名称。
这样好记了吧。。。
当然这只是速成理解法,简要说下,之所以将这些放在常量池中,是因为,以字段举例,会有public,static,volatile,final之类的修饰符,这些修饰符种类都是固定死了的,比如访问权限、静态、可变等类别,非常适合在class文件中使用true/false来描述某个类别的修饰符是有还是没有,然而变量的类型和名称确是由程序员来定的,这部分就直接存在常量池了!关于这个,后面在类文件结构篇会详细讲。
下面是helloworld代码对应的class文件内容,可以看看constant pool下面的内容,有个初步了解
这是示例HelloWorld2.java代码
public class HelloWorld2{
public static int a = 1;
private String b = "hello";
public void print(){
System.out.println(b);
}
public static void main(String args[]){
new HelloWorld2().print();
}
}
这是javap -v HelloWorld2.java后生成的
Last modified 2019-6-3; size 632 bytes
MD5 checksum b5f6f6f69f718827dbe80ff43ff15a1e
Compiled from "HelloWorld2.java"
public class HelloWorld2
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #10.#25 // java/lang/Object."<init>":()V
#2 = String #26 // hello
#3 = Fieldref #6.#27 // HelloWorld2.b:Ljava/lang/String;
#4 = Fieldref #28.#29 // java/lang/System.out:Ljava/io/Prin
Stream;
#5 = Methodref #30.#31 // java/io/PrintStream.println:(Ljava
lang/String;)V
#6 = Class #32 // HelloWorld2
#7 = Methodref #6.#25 // HelloWorld2."<init>":()V
#8 = Methodref #6.#33 // HelloWorld2.print:()V
#9 = Fieldref #6.#34 // HelloWorld2.a:I
#10 = Class #35 // java/lang/Object
#11 = Utf8 a
#12 = Utf8 I
#13 = Utf8 b
#14 = Utf8 Ljava/lang/String;
#15 = Utf8 <init>
#16 = Utf8 ()V
#17 = Utf8 Code
#18 = Utf8 LineNumberTable
#19 = Utf8 print
#20 = Utf8 main
#21 = Utf8 ([Ljava/lang/String;)V
#22 = Utf8 <clinit>
#23 = Utf8 SourceFile
#24 = Utf8 HelloWorld2.java
#25 = NameAndType #15:#16 // "<init>":()V
#26 = Utf8 hello
#27 = NameAndType #13:#14 // b:Ljava/lang/String;
#28 = Class #36 // java/lang/System
#29 = NameAndType #37:#38 // out:Ljava/io/PrintStream;
#30 = Class #39 // java/io/PrintStream
#31 = NameAndType #40:#41 // println:(Ljava/lang/String;)V
#32 = Utf8 HelloWorld2
#33 = NameAndType #19:#16 // print:()V
#34 = NameAndType #11:#12 // a:I
#35 = Utf8 java/lang/Object
#36 = Utf8 java/lang/System
#37 = Utf8 out
#38 = Utf8 Ljava/io/PrintStream;
#39 = Utf8 java/io/PrintStream
#40 = Utf8 println
#41 = Utf8 (Ljava/lang/String;)V
{
public static int a;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
public HelloWorld2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init
":()V
4: aload_0
5: ldc #2 // String hello
7: putfield #3 // Field b:Ljava/lang/String;
10: return
LineNumberTable:
line 1: 0
line 3: 4
public void print();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #4 // Field java/lang/System.out:Lja
a/io/PrintStream;
3: aload_0
4: getfield #3 // Field b:Ljava/lang/String;
7: invokevirtual #5 // Method java/io/PrintStream.pri
tln:(Ljava/lang/String;)V
10: return
LineNumberTable:
line 5: 0
line 6: 10
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: new #6 // class HelloWorld2
3: dup
4: invokespecial #7 // Method "<init>":()V
7: invokevirtual #8 // Method print:()V
10: return
LineNumberTable:
line 8: 0
line 9: 10
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_1
1: putstatic #9 // Field a:I
4: return
LineNumberTable:
line 2: 0
}
SourceFile: "HelloWorld2.java"
常量池介绍完了,方法区的普通部分当然是常量池以外的数据啦。。。
最后
这篇博客比较长,希望读者能读到这里吧!如果文中有不正确的地方欢迎指正!!!!