Java内存区域与内存溢出
-
程序计数器
程序计数器是一块较小的内存,它是线程私有的,可以看作是当前线程执行字节码的计数器。在虚拟机的概念模型中,字节码解释器就是通过这个计数器来找到下一个将要执行的指令。java中分支语句,循环,异常处理以及线程恢复都是通过程序计数器来实现的。
由于JVM在执行线程的时候是通过CPU轮流执行各个线程的,CPU每次只能执行一个线程的某个指令。这就要求每次在切换线程的时候要能恢复到正确的指令执行位置。因此线程的程序计数器必须是线程私有的。各个线程之间互不影响,独立存储。
如果该线程正在执行的是一个java方法,则该程序计数器的值就是该方法编译后的正在执行的当前的虚拟字节码的指令位置。如果当前线程执行的是一个native方法,则计数器值为空Undefined。该区域是虚拟机规范中未规定OutOfMemoryError情况的区域。
-
虚拟机栈
该区域也是线程私有的,并且其生命周期与对应线程生命周期一致。虚拟机栈描述的就是java方法执行的内存模型:每个方法在执行的时候都会在虚拟机栈上建立一个栈帧用来保存局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用就对应着一个栈帧在虚拟机栈中从入栈道出栈的过程。
我们看到经常有人把内存分为堆内存和栈内存。此处的栈内存就是指虚拟机栈上的局部变量表。局部变量表中存放了编译期可知的各种基本的数据类型,int,float,boolean……以及对象的引用。其中64位长的long和double类型的数会占用两个局部变量存储空间(slot)。其余的均是占用一个。局部变量表所需要的内存空间是编译期间完全确定的。运行期间不会改变大小。在java虚拟机规范中规定里对这个内存区的两种异常情况:如果线程请求的栈深度大于虚拟机允许的最大深度将抛出StackOverflowError.如果虚拟机栈可以动态扩展的话,在扩展的时候无法申请到在足够的内存,则会抛出OutOfMemoryError。
-
本地方法栈
同上述两个区域一样,该区域也是线程私有区域。它与虚拟机栈发挥的左右是类似的,不同的是虚拟机栈是为java方法服务的,而本地方法栈则是为java调用的本地方法服务的。本地方法栈区也会抛出同虚拟机栈相同的两个异常。
-
java堆
java堆内存是在虚拟机启动的时候创建,所有的对象实例都保存在这个区域,包括数组。因此堆内存也是垃圾收集器的主要工作区域。如果从内存回收的角度来看,现在收集器基本上采用的都是分代收集的策略,所以java堆中还可以细分为新生代和老年代。再细致一点的话可以分为:Eden空间,FromSurvior空间,ToSurvior空间等。
- 方法区
存储已经被虚拟机加载进来的类信息,常量,静态变量,编译后的代码,也是线程共享的。也有人称这个区域为永久代。java虚拟机规范对这一块区域的规定比较宽松,因此该区域上可以实现垃圾收集机制,也可以不实现。该区域的垃圾收集机制主要是针对常量池的回收以及对类型的卸载。该区域不需要连续的内存,并且是可扩展的,当方法区无法满足内存分配需求时候,也会抛出OutOfMemoryError。
运行时常量
运行时常量池是方法区的一个部分,Class文件中除了有类的版本,字段,方法,接口描述等信息之外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载之后放入方法区的常量池区域。
对象访问
Object obj = new Object();
假设上面这行代码出现在一个方法中,那么声明部分“Object obj”这部分语义将会反应到虚拟机栈的局部变量表中,作为一个reference类型的数据出现。而“new Object()”这部分的语义就会反应到堆内存中,形成一块存储了Object类型所有实例数据(对象中各个实例字段的数据)的结构化内存,这块内存的长度是不变的;另外对中还必须包含能查找到该对象数据类型的数据(如对象类型,父类,实现的接口,方法等)的地址信息,这些类型数据则存储在方法区域。综上所述,堆内存要存储两部分数据:(1)对象的实例数据(2)对象类型的数据的地址信息
由于reference类型在java虚拟机规范中没有规定这种引用该通过哪种方式去定位,因此不通的虚拟机实现的对象访问方式不一样,主要有以下两种方式:使用句柄和直接使用指针。
- 如果是通过句柄的方式访问对象,则在java堆中就需要划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中则包含了对象的实例数据地址信息和类型数据的地址信息。
-
-
- 如果通过指针的方式访问对象,reference中则直接存储对象的地址信息
-