运行内存区域划分图
栈
与程序计数器一样java虚拟机也是线程私有的,他的生命周期与线程相同,虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame,可以这么理解栈帧,虚拟机栈包含N个栈帧每个栈帧包含局部变量表,操作数栈,动态链接,方法出口等信息)。每个方法从调用到执行完成这个过程,就对应这一个栈帧在虚拟机栈中的入栈到出栈的过程。
局部变量表存放了编译期可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double),对象引用(reference类型,他不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此相关的位置)和returnAddress类型(指向了一条字节码指令的地址),其中64位长度的long和double类型会占用2个局部变量空间,其余的数据类型只会占用1个局部变量空间。局部变量表所需的内存空间在编译期间完成内存分配,当进入一个方法时,这个方法需要在帧中分配多大的内存空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
在java虚拟机规范中,对这个区域规定了两种异常状态:如果线程请求的栈的深度大于虚拟机允许的深度,将抛出StackOverFlowError异常(栈溢出),如果虚拟机栈可以动态扩展(现在大部分java虚拟机都可以动态扩展,只不过java虚拟机规范中也允许固定长度的java虚拟机栈),如果扩展时无法申请到足够的内存空间,就会抛出OutOfmMemoryError异常(没有足够的内存)。
程序计数器
程序计数器是一块较小的内存空间。他可以看做是当前线程所执行的字节码行号指示器。字节码解释器工作就是通过改变这个计数器的值来选择下一条需要执行的字节码指令。分支,循环,跳转,异常,线程恢复等基础功能都需要依赖这个计数器来完成。本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,他们之间的区别不过是虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的本地Native方法服务,在虚拟机规范中对本地方法栈中的使用方法,语言,与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(例如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样本地方法栈也会抛出StackOverFlowError和OutOfmMemoryError异常。
java堆
对于大多数应用来说,java堆(java Heap)是java虚拟机管理内存中的最大一块。java堆是所有线程共享的一块内存管理区域,在虚拟机启动时创建。此内存区域唯一目的就是存放对象的实例。
几乎所有对象实例都在堆中分配内存。这一点在java虚拟机规范中的描述是:所有对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸技术逐渐成熟,栈上分配,标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也不是变的那么“绝对”了。 java堆是垃圾回收器管理的主要区域,因此很多时候也被称为GC堆(Garbage Collected Heap)。
从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以java堆中还可以细分为:新生代和年老代:在细致一点的划分可以分为:Eden空间,From Survivor空间,To Survivor空间等。从内存分配的角度来看,线程共享的java堆中可能划分出多个线程私有的分配缓冲区 ,不过无论如何划分,都与存放内容无关,无论哪个区域存放的都是对象实例。进一步划分的目的是为了更好的回收内存,或者更快的分配内存。
根据java虚拟机规范的规定,java堆可以处在物理上不连续的内存空间,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现上既可以实现成固定大小,也可以是可扩展的大小,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中没有内存实例完成分配,并且堆也无法在扩展时将会抛出OutOfMemoryError异常。方法区
方法区和java堆一样,是各个 线程共享的内存区域,他用于存储已被虚拟机加载的 类信息, 常量, 静态变量,即时编译器编译后的代码等数据。虽然java虚拟机规范把方法区描述为堆的一部分,但是他还有个别名叫做Non-heap(非堆),目的应该是与java堆区分开来。java虚拟机规范对方法区的限制非常宽松,除了和java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样永久存在了。这区域的内存回收目标重要是针对常量池的回收和类型的卸载,一般来说这个内存区域的回收‘成绩’比较难以令人满意。尤其是类型的卸载条件非常苛刻,但是这部分的回收确实是必要的。在sun公司的bug列表中,曾出现过的若干个严重的bug就是由于低版本的HotSpot虚拟机对此区域未完成回收导致的内存溢出。
HotSpot虚拟机处理方法区的时候选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作。可是其他的虚拟机并不会这样处理。
方法区结构大概如下图:
- 常量池(Constant Pool):常量池数据编译期被确定,是Class文件中的一部分。存储了类、方法、接口等中的常量,当然也包括字符串常量。
- 字符串池/字符串常量池(String Pool/String Constant Pool):是常量池中的一部分,存储编译期类中产生的字符串类型数据。
- 运行时常量池(Runtime Constant Pool):方法区的一部分,所有线程共享。虚拟机加载Class后把常量池中的数据放入到运行时常量池。
常量池
- 常量池:可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目资源关联
- 常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)
- 字面量:文本字符串、声明为final的常量值等;
- 符号引用:类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符
常量池的变化
在JDK1.6之前字符串常量池是存在于方法区之中,在JDK1.7和以上字符串常量池存在了堆之中
常量池的理解
- 必须要关注编译期的行为,才能更好的理解常量池。
- 运行时常量池中的常量,基本来源于各个class文件中的常量池。
- 程序运行时,除非手动向常量池中添加常量(比如调用intern方法),否则jvm不会自动添加常量到常量池。
- 对于8种基本数据类型大部分都有自己的封装类,其中Byte,Short,Integer,Long,Character,Boolean都实现了常量池技术。而Float,Double无。
常量池的面试题
string的比较
public static void main(String[] args) {
String s1="abc1";//此处是数字1
String s2="abc"+1;
System.out.println(s1==s2);// 第一次比较 true
String s3="ab";
String s4="c";
String s5="abc";
String s6=s3+s4;
System.out.println(s5==s6);// 第二次比较 false
}
第一次比较的那里,因为字符串abc和数字1都是字面量,所以加起来还是个字面量,又因为常量池中已经有了s1指向的字面量abc1,所以s2也是指向了字面量abc1。
第二次比较那里,这时候的+两面是对象,其实是这样的,对于String s6=s3+s4; 其实运行时是这样的String s6=new StringBuilder().append(s3).append(s4).toString(); 这里的过程是通过StringBuilder这个类实现的,我们来看一下StringBuilder类中的toString()的源码:
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
它是通过new String()的方式来作为值进行返回的,所以是在堆中开辟的一块空间。所以和常量池中的不一样。结果是false。
String s1=newString("abc");到底创建了几个对象呢?
- 类加载时,对于一个类,类加载只会进行一次。此类进行加载时,会把字符串abc放进全局的常量池中,进行保存。
- 运行时,当你运行程序的时候,堆里开辟内存创建一个String对象,因此这条语句创建了两个对象。[jdk7 全在堆,new必创建]
基本类型的比较
用"=="比较 如果一边出现基本类型,包装类型就拆箱。全是包装,不拆箱。new必创建。
Integer x = 123;
Integer y = 123;
System.out.println(x == y); // true 基本类型的包装类 引入常量池技术,同一块内存区域。Byte,Short,Integer,Long,Character,Boolean都实现了常量池技术。
Integer a = 123456;
Integer b = 123456;
System.out.println(a == b); // false 数据范围超出常量池包括的内容
int c = 12345678;
int d = 12345678;
System.out.println(c == d); // true 基本类型相比
int e = 123456;
System.out.println(a == e); // true 如果一边出现基本类型,包装类型就拆箱,相当于基本类型相比
直接内存
直接内存并不是虚拟机运行内存的一部分,也不是java虚拟机规范中定义的内存区域。但是这部分内存区域也被频繁的使用,也可能导致OutOfMemoryError异常出现, 在jdk1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)和缓冲区(Buffer)的I/O方式,他可以使用本地的函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景中显著提高性能,因为避免了在java堆中和Native堆中来回复制数据。 显然本机直接内存的分配不会受到java堆大小的限制,但是既然是内存。肯定还会受到本机总内存的限制。服务器管理员在配置虚拟机内存参数时,会根据实际内存设置-Xmx等参数信息。但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理和操作系统级的限制),从而导致动态扩展时OutOfMemoryError异常。