目录
1.1运行时数据区域(《java虚拟机规范(java SE 7版)》规定)
1.java内存区域与内存溢出异常
1.1运行时数据区域(《java虚拟机规范(java SE 7版)》规定)
java虚拟机所管理的内存主要包括下面几个运行时数据区域
1.1.1程序计数器
程序计数器是一块较小的内存空间,他可以看作是当前线程所执行的字节码的行号指示器(线程私有)。是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的内存区域。
1.1.2java虚拟机栈
和程序计数器一样也是线程私有的,他的生命周期和线程相同。虚拟机栈描述的是一个java方法执行的内存模型:每个方法在执行的同时都会创建一个虚拟机栈桢,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行结束这个过程,就对应着一个栈桢从虚拟机栈中入栈到出栈的过程。
局部变量表存放了编译期可知的各种基本数据类型(byte,long,int,char,boolean,double,float,short)、对象引用(reference类型,他不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
在java虚拟机规范中,对这个内存区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,会抛出StackOverflowError异常;如果虚拟机栈可以自动扩展(当前大部分的java虚拟机都可以动态扩展,只不过java虚拟机规范也允许固定长度的虚拟机栈)如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
1.1.3本地方法栈
本地方法栈和java虚拟机栈发挥的作用很相似,只是java虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈是指虚拟机使用到的native方法服务。
与java虚拟机栈一样也会抛出StackOverflowError和OutOfMemoryError异常。
1.1.4java堆
对大多数应用来说,java堆是java虚拟机中内存最大的一块区域,java堆是被所有线程共享的一块内存区域,在java虚拟机启动是创建。此内存的唯一目的就是存放对象实例,几乎所有的对象实例都是在这里分配内存。在java虚拟机规范中的描述是这样的:所有的对象实例和数组都要堆上分配。
java堆是垃圾收集器管理的主要区域,因此也成为GC堆。
根据java虚拟机规范的规定,java堆可以是物理存储上不连续的内存空间,只要逻辑上是连续的就即可,就像我们的磁盘空间一样。在实现上既可以是固定大小的也可以是可扩展的。当前主流的虚拟机都是按照可扩展的来实现的(通过-Xmx和-Xms来控制)。如果堆中没有内存完成实例分配,且堆也无法再进行扩展时,会抛出OutOfMemoryError异常。
1.1.5方法区
方法区和java堆内存一样,都是线程共享的内存区域,它用于存储已经被java虚拟机加载的类静态信息,常量,静态变量,即时编译器编译后的代码等数据。
java虚拟机规范对方法区的限制非常宽松,除了和java堆一样不需要连续内存,可以选择固定大小和动态扩展外,还可以选择实现不实现垃圾收集。(这个区域的回收的目标主要是常量池的回收和对类型的卸载,这个区域进行垃圾回收的效果没有那么让人满意,尤其是类型的卸载,条件相当的苛刻,但是这部分的回收确实是有必要的。)
根据java虚拟机规范的规定,当方法区无法满足内存分配需求时将会抛出OutOfMemoryError异常。
1.1.6运行时常量池
运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译时生成的字面量和符号引用,这部分内容将在类加载后进入到方法区的运行时常量池中存放。
运行时常量池是方法区的一部分因此也会受到方法区内存的限制,当常量池无法再申请到内存时则会抛出OutOfMemoryError异常。
1.1.7直接内存
直接不是虚拟机运行时数据区域的一部分,也不是java虚拟机规范中定义的内存区域。但是这部分内存也会被频繁的使用,而且也可能会导致OutOfMemoryError异常。
本机直接内存的分配不会受到java堆大小的限制,但是既然是内存,肯定是会受到本机总内存大小以及处理器寻址空间的限制。当各个区域内存总和大于物理内存限制时,从而导致动态扩展时抛出OutOfMemoryError异常
1.2HotSpot虚拟机对象探秘
1.2.1对象的创建
1.2.2对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局可以分为三个部分:对象头(Header),实例数据(Instance Data)和对齐填充(Padding);
对象头:对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有锁、偏向线程id、偏向时间戳等。这部分数据的长度在32位和64位虚拟机(未开启压缩指针)中分别为32bit和64bit。另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,另外如果对象是一个java数组,那必须要在对象头中必须要一块用于记录数组长度的的数据。
实例数据:对象真正存储的有效信息,也是在程序中定义的各种类型的字段内容。无论是从父类继承下来的还是在子类中定义的都需要记录下来。
对齐填充:并不是必然存在的,也没有特别的含义,仅仅起着占位符的作用。HotSpot VM的自动内存管理系统要求对象的其实地址必须是8字节的整数倍,换句话说对象的大小必须要是8个字节的整数倍,而对象头正好是8字节的整数倍,因此当实例数据部分没有对齐时,就需要通过对齐填充来不全。
1.2.3对象的访问定位
java程序是通过线程栈中的reference数据来访问来操作堆上的具体对象。reference只是一个指向对象的引用,对象的访问方式是由不同的虚拟机实现决定的,目前主流的访问方式主要有使用句柄和直接指针两种方式。
句柄访问:
直接指针访问:
使用句柄访问的最大优势就是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference不用修改。
使用直接指针的最大优势是速度更快,它节省了一次指针定位的时间开销。HotSpot就是使用的直接指针方式实现的对象访问。
1.3实战:OutOfMemoryError异常
1.3.1java堆溢出
Java堆用于存储对象实例,只要不断地创建对象,并保证GC Roots到对象之间有可达路径来避免垃圾回收机制清楚这写对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。
要解决这个区域的异常,一般的手段是先通过内存映像分析工具对Dump出来的堆转储快照进行分析,重点是确认内存中的对象是不是必要的,也要分析清楚到底是发生了内存溢出或内存泄漏。
如果是内存泄漏,可进一步查看内存泄漏对象到GCRoots的引用链。这样就可以看到泄漏对象是通过怎样的路径与GCRoots关联并导致垃圾收集器无法自动回收他们。掌握了泄漏的对象信息和GCRoots的引用链信息,就可以比较准确的定位内存泄漏的位置。
如果是内存溢出,就需要检查内存参数(-Xmx ,-Xms)配置设置是否合理,或者从代码上减少程序运行时的内存消耗。
1.3.2虚拟机栈和本地方法栈溢出
HotSpot不区分虚拟机栈和本地方法栈,所以-Xoss参数(设置本地方法栈大小)虽然存在但是实际上是无效的,栈容量只由-Xss参数设定。
关于这两个内存区域,主要有两个异常:StackOverflowError和OutOfMemoryError异常。
出现StackOverflowError异常时有异常堆栈信息可以阅读,比较方便定位问题。
OutOfMemoryError多数是因为创建的线程数量过多导致,可以通过减少线程数量,减少最大堆(增加栈空间总容量),减少单个线程栈容量,来换取更多的线程。
1.3.3方法区和运行时常量池溢出
在jdk1.6版本及以前的版本 字符串常量池 ,我们可以通过XX:PermSize和XX:MaxPermSize 限制方法区的大小从而间接限制其中常量池的容量,例如我们不断向字符串常量池中存放string对象,就会导致运行时常量池溢出,在OuterOfMemoryError后面会出现”PermGen space“,说明运行时常量池是说与方法区(也就是永久代)的一部分。通过不断的创建类也会造成方法区的内存溢出异常。
方法区溢出我们可以考虑写在一些没有必要的类。
1.3.4本机直接内存溢出
通过-XX:MaxDirectMemory指定,如果不指定则默认与堆最大内存(-Xmx)一样,这类内存溢出明显的特征是dump文件不会看到明显的异常,如果读者发现OOM但是dump文件很小而程序中又直接或间接的使用NIO,就可以考虑检查下是不是这方面的原因。
2.垃圾收集器与内存分配策略
哪些内存需要回收?
什么时候回收?
如何回收?
2.1对象已死吗
2.1.1对象是否存活算法
(1)引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用他的时候,这个计数器就+1;当引用实效的时候,计数器就-1;任何时候计数器为0的对象就是不可能在被使用的。
优点:实现简单,判定效率也很高;
缺点:很难解决对象之间相互引用的问题;
(2)可达性分析算法
基本思想:通过一系列的称为“GC Roots”的对象作为其实点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连,则证明此对象是不可用的。
那么什么样的对象可以作为GC Roots呢?
在java语言中,可作为GCRoots的对象包括以下几种:
虚拟机栈中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即一般说的Native方法)引用的对象。
2.1.2对象引用类型
引用的含义:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。
那么我们所有的对象就只有两种状态,被引用和没有被引用两种状态,对于如何描述一些“食之无味,弃之可惜”的对象就显得无能为力。我们希望描述这样一类对象:当内存空间还足够的时候,则能保留在内存中;如果内存空间在进行垃圾收集后还是很紧张,则抛弃这些对象,很多系统的缓存功能都符合这样的场景。
基于这样的需求,java就对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用4种,这4种引用强度一次逐渐减弱。
(1)强引用
强引用就是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收被引用的对象。
(2)软引用
软引用使用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内幕才能溢出异常之前,将这部分数据列到回收范围之中进行第二次回收,如果这次回收还没有足够的内存才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用。
(3)弱引用
弱引用也是用来描述非必须对象的。但是它的强度比软引用更弱些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。也就是说无论当前内存是否足够都会回收掉只被弱引用的关联的对象。在JDK1.2之后,提供了WeakReference类来实现弱引用。
(4)虚引用
虚引用也称为幽灵引用或者幻影引用,他是最弱的一种引用关系,一个对象是否有虚引用的存在完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象呗手机期回收时受到一个系统通知。在JDK1.2之后,提供了PhantomReference类来实现虚引用。
2.1.3对象是否真的已死
即使在可达性分析算法中不可达的对象也并非是“非死不可”的,要真正宣告一个对象死亡,至少要经历两个标记过程:如果对象在进行可达性分析后发现没有与GCRoots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是次对象是否有必要执行finalize()方法。当对象没有覆盖方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行finalize()方法”。
如果这个对象有必要执行finalize()方法,最终由finalize()方法来最终决定这个对象是否需要被垃圾回收处理掉。
2.1.4方法区回收
永久代的垃圾回收主要回收两部分内容:废弃的常量和无用的类。回收废弃常量与回收java堆用的对象很相似,以常量池中字面量的回收为例。例如一个字符串”abc“已经进入了常量池中,但是当前系统更没有任何一个String对象是叫做"abc"的,这个时候如果发生垃圾回收,而且必要的话,这个abc常量就会被系统清理出创来那个池。常量池中的其他类、方法、字段的符号引用也是和这个类似。另一部分内容就是对类的回收,类需要同时满足下面3个条件才能算是”无用的类“:
该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
加载该类的ClassLoader已经被回收。
该类对应的Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法。
满足上面三个条件的无用类也仅仅只是说可以进行垃圾回收,是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制。
2.2垃圾回收算法
2.2.1标记-清除算法
标记-清除算法就像它的名字一样,算法分为"标记"和”清除“两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,它的标记过程就是上面讲到的对象标记过程。它主要有两个不足之处:
一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配大对象时,无法找到足够的连续内存而不得不提前出发另一次垃圾收集动作。
2.2.2复制算法
为了解决效率问题,一种被称为复制的算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存用完了,就将还存活的对象赋值到另一块上面,然后再把已使用过的内存空间一次清理掉,这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况了,直接按照顺序分配就好了,实现起来比较简单运行高效。问题就是把原来的内存缩小为了一半。
eg:新生代回收
IBM公司研究表明新生代中的对象98%是”朝生夕死“的。所以我们不需要按照1:1的比例来划分内存空间。而是将内存分为一块较大的Eden空间和两块较小的survivor空间。
2.2.3标记-整理算法
上面所说的复制算法在存活率高的时候就要复制较多的内容效率会变低。还有一点是,如果不想浪费50%的空间,就需要额外的空间进行分配担保,以应对被使用的内存中所有对象100%存活的极端情况,所以在老年代中一般不适用这种算法。
那么根据老年代的特点人们提供了另一个算法,标记整理算法,其中标记过程仍然与”标记清除“算法一样,但是后面不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
2.2.4分代收集算法
分代算法其实没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块,一般是根据java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
对比:
指标 | 标记清除算法 | 标记整理算法 | 复制算法 |
---|---|---|---|
时间开销 | 中等 | 最慢 | 最快 |
空间开销 | 少(堆积碎片) | 少(不堆积碎片) | 两倍空间(不堆积碎片) |
是否移动 | 否 | 是 | 是 |
参考文档:《深入理解java虚拟机(第二版)》
Java8内存模型—永久代(PermGen)和元空间(Metaspace):http://blog.csdn.net/java1993666/article/details/55805037