运行时数据区域
运行时的数据区域,就是JVM管理的内存区域。JVM 运行程序的时候,管理着运行时的内存(一般以静态的 main 方法进入本类的运行时数据区,几乎所有Java程序都是从此开始,也就是说这里的 “运行时的数据区域” 就是一个程序甚至是一个项目的内存开辟起始区,逻辑上不会有第二个与此不连通的区域了 )。JVM 将内存划分为多个不同的数据区域。这些不同的数据区域有着自己的创建和销毁时间,跟着进程或者线程的启动、结束而创建和销毁。
看看运行时的数据区域图示:
我们分别解释一下这些不同的区域的特点:
- 程序计数器(这里不是 PC寄存器,写错了):内存中的很小的一个区域,用来指示字节码运行的下一条指令。注意,每个线程都会独有一个程序计数器。这样也是为了能够让线程记住自己暂停的位置。互不影响。如果正在执行的方法是正儿八经的Java方法,那计数器就指着下一条指令,如果是 native 方法(非Java语言写的方法) 。计数器则为空。此外,这个内存区域是唯一一个不存在内存溢出的区域。
- 虚拟机栈:描述方法执行的内存模型,每个Java方法在执行的时候,会创造一个栈帧(一个方法对应一个栈帧)。这里要注意,如果方法中没有再调用子方法,那么这个虚拟机栈就只有一个栈帧。方法对应的栈帧里有局部变量表、操作数栈、动态链接、方法出口等。每个方法执行的开始就是入栈,结束就是出栈。
- 本地方法栈:和虚拟机栈类似,不同的是它执行的是非Java编写的方法。
- Java堆:Java堆有可能是运行时数据区域的最大一块区域。用来存放对象实例(这些对象实例都是类中创建或引用的实例,就好比是从main方法所在的哪个类中所创建的----> 运行时的数据区)。所以Java堆也是GC管理的主要区域(管理对象的回收)。
- 方法区(非堆,也经常称为永久代,持久区):注意区别于虚拟机栈,它管理的是整个类的逻辑。存放虚拟机加载的类的信息,常量、静态变量、jit编译后的代码等,是线程共享的。这个区域也会有GC,但是GC再这里的收益并不明显,而且此处执行GC条件苛刻。一般针对常量池的回收,或者类型的卸载回收。
- 运行时的常量池:方法区的一部分,用于存放所有被加载的Class文件中的常量池(系列号2)的 编译期的字面量和符号引用,并且这里的符号引用已经不是全类名的引用而是指针的物理引用。(字符串字面量存放于此,但是特意通过new String("abc")的字符串,先是在常量池中(如果没有"abc")建立"abc";再把常量池中的"abc"复制到堆里)运行时常量池相对于class文件常量池(系列号2)的另外一个特征是具有动态性,可以编译期产生,也可以在运行时产生。但是运行时常量位于方法区,也可能产生溢出。
对象的创建过程
虚拟机遇到 new 首先检查运行时的常量池中是否包含 创建类的符号引用,并且检查这个class是否已经被加载。接下来,虚拟机就开始为这个新生对象分配内存。因为对象所需的内存大小在类加载的时候就确定了。这时候如果 Java 堆内是规整的,就用指针碰撞的方法挪一个对象的空间出来使用。如果堆内存不规整,虚拟机就会维护一个列表,上面记录了空闲的空间,在列表上找一个大小合适的空间给新建的对象使用。这就是空闲列表的方式。
对象的在Java堆中的内存布局
- 对象头
- 实例数据
- 对齐填充
对象是存放于Java堆的,其中对象头是一部分是存放对象的整体信息,比如对象的hash码,GC年龄代,线程锁等。另外一部分是对象指向对象所属类的类型指针。
实例数据就是存放对象实例的(包括父类的和子类的);
对其填充:Java堆中分配给对象的空间是8字节的倍数,没有达到就进行占位符填充。
访问堆中的对象方式
- 句柄访问
在虚拟机栈的 reference 引用 句柄池中 对象实例的地址 和 对象类型数据;注意对象实例数据存放于Java堆,而对象类型数据存放于方法堆。
- 直接指针访问
reference 存放对象的引用地址,在对象实例对象中 包含对象类型的指针。
总结:这两种方式各有优势,句柄方式:在对象移动(GC过程中经常发生)的时候只需要改变对象的引用,对象类型数据的引用无需改变。直接指针方式:定位速度快,在无需对象类信息时,节省了一次定位开销。使用较为频繁。
数据运行时各个数据区的溢出分析
待续...p57