跳至主要內容

内存管理

blacklad大约 4 分钟JvmJavaJvm

内存管理

1 运行时数据区

1.1 程序计数器

每个线程独有的一块区域, 为当前线程下一条执行字节码行号的指示器,控制程序执行的流程。

  1. 分支 循环 跳转 异常等流程控制
  2. 线程恢复到正确的执行位置

当程序执行的是Native方法时, 当前区域的值为空

这个区域没有规定 OutOfMemoryError

1.2 Java虚拟机栈

每个线程独有,描述了java方法执行的线程内存模型, 在方法执行的时候会在栈内创建一个栈帧来存储: 局部变量、操作数栈、动态连接、方法出口。 在方法调用的时候栈帧会入栈,调用结束出栈。

1.2.1 局部变量表

局部变量表中存放了编译器可知的基本类型和引用类型(对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)) 。

编译期间分配,当方法运行时,局部变量表的大小是固定的,不会发生变化。

内存错误:

  1. StackOverflowError: 线程申请的栈深度大于虚拟机允许的范围
  2. OutOfMemoryError: 当栈深度允许动态申请,且无法申请到内存时

1.3 本地方法栈

和虚拟栈类似,虚拟机可以自己实现。

1.4 堆

所有线程共享的一块区域,所有的对象实例以及数组都应当在堆上分配.堆的内存在物理上是可以不连续的,但是在逻辑上必须是连续的。

分代设计是具体的虚拟机设计的,而并非由java虚拟机的规范中定义。

1.5 方法区

所有线程共享的一块区域,加载的类型信息常量静态变量即时编译器编译后的代码缓存等数据。

  1. jdk6及以前采用了永久代实现,有内存溢出的问题
  2. jdk8开始使用本地内存实现的元空间代替,主要存放类型信息,其他的被转移到堆中

OutOfMemoryError:无法进行内存分配时,抛出异常。

元空间的讲解open in new window 本地内存实现。

1.5.1 运行时常量池

运行时常量池是方法区的一部分,编译时产生的字面量与符号引用在加载时会存放在运行时常量区,如String.intern()可以在运行时产生常量。

关于常量池的解释open in new window

1.6 直接内存

不是java虚拟机的一部分,NIO使用直接内存提高性能。

如果没有计算进去也可能导致OutOfMemoryError。

2 HotSpot虚拟机对象

2.1 对象创建过程

在虚拟机中一个对象的创建过程。

  1. 当在字节码中遇到new指令时, 检测常量池中是否有类的符号引用,判断类是否被加载,否则执行类加载过程。

  2. 在堆中为对象分配内存

    1. 指针碰撞法: 如果堆中的已分配的在一边没有分配的在另一边, 可以通过移动指针来分配内存。
    2. 空闲列表: 如果堆中的内存已使用的和未使用的交叉在一起,则需要维护一个。
    3. 当多个线程同时申请分配时:
      1. 采用CAS直到分配成功
      2. 为每个线程提前划分划分区块
  3. 将分配到的内存初始化为零值,设置对象头的一些信息.

  4. 执行init构造函数对对象初始化

2.2 对象在内存中的存储结构

对象:存储结构可以分为 对象头 实例数据 对齐填充。

2.2.1 对象头

  1. 对象自身运行时的数据(Mark Word): 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等. 占用32位或者64位空间.
  2. 指向类型元数据(什么是类型元数据?)的指针, 确定该对象是哪个类的实例
  3. 如果是java数组,还需要存放记录数组长度的数据

2.2.2 实例数据

类中定义的字段内容,相同宽度的字段会被分配在一起(long/double int short/char byte/boolean), 父类定义的变量会在子类的前面。

2.2.3 对其填充

占位符保证是字节的整数倍。

2.3 对象的访问定位

java通过栈上的引用数据来操作堆上的数据,具体的访问方式由虚拟机实现。

2.3.1 句柄访问

划分一块内存作为句柄池,引用中存储句柄地址,句柄包含了: 实例数据和类型数据各自具体的地址信息。

当对象在垃圾回收时只需修改句柄中实例数据的指针。

2.3.2 指针访问

引用中存放了对象的地址,少一次间接访问的开销。

上次编辑于:
贡献者: blacklad