本文章是笔者对《深入理解java虚拟机》第2版这本书(基于jdk1.7)的理解和简要概括。
第二章 java内存区域和内存溢出异常
2.2 运行时数据区域
包括五个部分:程序计数器(PC计数器)、虚拟机栈、本地方法栈、堆、方法区。前三个是线程独立的,而后两个是线程共享的区域。
2.2.1 程序计数器
分为两种情况,当前线程正在执行java方法时,程序计数器保存的是正在执行的虚拟机字节码指令的地址;当前线程正在执行Native方法时,程序计数器的值为空。
(Native方法:是由java关键字native修饰的方法,表示当前方法的实现是由其他语言实现的,保存在本地的DLL文件中,当方法被调用时,DLL文件会被加载。当前方法只是一个调用接口)。
2.2.2 虚拟机栈
每个方法在执行的时候都会创建一个对应的栈帧,虚拟机栈就是保存这些栈帧的,而方法的调用和结束对应的是栈帧的入栈和出栈。
栈帧包括:局部变量表、操作数栈、动态链接、方法出口等信息。
局部变量表:存放了编译期可知的基本类型数据(int,boolean等)、对象引用和returnAddress类型(指向一条字节码指令的地址)。它的大小是在编译期就完成分配了。
2.2.3 本地方法栈
与虚拟机栈类似,虚拟机栈是为虚拟机使用java方法服务,而本地方法栈是为虚拟机使用Native方法服务。
2.2.4 堆
存放的是对象实例和数组。
虽然堆是线程共享的,但是有的虚拟机在实现的时候会为线程在堆里面独立地分配一个缓冲区(TLAB),目的是加快内存分配和更好地回收内存。
2.2.5 方法区
主要存放的是已被虚拟机加载的类信息、常量、静态变量等。
方法区中有一个区域叫做运行时常量池,是用来存放编译期生成的各种字面量(如1或者“ad”这样的)和符号引用(jdk1.7开始加入的特征),他们是在类加载后才进入方法区的运行时常量池的。
2.3 HotSpot虚拟机对象
2.3.1 对象的创建
两种方式:指针碰撞和空闲列表
指针碰撞:当堆内存是绝对规整时:使用过的内存在一边,空白的在一边,中间是一个指针。当需要为对象分配内存时,只需要指针向后移动对象大小的内存空间即可。
空闲列表:当堆内存不是规则时,虚拟机会维护一个空闲空间列表。当为对象分配内存时,虚拟机会找到一个满足大小的空间给对象。
2.3.2 对象的内存布局
对象在内存中的存储布局分成三个部分:对象头,实例数据和对齐填充。
对象头:包括Mark Word自身运行时数据(如哈希码,GC分代年龄,锁状态标志,线程持有的锁等)和类型指针(指向对象的类的元数据)。
实例数据:对象的有效信息,是代码中定义的各种类型的字段内容。
对齐填充:HotSpot VM要求对象的起始地址为8字节的倍数,所以当对象大小不够8的倍数时,用来填充的。
2.3.3 对象的访问
使用句柄:堆中分出一块内存作为句柄池。虚拟机栈中保存对象的引用,指向堆中一个句柄。句柄包括指向对象实例数据(在堆中)的指针和指向对象类型数据(在方法区)的指针。
直接指针:虚拟机栈中保存对象的引用,指向堆中一个对象(对象包含实例数据),同时对象里面有一个指针指向对象类型数据。
好处:前者——对象发生变化(如对象被移动)时,栈中引用本身不需要修改。后者——可以提高访问数据的效率。
2.4 内存溢出错误(OOM)
程序计数器是唯一一个不会发生OutOfMemoryError异常的地方,而其他四个都可能发生异常。
2.4.1 堆溢出
当堆中不能再创建对象且堆大小不能扩展的时候,堆就出现内存溢出的异常。
可能的原因有内存泄漏(某些不需要的对象没有被垃圾收集器回收)和内存溢出(存活的对象确实都是需要存在的)。
对应的解决办法是:内存泄漏——查看泄漏对象的类型信息和GC Roots引用链的信息,定位泄漏代码位置。内存溢出——尝试调大堆内存或者看看哪些对象的生命周期可以调小。
2.4.2 虚拟机栈和本地方法栈溢出
HotSpot虚拟机不区分这两块区域。他们可能出现的异常有两个原因。
1)如果线程请求的栈深度大于虚拟机允许的栈深度,就会抛出StackOverflowError异常。(如一个方法无限递归调用,线程中方法的栈帧过多)。
2)如果虚拟机在扩展栈的时候不能申请到足够的空间,就会抛出OutOfMemoryError异常。(如多个线程都持有栈,当再次创建一个线程时没有足够空间)。
2.4.3 方法区和运行时常量池溢出
运行时常量池存在动态性,即java允许在程序执行的过程中创建常量,所以当运行时常量池的大小不够时,就会出现溢出。
intern方法:动态创建常量如String类的intern方法,JDK1.6以前intern方法会把首次遇到的字符串复制到常量池中,然后返回它的引用。JDK1.7以后,intern方法直接复制首次出现的字符串的引用,然后返回此引用。
持续更新中