上面已经聊过JVM是什么东东,也谈过了JVM内存的垃圾回收机制。这一篇博客我们来聊聊JVM运行时数据区域。

JVM运行时数据区域由5块部分组成,分别是堆,方法区,栈,本地方法栈,以及程序计数器组成。

可以根据内存是否线程共享划分成线程独享内存区域/线程共享内存区域。

我们从简单的部分开始吧

1.程序计数器

特点:线程内存独享,占用内存小,生命周期与线程相同(随线程诞生而诞生,随线程消亡而消亡)

功能:当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复(cpu在不断轮询执行任务)等基础功能都需要依赖这个计数器来完成

异常:该区域没有定义异常

2.栈(方法执行的动态模型)

特点:先进后出,线程内存独享,生命周期与线程相同

单位:栈帧

功能:已先进后出执行方法体的方法,执行完成的栈帧出栈

 

举例:

public static void main(String[] args) {
    a();//调用a方法
    b();//调用b方法
}

执行顺序

1.main函数的栈帧入栈

2.a方法的栈帧入栈

3.a方法执行完成后,a栈帧出栈

4.b方法的栈帧入栈

5.b方法执行完成后,b栈帧出栈

6.main方法执行完成后,main栈帧出栈,程序结束

 

异常:StackOverFlow(栈溢出),OutOfMemory(可以扩展栈内存的情况下,内存溢出)

 

接下来我们来谈谈栈的基本单位栈帧吧

栈帧(每一个方法对应一个栈帧)

只有虚拟机栈顶的栈帧才是有效的,称为当前栈帧 (Current Stack Frame),这个栈帧所关联的方法称为当前方法(Current Method)

组成:局部变量表,操作数栈,动态链接,方法出口信息4部分组成

1.局部变量表:由基本数据类型和对象引用组成的

作用:用来存储方法中的局部变量 

基本单位:slot

局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。

 

如果存储的是基本数据类型那么直接存储值
如果存储的是对象引用那么存储对象的引用地址( reference)(堆中)

 

补充:比较reference的两种实现方式

直接引用  vs 使用句柄池

 

直接引用 

reference直接指向对象,对象中指向对象类型数据

优点:速度快,节约指针开销。HotSpot采用的主要方式

 

 

使用句柄池:

java堆中会维护一个句柄池,句柄池分别指向对象实例(堆)的和对象类型数据(方法区)

优点:对象移动后只需改变句柄池的指向地址,而不需要改变引用的指向地址。稳定

 


2.操作数栈

操作数栈的深度在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。

功能:实现程序功能

 

3.动态连接

补充下直接引用与符号引用

直接引用:当类已经加载到虚拟机时,通过地址直接调用该类
符号引用(常量池中):在编译的时候还不知道类是否被加载,先用符号代替该类,等实际运行时再用直接引用替换间接引用。

 

静态解析:符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用
动态连接: 将在每一次的运行期期间转化为直接引用

4.方法出口信息

当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。

但是出现异常会不会返回地址

补充:

局部变量,在方法内部声明,当该方法运行完时,内存即被释放。
成员变量,只要该对象还在,哪怕某一个方法运行完了,还是存在。
从系统的角度来说,声明局部变量有利于内存空间的更高效利用(方法运行完即回收)。
成员变量可用于各个方法间进行数据共享。

 

补充:栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用返回时就可以共用一部分数据,而无须进行额外的参数复制传递了

{
    int a=0;
    a+=10;
    method(a);
}void method(int num)
{
    System.out.print(a);
}

通过这个例子我们看到如果不重叠的话,每次都要重新计算a+=10的值才能执行下面的方法。

3.本地方法栈

大体上都类似于虚拟机栈
不同点:栈执行的java方法服务
本地方法栈执行的是Native方法(不一定是用java开发的)服务

 

4.堆

特点:存储对象,线程间内存共享,占用大量内存,垃圾回收关注的重点区域
关于的堆的分类可以参考上一篇的java垃圾回收机制

异常:OutOfMemoryError

 

每次都向堆中存放对象,方法结束后,销毁栈帧的局部变量表时同时销毁引用,该对象就成了可回收的垃圾。咋看起来没什么不对呀,可是仔细思考下还是存在两个问题
1.不断的来回增加删除对象,对于GC的工作量太大。
2.java使指针碰撞(堆中存入新对象的时候,指针根据对象大小移动到相应位置)来为对象分配内存。如果在多线程的环境下,就会出现两个对象同时移动当前前指针的情况,造成线程不安全的情况。

 

这里就要引入TLAB的概念了

TLAB的全称是Thread Local Allocation Buffer,这是一个线程专用的内存分配区域。每个线程都会从Eden分配一块空间,当线程销毁时,我们自然可以回收掉TLAB的内存。

使用TLAB指令 -XX:UseTLAB

优点:线程安全,减少垃圾回收的压力。

缺点:TLAB空间大小是固定的,面对大对象的时候不够灵活

 

5.方法区

特点:存储类,线程间内存共享

存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

异常:OutOfMemoryError

注意:JDK 6 时,String等字符串常量的信息是置于方法区中的,但是到了JDK 7 时,已经移动到了Java堆。所以,方法区也好,Java堆也罢,到底详细的保存了什么,其实没有具体定论,要结合不同的JVM版本来分析。

 

提到方法区不得不说的就是常量池

补充:方法区不是永久代,只是Hotspot的实现方式而已。

常量池

什么是常量?

常量是指被final修饰的变量,值一旦确定就无法改变。

 

Class文件中的常量池

常量池主要用于存放两大类常量:字面量和符号引用量(在上面已经介绍过了),字面量相当于Java语言层面常量的概念(如文本字符串,声明为final的常量值等),符号引用则属于编译原理方面的概念

 运行时常量池

class文件中的常量池中的内容会在类加载后进入方法区的运行时常量池。

相对于常量池,运行时常量池的重要特征是具有动态性,java并不要求常量只有在编译器才会产生,运行期间也可以将新的常量存放入池中,这种特性用的最多的String类中的intern()方法。

 

那么变量存放在哪里?

对于字符串:其对象的引用都是存储在栈中的,如果是编译期已经创建好(直接用双引号定义的)的就存储在常量池中,如果是运行期(new出来的)才能确定的就存储在堆中。对于equals相等的字符串,在常量池中永远只有一份,在堆中有多份。

 

int i1 = 9;  
int i2 = 9;  
int i3 = 9;   
public static final int INT1 = 9;  
public static final int INT2 = 9;  
public static final int INT3 = 9; 

 

对于基础类型的变量和常量:变量和引用存储在栈中,常量存储在常量池中。

 

int i1 = 9;  
int i2 = 9;  
int i3 = 9;   
public static final int INT1 = 9;  
public static final int INT2 = 9;  
public static final int INT3 = 9; 

 

补充:最后一个疑问,jvm怎么调用方法

 类加载机制链接部分,在类加载的准备阶段,会为静态字段分配内存,还会构造与该类相关联的方法表

这个数据机构就是java虚拟机实现动态绑定的关键所在。
方法表本质上是一个数据,每个数据元素指向一个当前类以及其祖先类中非私有的实例方法
方法表的两个特质
1,子类方法表中包含父类方法表中的所有方法
2,子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同
索引值
方法调用指令中的符号引用会在执行之前解析成实际引用。对于静态绑定的方法调用而言,实际引用将指向具体的目标方法,对于动态绑定的方法调用而言,实际引用则是方法表的索引值
在执行过程中,java虚拟机将获得调用者的时间类型,并在该时间类型的虚方法表中,根据索引值获得目标方法。这个过程便是动态绑定