内存管理
内存管理
1 运行时数据区
1.1 程序计数器
每个线程独有的一块区域, 为当前线程下一条执行字节码行号的指示器,控制程序执行的流程。
- 分支 循环 跳转 异常等流程控制
- 线程恢复到正确的执行位置
当程序执行的是Native方法时, 当前区域的值为空
这个区域没有规定 OutOfMemoryError
1.2 Java虚拟机栈
每个线程独有,描述了java方法执行的线程内存模型, 在方法执行的时候会在栈内创建一个栈帧来存储: 局部变量、操作数栈、动态连接、方法出口。 在方法调用的时候栈帧会入栈,调用结束出栈。
1.2.1 局部变量表
局部变量表中存放了编译器可知的基本类型和引用类型(对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)) 。
编译期间分配,当方法运行时,局部变量表的大小是固定的,不会发生变化。
内存错误:
- StackOverflowError: 线程申请的栈深度大于虚拟机允许的范围
- OutOfMemoryError: 当栈深度允许动态申请,且无法申请到内存时
1.3 本地方法栈
和虚拟栈类似,虚拟机可以自己实现。
1.4 堆
所有线程共享的一块区域,所有的对象实例以及数组都应当在堆上分配.堆的内存在物理上是可以不连续的,但是在逻辑上必须是连续的。
分代设计是具体的虚拟机设计的,而并非由java虚拟机的规范中定义。
1.5 方法区
所有线程共享的一块区域,加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
- jdk6及以前采用了永久代实现,有内存溢出的问题
- jdk8开始使用本地内存实现的元空间代替,主要存放类型信息,其他的被转移到堆中
OutOfMemoryError:无法进行内存分配时,抛出异常。
元空间的讲解 本地内存实现。
1.5.1 运行时常量池
运行时常量池是方法区的一部分,编译时产生的字面量与符号引用在加载时会存放在运行时常量区,如String.intern()可以在运行时产生常量。
1.6 直接内存
不是java虚拟机的一部分,NIO使用直接内存提高性能。
如果没有计算进去也可能导致OutOfMemoryError。
2 HotSpot虚拟机对象
2.1 对象创建过程
在虚拟机中一个对象的创建过程。
当在字节码中遇到new指令时, 检测常量池中是否有类的符号引用,判断类是否被加载,否则执行类加载过程。
在堆中为对象分配内存
- 指针碰撞法: 如果堆中的已分配的在一边没有分配的在另一边, 可以通过移动指针来分配内存。
- 空闲列表: 如果堆中的内存已使用的和未使用的交叉在一起,则需要维护一个。
- 当多个线程同时申请分配时:
- 采用CAS直到分配成功
- 为每个线程提前划分划分区块
将分配到的内存初始化为零值,设置对象头的一些信息.
执行init构造函数对对象初始化
2.2 对象在内存中的存储结构
对象:存储结构可以分为 对象头 实例数据 对齐填充。
2.2.1 对象头
- 对象自身运行时的数据(Mark Word): 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等. 占用32位或者64位空间.
- 指向类型元数据(什么是类型元数据?)的指针, 确定该对象是哪个类的实例
- 如果是java数组,还需要存放记录数组长度的数据
2.2.2 实例数据
类中定义的字段内容,相同宽度的字段会被分配在一起(long/double int short/char byte/boolean), 父类定义的变量会在子类的前面。
2.2.3 对其填充
占位符保证是字节的整数倍。
2.3 对象的访问定位
java通过栈上的引用数据来操作堆上的数据,具体的访问方式由虚拟机实现。
2.3.1 句柄访问
划分一块内存作为句柄池,引用中存储句柄地址,句柄包含了: 实例数据和类型数据各自具体的地址信息。
当对象在垃圾回收时只需修改句柄中实例数据的指针。

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