2019独角兽企业重金招聘Python工程师标准>>>
##JVM运行时数据区
一谈到数据区域,大多数都会和C,C++相比较;对于Java程序员来说,不再需要为每一个new操作都写一个配对的delete、free代码,也很少会遇到像C++程序中那样的内存泄漏问题。但尽管JVM帮助了会自动对内存进行分配和回收,但一遇到内存泄漏,也是个相当棘手的问题。
Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。根据Java虚拟机规范,运行时数据区域主要包含下面6个:
PC寄存器(线程私有)
Java 虚拟机可以支持多条线程同时执行,当程序线程数量超过CPU数量时,线程之间根据时间片轮询抢夺CPU资源,对于单核CPU来说,每一个时刻,只能有一个线程在运行,而其他线程必须被切换出去。因此,为了线程切换后能恢复到正确的执行位置,每个线程都有一个独立的程序计数器,用于记录下一条要运行的指令,各个线程之间的计数器互不影响,是一块线程的私有内存空间。在任意时刻,正在被线程执行的方法称为该线程的当前方法。如果这个方法不是native的,那PC寄存器就保存Java虚拟机下一条要执行的字节码指令的地址,如果该方法是native的那PC寄存器的值是undefined。PC寄存器的容量至少应当能保存一个returnAddress类型的数据或者一个与平台相关的本地指针的值。
此区域是唯一一个Java虚拟机没有规定任何OutOfMemoryError情况的区域,一般情况下也不会出现StackOverflow,因为PC寄存器所占内存很小。
Java虚拟机栈(线程私有)
每一条Java虚拟机线程都有自己私有的Java虚拟机栈,这个栈与线程同时创建,线程每执行一个方法就在栈上创建一个栈帧,用于保存该方法的局部变量表,操作数栈,动态链接,方法返回地址等信息,每个方法的调用和都伴随着栈帧的入栈操作,方法的返回则表示栈帧的出栈操作。方法调用时,方法的参数和局部变量越多,局部变量表就越大,栈帧相应变大以满足方法调用所需传递的信息。
Java虚拟机规范允许Java虚拟机栈被实现成固定大小
的或者是根据计算动态扩展和收缩
的。
如果采用固定大小的Java虚拟机栈设计,那每一条线程的Java虚拟机栈容量应当在线程创建的时候独立地选定。Java虚拟机实现应当提供给程序员或者最终用户调节虚拟机栈初始容量的手段。对于可以动态扩展和收缩Java虚拟机栈来说,则应当提供调节其最大、最小容量的手段。
Java虚拟机栈可能发生如下异常情况:
- 如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量时,Java虚拟机将会抛出一个StackOverflowError异常。
- 如果Java虚拟机栈可以动态扩展(当前大部分虚拟机都可以动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。
线程每调用一个方法,就会在栈中创建一个栈帧,栈桢不断增加,该线程的栈内存也不断被使用,最终达到-Xss的值时,会抛出StackOverFlowError。下面是个例子
```
public class TestStack{
private static int count=0;
public static void recursion(long a,long b,long c){
long d=0;
long e=0;
long f=0;
count++;
recursion(a,b,c);
}
public static void main(String []args){
try{
recursion(1L,2L,3L);
}catch(Throwable e){
System.out.println("count:"+count);
System.out.print(e);
}
}
public void recursion1(long a,long b,long c){
long d=0;
long e=0;
long f=0;
count++;
recursion1(a,b,c);
}
}
/*
直接运行
输出:
count:4927
使用-Xss来设置每个线程的栈大小,运行 java -Xss1m TestStack
输出:
count:27590
*/
```
对于无法申请到足够的内存去完成扩展而抛出的OutOfMemory,我在代码中一直无法重现,只能在多线程下,不断的创建一直运行的线程,让其不断的申请线程运行所需要的栈空间,导致内存不足而抛出OOM。此处还需要再研究。
Java堆(线程共享)
Java堆是Java虚拟机中被所有线程共享的一块内存区域,跟随虚拟机创建而创建;它存储了被自动内存管理系统所管理的各种对象,这些受管理的对象无需,也无法显式地被销毁。几乎
所有的对象和数组都是在这里分配的,但是随着JIT编译器和逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化(这里还不是很懂,待研究。不过好像在hotspot中,Class类对象应该是存储在方法区的)。 如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那Java虚拟机将会抛出一个OutOfMemoryError异常。
方法区(线程共享)
和堆一样,也是线程共享的。它用于存储已被Java虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。一般来讲,程序启动一段时间后该区大小也就固定了,但对于项目中存在对类的动态编译生成时(比如spring代理类),则需要注意方法区大小是否满足,经常碰到的java.lang.OutOfmemoryError: PermGen Space
就是出现在这个区域。还有一点就是,从jdk7开始, interned strings(String的intern(),我理解的就是字符串常量池)已经从方法区移出到堆内了。而对于永久代
这个说法,只存在与hotspot虚拟机中,其它的虚拟机(像IBM J9)是不存在永久代的。如果方法区的内存空间不能满足内存分配请求,那Java虚拟机将抛出一个OutOfMemoryError异常
运行时常量池(线程共享)
运行时常量池(Runtime Constant Pool)是每一个类或接口的常量池的运行时表示形式,它包括了若干种不同的常量:编译期可知的数值字面量,必须运行期解析后才能获得的方法或字段引用。每一个运行时常量池都分配在 Java 虚拟机的方法区之中,在类和接口被加载到虚拟机后,对应的运行时常量池就被创建出来。
在创建类和接口的运行时常量池时,可能会发生如下异常情况:当创建类或接口的时候,如果构造运行时常量池
所需要的内存空间超过了方法区
所能提供的最大值,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。
本地方法栈
Java 虚拟机实现可能会使用到传统的栈(通常称之为“C Stacks”)来支持native方法的执行,这个栈就是本地方法栈。当 Java 虚拟机使用其他语言(例如 C 语言)来实现指令集解释器时,也会使用到本地方法栈。如果线程请求分配的栈容量超过本地方法栈允许的最大容量时,Java 虚拟机将会抛出一个StackOverflowError 异常。如果本地方法栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的本地方法栈,那 Java 虚拟机将会抛出一个OutOfMemoryError 异常。