说到虚拟机的内存划分,一般会笼统地分为堆跟栈,因为这两块在开发当中接触较多。
其实应该分为:虚拟机栈、本地方法栈 、程序计数器、堆、方法区,这5大部分。
1.虚拟机栈
对栈帧进行管理,即入栈和出栈。
(1)该区域是线程私有;
(2)当线程请求栈深度超出虚拟机栈允许的范围,则抛出StackOverflowError,例如递归调用;
(3)当虚拟机栈超出可用的内存大小,则抛出OutOfMemoryError;
(4)创建线程,它就被创建,线程执行完,它也就被销毁。
1.1 栈帧
用来支持方法调用的数据结构;执行某个方法,就会创建一个栈帧压入虚拟机栈,方法执行完,栈帧就被虚拟机栈弹出,内存自动清理;栈帧包括局部变量表、操作数栈、动态链接、返回地址。
Tips1:每个方法从开始到执行完成,都有一个对应的栈帧在虚拟机栈中入栈(创建)和出栈(清理)。
Tips2:JVM基于栈,也就是内存,DVM基于寄存器,寄存器比内存快,所以DVM比内存快;
1.1.1 局部变量表
存储方法参数以及局部变量,基本数据类型以及对象的引用都存放在局部变量表中;表的大小在Java编译成class文件时已经确定了。
Tips:局部变量表下标为0的位置默认存储this。
1.1.2 操作数栈
是一个栈类型的数据结构,方法执行时,将局部变量表中的元素按照指令的要求压入或者弹出操作数栈。
1.1.3 动态链接
常用在多态链接以及so库的动态链接 ,以下代码在调用eat方法的时候,会用到动态链接;
//Girl Boy分别是Person的子类
public void add(){
Person p=new Girl();
p.eat();
p=new Boy();
p.eat();
}
1.1.4 返回地址
用来帮助当前方法恢复到它的调用位置。
正常退出时,返回地址为程序计数器的值。
异常退出时,返回地址是通过异常处理器表确定的。
1.1.4 实例讲解
以下是比较简单的运算,通过查看字节码可以看到具体的运算过程;
public void add(){
int i=1;
int j=2;
return i+j;
}
开始,虚拟机栈压入该栈帧;
(1)locals=3,局部变量表有三个元素,默认第0个元素为this;
(2)iconst_1,将常量1压入操作数栈;
(3)istore_1,将操作数栈 栈顶元素(值为1)存入局部变量表,下标为1;
(4)iconst_1,将常量0压入操作数栈;
(5)istore_2,将操作数栈 栈顶元素(值为2)存入局部变量表,下标为2;
(6)iload_1、iload_2,局部变量表中下标1、2的元素压入操作数栈;
(7)iadd,将栈顶元素分别出栈相加,把结果压入操作数栈;
(8)ireturn,返回到调用处;
最后,虚拟机栈弹出该栈帧;
public int add();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: iconst_1
1: istore_1
2: iconst_1
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: ireturn
2.本地方法栈
与虚拟机栈类似,只不过是针对native本地方法。HotSpot以及ART将两者合二为一 ,本地方法栈也会抛出 StackOverflowError 和OutOfMemoryError。
3.程序计数器
(1)计数器是线程私有的;
(2)用于记录当前线程指令执行的位置;
(3)计数器没有OOM;
(4)创建线程,它就被创建,线程执行完,它也就销毁;
(5)当程序在执行native方法时,该计数器值为空;
(6)线程执行、线程恢复依赖这个值。
4.堆
(1)该区域是线程共享的,不过可以通过ThreadLocal划分出线程私有的区域;
(2)存放对象以及数组;
(3)发生OOM的主要区域,区域当前所占内存加上需要分配的内存超过系统允许的值,则抛出OOM异常。
5.方法区
(1)该区域是线程共享的;
(2)保存类的Class对象(版本、属性、方法、接口)、静态变量、常量等;
Tips:常量如果没有任何对象引用,在GC必要时,会被移出方法区,Class对象也是一样,如果没有任何对应的实例、ClassLoader被回收、没有被任何地方反射调用Class,就可能会被回收。
(3)也会出现OutOfMemoryError,例如加载的类太多。
6.实例分析哪些变量在栈和哪些在堆
(1)先分析test方法,age对应的值30,aArray引用,a1引用,都在栈中,具体点就是在局部变量表中,当方法执行完,局部变量表被清空,变量就失效;aArray、a1引用的对象都在堆中,注意aArray数组每个元素都是引用,引用的对象都在堆中;
(2)外部调用的 A a1 = new A(),a1引用也在栈中,引用的对象在堆中,那么就引申一个问题:a1对象中的name、array引用、array对象在哪里呢?是都在堆中。
Tips:只有方法中的局部变量才会在栈中。
public class A {
String name;
String[] array=new String[10];
public void test() {
int age = 30;
String[] aArray = new String[10];
A a1= new A();
}
//外部调用
A a1 = new A();
}
7.JVM、DVM、ART区别
(1)JVM是Java虚拟机,是实现了以上内存划分的实例,它基于栈(我理解是栈帧),执行的是class文件;
(2)DVM是Dalvik VM,是Google设计用于Android平台的Java虚拟机,它是基于寄存器,执行的dex文件;
Tips:dex文件是通过Dex工具打包而成的,是多个class文件的集合,它对class文件去冗余,例如多个class文件包含同一个字符串常量,就会将他们合并在同一个;多个class文件合成一个dex文件,带来的问题就是方法数变多,超过65535个;
(3)DVM的堆跟JVM的稍有不同,前者分成Active堆以及Zgoyte堆,Active堆跟JVM的堆一样存放创建的对象以及数组,而Zgoyte堆就是从Zgoyte复制给过来的那部分,例如预加载的资源;
(4)ART大体跟DVM差不多,增加了AOT预编译技术,也就是在安装Apk时,提前将字节码转换成机器码,这样App运行速度就快,不过明显能感觉到安装速度变慢了,ART是Android 5.0以后默认的虚拟机;
(5)无论是JVM、DVM、ART,堆内存都是按需分配的,随着指令的执行不断地向系统申请内存,直到最大可使用内存;
Tips:堆内存是按需申请的,指的是物理内存。应用启动后,就会得到一个Runtime.getRuntime().maxMemory()值那么大的虚拟内存,目的就是防止后续虚拟内存动态增长,不断地需要进行数据拷贝。
(6)Android几个重要的指标:
Runtime.getRuntime().maxMemory() / 1024 * 1024 ;//最大可使用堆内存;
Runtime.getRuntime().totalMemory() / 1024 * 1024 ;//目前可使用堆内存,包括freeMemory()
Runtime.getRuntime().freeMemory() / 1024 * 1024 ;//目前空闲内存
7.总结
从下图可以清晰看出:
(1)Java虚拟机内存划分为堆、虚拟机栈、本地方法栈 、方法区、程序计数器;
(2)堆、方法区是线程共享的,虚拟机栈、本地方法栈 、程序计数器是线程私有;
(3)以线程1为例,线程1有对应的虚拟机栈,当执行到某个方法就会产生一个栈帧,每个栈帧包含局部变量表、操作数栈、返回地址、动态链接;
JVM虚拟机内存划分.png
以上内存划分,是Java虚拟机规范中定义的内容,不同的虚拟机有不同实现。
以上分析有不对的地方,大家可以讨论下,互相学习哦!
网友评论