java虚拟机在运行时将会包括以下几个运行时数据区域:
1.程序计数器
程序计数器是一块较小的内存空间,它是当前线程执行字节码的行号指示器(注意是当前线程),因此每个线程都有一个独立的程序计数器。在虚拟机里,字节码解释器就是改变记数器的值来选取下一条要执行的字节码指令。在这里有人可能问,为什么会为每个线程分配一个独立的计数器? 这是针对java多线程的而设计的。 实际上多线程其实不是真正意义上同时在并发执行,实际上cpu是在很短的时间内运行不同的线程,让人有一种错觉是所有线程是同时运行的,实际上每一刻都只运行一个线程。Java也是如此,想想,如果java现在运行线程A,跑到了一半去运行线程B,B跑到一半又准备跑线程A,此时如果没有自己的计数器怎么知道接下来A从哪里开始?
因此为了线程切换后能恢复到正确的执行位置,每条线程都需要由一个独立的程序计数器,各线程之间的计数器互不影响,独立存储。并且我们定义对于这种的内存区域为“线程私有”的内存。
注:如果线程正在执行一个Java方法,这个计数器记录的是执行的虚拟机字节码指令的地址;如果执行的是一个native方法,这个计数器为空(undifined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError的区域。
2.Java虚拟机栈
它的最大默认容量:1m
java虚拟机栈是线程私有的,每一个线程都有自己的java虚拟机栈,这个栈与线程同时创建,它的生命周期与线程相同。
它描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。(这里,注意我打红的字。注意栈和栈帧的区别)一个线程并不是只会运行一个方法,比如在线程X我先运行A(),A()运行完了运行B(); 每当遇到方法调用,java虚拟机会在栈中划分一块内存成为栈帧,栈帧中的内存供 局部变量(包括基本类型和引用类型) 使用,当方法调用结束后,java虚拟机是收回栈帧所占的内存。
局部变量表存放了编译器可知的各种基本数据类型、对象引用、returnAddress类型(指向一条字节码指令的地址)。
如果线程请求的栈深度大于虚拟机所允许的深度将抛出StackOverflowError;
如果JVM Stack可以动态扩展,但是在尝试扩展时无法申请到足够的内存时抛出OutOfMemoryError。
3.Java堆
最大默认容量:64M
Java堆是被所有线程共享的一块内存区域,在虚拟机启时创建。
它主要的目的就是存放对象实例。因为所有的对象实例和数组都要在堆上分配。
如果在堆中没有内存完成实例分配并且堆也无法扩展,就会抛OutOfMemoryError异常。
具体实例和下面的方法区一起举例讲解。
4. 方法区
是所有线程共享的内存区域,在虚拟机启时创建。
它用于存储已经被虚拟机加载的类信息、常量、静态常量。
如果堆中没有内存完成实例分配并且堆也无法扩展,就会抛OutOfMemoryError。
举个例子;
public class student {
public String name;
public int age;
public void study(){
System.out.println("123");
}
}
public class demo {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
int i = 10;
student stu1 = new student(); //new student()在堆上分配 stu1 栈区 student的信息保存在方法区
stu1.name="Tom";
stu1.age=18;
student stu2 = new student();
stu2.name= "Jerry";
stu2.age=22;
}
}
5.运行时常量池
它是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只能在编译期产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。
既然运行时常量池是方法区的一部分,自然会受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
其中比较特殊的是字符串常量池。它将编译时可以确定的字符串常量放入池中,将相同内容的字符串引用字符串池中的同一个对象。下例
package testString;
public class testString {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
String a = "aaa";
String aa = "aaa";
//输出为true ,因为a和aa是在编译期间确定了的表达式,a和aa使用了字符串常量池,他们的地址相同
System.out.println(a == aa);
String b = "a"+"a"+"a";
//输出为true,编译期间确定
System.out.println(a == b);
String c = new String("aaa");
//输出为false ,这里new String 是在堆上分配的,因此地址不同
System.out.println(a==c);
//输出为true ,c.intern()的含义:如果常量池有一个等于此String对象的字符串,则返回地址中的字符串;否则
//在将该对象添加到常量池中,并且返回此String对象的引用。
System.out.println(a == c.intern());
String d = "a";
String e = d +"aa";
//输出为false,表达式中存在变量,无法再编译是确定,不存放于常量池
System.out.println(a ==e);
}
}
附录两个,不太常用的。
6. 本地方法栈
本地方法栈与虚拟机栈作用相似,后者为虚拟机执行Java方法服务,而前者为虚拟机用到的Native方法服务。
虚拟机规范对于本地方法栈中方法使用的语言,使用方式和数据结构没有强制规定,甚至有的虚拟机(比如HotSpot)直接把二者合二为一。
native方法是指由java调用另外一种语言(比如c/c++)实现的的方法。
7. 直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
JDK1.4加的NIO中,ByteBuffer有个方法是allocateDirect(int capacity) ,这是一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。