作为一名java初学者,我发现网上对Java内存这部分知识讲解粗细不一、深浅不定,理解起来难度较大。于是,自己动手整理了一份资料,以供交流学习。
Java的编程环境如图所示。
从上图可以看出,Java虚拟机是程序运行的场所 。那么什么是虚拟机呢?要理解Java虚拟机,你首先必须意识到,当你说“Java虚拟机”时,可能指的是如下三种不同的东西:1、抽象规范。2、一个具体的实现 。3、一个运行中的虚拟机实例。每个Java程序都运行于某个具体的Java虚拟机实现的实例上。
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,根据《Java虚拟机规范(第2版)》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域,如下图所示。
程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的信号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
经常有人把Java内存区分为堆内存(Heap)和栈内存(Stack),这种分法比较粗糙,事实远比此复杂。这种划分方式的流行只能说明大多数程序员最关注的、与对象内存分配关系最密切的内存区域就是这两块。而所指的“栈”就是我们现在所说的虚拟机栈,或者说是虚拟机栈中的局部变量部分。
局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int等)、对象引用(reference类型,它不同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
堆(Heap),对于大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动是创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是他却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行常量池中。
直接内存(Direct Memory)不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁使用。
说了这么多,其实初学者根本不需要全都掌握,只需暂时先了解虚拟机栈(简称“栈”)、堆和方法区的部分知识即可。
因此简化以上内容如下,
栈内存:存储的都是局部变量(方法中的变量和参数),而且变量所属的作用域一旦结束,该变量就自动释放。
class Test { public static void main(String[]args) { int sum=0; for(int i=0;i<8;i++) { sum+=i; } System.out.println(sum); } } //像sum、i之类的就是局部变量,所属作用域不同而已
堆内存:存储的是数组和对象(数组就是对象),凡是new的就建立在堆中。前面介绍已经说过,堆的唯一目的就是存放对象实例,实例是什么,就是实实在在的个例,实例也是存储多个数据的个例。实例一创建,堆就会对实例中数据默认初始化。
特点:1、每一个实例都有一个首地址值。2、堆内存中的每一个变量都有默认值,根据类型的不同而不同。3、释放方式,垃圾回收机制。
例—1:一维数组的内存体现
class Test { public static void main(String[]args) { int arr = new int[3]; arr[0]=89; //若arr赋null,即其是空的,不指向实例(对象)。 } } //如下图来解释该程序的运行过程。
1、程序开始,主函数进栈(方法进栈就是局部变量进栈)
2、arr 变量分配内存
3、new,在堆中为数组实例分配一个空间 (内存地址)
4、实例中的数据默认初始化
5、实例将其地址赋给了arr变量 (引用数据类型就是这样来的)
6、arr变量获得地址,指向了实例(与指针类似)
7、通过地址找到相应的成员,进行复制
finally,执行完毕后,堆中的数据会被当做垃圾,在不定时内被回收
大致过程就是这样的。
例—2:二维数组的内存图解
class Test { public static void main(String[]args) { int [][]arr =new int[3][2]; } }
1、程序开始,主函数进栈
2、arr变量分配内存
3、数组在堆内存中得到地址,开辟空间
4、二维数组元素还是数组,也即是实例,默认初始化为null
5、为二维数组元素开辟空间
6、二维数组元素的实例中的成员默认初始化
7、地址赋给元素
8、二维数组地址赋给arr变量
9、指向实例
例—3:基本类型数据参数传递图解
1、程序开始
2、局部变量x分配内存并赋值
3、方法show压栈(进栈)
4、局部变量x分配内存并赋值
5、方法show弹栈(退栈)变量释放
例—4:引用数据类型参数传递图解
1、程序开始
2、局部变量d分配内存
3、堆内存分配空间给Demo对象,创建对象
4、x默认初始化为0
5、初始化x为3
6、地址赋给d
7、d指向对象
8、将9赋值给x
9、调用show方法,进栈
10、参数分配内存
11、将d指向的地址传给参数d
12、d指向对象
13、将4赋值给x
14、show方法退栈
例子—5:构造函数—内存图解
简单解释:当对象默认初始化后,接下来调用构造函数,进栈、初始化、出栈之后过程都差不多了。
方法区正如前面所说,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。下面我们举例展示虚拟机如何使用方法区中的信息,看下面这个类:
class Lava { private int speed = 5; void flow() { } } class Volcano { public static void main(String[]args) { Lava lava = new Lava(); lava.flow(); } }
要运行Volcano程序,首先得以某种“依赖与实现的”方式告诉虚拟机“Volcano”这个名字。之后,虚拟机将找到并读入相应的class文件“Volcano.class”,然后它会从导入的class文件里的二进制数据中提取类型信息并放到方法区中。通过执行保存在方法区中的字节码,虚拟机开始执行main()方法,在执行时,它会一直持有指向当前类(Volcano类)的常量池(方法区中的一个数据结构)的指针。
虚拟机开始执行Volcano类中的main()方法的字节码时,尽管Lava类还没有被加载,但是和大多数(也许所有)虚拟机实现一样,它不会等到程序中用到的所有类都装载后才开始运行程序。恰好相反,它只在需要是才装载相应的类。
main()的第一条指令告知虚拟机为列在常量池第一项的类分配足够的内存。所以虚拟机使用指向Volcano常量池的指针找到第一项,发现它是一个对Lava类的符号引用,然后它就检查方法区,看Lava类是否已经被加载了。
当虚拟机发现还没有加载过名为“Lava”的类时,它就开始查找并加载文件“Lava.class”,并把从读入的二进制数据中提取的类型信息放在方法区中。
接着,虚拟机以一个直接指向方法区Lava类数据的指针来替换常量池第一项(即“Lava”)——以后就可以用这个指针来快速地访问Lava类了。
终于,虚拟机准备为一个新的Lava对象分配内存,当Java虚拟机确定了一个Lava对象的大小后,它就在堆上分配这么大的空间,并把这个对象实例的变量speed初始化为默认值0。当把新生成的Lava对象的引用压到栈中,main()方法的第一条指令也完成了,接下来……
就这一个小小的过程,讲了这么一大堆,而且还不完整,要是详细地讲解完,天都黑了。而且作为初学者如此深究,必要吗?这就好比我们高中物理里的“速度”,放在小学、初中,它就是当成“速率”来说的。为什么要把这事实看来是不正确或不准确的理念给初学者,个中原因,相信大家都深有体会。而这也是一样的,何必深究正确、准确,只要不偏离大方向,够用就行,循序渐进,到时候自然柳暗花明。
所以,现在我们只需知道方法区就是用于存储已被虚拟机加载的类信息、常量、静态变量。
到此我们先理清一下思路。
栈:存储局部变量,加载方法的区域
堆:存储数组和对象的区域
方法区:加载类、常量、静态变量的区域。
下面我们再举几个例子理解理解。
例—1:static关键字—内存图解
1、加载类StaticDemo2
2、StaticDemo2的默认构造函数
3、静态方法主函数main()加载
4、main方法进栈
5、Person才开始加载,注意静态、非静态的空间不同
6、method方法进栈,用完自动退栈
7、p分配内存
8、在堆内存中创建Person对象,分配空间
9、Person中的变量默认初始化
10、加载构造函数
11、对Person中的变量初始化
12、类Person的地址赋给p变量
13、p变量指向对象p
14、show方法进栈
15、this指向对象,show调用完直接退栈
例—2:继承—子父类中成员变量的内存图解
由于过程大致都差不多,在此就不多加解释。注意主函数所在类的加载省略了,另外,在堆中子类中,只需知道子类分配了一个空间给Fu类的成员变量。
至此,未完待续……
欢迎批评指正,交流学习!
参考资料
1、《深入Java虚拟机(原书第二版)》 (美)Bill Venners 著 曹晓钢 蒋靖 译
2、《深入理解Java虚拟机JVM高级特性与最佳实践》 周志明 著
3、毕向东(Java基础视频)