Java虚拟机的内存模型


概述

为了Java程序能更好的管理内存,Java将运行时数据区分为程序计数器、虚拟机栈、本地方法栈、方法区和堆区五个空间。

程序计数器(线程私有)

很小的内存空间,高速,在处理器内部,用于存储字节码指令的地址,该地址是程序将要执行的下一条指令地址,指示程序的完成。通过字节码解释器改变计数器的值选择下一条执行的指令。若执行的是本地方法计数器值则为空。

虚拟机栈(线程私有)
虚拟机栈的概述和异常

可以快速访问的内存空间,位于RAM,由多个栈帧形成虚拟机栈,栈帧中存储局部变量表、操作数栈、部分结果和返回地址等。每一次随着方法的调用入栈随着方法的返回出栈。每一个虚拟机栈都有一个栈指针,通过他的上下移动进行分配和回收内存。

分配栈时就会知道栈的大小,因此在不断栈帧入栈时就可能会发生栈溢出StackOverflowError,又当申请的栈大小超出一定内存大小时就会发生内存泄露OutOfMemoryError。

栈帧

局部变量表

局部变量表存储有基本类型或引用类型变量的地址。局部变量表被当做一个从0开始的数组,0是false,若是非int基本类型存储前则转化成int类型,而long和double占据了两个字长,局部变量则是通过数组下标访问。

操作数栈

操作数栈也是一个数字数组,但与局部变量表不同的是,它是根据push和pop进行入栈和出栈,而非通过下标访问。

帧数据区

主要作用:1、解析方法中的常量池 2、处理方会返回,恢复现场 3、处理调用过程中可能出现的异常,存在一个异常表,若有catch将异常处理,否则中断方法调用。

本地方法栈(线程私有)

与虚拟机栈类似,用于管理本地方法调用,但它拥有和虚拟机一样的权限,能访问虚拟机也能在本地堆区分配对象。可以看做是虚拟机栈的动态扩展。在Hotspot虚拟机中,是不区分虚拟机栈和本地方法栈的,因此也会抛出StackOverflow和OutOfMemory错误。

方法区(线程共享)

方法区也一块独立的内存空间,保存的主要是类的元信息。这些信息主要来源于class文件。在Hotspot中,也被叫做永久区。主要包含:

类型信息:类的完整名称、父类的完成名称、类的修饰符、类的直接接口类表

常量池:包含类方法域信息等所引用的常量

域信息:域名称、域类型、域修饰符

方法信息:方法名,修饰符,返回类型、方法参数、操作数栈及 方法栈局部变量区的大小和异常表

同时,方法区也会被gc回收,主要回收的是常量池和类元数据。对于常量池可能因为常量的不断加入而引发OutOfMemory,所以也需要进行垃圾回收,回收没有被任何地方引用的常量。

堆区(线程共享)
堆的位置与优劣势

堆位于RAM,是一种常用性内存池,存放所有Java对象。堆是在jvm启动时创建。

优势:堆是运行时动态分配内存,同时也不需要知道数据的存活时间。具有很大的灵活性。

劣势:访问速度慢,创建和回收对象都会消耗资源甚大。因此这也是gc回收时决定系统性能的瓶颈。

介绍GCIH:GCIH技术实现了堆外存储,即将大内存的Java对象移到堆外,GC不能直接管理GCIH内的对象,这样降低了GC回收频率和提升了GC回收效率。GCIH是基于OpenJDK深度定制的TaobaoJVM的一种创新。

逃逸分析介绍

逃逸分析定义

计算机软件方面,逃逸分析指的是计算机语言编译器语言优化管理中,分析指针动态范围的方法。通俗点讲,如果一个对象的指针被多个方法或线程引用时,那我们可以称这个指针发生了逃逸 。[1]

常见逃逸分析发生场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class G {
public static B b;

public void globalVariablePointerEscape() {
b = new B();//给全局变量赋值,发生逃逸
}
public B methodPointerEscape() {
return new B();//方法返回值,发生逃逸
}
public void instancePassPointerEscape(){
methodPointerEscape().printClassName(this); //实例引用发生逃逸
}
}
class B {
public void printClassName(G g) {
System.out.println(g.getClass().getName());
}
}

如何优化逃逸分析

对栈重新分配,分析未逃逸变量,在栈中分配对象,这样就非逃逸对象的调用执行与回收就会随着栈的进行创建于销毁。同时也大大减少了堆中对象分配得负担。提升了Java运行的性能。

逃逸优化

  • 同步消除。线程同步的代价是相当高的,同步的后果是降低并发性和性能。逃逸分析可以判断出某个对象是否始终只被一个线程访问,如果只被一个线程访问,那么对该对象的同步操作就可以转化成没有同步保护的操作,这样就能大大提高并发性和性能。

  • 矢量替代。逃逸分析方法如果发现对象的内存存储结构不需要连续进行的话,就可以将对象的部分甚至全部都保存在 CPU 寄存器内,这样能大大加快访问速度。

堆的组成

按代分

将堆分为年轻代和年老代,年轻代又分为Eden区、from survivor和to survivor区,对象的出生在Eden区,survivor区是对象至少经过一次年轻代的垃圾回收,当此区对象经过指定年龄还未被回收就会进入年老代。而年老代存储大对象和经过多次垃圾回收的对象。

按区分

G1GC垃圾回收规则,将堆不再按代分,而是将堆分为一个个独立的区间。每一个区间的代不是固定的,根据区间内的对象进行判定区间属于哪个代。

[1]:《深入理解JVM&G1GC》