在前面的几篇博文中,我们一起简单的了解jvm的基本知识,例如jvm对字符串的处理等等,或许大家看完后就把这当成一条准则来记住了,但是一些比较好奇的朋友有没有想过,这是为什么呢?他的原理是什么呢?下面就让我们开始一步一步的深入学习。
在这篇博文中呢,我打算主要就讲Java内存区域与内存溢出异常吧。下面言归正传吧。
1.Java虚拟机运行时数据区
在前面的几篇博文中,我们只是简单的把内存区域分为了堆和栈,但其实,这种分法是十分粗糙的,jvm在实际运行的时候,内存区域的划分绝对不是那么简简单单的就两块,我们一起看下面这个图就知道了。
从上图我们知道了,JVM虚拟机运行时数据区主要划分为:方法区、虚拟机栈、本地方法栈、堆、程序计数器。
1.1程序计数器
虽然在上图中,程序计数器这块占用的区域画的很大,但其实,在内存中,它只是较小的一块内存空间。
1)生命周期:线程私有的,与它所绑定的线程相同。
2)作用:当前线程所执行的字节码的行号指示器,简单的可以理解成,程序计数器记录着下一行要执行的代码行数,比如我们经常写的分支、循环、跳转都是通过改变该计数器的值来完成的。如果一个线程正在执行的是Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器则为空。
3)程序运行时该区域可能发生的异常:此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
1.2Java虚拟机栈
其实这里所说的栈,就是前面我们博文中粗糙分法堆栈中的栈了。精确的讲的话,其实是Java虚拟机栈中的局部变量表部分吧;同时这里是专门为虚拟机执行java方法服务的,为什么这么说呢?因为内存区域中还有本地方法栈,本地方法栈是只为虚拟机使用到的Native方法服务的。
1)生命周期:线程私有的,与它所绑定的线程相同。
2)作用:每个方法被执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。其中局部变量表存储的是以下类型:
①基本数据类型:boolean, byte, char, short, int, float, long, double;
②对象引用:reference类型,它不等于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用文章,也可能是指向一个对象句柄或其他与此对象有关的位置;
③returnAddress类型:指向了一条字节码指令的地址,简单的说,就是下一条要执行的代码的地址;
3)程序运行时该区域可能发生的异常:
①线程请求栈深度 > 虚拟机允许深度,抛出*Error;eg:
1 package edu.outofmemoryerror.heap;
2
3 public class JavaStackSOF {
4 private int stackLength = 1;
5
6 public void stackLeak() {
7 stackLength++;
8 stackLeak();
9 }
10
11 /**
12 * -Xss128K
13 * @param args
14 */
15 public static void main(String[] args) {
16 JavaStackSOF sf = new JavaStackSOF();
17 sf.stackLeak();
18 }
19 }
运行结果:
注意在运行的时候注意要改变那些参数;如果是通过控制台命令来执行的,可以直接跟在Java命令之后书写,如果是通过Eclipse来执行的可以在下面页面添加上再来执行,如图:
②该栈允许动态拓展,当无法申请到足够的空间时,抛出OutOfMemoryError异常;
1.3本地方法栈
本地方法栈跟虚拟机栈相似,他们的区别在前面已经提过了:本地方法栈是只为虚拟机使用到的Native方法服务的,而java虚拟机栈是专门为虚拟机执行java方法服务的。
1)生命周期:线程私有的,与它所绑定的线程相同。
2)作用:为使用到的Native方法服务。
3)程序运行时该区域可能发生的异常:跟虚拟机栈一样。
1.4Java堆(Java Heap)
对于很多应用来说,Java堆是Java虚拟机所管理的内存中最大的一块。
1)生命周期:线程共享的,在虚拟机启动时就创建
2)作用:存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
3)程序运行时该区域可能发生的异常:堆中没有内存完成实例分配或者堆无法再拓展时,将会抛出OutOfMemoryError异常,eg:
1 package edu.outofmemoryerror.heap;
2
3 import java.util.ArrayList;
4 import java.util.List;
5
6 public class HeapOOM {
7 static class OOMObject { }
8
9 /**
10 * -Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError
11 * @param args
12 */
13 public static void main(String[] args) {
14 List<OOMObject> list = new ArrayList<OOMObject>();
15 while (true) {
16 list.add(new OOMObject());
17 }
18 }
19 }
运行结果:
1.5方法区(Method Area)
方法区有一个别名叫做非堆(Non-Heap),对于习惯在HotSpot虚拟机上开发和部署程序的开发者来说,它也叫“永久代”。
1)生命周期:线程共享的,在虚拟机启动时就创建
2)作用:存储已被虚拟机加载的类信息、常量、静态变量,也就是编译器编译后的代码数据。
3)程序运行时该区域可能发生的异常:当方法区无法满足内存分配需求时,将会抛出OutOfMemoryError异常。比如java.lang.OutOfMemoryError,这个错误其实对于经常使用SSh框架进行开发的我们应该很熟悉,它就是由于方法区空间不足导致的内存溢出。
因为该区域中的垃圾收集行为是比较少出现的,所以对象相当于永久不被回收一般,所以在这个区域就叫做“永久代”了。
1.5.1运行时常量池
运行常量池是方法区的一部分。
1)生命周期:线程共享的,在虚拟机启动时就创建
2)作用:存放Class文件中类的版本、字段、方法、接口等描述信息外,还有一些信息是常量池,用于存放编译期间生产的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行是常量池中。
3)程序运行时该区域可能发生的异常:运行时是方法区的一部分,自然收到方法区内存的限制,当常量池无法在申请到内存是就会抛出OutOfMemoryError异常
看到这里,不知道大家会不会想起前面一篇博文中我们说的JVM对字符串的处理呢?
没错,在那篇博文中讲的Java字符串缓冲池,其实就是运行时常量池的一部分,而运行池是在方法区中的,方法区中的垃圾收集行为是几乎没有的,相当于不会被回收。这就是前面的JVM对字符串处理的原理了。
1.6直接内存
其实直接内存不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中的一部分,但是这部分也频繁的使用,也可能会导致OutOfMemoryError异常的出现。
2引用对象的访问
在之前我们有一篇博文讲了关于对象与内存管理的知识,但是那里只是比较简单的,下面我们更加深入了解一下,在Java语言中,对象访问是如何进行的?
其实对象访问并不像前面那篇博文中讲的那么简单只涉及到堆和栈,因为即使是最简单的访问,也会涉及到Java虚拟机栈,Java堆,方法区这三个重要的内存区域。之所以只讲堆和栈是因为开发人员在大多数情况下关注的最多的是堆和栈两个区域。
关于基本类型的访问,在前面那篇博文中我们已经讲了,下面,我们先提一下相关的概念:
1)对象类型数据:指的是该对象的对象类型,父类,实现的接口,方法等,其实就是前面说的,方法区存储的是加载的对象的信息。
2)对象实例数据:指的是该类实例化后的对象的实例变量的值。
接着,我们继续重点深入讲解下关于引用类型对象的两种访问方式:
1)使用句柄方式访问
使用这种对象的访问方式的优势是:存储的是稳定的句柄地址,在对象移动时只会改变句柄中的实例数据指针,而reference本身不用改变。
使用句柄池访问方式,Java堆中将会划分出一块内存来作文句柄池,reference中存储的是对象的句柄地址,而句柄中包含了对象实力数据和类型数据各自的具体地址信息。
其实从上图中,我们还能看出一个知识点,那就是:64位长度的long和double类型的数据会占用2个局部变量空间
2)使用直接指针访问方式
使用这种访问方式的优势是:访问速度快。