码迷,mamicode.com
首页 > 编程语言 > 详细

002 Java内存区域与内存溢出异常

时间:2016-07-25 16:01:53      阅读:230      评论:0      收藏:0      [点我收藏+]

标签:

1、运行时数据区域
技术分享

① 程序计数器(Program Counter Register)
是一块较小的内存区域,可以看作是当前线程所执行的字节码的行号指示器
如果线程正在执行一个java方法,那么这个计数器记录的是正在执行的虚拟机字节码的指令地址
如果正在执行的是Native方法,则这个计数器的值为空(Undefined)
此区域是唯一一个在java虚拟机中没有规定任何OutOfMemoryError情况的区域

②java虚拟机栈
线程私有的,生命周期与线程相同
描述java方法执行的内存模型:每个方法执行的同时,回创建一个栈帧(Stack Frame),用于存储局部变量表、操作数、动态链接和方法出口等信息
局部变量表存放了编译期可知的各种基本数据类型、对象的引用和returnAddress类型(指向一条字节码指令的地址)
64为长度的long和double类型的数据会占用2个局部变量空间,其他的占用1个。
局部变量表所需内存空间在编译期完成分配,运行期不会改变局部变量表的大小
对虚拟机栈规定了两种异常状况:1. 如果线程请求的深度大于虚拟机所允许的深度,则抛出StackOverflowError;2. 如果栈动态扩展时无法申请到足够的内存,就会抛出OutOfMemoryError

③本地方法栈
与虚拟机栈作用相似。
虚拟机栈为虚拟机执行的java方法服务,本地方法栈是为虚拟机使用到的Native方法服务(HotSpot将两个栈合二为一)
会抛出的异常与虚拟机栈一致

④Java堆
Java Heap是java虚拟机所管理的内存中最大的一块,是被所有线程共享的,在虚拟机启动时创建
唯一目的是存放对象实例,几乎所有的对象实例都在这里分配内存
随着JIT和逃逸分析技术的发展,栈上分配、标量替换等优化技术的原因,所有的对象实例分配在堆上并不是那么绝对了‘
Java堆是垃圾收集器管理的主要区域,因此也可以称为“GC堆”

垃圾收集器大多使用分代算法,可以将Java堆分为:新生代和老年代。再细致一些可以分为Eden空间、From Survivor空间、To Survivor空间等
内存分配的角度:线程共享的Java堆可以划分出多个线程私有的分配缓冲区
目的是为了更好地进行内存回收和内存分配

java堆可以处于物理上连续的空间,也可以处于逻辑上连续的空间
可以固定大小,也可以是可扩展的,当前主流虚拟机都是按照可扩展实现的(通过-Xmx和-Xms控制)
当堆中没有内存完成实例分配,也不能扩展时,会抛出OutOfMemoryError异常

⑤方法区
与堆一样,所有线程共享的内存区域,存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
HotSpot使用永久代实现方法区,可以向管理Java堆一样管理这部分内存
使用永久代更容易遇到内存溢出的问题,永久代有-XX:MaxPermSize的上限,在J9和JRockit虚拟机中只要没有达到进程可用上限就不会出现问题

除了向Java堆一样可以不需要连续的内存,可以选择固定大小或者可扩展,还可以选择不实现垃圾收集
当方法区无法满足内存分配时,或抛出OutOfMemoryError异常

⑥运行时常量池(Runtime Constant Pool)
是方法区的一部分。
Class文件中有类的版本、字段、方法、接口等描述信息外,还有一个常量池,用于存储编译期生成的各种字面量和符号引用,类加载后进入方法区常量池中存放
一般来说,虚拟机把翻译出来的直接引用也存放在运行时常量池中
运行时常量池相对于Class文件中的常量池的另一个重要特性具备动态性:Java不要求常量一定只有编译期才能产生,并非预置入Class文件中常量池的内容才能进入运行时常量池,运行期,也可能有新的常量放入池中,这种用的比较多的是String类的intern()方法
作为内存区的一部分,会受到方法区的内存限制,当无法再申请到内存时抛出OutOfMemoryError异常

⑦直接内存
不是java虚拟机运行时数据区域的一部分,也不是虚拟机规范的定义的内存区域,但是这部分频繁使用,也可能导致OutOfMemoryError异常

2、HotSpot虚拟机对象探秘
HotSpot在Java堆中对象分配、布局和访问的过程
①对象的创建
java对象规整,即已分配都放在一块,未分配的内存都放在一块,以一个指针作为分界点,当需要为对象分配内存时,将指针向空间区域移动对象需要内存的大小,这种分配方式为“指针碰撞(Bump the Pointer)”
java对象非规整,用一个列表来记录那些块是可用的,那些事不可用的,分配时将对象需要的内存分配给它,然后修改分配表,这种分配方式为“空闲列表(Free List)”
虚拟机分配方式由垃圾收集器是否带有压缩功能决定的
使用Serial、ParNew等带Compact过程的收集器,使用指针碰撞,使用CMS这种基于Mark-Swap算法的收集器,使用空闲列表

内存分配的同步:使用CAS和失败重试保证更新操作的原子性,还有一种是每个线程分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),虚拟机使用使用TLAB使用-XX:+/-UseTLAB

内存分配完后,虚拟机需要将分配的内存呢空间都初始化为零值(不包括对象头)。如果使用TLAB,则这一过程可以提前至TLAB分配时进行
分配后虚拟机要对对象做必要设置,设置类型的信息(属于哪个类,类的元数据信息)、对象哈希码、对象GC分代年龄,存放在对象头中。不同虚拟机设置的方式不同

②对象的内存布局
对象在内存中的布局可以分为3块区域:对象头、实例数据和对齐填充
HotSpot的对象头包括两部分信息:
(1) 用于存储对象自身运行时数据:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,在32为和64为虚拟机中分别为32bit和64bit,称为Mark Word,由于此部分存储的是与对象自定义数据无关的信息,Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。
(2) 对象头的另一部分是类型指针(Klass Pointer),指向它的类元数据指针,通过该指针确定是哪个类的实例。并不是所有虚拟机必须在对象上保留类型指针,对象的元数据信息不一定经过对象本身。
如果是Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小,但是从数组的元数据中无法确定数组的大小。

对象真正的存储的有效信息,程序中定义的各种字段(继承自父类和子类自定义的都需要记录)
分配的顺序会受虚拟机分配策略参数(FieldsAllocatationStyle)和字段定义的顺序影响
HotSpot的默认分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),相同字段总是被分配到一起
满足上述分配情况下,父类中定义的变量会出现在子类之前。如果CompactFields参数值为true(默认为true),那子类之中较窄的变量也可能会插入到父类变量的空隙之中。

第三部分对齐不是必然存在的,仅仅作为占位符
由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。对象头部分正好似8字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。  

③对象的访问定位
取决于虚拟机的实现
(1)使用句柄访问,java堆中会分出一块内存,作为句柄池。reference中存储的是对象句柄,对象句柄中包含了对象实例数据与类型数据这两个的具体地址。
技术分享
(2)使用直接指针访问,java堆的对象布局中必须考虑如何访问访问类型数据的相关信息,refere中存储的对象地址
技术分享

两种对象访问方式优点
  • 使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。
  • 使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销
虚拟机HotSpot而言,它是使用第二种方式进行对象访问。

4 OUtOfMemoryError异常
除程序计数器外,java虚拟机其他的运行时数据区域都可能发生该异常(简称OOM)。
 ①Java堆溢出
控制堆不可扩展:设置堆的最小值-Xms参数与最大值-Xmx参数为一样的值
通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后进行分析。

②虚拟机栈和本地方法栈溢出
HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此,对于HotSpot来说,虽然-Xoss参数(设置本地方法栈大小)存在,但实际上是无效的,栈容量只由-Xss参数设定。
在Java虚拟机规范中描述了两种异常:
  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
  • 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
在单线程中,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。使用-Xss参数减少栈内容容量和定义大量本地变量增大本地变量表长度均抛出StackOverflowError异常。
在不断创建线程的情况下,每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。
建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。
在Windows平台的虚拟机中,Java的线程是映射到操作系统的内核线程上的

③方法区和运行时常量
在JDK 1.6及之前的版本中,由于常量池分配在永久代内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区大小,从而间接限制其中常量池的容量

String.intern()返回引用的测试
  1. public class RuntimeConstantPoolOOM {
  2. public static void main(String[] args) {
  3. public static void main(String[] args) {
  4. String str1 = new StringBuilder("计算机").append("软件").toString();
  5. System.out.println(str1.intern() == str1);
  6. String str2 = new StringBuilder("ja").append("va").toString();
  7. System.out.println(str2.intern() == str2);
  8. } }
  9. }
JDK1.6 两个false ;JDK1.7 ture和false
在JDK 1.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例的引用,而由StringBuilder创建的字符串实例在Java堆上,所以必然不是同一个引用,将返回false。而JDK 1.7(以及部分其他虚拟机,例如JRockit)的intern()实现不会再复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个。
对str2比较返回false是因为“java”这个字符串在执行StringBuilder.toString()之前已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”的原则(JDK1.6和1.7均是),返回的是常量池的中引用,而“计算机软件”这个字符串则是首次出现的,因此返回true。
方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。

④直接本机内存溢出
DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样。
由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果读者发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,那就可以考虑检查一下是不是这方面的原因。




002 Java内存区域与内存溢出异常

标签:

原文地址:http://www.cnblogs.com/ggmfengyangdi/p/5703696.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!