Java 内存管理机制:01 内存区域详解

目录

 JVM 运行时的数据区域

首先获取一个直观的认识:

 

总共也就这么5个区(直接内存不属于JVM运行时数据区的一部分),除了程序计数器其他的地方都有可能出现OOM(OutOfMemoryError),其中像是程序计数器和两个栈(Java 虚拟机栈&本地方法栈)都是线程私有的,所以肯定是线程隔离的。而其他2个区就是线程共享的了,也就是说,如果有多个线程要同时访问者两个区的数据,是会出现线程安全问题的。接下来,我们将对这些区域进行详细的介绍。

 程序计数器

  • 当前线程所执行的字节码的行号指示器,用于存放下一条需要运行的指令。
  • 运行速度最快位于处理器内部。
  • 是 5 个区域中唯一不会出现 OOM 的区域。

Java 虚拟机栈

  • Java 方法执行的内存模型,每个方法执行的过程,就是它所对应的栈帧在虚拟机栈中入栈到出栈的过程;
  • 每个方法执行的时候都会创建一个栈帧(stack frame)用于存放 局部变量表、操作栈、动态链接、方法出口。
  • 线程私有,生命周期与线程相同。
  • 虚拟机参数设置:-Xss.

本地方法栈

  • 服务于 native 方法;
  • 可能抛出的异常:与 Java 虚拟机栈一样。

Java 堆

  • 虚拟机最大的一块区域,虚拟机启动的时候创建。
  • 唯一的目的:存放对象实例,所有对象实例和数据的在堆上分配内存。
  • 垃圾收集器管理的主要区域
  • 虚拟机参数设置:
    • 最大值:-Xmx
    • 最小值:-Xms
    • 两个参数设置成相同的值可避免堆自动扩展。
  • 线程共享的区域。

方法区

  • 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;
    • 类信息:即 Class 类,如类名、访问修饰符、常量池、字段描述、方法描述等。
  • 垃圾收集行为在此区域很少发生;
    • 不过也不能不清理,对于经常动态生成大量 Class 的应用,如 Spring 等,需要特别注意类的回收状况。
  • 运行时常量池也是方法区的一部分;
    • Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量池,用于存放编译器生成的各种字面量(就是代码中定义的 static final 常量)和符号引用,这部分信息就存储在运行时常量池中。
  • 线程共享的区域。

直接内存

  • JDK 1.4 的 NIO 类可以使用 native 函数库直接分配堆外内存,这是一种基于通道与缓冲区的 I/O 方式,它在 Java 堆中存储一个 DirectByteBuffer 对象作为堆外内存的引用,这样就可以对堆外内存进行操作了。因为可以避免 Java 堆和 Native 堆之间来回复制数据,在一些场景可以带来显著的性能提高。
  • 虚拟机参数设置:-XX:MaxDirectMemorySize
    • 默认等于 Java 堆最大值,即 -Xmx 指定的值。
  • 将直接内存放在这里讲解的原因是它也可能会出现 OutOfMemoryError;
    • 服务器管理员在配置 JVM 参数时,会根据机器的实际内存设置 -Xmx 等信息,但经常会忽略直接内存(默认等于 -Xmx 设置值),这可能会使得各个内存区域的总和大于物理内存限制,从而导致动态扩展时出现 OOM。

HotSpot 虚拟机堆中的对象

这一小节将对 JVM 对 Java 堆中的对象的创建、布局和访问的全过程进行讲解。

对象的创建(遇到一条 new 指令时)

  1. 检查这个指令的参数能否在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,先把这个类加载进内存;
  2. 类加载检查通过后,虚拟机将为新对象分配内存,此时已经可以确定存储这个对象所需的内存大小;
  3. 在堆中为新对象分配可用内存;
  4. 将分配到的内存初始化;
  5. 设置对象头中的数据;
  6. 此时,从虚拟机的角度看,对象已经创建好了,但从 Java 程序的角度看,对象创建才刚刚开始,构造函数还没有执行。

第 3 步,在堆中为新对象分配可用内存时,会涉及到以下两个问题:

 如何在堆中为新对象划分可用的内存?

  • 指针碰撞(内存分配规整)
    • 用过的内存放一边,没用过的内存放一边,中间用一个指针分隔;
    • 分配内存的过程就是将指针向没用过的内存那边移动所需的长度;
  • 空闲列表(内存分配不规整)
    • 维护一个列表,记录哪些内存块是可用的;
    • 分配内存时,从列表上选取一块足够大的空间分给对象,并更新列表上的记录;

如何处理多线程创建对象时,划分内存的指针同步问题?

  • 对分配的内存空间进行同步处理,采用CAS配上失败重试的方式保证操作的原子性。
  • 线程分配缓冲区,把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配好内存。

对象的内存布局

  • 对象在内存中存储的布局可以分为三块区域对象头、实例数据和对齐填充。
  • 对象头,又可以分为两部分,第一部分存储自身运行时数据,第二部分是类型指针
  • 自身运行数据主要包括哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳。
  • 类型指针即对象指向他的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
  • 实例数据,对象真正存储的有用的有效信息,继承父类的字段也会包含。
  • 对齐填充,对象起始地址必须是8的整数倍,对齐填充起到了占位符的作用。

对象的访问

Java 程序需要通过虚拟机栈上的 reference 数据来操作堆上的具体对象,reference 数据是一个指向对象的引用,不过如何通过这个引用定位到具体的对象,目前主要有以下两种访问方式:句柄访问和直接指针访问。

句柄访问

句柄访问会在 Java 堆中划分一块内存作为句柄池,每一个句柄存放着到对象实例数据和对象类型数据的指针。

优势:对象移动的时候(这在垃圾回收时十分常见)只需改变句柄池中对象实例数据的指针,不需要修改reference本身。

 

 直接指针访问

直接指针访问方式在 Java 堆对象的实例数据中存放了一个指向对象类型数据的指针,在 HotSpot 中,这个指针会被存放在对象头中。

优势:减少了一次指针定位对象实例数据的开销,速度更快。

posted @ 2020-02-01 16:23  我想和这个世界谈谈,  阅读(107)  评论(0编辑  收藏  举报