每个Java虚拟机都有一个类加载器子系统,根据某个全限定名来装入类型,同样每个Java虚拟机都有一个执行引擎,它负责执行那些包含在被装载类的方法中的指令。
当虚拟机运行一个程序时,就需要从已加载的文件中得到信息,将这些信息组织到运行时数据区,以便于管理。
Java运行时的数据区域划分
1、程序计数器:程序计数器是一块较小的内存空间,可以看做是当前线程的字节码的行号指示器。
Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个时刻,一个处理器只会执行一条线程中的指令。因此为了线程切换后能恢复到正确的执行位置,每一个线程都需要有一个独立的程序计数器,各条线程之间互不影响,独立存储。所以也称该内存区域为线程私有的内存。
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OOM(OutOfMemoryError)情况的区域。
2、Java虚拟机栈
其生命周期与线程相同,每个方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接等。每一个方法调用到完成的过程,对应着栈帧入栈到出栈的过程。
人们经常说的java内存分为堆内存和栈内存,所谓的栈内存就是指Java虚拟机栈,或者说是虚拟机栈中局部变量表的部分。
局部变量表中存放各种基本类型和对象引用。
若线程请求的栈的深度大于虚拟机所允许的深度,将抛出StackOverFlowError
如果虚拟机的栈可以动态扩展,但扩展也有一个上限,扩展时无法申请到足够的内存,抛出OutOfMemoryError
3、本地方法栈
本地方法栈的作用类似于虚拟机栈。
区别在于Java虚拟机栈为虚拟机执行Java方法服务
而本地方法栈则为虚拟机使用到的Native方法服务
类似虚拟机栈,本地方法栈也会抛出StackOverFlowError和OutOfMemoryError异常
4、Java堆
Java堆是Java虚拟机所管理的内存中最大的一块,是线程共享的区域。
Java堆由新生代、老年代和永久代组成
新生代:
是类的成长、诞生、消亡的区域,最后被垃圾收集器收集,结束生命。新生代又分为Eden、From survivor和 To survivor。对象首先是分配在Eden,当Eden中不够存储时,会将其存到survivor,如果survivor中无法存储会移动到老年代。
老年代:
存放从新生代中筛选出来的对象。
JAVA堆可以处于物理上的不连续空间,只要逻辑上是连续的即可。
堆在实现时,可以是固定大小、也可以动态扩展(-Xmx和-Xms)。当堆无法在扩展时,会抛出OOM异常。
5、方法区(永久代):
JAVA虚拟机规范把方法区描述为堆的一个逻辑部分,但是它还有另外一别名是非堆(Non-Heap),目的应该与Java堆区分开来。
是一个常驻内存区域,存储了每一个类的结构信息,例如运行时常量
池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包
括一些在类、实例、接口初始化时用到的特殊方法,也就是运行环境必须的类信息,此区域的是不会被垃圾收集器回收掉的。JVM关掉时,才会释放此区域所占用的内存。方法区无法满足内存分配需求时,将抛出OOM。
Ps:方法区和永久代本质上不等价,但是对HotSpot来说,使用永久代来实
现方法区,对于其他虚拟机(BEA JRockit、IBM J9等)是不存在永久代
的概念的。
6、运行时常量池
运行时常量池是方法区的一部分,用于存放编译时期生成的各种字面量和符号引用。运行期间也可以将新的常量放入池中,比如String类的intern()方法。当常量池无法再申请到内存时会抛出OOM异常。
7、直接内存
直接内存是java虚拟机内存管理之外的一个内存区域,java引入NIO后,引入了一种基于通道与缓冲区的I/O方式,可以使用Native函数库直接分配堆外内存,然后通过堆内对象对这块内存进行操作。
显然,本机直接内存的分配不会受到JAVA堆大小的限制,这类内存也会产生OOM异常,因为本机的内存大小是有限的。
- 常见内存溢出异常例子
JAVA堆溢出
JAVA堆用于存储对象实例,不断创建对象,超过堆的大小,即可造成堆溢出。
/**
* 参数:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* -Xms20m 堆的最小值
* -Xmx20m 堆的最大值
* 设置为一样,可避免自动扩展
*
* -XX:+HeapDumpOnOutOfMemoryError
* 让虚拟机在出现内存溢出的时候DUMp出当前内存堆转储快照以便事后分析
*
* */
public class HeapOOM {
public static void main(String[] args) {
List<HeapOOM> list = new ArrayList<HeapOOM>();
while(true)
{
list.add(new HeapOOM());
}
}
}
//输出: Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
虚拟机栈和本地方法栈溢出
在单线程的情况,只能形成的异常是StackOverflowError,我们使用递归造成栈溢出。
/**
*使用 -Xss128k 减小栈容量
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak()
{
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try{
oom.stackLeak();
}
catch(Throwable e){
System.out.println("stack length::"+oom.stackLength);
throw e;
}
}
}
//输出:Exception in thread "main" java.lang.StackOverflowError
周志明老师在书上说到,在多线程情况下,会造成OutOfMemoryError异常,可是我试了一下,结果是死机了,最后Eclipse没反应,等了好久,强制关机了。
我贴上代码,但是运行时可能会死机,大家还是小心行事。
public class JavaVMStackSOF {
private void dontStop()
{
while(true)
{}
}
public void stackLeakByThread()
{
while(true)
{
Thread th = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
} );
th.start();
}
}
public static void main(String[] args) {
JavaVMStackSOF oom = new JavaVMStackSOF();
oom.stackLeakByThread();
}
}
方法区溢出
/*
* VM args : -XX:PermSize=10M -XX:MaxPermSize=10M 限制方法区的大小
*
*
* Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
* */
public class RuntimeConstantPoolOOM1 {
public static void main(String[] args) {
//使用List保持引用,避免FullGC回收常量池
List<String> list = new ArrayList<String>();
int i = 0;
while(true)
{
list.add(String.valueOf(i++).intern());
System.out.println(i);
}
}
}
//输出:Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
关于书上String.intern的例子,我从网上转载了一篇很详细的文章解释
版权声明:本文为博主原创文章,未经博主允许不得转载。