JVM虚拟机(这里讲的是HotSpot)的内存可以抽象的分为两大块
程序计数器是一块比较小的内存区域,主要的作用是记录当前线程执行的字节码指令位置。线程在执行字节码指令时,根据这个来定位当前执行的字节码指令位置,而且还可以通过它来计算下一个字节码指令。
例如以下某个方法的字节码,程序计数器的作用是保存本线程当前执行的指令位置,因此 程序计数器是线程私有的
,每个线程都有。
0: iconst_1 1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 4: astore_1 复制代码
线程在上下文切换的时候要保存运行现场,在恢复现场时,就可以根据程序计数器来确定该线程上次执行的指令的位置,从而让线程继续往下执行。
如果线程执行的是本地方法,此时程序计数器的值为空
虚拟机会为每个正在执行的方法创建一个栈帧,保存该方法的局部变量、操作数栈、动态链接、方法出口等信息。
每个方法从调用到执行完成的过程,对应着一个栈帧在虚拟机中入栈到出栈的过程。
存放着方法运行时在该方法内定义的变量,这些变量在方法执行结束后会被回收
进行算术操作时存储变量,方法的参数也存在操作数栈中
每一个栈帧中都有一个指向常量池中该方法的引用,持有这个引用是为了在方法调用过程中的动态连接。在Class文件的常量池中存有大量的符号引用,执行方法调用直接调用这些符号引用即可。一些符号引用在类加载阶段或第一次使用时转为直接引用,这种转化成为静态解析。如果符号引用在运行期才转化为直接引用,则称为动态连接。
方法退出有两种方式:return字节码退出和遇到异常退出。
本地方法栈和java虚拟机栈非常类似,也是线程私有,也是具有一些信息存储。他们之间唯一的差别就是java虚拟机栈是为了java程序中的方法也就是字节码的方法服务的,而本地方法栈是给Native方法服务的(即使用c语言实现的方法)。
方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
运行时常量池
是方法区的一部分,这部分虽属于方法区,但在具体的环境中的作用很关键。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
在Hotspot虚拟机中常把方法区成为“永久代”,但是两者有本质的区别,仅仅是因为HotSpot虚拟机使用永久代来实现方法区而已,这样虚拟机的垃圾回收就可以不去处理该区域的数据。在其他的虚拟机(如BEA JroJrockit、IBM J9等)是没有永久代的概念的。实际上使用永久代来实现方法区并不是一个好主意,它有可能导致内存溢出问题。
堆可以说是虚拟机中内存分布最大的一块,它也是线程共享的。在java中,几乎所有的对象实例都存放在堆区域,但随着JIT编译器和逃逸分析的技术逐渐成熟,线上分配、标量替换优化技术会导致一些微妙的变化,所有实例都在堆上存储变得不再那么绝对。
java堆是垃圾回收的主要区域,从垃圾回收的角度来看,它可以分为新生代和老年代。在新生代中又可以细分为Eden空间、From Survivor空间和To Survivor空间。其实无论怎么划分,都与存放的内容无关,进一步的划分只是为了更好更快的分配内存。
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的的一部分。但这部分内存在项目中也会被频繁的使用,而且也可能导致OOM异常,所以我们一起进行归类。
在JDK1.4中新加入了NIO类,引入了一种基于通道与缓冲区的I/O方法,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的对象堆这块内存进行操作。
本文主要讲述了JVM的内存分布,主要分为线程私有和公用两部分