深入理解Java虚拟机(一)——Java内存区域与内存溢出异常

本文分为两个部分:Java内存划分、JVM异常简述

一、Java内存划分

Java虚拟机所管理的内存包括以下几个运行书数据区域:


1.程序计数器

程序计数器是一块较小的内存空间。它是线程私有的,可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变该计数器的值来选择下一条需要执行的字节码指令,分支、跳转、循环等基础功能都要依赖它来实现。在JVM规范中,每个线程都有自己的程序计数器,并且任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;如果执行的是本地方法,则计数器值为空(Undefined)。该内存区域是唯一一个在Java虚拟机规范中没有规定任何OOM(内存溢出:OutOfMemoryError)情况的区域。


2.Java虚拟机栈

该区域也是线程私有的,它的生命周期也与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈桢在虚拟机栈中从入栈到出栈的过程


  • 局部变量表

局部变量表用于存放编译期间可知的各种基本数据类型、对象引用类型(它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他榆次对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。系统不会为局部变量赋予初始值(实例变量和类变量都会被赋予初始值)。                                单位:Slot    一个Slot可以存放一个32位以内的数据类型:boolean、byte、char、short、int、float、reference和returnAddresss。


  • 操作数栈

Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",其中所指的栈就是指-操作数栈。操作数栈也常被称为操作栈。和局部变量区一样,操作数栈也是被组织成一个以Slot为单位的数组。但是和前者不同的是,它不是通过索引来访问,而是通过标准的栈操作—压栈和出栈—来访问的。比如,如果某个指令把一个值压入到操作数栈中,稍后另一个指令就可以弹出这个值来使用。

 虚拟机在操作数栈中存储数据的方式和在局部变量区中是一样的:如int、long、float、double、reference和returnType的存储。对于byte、short以及char类型的值在压入到操作数栈之前,也会被转换为int。

 虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。如下:
package com.qianzy.test;
public class Test {
	public void test(){
		int a= 98;
		int b =100;
		int c = a+b;
		System.out.println(c);
	}
	
	public static void main(String[] args) {
	
			new Test().test();
		
		
	}

通过javap -c  Hello指令,查看上述代码的字节码


        0: bipush        98         //将8位带符号整数98入操作数栈
         2: istore_1                    //将操作数栈顶的值98弹出,并存储在位置为1的局部变量中(i)
         3: bipush        100       //将8位带符号整数100入操作数栈
         5: istore_2                    //将操作数栈顶的值100弹出,并存储在位置为2的局部变量表中(i)
         6: iload_1                     //加载局部变量表的第1个变量到操作数栈顶
         7: iload_2                     //加载局部变量表的第2个变量到操作数栈顶
         8: iadd                          //将栈顶两个元素出栈,做加法,然后把结果再入栈(即98,100出栈,将98+100入栈)
         9: istore_3                    //将操作数栈顶得到的结果弹出,并存储在位置3的局部变量表中(i)
        10: getstatic     #2                 
        13: iload_3
        14: invokevirtual #3                  
        17: return

  • 动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用,一部分会在类加载阶段或第一次使用的时候转化为直接引用(如final、static域等),称为静态解析,另一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。

简单来说:将Class文件在常量池中存在的符号引用,在运行期间转化为直接引用,称为动态连接


  • 方法出口

当一个方法被执行后,有两种方式退出该方法:执行引擎遇到了任意一个方法返回的字节码指令或遇到了异常,并且该异常没有在方法体内得到处理。无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行。方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能保存了这个计数器值,而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。

方法退出的过程实际上等同于把当前栈帧出站,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,如果有返回值,则把它压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令。


3.本地方法栈

该区域与虚拟机栈所发挥的作用非常相似,只是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为使用到的本地操作系统(Native)方法服务。

4.Java堆

Java Heap是Java虚拟机所管理的内存中最大的一块,它是所有线程共享的一块内存区域,在虚拟机启动时创建,我们可以用“Xmx”之类的参数来指定最大对空间的指标。此内存区域微医目的就是存放对象实例。几乎所有的对象实例和数组都在这类分配内存。Java Heap是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”。

5.方法区

线程共享区域,用于存储所有的元(Meta)数据,例如类结构信息,以及对应的运行时常量池、字段、方法代码等。Java虚拟机规范把方法区描述为Java堆的一个逻辑部分,而且它和Java Heap一样不需要连续的内存,可以选择固定大小或可扩展,另外,虚拟机规范允许该区域可以选择不实现垃圾回收。相对而言,垃圾收集行为在这个区域比较少出现。该区域的内存回收目标主要针是对废弃常量的和无用类的回收。

很多习惯在HotSpot虚拟机上开发、部署的程序员,喜欢将方法区成为“永久代”(Permanent Generation),这仅仅对于Sun HotSpot来讲,JRockit和IBM J9虚拟机中并不存在永久代的概念。本质上两者并不等价,仅仅因为HotSpot的设计团队选择把GC分代收集扩展到方法区,或者说用永久代来实现方法区。JDK8中将永久代移除(验证见第二部分),并增加了元数据区(Metaspace)。元数据区的本质和永久代类似,都是对JVM规范中方法区的实现。不过元数据区与永久代之间最大的区别在于:元数据区并不在虚拟机中,而是使用本地内存。

  • 运行时常量池

Jdk1.6(包括)以前,运行时常量池是方法区的一部分,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。运行时常量池相对于Class文件常量池的另一个重要特征是具备动态性,Java语言并不要求常量一定只能在编译期产生,也就是并非预置入Class文件中的常量池的内容才能进入方法区的运行时常量池,运行期间也可能将新的常量放入池中

Java1.7中符号引用(Symbols)转移到了native heap(笔者未验证,可自行验证);字面量(interned strings)转移到了java heap(验证见第二部分);类的静态变量(class statics)转移到了java heap(笔者未验证,可自行验证)。 



以上是运行时数据区域,另外,还有一部分既不属于虚拟机运行时数据区,也不是Java虚拟机规范中定义的内存区域,但是却被频繁使用,并且可能导致OutOfMemoryError异常出现,它就是:

直接内存:

在JDK1.4中新引入了NIO类,引入了一种基于通道(Channel)与缓冲区的I/O方式,它可以使用Native函数库直接分配对外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景中提高性能,因为避免了在Java堆和Native堆中来回复制数据。

直接内存又名堆外内存,为什么堆外内存能够提升IO效率?

堆内内存由JVM管理,属于“用户态”;而堆外内存由OS管理,属于“内核态”。如果从堆内向磁盘写数据时,数据会被先复制到堆外内存,即内核缓冲区,然后再由OS写入磁盘,使用堆外内存避免了数据从用户内向内核态的拷贝。

本机直接内存的分配不受Java堆大小的限制,但是会受到本机总内存的大小及处理器寻址空间的限制。

二.Java OutOfMemoryError异常简述

1.Java堆溢出

例如:

public void test(){
		List s = new ArrayList();
		while(true){
			s.add(new Object());
		 }
		}

java.lang.OutOfMemoryError: Java heap space

需要判断是内存泄露还是内存溢出。如果是内存泄露,需要通过工具查看泄露对象到GC Roots的引用链,准确定位泄露代码的位置;如果是内存溢出,则应当检查迅疾的堆参数(-Xmx与-Xms),与机器物理内存对比是否还可以调大,从代码上检查某些对象是否存在生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

2.虚拟机栈和本地机栈方法溢出

在Java虚拟机规范中,对这个区域规定了两种异常情况:

    1、如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。

例如:无限递归方法

package com.qianzy.test;
import java.util.ArrayList;
import java.util.List;
/**
 * 栈溢出
 * @author thinkpad
 *
 */
public class Test {
	public void test(){
		List s = new ArrayList();
		test();
		}
	
	public static void main(String[] args) {
			new Test().test();
	
	}
}

java.lang.StackOverflowError

    2、如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

    这两种情况存在着一些互相重叠的地方:当栈空间无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其本质上只是对同一件事情的两种描述而已。

深入理解Java虚拟机作者实现表明,在单线程的操作中,无论是由于栈帧太大(通过定义大量本地变量增大方法帧中本地变量表的长度),还是虚拟机栈空间太小(通过设置-Xss减小栈内存容量),当栈空间无法分配时,虚拟机抛出的都是StackOverflowError异常,而不会得到OutOfMemoryError异常(我觉得可能是单线程试数据不容易达到临界值)。而在多线程环境下,则会抛出OutOfMemoryError异常。

多线程环境每个线程分到的机栈内存越大,越容易抛出OutOfMemoryError异常,因为操作系统分给每个进程的内存是有限的(32位windows 2GB),减去公有内存堆、方法区、程序计数器可忽略不计,剩余内存被本地方法栈和虚拟机栈瓜分,若虚拟机栈越大,则可以创建的线程数量越少,越容易将剩余内存瓜分。

HotSpot虚拟机不区分虚拟机栈和本地方法栈,所以设置本地方法栈大小的参数-Xoss对它无效,可根据-Xss控制栈容量


3.方法区和运行时常量池溢出

前面讲过,JDK1.8已经移除了永久代,目前我在JDK1.8上运行程序,设置虚拟机参数-XX:PermSize=10M -XX:MaxPermSize=10M(设置方法区永久代大小10M),会出现以下提示:


表明永久代已经移出JDK1.8。


	//验证字符串常量池已在堆中
	        List<String> list = new ArrayList<String>();
	        String s = "abc";
	        while(true){
	        	s = s + s;
	        	list.add(s.intern());
	        }

提示错误:

这里我是在Java8上运行的,Java7出现结果应该一样,可以验证从Java7开始,字符串常量池就移到了堆中


  • 元数据区溢出

-XX:MetaspaceSize=N (初始化元空间大小)

-XX:MaxMetaspaceSize=N (限制Metaspace增长的上限)

//-XX:MaxMetaspaceSize=4m -XX:MetaspaceSize=4m  
                URL url = null;
	        List<ClassLoader> classLoaderList = new ArrayList<ClassLoader>();
	        try {
	            url = new File("C:\\Users\\thinkpad\\workspace\\Test\\src\\com\\qianzy\\test").toURI().toURL();
	            URL[] urls = {url};
	            while (true){
	                ClassLoader loader = new URLClassLoader(urls);
	                classLoaderList.add(loader);
	                loader.loadClass("com.qianzy.test.Test2");
	            }
	        } catch (Exception e) {
	            e.printStackTrace();
	        }

OutOfMemoryError: Metaspace

4.本机直接内存溢出

-XX:MaxDirectMemorySize(当分配的堆外内存到达指定大小后,即触发Full GC)

//-XX:MaxDirectMemrySize=2m
ByteBuffer mByteBuffer = ByteBuffer.allocateDirect(80000000);




参考:

《深入理解Java虚拟机》

https://www.jianshu.com/p/08a6bd8fe606

https://blog.csdn.net/airjordon/article/details/72867397




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值