运行时数据区域
Java和C++之间有一堵由内存动态分配和垃圾收集机制所围成的墙,墙外面的人想进来,墙里面的人想出去。深入理解Java虚拟机这本书的作者在介绍自动化内存管理时写到。个人认为内存动态分配和垃圾收集机制是Java的特点,不同于C++,一个对象从创建的内存分配,到销毁的内存回收都需要程序员来掌控,Java把这部分权限从程序员手中收回,限制了程序员的权限,但同时也减少了开发中存在的内存泄漏,内存溢出的频率。Java中不是没有内存泄漏和内存溢出,但是相对来说没有c++里面那么频繁,排查起来相对简单。了解虚拟机的相关知识对一个Java程序员是必不可少的技能。
Java虚拟机是Java实现跨平台的基础,也就是一直宣称的一次编译,到处执行,Java文件编译后生成的文件并不是调用操作系统的native方法,而是运行在JVM上,由JVM来调用操作系统的native方法。JVM在执行Java程序时,会将自身管理的内存划分为若干个数据区域:
- 程序计数器:
- 线程私有内存,
- 不会产生OOM(唯一)
- 一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。
- 字节码解释器工作时就是通过改变计数器的值来选取下一条执行的字节码指令。
- Java虚拟机栈
- 线程私有
- 声明周期和线程一样
- 描述的是Java方法执行的内存模型(也就是说每个方法在执行过程中都会创建一个栈帧,栈帧中存储着局部变量表,操作数栈,动态链接,方法出口等信息,方法的执行过程对应着栈帧的入栈和出栈过程)
- 平常说的栈指的是虚拟机栈或者说是其中局部变量表部分(局部变量表存放着编译期可知的各种基本数据类型8+reference,也就是说在编译完成之后其方法的局部变量表的大小就已经确定)
- 本地方法栈
- 线程私有
- 服务Native方法
- hotspot虚拟机将本地方法栈和Java虚拟机栈合二为一
- Java堆
- 所有线程共享
- 原则上存放所有的对象实例(JIT和逃逸分析技术,栈上分配,标量替换优化等可能会在堆外创建实例)
- GC堆,垃圾收集的主要区域
- 新生代和老年代,Eden和Survivor
- 可以划分多个Thread Local Allocation Buffer TLAB
- 方法区
- 所有线程共享
- 存储已被虚拟机加载的类信息,常量,静态变量,即时编译器变异后的代码等数据
- Non-Heap
- hotspot将其称为永久代,不过已经取消
- 运行时常亮池是方法区的一部分,存放字面量和符号引用
- 直接只存:Direct Memory,NIO类,可以在native函数库外直接分配堆外内存,可以通过在堆中存储的DirectByBuffer对象作为引用来操作
- 程序计数器:
HotSpot
对象创建流程
虚拟机为新生对象分配内存时,对象所需的内存大小在类加载完毕之后就已经确定。分配方式有两种
- Bump the Pointer,碰撞指针,此时内存空闲内存时连续规整的,Serial, ParNew等垃圾收集器采用
- Free List 空闲列表,空闲内存时分散的,找一个足够大的空间,Mark-Sweep收集器
线程安全问题,也就是同时有两个对象申请同一块内存
- 对分配内存空间的动作进行同步处理,CAS+失败重试
- 将内存分配的动作按照线程划分在不同的空间中进行,TLAB,TLAB满了并且分配新的TLAB才需要同步
对象的内存布局
- 对象头 Header
- 存储对象自身运行时的数据
- 哈希码
- GC分代年龄
- 锁状态标志
- 线程持有的锁
- 偏向线程ID
- 偏向时间戳
- 类型指针:指向类元数据的指针
- 存储对象自身运行时的数据
- 实例数据
- 对象真正存储的有效信息,代码中定义的字段内容
- 对齐填充
- 非必须,对齐实例数据,使其为8的倍数。
- 对象头 Header
对象的访问定位
句柄:Java堆中划分出一块内存来作为句柄池,reference中存储的是对象的句柄地址,句柄中包含了对象实例数据和类型数据各自的具体地址信息
直接指针:reference中存储的是对象地址,对象中存储着到对象类型指针的地址。