Java 内存模型
c ++ 与 java 很大的一点区别就是 c++ 每次创建对象,都要写配对的delete/free 语句,否则会出现内存泄漏或者内存溢出。而java在强大的内存管理机制下(gc),不用去担心这些问题。因为gc会被那些“失去联系”的对象清理掉。想要了解GC,就要先了解一下JVM运行内存区域是怎么样的。按照Java虚拟机规范 8中的描述,JVM运行内存区域可分为Java虚拟机栈,pc寄存器,Java堆,方法区,运行时常量池,本地方法栈;以及不属于JVM运行内存区域的直接内存。

Java虚拟机栈
线程-Java虚拟机栈-栈帧
JVM每创建一个线程会为之创建对应的java虚拟机栈,用来存储栈帧。Java虚拟机栈是Java线程的私有内存区域,即其它线程无法访问到该线程内的数据。

栈帧
栈帧由局部变量表,操作数栈,指向当前方法所属类的运行时常量池的引用,返回地址组成。它随着方法的调用而创建,随着方法的结束而销毁。线程每调用一个方法,就会往它的Java虚拟机栈中压人一个栈帧。比如:
public static void main(String[] args) {
Cat cat = new Cat();
cat.say();
System.out.println("end ...");
}
在这个例子中,jvm先压入main的栈帧,执行到cat.say()的时候压入say()的栈帧,等say()执行完以后,say()栈帧销毁,继续执行main栈帧,等main都执行完以后,方法结束,Java虚拟机栈销毁。
- 局部变量表
一个局部变量可以保存一个int,float,byte,char,boolean,short,reference,returnAddress。两个局部变量保存一个long或者double。可以把局部变量表看做是一个数组,使用索引定位,即a[0]表示第一个局部变量。我们可以在局部变量表中找到方法参数,方法中局部变量,返回值对应的局部变量。通常情况下,第一个局部变量保存该方法所在的实例对象的引用。 - 操作数栈
保存方法执行过程中的操作数。比如我们运行了代码int a = 100 + 98
,代码被编译成汇编是这样子的:
begin
iload_0 // push the int in local variable 0 onto the stack
iload_1 // push the int in local variable 1 onto the stack
iadd // pop two ints, add them, push result
istore_2 // pop int, store into local variable 2
end
在这个字节码序列里,前两个指令iload_0和iload_1将存储在局部变量中索引为0和1的整数压入操作数栈中,其后iadd指令从操作数栈中弹出那两个整数相加,再将结果压入操作数栈。第四条指令istore_2则从操作数栈中弹出结果,并把它存储到局部变量区索引为2的位置。图5-10详细表述了这个过程中局部变量和操作数栈的状态变化,图中没有使用的局部变量区和操作数栈区域以空白表示。

- 动态链接
栈帧中包含一个指向当前方法所在类型的运行时常量池的引用,以便对当前方法的代码实现动态链接。
可能错误
StackOverFlowError : 栈帧调用链太长了。
OutOfMemoryError : 没有足够内存去创建新的虚拟机栈。
pc寄存器
pc寄存器也是线程的私有内存区域。每条Java线程都会有自己的Java虚拟机栈,也有自己的pc寄存器。pc寄存器保存当前方法的字节码指令的地址。当CPU再又切到该线程的时候,可能根据pc寄存器的值地址调用到字节码指令。该如果方法是native,那么pc寄存器的值为undefined。
本地方法栈
本地方法栈也是线程的私有内存区域。和Java虚拟机栈一样,区别是本地方法栈是调用native方法的所使用的栈。各个JVM对它的实现不尽相同。在HotSpot的实现中,本地方法栈和虚拟机栈合二为一。
Java堆
简介
Java堆是各个线程共享的内存区域。java生成对象实例,会在堆上面分配内存(栈存放引用,实际指向这里)。GC管理就是运行在堆上面的。基于现在gc收集器采用分代收集算法,堆又可以分为新生代和老年代。而新生代又可以分为Eden和from survivor/to survivor区域。
错误
当堆内存不足的时候会报OutOfMemoryError 的错误。
方法区
简介
方法区也是各个内存共享的内存区域。存放java的所有类的结构信息。比如运行时常量池,字段,方法等字节码。类的元数据是在类的加载过程阶段存储到方法区的。在加载完元数据以后还会生成一个class对象,该对象放在堆中。
实现
方法区是一个规范,HotSpot JDK1.7对该区域的实现叫做永久代,1.8的实现叫做元数据区。元数据区和永久代最大的不同就是元数据区的数据放在本地内存中,而永久代则放在堆中。
运行时常量池
运行时常量池也是各个线程共享的内存区域。要理解运行时常量池,首先要了解class常量池。
class常量池
public class ClassPoolTest {
private static String word = "hello world";
public static void main(String[] args) {
System.out.println(ClassPoolTest.word);
}
}
java文件编译成class文件后,有个constant pool的数据结构,这个数据结构就是class常量池。如下图:
$ javap -verbose ClassPoolTest.class
Classfile /D:/practice/target/classes/pool/ClassPoolTest.class
Last modified 2018-8-9; size 725 bytes
MD5 checksum ce4cb04b698854efd49f64942ae38d35
Compiled from "ClassPoolTest.java"
public class pool.ClassPoolTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Class #2 // pool/ClassPoolTest
#2 = Utf8 pool/ClassPoolTest
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 word
#6 = Utf8 Ljava/lang/String;
#7 = Utf8 <clinit>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = String #11 // hello world
#11 = Utf8 hello world
#12 = Fieldref #1.#13 // pool/ClassPoolTest.word:Ljava/lang/String;
#13 = NameAndType #5:#6 // word:Ljava/lang/String;
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 <init>
#17 = Methodref #3.#18 // java/lang/Object."<init>":()V
#18 = NameAndType #16:#8 // "<init>":()V
#19 = Utf8 this
#20 = Utf8 Lpool/ClassPoolTest;
#21 = Utf8 main
#22 = Utf8 ([Ljava/lang/String;)V
#23 = Utf8 org.aspectj.weaver.MethodDeclarationLineNumber
#24 = Fieldref #25.#27 // java/lang/System.out:Ljava/io/PrintStream;
#25 = Class #26 // java/lang/System
#26 = Utf8 java/lang/System
#27 = NameAndType #28:#29 // out:Ljava/io/PrintStream;
#28 = Utf8 out
#29 = Utf8 Ljava/io/PrintStream;
#30 = Methodref #31.#33 // java/io/PrintStream.println:(Ljava/lang/String;)V
#31 = Class #32 // java/io/PrintStream
#32 = Utf8 java/io/PrintStream
#33 = NameAndType #34:#35 // println:(Ljava/lang/String;)V
#34 = Utf8 println
#35 = Utf8 (Ljava/lang/String;)V
#36 = Utf8 args
#37 = Utf8 [Ljava/lang/String;
#38 = Utf8 SourceFile
#39 = Utf8 ClassPoolTest.java
{
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #10 // String hello world
2: putstatic #12 // Field word:Ljava/lang/String;
5: return
LineNumberTable:
line 15: 0
LocalVariableTable:
Start Length Slot Name Signature
public pool.ClassPoolTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #17 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 13: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lpool/ClassPoolTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Error: unknown attribute
org.aspectj.weaver.MethodDeclarationLineNumber: length = 0x8
00 00 00 11 00 00 00 EB
Code:
stack=2, locals=1, args_size=1
0: getstatic #24 // Field java/lang/System.out:Ljava/io/PrintStream;
3: getstatic #12 // Field word:Ljava/lang/String;
6: invokevirtual #30 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
9: return
LineNumberTable:
line 18: 0
line 20: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
}
SourceFile: "ClassPoolTest.java"
class常量池里面主要放两种类型的常量:字面量和符号引用。
字面量
比较接近于java中的常量,比如文本字符串,声明为final的常量值等。在上面的例子中,hello world就是一个字面量。
符号引用
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。在上面的例子中,word这个字段就是一个符号引用。符号引用一般包括下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
直接引用
我们可以从上面知道可以用一组符号来描述所引用的目标,比如一个类名,但是实际上,jvm获取一个类都是用的地址引用。我们可以把这个地址引用理解为直接引用,把这个类名理解成符号引用。
直接引用可以是:
(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
(3)一个能间接定位到目标的句柄
直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。
运行时常量池
class常量池被加载进内存后的形态,但是它会把class常量池中的符号引用改成直接引用。一个类对应着一个运行时常量池。
string常量池
每个类都有自己的运行时常量池。每个类可能会有很多字符串是相同的,为了更节省空间,java把字符串单独抽出来放到一个string常量池中,多个运行时常量池可以共用一个字符串常量池的东西。
直接内存
不属于java虚拟机的内存区域。JDK1.4以后 NIO,引入一种基于通道和缓冲区的I/0方式,使用native函数分配对外内存。然后通过一个存储在java堆中的directbybnuffer对象对这块内存引用进行操作。这样子能够在显著提升性能,避免在java堆和native对中来回复制数据。