Java内存区域

152 阅读9分钟

一、运行时数据区域


程序计数器


1、是一块较小的内存空间,他可以看作是当前线程所执行的字节码的行号指示器。 2、每条线程之间都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为线程私有的内存。 3、如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果为native方法,这个计数器值为空。

java虚拟机栈


1、该区域为线程私有的,生命周期与线程相同。 2、虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法调用直至执行完成的过程,对应着一个栈帧在虚拟机栈中入栈到出栈的过程。 3、该区域可能抛出的异常:

  • 当线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
  • 当虚拟机栈动态扩展,但无法申请到足够的内存,会抛出OutOfMemoryError异常。

本地方法栈


线程私有的,与虚拟机栈类似,区别在于,虚拟机栈为虚拟机执行java方法服务,本地方法栈为虚拟机使用的native方法服务。

java堆


1、线程共享的区域,在虚拟机启东时创建。 2、存放对象实例,所有的对象实例和数组都在这里进行分配。 3、是垃圾收集器管理的主要区域(“GC堆”),现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。可以将堆分成两块:

  • 新生代(Young Generation)
  • 老年代(Old Generation)

4、对不需要连续的内存,如果在堆中没有内存完成实例分配,并且对也无法在扩展时,将会抛出OutOfMemoryError异常。

方法区


1、与java堆一样是内存共享的区域,同样不需要连续的内存空间,无法满足内存分配需求时将会抛出OutOfMemoryError异常。 2、用于存储已被虚拟机加载的类信息、常量、静态变量、及时编译器编译的代码等数据。 3、对这一区域的垃圾收集主要是针对常量池的垃圾回收和对类型的卸载,但是一般比较难实现。 4、HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息,静态变量和常量池等放入堆中。

运行时常量池


1、是方法区的一部分,当常量池无法在申请到内存时会抛出OutOfMemoryError 异常。 2、Class文件中除了有类的版本、字段、方法、接口等信息还有常量池用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池进行存放。 3、具备动态性,运行期间也可以将新的常量放入池中,例如String类中的intern()方法。

直接内存


1、直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。 2、在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。

二、 HotSpot虚拟机对象探秘


对象的创建


1、类加载检查: 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。 2、分配内存: 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

  • 指针碰撞:假设Java堆中的内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存就是把这个指针向空闲的内存那边挪动一段与对象大小相等的距离。Serial、ParNew等带Compact过程的收集器采用。
  • 空闲列表:假设Java堆中的内存是不规整的,虚拟机就必须维护一个表,用来记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间分对象,并更新表上的记录。CMS这种基于Mark-Sweep算法的收集器时使用。

3、初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。 4、设置对象头:虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。 5、执行init()方法:一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

内存分配并发问题(虚拟机采用两种方式来保证线程安全)


  • CAS+失败重试:CAS是乐观锁的一种实现方式。所谓乐观锁就是每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用CAS加上失败重试的方式来保证更新操作的原子性。
  • TLAB:把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存,称之为本地线程分配缓冲(TLAB)。哪个线程需要分配内存就在那个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。

对象的内存布局


对象在内存中存储的布局可以分为3块区域:对象头、实例数据、对齐填充。

1、对象头:包括两部分内容
  • 第一部分用于存储对象自身的运行时数据,例如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分在32位和64位的虚拟机中长度分别为32位和64位。
  • 另一部分为类型指针,即对象指向它的类元数据指针。虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,即查找对象的元数据信息并不一定要经过对象本身。
2、实例数据
  • 实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。
  • 这部分的存储顺序会受到虚拟机分配策略参数和字段在java源码中定义顺序的影响。
3.对齐填充

并不是必然存在的也没有特殊的含义,它仅仅起着占位符的作用。

对象的访问定位


java程序通过栈上的reference数据来操作堆上的具体对象。目前有两种方式分别为:使用句柄和直接指针。 1、使用句柄:Java堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。

2、直接指针:Java 堆对像的布局中就必须考虑如何防止访问类型数据的相关信息,reference 中存储的直接就是对象的地址。
3、两者优势:

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