运行时数据区域:
包括 方法区,虚拟机栈,本地方法栈,堆 和程序计数器。
程序计数器:
是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。
每一个线程都有自己私有的程序计数器。
如果线程正在执行的是一个JAVA方法,该计数器记录的是正在执行的虚拟机字节码指令的地址,如果正在执行的是native方法,则计数器值为空(undefined)。此内存区域是唯一一个在JAVA虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
JAVA虚拟机栈:
也是线程私有的,生命周期和线程相同。每个方法被执行的时候都会同时创建一个栈帧(stack frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每个方法被调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中从入栈道出栈的过程。
这个JAVA虚拟机栈就是我们常说的“栈”。局部变量表存放了元数据类型、引用类型
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
在JVM规范中,对这个区域规定了两种异常情况:
- 如果线程请求的栈深度大于JVM允许的深度,抛出StackOverflowError
- 如果虚拟机栈可以动态扩展(目前大部分JVM都可动态扩展),当扩展时无法申请到足够的内存时,会抛出OutOfMemoryError异常
本地方法栈:
与虚拟机栈作用类似,
只不过前者是执行JAVA方法服务,而本地方法栈是为Native方法服务。
HotSpot将本地方法栈和虚拟机栈合二为一。
和虚拟机栈一样会抛出来两种异常。
JAVA堆:
JAVA堆是被
所有线程共享的一块内存区域,
在JVM启动时创建。
其作用就是存放对象实例。
JVM规范规定:所有的对象实例及数组都要在堆上分配。
JAVA堆也是垃圾回收管理的主要区域。
由于现在收集器基本采用分代收集算法,所以JAVA堆还可以细分为:新生代和老年代,再细分还有Eden空间、From Survivor空间、To Survivor空间。
根据JVM规范,JAVA堆可以处于
物理上不连续的内存空间中,只要
逻辑上是连续的即可,
可以是固定大小的,也
可以是可扩展的(用
-Xmx和-
Xms控制),如果堆中没有内存,也无法扩展,抛出
OutOfMemoryError。
方法区:
也是
所有线程共享的内存区域。它用于存放虚拟机加载的
类信息、
常量、
静态变量、
即时编译器编译后的代码等数据。
在JVM规范中被描述为堆的一部分,但却又有一个non-heap的别名。
对于HOTSPOT虚拟机,方法区又被称为永久代(Permanent Generation)
这个区域一样不需要连续的内存,可以选择固定大小或者扩展,还可以选择不实现垃圾回收。确实这个区域的数据一般不参与回收,但这些数据并不一定就是永久存在了,常量池和对类型的卸载也可以成为回收的目标。
抛出OutOfMemoryError
运行时常量池:
是方法区的一部分。
在
编译期生成的
各种字面量和符号引用,这部分内容将在类加载后存放到
方法区的运行时常量池中。
这个区域并不是只有编译时产生的常量,也有运行时产生的常量,比如String.intern()方法。
OutOfMemoryError异常
直接内存:
Direct Memory
在JDK1.4中引入了NIO,是一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接
分配堆外内存,然后通过一个
存储在JAVA堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。
抛OutOfMemoryError异常
Object obj = new Object();
Object obj反映到JAVA栈的本地变量表中
new Object()反映到JAVA堆中,长度不确定,在JAVA堆中还必须包含能查找到此对象类型数据(如
对象类型、父类、实现的接口、方法等)的地址信息,这些
类型数据则存放在方法区中。
引用访问对象的方法主流的有两种:
- 使用句柄访问方式,JAVA堆中将会划分出一块内存来作为句柄池,引用对象存储的是对象的句柄地址,而句柄中包含着对象实例数据和类型数据各自的具体地址信息。
- 直接指针访问方式。引用对象中直接存储的就是对象地址。,在堆中的对象又有一个指针指向方法区的对象类型数据。
句柄访问方式最大的好处是在对象被移动时(
垃圾回收经常会移动对象),只用改变句柄中的实例数据指针,而引用对象不用修改。
直接指针方式最大的好处是
速度更快。
HOTSPOT使用这种方式。
设置堆不可扩展,将-Xms和-Xmx参数设置一样即可。
栈溢出
-Xoss参数设置本地方法栈大小(在HOTSPOT虚拟机中,由于本地方法栈和虚拟机栈是一起的,所以这个参数是无效的),-Xss参数设定栈大小。
栈溢出会抛出两种异常stackoverflow和outofmemory,前者表示线程请求的栈深度大于虚拟机所允许的最大深度,后者表示无法申请到足够的内存空间。
实验表明,单线程只会有stackoverflow,不会有outofmemory.但多线程会产生Outofmemory。
这是因为,
OS分配给每个进程的内存是有限制的,比如32位windows限制为2GB,栈内存的总量是由2GB减去堆内存(Xmx),再减去方法区容量(MaxPermSize),因为程序计数器消耗内存很小,可以忽略不计。而栈内存又被多个线程所瓜分,这样在线程很多的情况下,内存就可能会耗尽。
运行时常量池溢出
通过String.intern()方法可以向运行时常量池增加内容,
该方法会检测常量池中是否有某个字符串,如果有就返回池中的字符串,否则将该字符串加入常量池,再返回该常量池字符串的引用。
由于常量池分配在方法区中,可以通过
-XX:PermSize和
-XX:MaxPermSize来限制方法区大小。
方法区溢出的标志就是OutOfMemoryError: PermGen Space.
方法区溢出
主要是由于
动态生成了太多的类造成的,这种溢出的实际场景比如
大量JSP或动态生成JSP文件的应用(JSP第一次运行时会需要编译成JAVA类)CGLIB等动态代理生成框架产生的动态类等
本机直接内存溢出
DirectMemory容量可以由-XX:MaxDirectMemorySize指定,如果不指定,则默认与JAVA堆最大值(-Xmx)一样。实际上DirectByteBuffer并没有直接申请分配内存,而是先计算,如果计算得知无足够内存可分配,就抛出异常。