java虚拟机基本结构:
JVM是一个内存中的虚拟机,那它的存储就是内存了,我们写的所有类、常量、变量、方法都在内存中,因此明白java虚拟机的内存分配非常重要,本部分主要讲解java虚拟机内存分配。
本部分会从概念上介绍java虚拟机内存的各个区域,讲解这些区域的作用、服务对象以及其中可能产生的问题。
下面通过一个简单的示例,来展示java堆、方法区和java栈之间的关系。
public class SimpleHeap { private int id; public SimpleHeap(int id) { this.id = id; } public static void main(String[] args) { SimpleHeap s1 = new SimpleHeap(1); SimpleHeap s2 = new SimpleHeap(2); s1.show(); s2.show(); } public void show() { System.out.println("my id is" + id); } }
??程序计数器,是一块较小的内存区域。它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
??虚拟机栈描述的是java方法执行的内存模型:每个方法在执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口灯信息。每个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
??经常有人把java内存区域分为堆内存和栈内存,这种分法比较粗糙,java内存区域的划分实际上远比这复杂。这种划分方式的流行只能说明大多数程序员最关注的,与对象内存分配关系最密切的内存区域是这两块。其中所指的栈就是这里的栈,或者更具体说是虚拟机栈中的局部变量表部分。
??在java栈中保存的主要内容为栈帧。每一次函数调用,都会有一个对应的栈帧被压入java栈,每一个函数调用结束,都会有一个栈帧被弹出java栈。如下图所示,函数1对应栈帧1,函数2对应栈帧2,依次类推。函数1中调用函数2,函数2中调用函数3,函数3中调用函数4.当函数1被调用时,栈帧1入栈,当函数2被调用时,栈帧2入栈;当函数3被调用时,栈帧3入栈;当函数4被调用时,栈帧4入栈。当前正在执行的函数所对应的帧就是当前的帧(位于栈顶),它保存着当前函数的局部变量、中间结果等数据。
??当函数返回时,栈帧从java中被弹出。java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令,另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
备注:每次函数调用都会生成对应的栈帧,如果请求的栈深度大于最大的可用栈深度时,系统就会抛出stackoverflowerror栈溢出错误。
在一个栈帧中,至少要包含局部变量表、操作数栈和帧数据区等几个部分。
1)局部变量表
局部变量表存放了编译器可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double)、对象引用(reference类型,他不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和return address类型(指向一条字节码指令的地址)。
returnadress类型(A给命令于B,b反馈于A,这个时候A即为返回地址。)
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
局部变量表用于保存函数的参数以及局部变量。局部变量表中的变量只在当前函数调用中有效,当函数调用结束后,随着函数栈帧的销毁,局部变量表也就随之销毁。
由于局部变量表在栈帧之中,因此,如果函数的参数和局部变量较多,会使得局部变量表膨胀,从而每一次函数调用都会占用更多的栈空间,最终导致函数的嵌套调用次数减少。
2)操作栈
操作数栈主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。
操作数栈也是一个先进后出的数据结构,只支持入栈和出栈两种操作。
把局部变量区的东西拿过来入栈,出栈等等
a =2;
b = 3;
c = a + b;
return c;
c = a +b 时会把局部变量表的a 和 b拿过来入栈,进行运算
3)动态链接
4)方法出口
??本地方法栈与虚拟机栈所发挥的作用是类似的,其区别不过是虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的native方法服务。
??对于大多数应用来说,java堆是java虚拟机所管理的内存中最大的一块。java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的是存放对象实例,几乎所有的对象实例都在这里分配内存。
??java堆是垃圾回收器管理的主要区域,因此很多时候也被称为“GC堆”,如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以java堆中还可以细分为:新生代和老生代;再细致一点的有Eden空间,From Survivor空间、To Survivor空间等。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好的回收内存,或者更快的分配内存。
??是各个线程共享的内存区域,它用于存放已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。它有一个别名NonHeap(非堆),目的应该是与java堆区分开来。方法区的大小决定了系统可以保存多少个类。
??运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段,方法、接口等信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
常量池之中主要存放两大类常量:字面量、符号引用。字面量比较接近于java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
被更新了,自己之前一直以为只有字面量
??直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。但是该部分内存在被频繁的使用,而且也可能导致oom异常出现。
??在jdk1.4中新加入了NIO类,引入了一种基于通道(channel)与缓冲区(buffer)的I/O方式,它可以使用native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在java堆和native堆中来回复制数据。
??显然,本机直接内存的分配不会受到java堆大小的限制,但是受限于本机总内存的大小,同样会产生OOM。