java内存区域与内存溢出异常

来源:互联网 发布:淮南市大数据技能大赛 编辑:程序博客网 时间:2024/05/16 15:59

    最近在看java虚拟相关知识,把每天看到的一些内容做一个归纳总结。

一、运行时数据区域分类

1.程序计数器

   程序计数器可以作为当前线程所执行的字节码的行号指示器,对于程序分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成.
   通常,在一个确定的时刻,一个处理器都只会执行一条线程中的指令,因此,每条线程都要一个独立的程序计数器

2.java虚拟机栈

   首先,虚拟机栈也是线程所私有的,每个方法在执行时都会创建一个栈帧,用来存储局部变量表、操作数栈、动态链接、方法出口等。每个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈到出栈的过程。
   通常所说的java内存中的栈内存就是虚拟机栈,或者说虚拟机栈中的局部变量表部分。

   虚拟机栈可以存储boolean、byte、char、short、int、float、long、double等基本数据类型以及reference对象引用类型

3.本地方法栈

主要是为虚拟机使用到的native方法服务,其他的与java虚拟机栈类似。

   native方法主要是指由非java语言实现的方法

4.Java堆

   java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,用以存放对象实例,几乎所有的对象实例及数组都要在堆上分配,也有栈上分配、标量替换优化技术导致在其他区域分配。
   java堆是垃圾收集器管理的主要区域,java堆按照分代收集算法可以分为新生代和老年代,再分细致一点可以分为Eden空间、From Survivor空间、To Survivor空间等。从内存分配角度来看,线程共享的堆中还可以划分出多个线程私有的分配缓冲区(TLAB)。
   java堆可以处于物理上不连续的内存空间中。只要逻辑上是连续的即可。

5.方法区

   方法区同样也是各个线程所共享的内存区域,用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。对于HotSpot虚拟机来说,也可以叫他”永久代”。
   除了不需要连续的内存和可以选择固定大小或者课扩展外,还可以选择不实现垃圾收集。而对于这区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

6.运行时常量池

   运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法去的运行时常量池存放。
   java虚拟机规范没有做任何细节的要求,一般来说除了保持Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量中。
   还有一个特性是具备动态性,并非预置入Class文件中常量池的内容才能进入方法去运行时常量池,运行期间也可能将新的常量放入池中,例如String的intern()方法。

7.直接内存

   直接内存不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。JDK1.4新加入NIO类,是一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在java中的DirectByteBuffer对象作为这块内存的引用进行操作。
   直接内存不受java堆大小限制,但是还是要受本机内存限制

二、HotSpot虚拟机

1.对象的创建

   常用的对象创建方式是使用new关键字,当虚拟机遇到new关键字时,首先会去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载、解析、和初始化过,若没有需要先执行相应得类加载过程。
   在类加载检查通过后,需要为新生对象分配内存,对象所需内存大小在类加载完成之后便可以确认。分配内存有两种方式
  1)指针碰撞
   假设java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲内存放在另外一边,中间放着一个指针作为分界点的指示器,那所分配的内存就仅仅是把指针向空闲空间那边挪动一段与对象大小相等的距离。

  2)空闲列表
   如果java中内存并不是规整的,已经使用的和空闲的相互交错,虚拟机就维护一个列表,记录上哪些内存是可用的,分配时找一块足够大的空间划分给对象实例,并更新列表上的记录。
在使用Serial、Parnew等带Compact过程的收集器时,采用指针碰撞,CMS等基于Mark-Sweep算法的采用空闲列表

  创建对象时线程安全问题:
  1)对分配内存空间的动作进行同步处理–虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
  2)把内存分配的动作按照线程划分在不同的空间之中进行,即本地线程分配缓冲。


  内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值,接着虚拟机要对对象进行必要的设置,主要是对象头中相关信息设置。
  执行new指令之后会接着执行方法,把对象按照代码中的配置进行初始化,这样一个对象才算完成。

2、对象的内存布局

   1)对象头
   对象头分为两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、线程持有的锁、偏向线程ID、偏向时间锁等,这部分数据在32位或者64位中分别为32bit和64bit,也是就是”Mark Word”,通常被设计为一个非固定的数据结构以便在极小的空间内存尽量多的存储信息,并且会根据对象的状态复用自己的存储空间。
   HotSpot中如果对象处于未被锁定的状态,这Mark Word的32bit空间中的25bit用于存储哈希码,4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定为0,其他状态如下。

存储内容 标志位 状态 对象哈希码、对象分代年龄 01 未锁定 指向锁记录的指针 00 轻量级锁定 指向重量级锁的指针 10 膨胀 空,不需要记录信息 11 GC标记 偏向线程ID、偏向时间戳、对象分代年龄 01 可偏向

   另外一部分是类型指针,即对象指向它的类元数据的指针,但是查找对象的元数据并不一定要经过对象本身。若对象为一个数组,那么对象头中还需要有一块用于记录数组长度的数据。虚拟机无法从数组的元数据中确定数组的大小。
   2)实例数据
   对象真正存储的有效信息,也是在代码中定义的字段内容,无论父类中继承的还是子类中定义的。在满足前提条件下,父类中定义的变量会出现在子类之前。
   3)对齐填充
   这部分并不是必然存在的,仅仅起占位符作用。,对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

3、对象的定位

  java程序需要通过栈上的reference数据来操作堆上的具体对象,reference类型在java虚拟机上只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中对象的具体位置

   1)句柄
   如果使用句柄访问的话,java堆中将会划分出一块内存来作为句柄,reference中存储的对象就是句柄地址。

  2)直接指针
    java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。

    使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。
   而使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销。

原创粉丝点击