这里写目录标题

  • 一、前言
  • 二、运行时数据分区
  • 2.1程序计数器(PC)
  • 2.2 Java虚拟机栈
  • 2.3 本地方法栈
  • 2.4 Java堆
  • 2.5 方法区
  • 2.5.1 运行时常量池
  • 2.6 直接内存
  • 三、HotSpot虚拟机对象探秘
  • 3.1 对象的创建
  • 3.2 对象的内存布局
  • 3.3 对象的访问定位


一、前言

C/C++需要自行回收和释放已经没用的对象,但是对于Java程序员来说,在虚拟机自动内存管理机制的帮助下, 不再需要为每一个new操作去写对应的free代码,就如同吃完饭之后不需要自己收拾盘子一样。看起来一切都师范的美好,但是一旦出现内存泄漏和溢出,如果不了解虚拟机是如何使用内存的,那么排查错误、纠正问题就会是一个艰难的问题。

二、运行时数据分区

Java虚拟机再执行的时候,会将它管理的内存划分为若干个不同的数据区,这些区域各有用途,本节就是了解这些内容。根据Java虚拟机规范,JVM所管理的内存将会包括以下几个运行时区域

Java内存区分为 java的内存分区_Java内存区分为

2.1程序计数器(PC)

程序计数器是一块较小的内存空间,是当前所执行的指令的行号指示器,在JVM中,字节码解释器工作是就是通过改变这个计数器来选取下一条需要执行的字节码指令,一系列分支、循环、跳转和异常处理、线程恢复都需要使用到PC。需要注意到的是,这个程序计数器是线程私有的,用于标识当前线程运行到的指令位置,它和计算机自身的PC不一样,计算机的PC只有一个,用于表示整个计算机运行到的指令的位置,而JVM中的PC是指各个Java进程运行到的位置,每一个Java进程都有一个。

2.2 Java虚拟机栈

和程序计数器一样,Java虚拟机栈也是线程私有的,他的生命周期和线程相同。虚拟机栈是Java方法执行的线程内存模型:每个方法被执行的时候,JVM会在对应线程的虚拟机栈中创建一个栈帧。这个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法被调用直到执行完毕的过程,就对应着一个栈帧在虚拟机栈中入栈出栈的过程。方法的调用和结束调用类似于栈的入栈和出栈,因此虚拟机栈中栈帧的出入栈和方法的调用是保持一致的。

局部变量表存放着许多编译器就可知的JVM基本数据类型(boolean, byte, char, short, int, float, long, double)以及对象引用、return Address类型。这些数据类型在局部变量表中以局部变量槽Slot表示,其中long和double作为64B的变量会占用两个Slot,其余的会占用一个

在虚拟机栈中,如果线程请求的栈深度大于虚拟机允许的深度,则会抛出StackOverflowError异常;如果JVM栈可以动态扩展,但是扩展时服务申请到足够多的内存,则会抛出OutOfMemoryError异常。

2.3 本地方法栈

本地方法栈和虚拟机栈发挥的作用十分相似,区别是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到本地方法服务。

2.4 Java堆

Java堆事JVM的内存管理中最大的一块,是一个线程共享的区域,JVM启动时就会创建这片区域,所有的对象实例以及数组都应该在堆上分配,但是随着Java语言的发展,现在已经看到了一些迹象表明日后可能支持将值存放在堆上,并且由于即时编译技术的进步,栈上分配、标量替换优化手段使得将其他内容存放在堆上成为可能,因此还需要密切关注技术变化。

堆也是垃圾收集器管理的内存区域,对于垃圾回收期GC的详细介绍,会在后面详细介绍

2.5 方法区

方法区和堆一样,是各个线程共享的内存区域,用于存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

在以前HotSpot设计团队选择把垃圾收集器的分区设计扩展至方法区,使用管理永久代的策略管理方法区,使得垃圾收集器能够像管理堆一样管理这部分内存,省去专门为方法区编写内存管理代码的工作。这使得JDK 8之前的程序员喜欢用永久代来称呼方法区,但实际上两者是不一样的,在《JMV规范》中并没有统一要求方法区需要使用何种垃圾收集策略,而常用的HotSpot虚拟机则在JDK8之前都是将方法区作为永久代进行管理,但是将方法区视作为永久代并不是一个好主意,这会导致Java更容易遇到内存溢出的问题,HotSpot从JDK6逐渐开始放弃永久代,改为使用本地内存实现方法区,在JDK8完全放弃了永久代的概念,改用在本地内存中实现元空间来替代。

2.5.1 运行时常量池

运行时常量池是方法区了一部分,class文件除了有类的版本,字段方法,接口等描述信息外,还有一项信息就是常量池表,用于存放编译期生成的各种字面量和符号引用,常量池表会在类加载后放入运行时常量池。

运行时常量池相对于Class文件中的常量池具有动态性,运行期间可以将新的常量放入到池中,这种特性用的较多的是String的intern()方法

2.6 直接内存

直接内存并不是JVM数据区的一部分,他就是平时我们讲的一般的内存空间

直接内存与堆内存的区别:
直接内存申请空间耗费很高的性能,堆内存申请空间耗费比较低
直接内存的IO读写的性能要优于堆内存,在多次读写操作的情况相差非常明显

三、HotSpot虚拟机对象探秘

本节主要探讨HotSpot虚拟机在Java堆中对象分配,布局和访问的全过程

3.1 对象的创建

当虚拟机碰到一条字节码new指令的时候,首先去检查这个指令是否能在常量池中定义到一个类的符号引用(也就是检查有没有这个类),并且检查这个类是否被加载、解析和初始化过。如果没有则先执行类加载过程。在类加载检查通过后,接下来虚拟机将为新生的对象分配内存,对象所需内存大小在类加载完成后可以完全确定,为对象分配空间实际上是等同于将一块确定大小的内存块从堆内划分出来。

如果堆中内存块是绝对规整的,那么就会将所有已经使用的内存放在边,没有使用的内存放在另一边,管理内存只需要一个指针,分配内存只是将指针向空闲空间方向挪动相同大小的距离,这种方式称为指针碰撞,Serial,ParNew等带压缩整理过程的收集器会使用此法,简单高效。另外如果堆中内存块大小不一,那么已使用和空闲的内存相互交错,JVM就需要维护一个列表,记录哪个内存块是可用的,称为空闲列表法,CMS等基于清除算法的收集器会使用该法

另外还有一个问题就是:对象创建在虚拟机中时十分频繁的过程,因此需要考虑并发问题。这有两种方式解决问题:一个是对分配内存空间的动作进行同步处理,也就是保证分配内存空间的操作的原子性。另外一个就是为各个线程预先分配一小块内存,称为本地线程分配缓冲(TLAB),某个线程需要给对象实例分配内存,则先分配到TLAB中,只有本地缓冲区用完了,才会同步锁定。

接下来JVM还需要对对象进行必要的设置,比如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象哈希码和对象的GC分代年龄信息等。完成这些共走后,从虚拟机的视角来看,一个新的对象已经产生了,但是从Java程序视角来看,对象创建才刚刚开始,构造函数(也就是Class文件中的<init>()方法)还没开始执行,所有字段都默认为零值。

3.2 对象的内存布局

在HotSpot虚拟机中,对象在堆内存中的存储布局可以划分为三个部分:对象头(header),实例数据(instance data)和对齐填充(padding)。

HotSpot虚拟机对象的对象头部分包括两类信息。第一类是用于存储对象自身的运行时数据,如哈 希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。对象头的另外一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。这是Java实现反射的重要组成部分

接下来实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字 段内容,无论是从父类继承下来的

第三部分是对齐填充,HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,因此对齐填充用于填充空余的字节,使得一个对象的大小是8字节的整数倍。

3.3 对象的访问定位

创建对象自然是为了后续使用该对象,Java程序会通过栈上的reference数据来操作堆上的具体对象。对象访问方式也是由虚拟机实现而定的,主流的访问方式主要有使用句柄和直接指针两种:

如果使用句柄访问,Java堆中将划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息。

Java内存区分为 java的内存分区_java_02

如果使用直接指针访问,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销

Java内存区分为 java的内存分区_Java内存区分为_03

使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象非常普遍)时只会改变句柄中的实例数据指针,但是需要额外一次的寻址开销;使用直接指针最大的好就是速度更快,它节省了一次指针定位的时间开销