1、java内存区域
1.1运行时数据区域:java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁时间,有的区域随着虚拟机进程的启动而存在,有些区域依赖用户线程的启动和结束而建立和销毁。根据《Java虚拟机规范SE 7版》的规定,Java虚拟机所管理的内容将会包括以下几个运行时数据区域,如果所示:
Java虚拟机运行时数据区
1.2、程序计数器:程序计数器是一块较小的空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。Java虚拟机是多线程轮流切换执行的,所以程序计数器是每个线程所独有的,各个线程之间的计数器互不影响,独立存储,我们称这内存区域为“线程私有”的内存。改内存区域是java虚拟机规范中唯一一个没有规定任何OutOfMemoryError情况的区域。
1.3、Java虚拟机栈:java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。在java虚拟机规范中,对这个区域规定了两种异常状况,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError;如果虚拟机可以动态扩展,如果扩展是无法申请到足够的内存,就会抛出OutOfMemoryError异常。
1.4、本地方法栈:本地方栈的作用与虚拟机栈发挥的作用是非常相似的,他们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。Native方法常用于两种情况:一是在方法中调用一些不是java语言写的代码,二是在方法中用java语言直接操纵计算机的硬件。
1.5、java堆:java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存;Java堆是垃圾收集器管理的主要区域,也叫GC堆;java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
1.6、方法区:方法区与java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。该区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
1.7、运行时常量池:运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,java语言并不要求常量一定只有编译器才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。
1.8、直接内存:直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常。在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景中显著提高性能,因为避免了在java堆和Navtive堆中来回复制数据。
2.HotSpot虚拟机对象探秘
(基于HotSpot和常用的内存区域java堆为例,探讨HostSpot虚拟机在java堆中对象分配,布局和访问的过程)
2.1对象的创建:当虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从java堆中划分出来,其中有“指针碰撞”和“空闲列表”两种方式。出开如何划分可用空间之外,还有另外一个需要考虑的问题就是对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现在正给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案,一种是对分配内存空间的动作进行同步处理--实际上虚拟机采用CAS配上失败重试的方法保证更新操作的原子性;另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。哪个线程需要分配内存,就在那个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。
内存分配完毕后,虚拟机需要将分配到内存的空间都初始化为零值(不包括对象头),以保证对象实例字段在java代码中可以不赋初值就直接使用。
接下来就是对对象进行必要的设置,如改对象是哪个类的实例、如何找到类的元数据、对象的哈希码、独享的GC分代年龄等信息。
从虚拟机角度来看,一个新的对象已经产生,但从程序的角度来讲,对象的创建才开始----即执行<init>方法,把对象按照程序员的意愿进行初始化,这才算一个真正可用的对象完全产生出来。
2.2对象的内存布局:在HotSpot虚拟机中,独享在内存中存储的布局可以分为三块区域:对象头、实例数据和对齐填充。
对象头:HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标识、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称他为“Mark Word”。另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身。
实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各个类型的字段内容。这部分内容的存储顺序会受到虚拟机分配策略参数和字段在java源代码中定义顺序的影响。HotSpot虚拟机默认的分配策略为longs/doubles 、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Points),从分配策略可以看出,相同宽度的字段总是被分配到一起。
填充对齐:这一部分并非必须存在,它仅仅是起占位符的作用,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全8位字节。
2.3对象的访问定位:在java程序中需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在java虚拟机中值规范了一个指向对象的引用,并没有去定义这个引用应该以何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。目前主流的访问方式有使用句柄和直接指针两种。
句柄访问方式:在java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含对象实例数据与类型数据各自的具体地址信息,如图:
通过句柄访问对象
直接指针访问:在java堆中的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的就是对象的地址,如图:
通过指针直接访问对象
这两种对象在访问时各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象移动(GC中)时只会改变句柄中的实例数据指针,而reference本身不需要修改。使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针对位的时间开销,由于对象的访问在java中非常频繁,因此这类开销积少成多后也是一种非常可观的执行成本。Sun HotSpot中采用第二种。
3.OtuOfMemoryError异常
3.1java堆溢出:java堆用于存储对象实例,只要不停地创建对象,病情保证GC Roots到对象之间有可达的路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大的容量限制后就会产生内存溢出异常。eg:
public class HeapOOM{
static class OOMObject{
}
public static void main(String[] args){
List<OOMObject> list = new ArrayList<OOMObject>();
while(true){
list.add(new OOMObject());
}
}
}
java堆内存的OOM异常是实际应用中常见的内存溢出异常情况。当出现java堆内存溢出情况,异常堆栈信息“java.lang.OutOfMemaryError”会跟着进一步提示“Java heap space”。要解决这个区域异常,一般手电是先通过内存映像分析工具对Dump出来的堆转储快照进行分析,区分是内存泄露还是内存溢出。
如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链。
如果是不存在泄露,换句话说内存中的对象确实还活着,那就应当检查虚拟机的堆参数与机器物理内存对比是否还可以调大,从代码上检查是否存在某些对象的生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
3.2虚拟机栈和本地方法栈溢出
关于虚拟机和本地方法栈,在java虚拟机规范中描述了两种异常:
1)如果把线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOutflowError异常。
2)如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
这里把异常分成两种,看似更加严谨,但却存在着一些相互重叠的地方,当栈空间无法继续分配时,到底是内存太小还是已使用的栈空间太大,其本质上只是对同一件事的两种描述。对于HotSpot虚拟机来说,并不区别本地方法栈和虚拟机栈。
3.3方法区和运行时常量池溢出
3.4本机直接内存溢出