1. JVM运行时数据区域
1.1. 程序计数器
- 是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
- 每条线程都有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,这类内存区域被称为“线程私有”的内存。
- 是唯一一个在JVM规范中没有规定任何OutOfMemoryError情况的区域。
1.2. 虚拟机栈
- 描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出入口等信息。每个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
- 也是线程私有的,其生命周期与线程相同。
- 局部变量表存放了编译器可知的各种基本数据类型,所需的内存空间在编译期间完成分配。
- 这个区域有两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果动态扩展虚拟机栈时,无法申请到足够的内存,就会抛出OutOfMemoryError异常。
1.3. 本地方法栈
- 和虚拟机栈作用相似,只不过虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
- 抛出的两种异常也与虚拟机栈相同。
1.4. Java堆
- 被所有线程共享的内存区域,在虚拟机启动时创建。此区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
- Java堆中还可以细分为:新生代和老年代;Eden、From Survivor、To Survivor空间等;多个线程私有的分配缓冲区(TLAB)。
- 可以处于物理上不连续的内存空间,只要逻辑上是连续的即可。
- 可以实现成固定大小的,也可以是可扩展的。
- 如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
1.5. 方法区
- 是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- HotSpot虚拟机中的方法区常被称为“永久代”,可以像管理Java堆一样管理方法区,但这样更容易遇到内存溢出的问题。现在,HotSpot虚拟机已经逐步改为采用Native Memory来实现方法区了。
- JVM规范中,方法区可以选择不实现垃圾收集。
- 当方法区无法满足内存分配的需求时,将抛出OutOfMemoryError异常。
1.6. 运行时常量池
- 是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,在类加载后进入方法区。
- 对运行时常量池的格式,JVM规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。
- Java语言并不要求常量一定只有编译器才能产生,运行期间也可能将新的常量放入池中,如String类的intern()方法。
- 当常量池无法再申请到内存时,会抛出OutOfMemoryError异常。
1.7. 直接内存
- 并不是虚拟机运行时数据区的一部分,也不是JVM规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
- JDK 1.4中新加入了NIO类,引入了一种基于通道与缓冲区的I/O方式。使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样避免了在Java堆和Native堆之间来回复制数据,能在一些场景中显著提高性能。
- 直接内存的分配不会收到Java堆大小的限制,但是还是会受到本机总内存的限制。
2. HotSpot虚拟机对象探秘
2.1. 对象的创建
- 当虚拟机遇到一条new指令时,首先检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
- 在类加载检查通过后,虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
- 如果堆内存是规整的,则使用指针碰撞的分配方式(Serial、ParNew),否则使用空闲列表(CMS)。
- 并发情况下的线程安全问题的解决方案:
- 对分配内存空间的动作进行同步处理。虚拟机采用CAS加上失败重试的方式保证更新操作的原子性。
- 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在堆中预先分配一小块内存(TLAB)。只有TLAB用完并分配新的TLAB时,才需要同步锁定。
- 接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
- 执行new指令之后,接着执行<init>方法,把对象按照程序员的意愿进行初始化。
2.2. 对象的内存布局
-
对象头
- 一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit,官方称之为“Mark Word”。
- 另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个数组,则还需要有一块用于记录数组长度的数据。
- 实例数据:是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。
- 对齐填充:并不是必然存在的,也没有特别的含义,仅仅起着占位符的作用。
2.3. 对象的访问定位
通过栈上的reference数据来操作堆上的具体对象,对象的访问方式取决于虚拟机的实现:
- 句柄访问:堆中划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
- 直接指针访问:堆对象的布局中放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。
网友评论