Java和C++之间有一堵内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。
运行时数据区域
java虚拟机在执行程序的过程中会把所管理的内存划分为若干个不同的数 据区域。这些区域各自有自个的用途,以及创建和销毁时间。有的区域生命周期和线程同步,有的则是和虚拟机同步。
程序计数器
程序计数器是一块较小的内存区域,它可以看作当前线程的所执行的字节码行号指示器。在虚拟机概念模型里面,虚拟机通过改变这个计数器的值来选取下一条需要执行的字节码指令。同时它是线程私有的。
需要注意的是,如果线程执行的是native方法,那么这个计数器的值应该为0。同时,它还是虚拟机规范中唯一不会发生OutOfMemoryError的区域。
java虚拟机栈
线程私有,它的生命周期与线程相同。java虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧用来存储局部变量表、操作数栈、动态链接、方法出入口的信息。一个方法的执行到结束就对应着栈帧在虚拟机栈中的入栈和出栈。
对于执行引擎来说,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。
局部变量表
局部变量表用来存放方法的参数和方法中定义的局部变量。局部变量表达大小在编译成class文件的时候就确定了,在方法的Code属性中max_locals确定了局部变量的最大容量。
局部变量局部的容量以变量槽(Variable Slot)为最小单位。虚拟机规范中并没有规定一个slot所占用的内存空间,只是说每个slot应该存放一个char,boolean,byte,short,float,reference,int,returnAddress。64位的数据类型long和double需要两个连续的slot来存放。
局部变量表是用基于索引的方式来访问的。索引返回从0到局部变量的最大数。
在方法的执行过程中,使用局部变量表完成从参数值到参数列表的传递过程。比如,如果执行的是实例方法(不是static方法),那么在默认的情况下局部变量表索引为0的位置存放的是当前调用方法的实例对象,即this。同时,为了节省栈局部变量表中已经被占用的slot可以会被覆盖,因为,如果超出了局部变量的作用于范围,那么该局部变量所占用的slot就可以被覆盖。但是,Slot对对象的引用会影响GC(要是被引用,将不会被回收)。
操作数栈
操作数栈和局部变量表一样。不过操作数栈的访问是基于标准的栈操作(入栈和出栈)。
在方法执行的过程中字节码指令往往会从操作数栈中写入和提取数据。比如说要执行两个int型的数据相加,就需要从操作数栈的栈顶弹出两个int型的数据然后相加,把相加之后的结果压入栈中。下面演示虚拟机是如何将两个int类型的数据相加的。
虚拟机指令:
begin
iload_0 // push the int in local variable 0 ontothe stack
iload_1 //push the int in local variable 1 onto the stack
iadd // pop two ints, add them, push result
istore_2 // pop int, store into local variable 2
end
图片描述:
1.执行iload_0,将局部变量表索引为0位置的int型数据压入操作数栈中;
2.执行iload_1 ,将局部变量表索引为1位置的int型数据压入操作数栈中;
3.执行iadd,将弹出操作数栈栈顶的连个int类型数据然后相加,把相加的结果压入栈顶;
4.执行istore_2,将操作数栈栈顶的int类型数据存储到局部变量表索引为2的位置处。
动态连接
每一个栈帧中都存放着一个指向运行时常量池的符号引用。在类加载的解析阶段将符号引用转换为直接引用叫做静态解析,在运行阶段将符号引用转换为直接引用叫做动态链接。
返回值地址
每个返回执行完成都有两种形式。第一种是正常执行完成,第二种是在方法的执行的过程中发生了异常,同时方法中又不存在对应的异常处理器。不管方法以何种方式接触,方法都需要在执行完成之后会到方法调用的位置,程序才能正常执行,方法返回时可能在栈帧中保存一些信息来帮助恢复上层方法的执行状态。如果方法是正常退出的,则调用者的PC计数器的值就可以作为返回地址,,果是因为异常退出的,则是需要通过异常处理表来确定。
异常
java虚拟机规范中规定,虚拟机栈存在两种异常:
1.如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常。
2.一般而言虚拟机的栈的深度允许动态扩展的,但是当扩展之后还是无法申请到足够的内存就会发生OutOfMemoryError异常。
本地方法栈
本地方法栈(Native MethodStacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。
与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
堆
是java虚拟机内存中占用最大的一块区域,几乎所有的java对象的内存分配都是在这里进行的。它是所有线程共享的一个区域。
同时它也是垃圾的最主要的区域,所以它也可以叫做GC堆。由于现在的垃圾收集器都是基于分代算法实现的。所以更加细致的划分堆区可以划分为新生代和老年代。新生代又可以划分为Eden,from survivor,to survivor。
根据虚拟机规范的规定,java堆可以处于物理上不连续的内存空间。只要逻辑上面连续即可,就像我们的磁盘空间一样。
堆的大小可以通过-Xms(最小值)和-Xmx(最大值)参数设置,-Xms为JVM启动时申请的最小内存,默认为操作系统物理内存的1/64但小于1G,
-Xmx为JVM可申请的最大内存,默认为物理内存的1/4但小于1G,默认当空余堆内存小于40%时,
JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRation=来指定这个比列;当空余堆内存大于70%时,
JVM会减小heap的大小到-Xms指定的大小,可通过XX:MaxHeapFreeRation=来指定这个比列,对于运行系统,
为避免在运行时频繁调整Heap的大小,通常-Xms与-Xmx的值设成一样。
方法区
主要是用来存储类信息,常量和静态变量的。
对于在HotSpot上面开发的人来说,更加习惯上把方法区称作“永久代”。但是方法区并不等于永久代,只是HotSpot的设计团队把垃圾回收扩展到方法区了。方法区除了和堆区一样可以使用不许连续的物理内存之外,还可以不实现垃圾回收。但是该区域的垃圾回收是必要的,它的主要工作是常量的回收和类的卸载。
由于所有的线程都共享方法区,所以,方法区里的数据访问必须被设计成线程安全的。例如,假如同时有两个线程都企图访问方法区中的同一个类,而这个类还没有被装入JVM,那么只允许一个线程去装载它,而其它线程必须等待。
根据java虚拟机规范规定,当方法区无法满足内存分配需求的时候,就会抛出OutOfMemoryError异常。
运行时常量池
运行时常量池是方法区中的一部分。编译的时候,将各种字面量和符号引用存放在Class文件的常量池中,而运行常量池主要是将Class文件中的常量池在类加载的过程加载进来。
同时运行时常量池相对于Class文件中的常量池中还具备一个重要的特性,那就是可以动态扩展。
因为运行时常量池是方法区的一部分,自然而然受到方法去的内存限制。当无法申请到内存的时候就会抛出OutOfMemoryError异常。
我的理解
在刚刚开始学习jvm内存划分的时候我是这样理解的:就将jvm内存划分为栈和堆,栈用来存放局部变量和引用,堆区是用来创建对象的,所有的对象都在这里创建,方法区是堆区的一部分用来存放常量。
直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError 异常出现,所以我们放到这里一起讲解。
在JDK 1.4 中新加入了NIO(NewInput/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java 堆里面的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
对象的访问
逻辑内存模型我们已经看到了,那当我们建立一个对象的时候是怎么进行访问的呢?
在Java 语言中,对象访问是如何进行的?对象访问在Java 语言中无处不在,是最普通的程序行为,但即使是最简单的访问,也会却涉及Java 栈、Java 堆、方法区这三个最重要内存区。
以下面这段代码为例进行分析
Object obj = new Object();
这段代码看似简单,却牵涉到了java虚拟机栈,堆区和方法区三个重要的区域。
1、首先Object obj会在java虚拟机栈的当前帧中创建obj的局部变量,类型为reference;
2、当遇到new这个关键词的时候,虚拟机会检查虚拟机中对对应的类型信息是否已经加载。如果没有加载,就先加载这个类。本例中会检查Object类信息是否被加载。
3、确认类信息被加载之后虚拟机会为对象分配内存空间(这里分配的是实例数据的内存空间)。
4、接下来是将obj变量指向堆中创建的Object对象的实例,同时通过obj的引用应该还可以找对Object类在方法区中对应的类型信息。