数据区域划分
程序计数器
虚拟机栈
本地方法栈
堆
方法区
运行时常量池
StringTable
直接内存
创建新对象说明
对象的创建
对象的内存布局
对象头
实例数据
对齐填充
对象的访问定位
数据区域划分
运行时内存区域划分: 程序计数器, 虚拟机栈, 本地方法栈, 堆, 方法区
程序计数器
线程私有
通过寄存器实现
不会存在运行溢出
当前线程所执行的行号指示器, 记住下一条 JVM 指令的执行地址
虚拟机栈
垃圾回收不涉及栈内存
栈内存是线程私有的, 可以理解为线程运行需要的内存空间
栈由栈帧组成, 每个栈帧代表一个方法执行时需要的内存(参数, 局部变量, 返回地址)
每个线程只能有一个活动栈帧, 对应着当前正在执行的那个方法
栈内存分配过大只能支撑一定的递归调用, 并不会影响运行速度, 还可能减少线程数量(因为物理内存是一定的)
本地方法栈
为运行本地方法时分配的内存(HotSpot 把虚拟机栈和本地方法栈合二为一了)
堆
有垃圾回收机制
线程共享, 需要考虑线程安全问题
存储的都是对象的实例(通过 new 关键字创建的对象)
从内存分配的角度来说: 堆中可以划分出多个线程私有的分配缓冲区(TLAB), 以提升对象分配时的效率
Java 堆可以处于物理上不连续的内存空间, 但在逻辑上应该视为连续的(但是对于比如数组这种大对象, 可能会要求连续的内存空间)
方法区
线程共享区
存储已被虚拟机加载的类型信息, 常量, 静态变量, 即时编译器编译后的代码缓存
在虚拟机启动时被创建, 逻辑上属于堆的一部分(不同 JVM 实现的方式不同)
JDK1.6 使用永久代 (PerGen) 作为方法区的实现
JDK1.8 使用元空间 (Metaspace) 对方法区进行实现(包含 Class ClassLoader 常量池三个部分, 放在直接内存中)StringTable 放在堆中(有助于垃圾回收管理)
使用场景: 如 Spring Mybatis 使用的动态加载
运行时常量池
运行时常量池是方法区的一部分
二进制字节码内容: 类基本信息 \ 常量池表 \ 类方法定义, 包含了虚拟机指令
其中, 常量池表中存放编译期间生成的各种字面量 (比如各种基本数据类型) 与符号引用(比如, 类名 \ 方法名 \ 参数类型), 这部分内容将在类加载后存放到方法区的运行时常量池中, 并把符号地址变为真实地址
StringTable
类似于 hashTable 结构, 不能自动扩容
常量池中的字符串只是符号, 第一次使用时才变为对象
利用串池机制, 避免重复创建字符对象
案例
字符串拼接的原理是编译期优化
字符串拼接原理是 StringBuilder(JDK1.8)
使用 intern()方法, 主动将串池中还没有的字符串对象放入串池
- // StringTable [ "a", "b" ,"ab" ] hashtable 结构, 不能扩容
- public class Demo1_22 {
- // 常量池中的信息, 都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号, 还没有变为 java 字符串对象
- // ldc #2 会把 a 符号变为 "a" 字符串对象
- // ldc #3 会把 b 符号变为 "b" 字符串对象
- // ldc #4 会把 ab 符号变为 "ab" 字符串对象
- public static void main(String[] args) {
- String s1 = "a"; // 懒惰的
- String s2 = "b";
- String s3 = "ab";
- String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab")
- String s5 = "a" + "b"; // javac 在编译期间的优化, 结果已经在编译期确定为 ab
- System.out.println(s3 == s5);
- }
- }
JDK1.7 以后, 利用 intern()方法, 会将字符串对象尝试放入串池, 如果有则并不会放入, 如果没有则放入串池, 会把串池中的对象返回; 而 JDK1.6 调用 intern()方法, 是将对象拷贝一份到串池中, 指向堆中的对象本身引用并不不改变
- public class Demo1_23 {
- // ["ab", "a", "b"]
- public static void main(String[] args) {
- demo1();
- demo2();
- }
- static void demo1() {
- // 串池中事前没有 "ab",intern()之后, s 返回的是串池中的对象
- String s = new String("a") + new String("b");
- String s1 = s.intern();
- System.out.println(s == "ab"); // true
- System.out.println(s1 == "ab"); //true
- }
- static void demo2() {
- // 串池中事前已有 "ab",s 返回的仍是堆中的对象
- String x = "ab";
- String s = new String("a") + new String("b");
- // 堆 new String("a") new String("b") new String("ab")
- String s2 = s.intern(); // 将这个字符串对象尝试放入串池, 如果有则并不会放入, 如果没有则放入串池, 会把串池中的对象返回
- System.out.println( s2 == x); // true
- System.out.println( s == x ); //false
- }
- }
位置
JDK1.6 时, StringTable 放在元空间内, 属于永久代的位置, 但是 StringTable 占用内存容易触发 full gc 耗时较久; JDK1.7 以后将 StringTable 放在堆内存中, 随着内存占用增大首先触发 minor gc, 耗时较短.
直接内存
使用 Native 函数直接分配堆外内存, 然后通过 Java 堆里的 DirectByteBuffer 对象作为引用对这块内存的引用进行操作.
原理说明:
使用 Unsafe 对象完成直接内存的分配和回收, 回收时需要主动调用 freeMemory 方法
ByteBuffer 的实现类内部使用了 Cleaner(虚引用)来监测 ByteBuffer(BB)对象, 一旦 BB 对象被垃圾回收, 会有 ReferenceHandler 线程通过 Cleaner 方法调用 freeMemory 来释放内存
创建新对象说明
HotSpot 虚拟机在 Java 堆中对象分配, 布局和访问的过程
对象的创建
new 字节码指令
虚拟机遇到 new 字节码指令时, 首先检查能否在常量池中定位到一个类的符号引用, 并检查该符号引用的来是否已被加载, 解析和初始化. 如果没有, 则执行相应的类加载过程.
类加载检查后, 虚拟机为新生对象分配内存
内存分配
对象所需的内存大小在类加载过程中可以确定, 在 Java 虚拟机中为对象划分内存时有两种方式: 指针碰撞, 空闲列表
指针碰撞: 利用一个指针作为已用内存和未用内存的分界点的指示器, 内存分配就仅仅是指针的移动. 优点在于不会造成内存碎片化, 但是速度较慢
空闲列表: 虚拟机维护一个内存使用记录表, 使用时, 从空闲的内存区域直接划分一块足够大的空间给对象实例.
内存分配的线程安全问题
划分可用空间后仍要考虑并发情况下对内存的使用, 有两种方式解决内存冲突的问题: CAS 配上失败重试, TLAB 本地线程分配缓冲
TLAB: 把内存分配的动作按照线程划分在不同的空间之中进行, 即每个线程在 Java 堆中预先分配了一小块内存空间
对象的内存布局
对象在堆内存中的布局可以划分为三个部分: 对象头, 实例数据, 对齐填充
对象头
对象头中包含两类信息: Mark Word, 类型指针
Mark Word
存储对象自身运行时数据, 考虑到虚拟机的空间效率, 被设计成一个动态定义的数据结构, 即根据对象的状态复用自己的存储空间(数据长度在 32 位和 64 位虚拟机上分别为 32 个比特和 64 个比特)
类型指针
对象中指向它类型元数据的指针, Java 虚拟机通过这个指针来确定该对象是哪个类的实例 (不是所有虚拟机都必须在对象数据上保留类型指针) 此外, 如果对象是一个数组, 对象头中还必须拥有一块记录数据长度的数据
实例数据
即程序代码里定义的各种类型的字段内容, 包括从父类继承的或子类中定义的字段. 各类数据存储是按照一定顺序的(long/double,ints...), 而宽度相同的字段总是被分配到一起存放, 所以父类中定义的变量可能会出现在子类之前.
对齐填充
占位符, 无特殊意义
HotSpot 虚拟机的自动内存管理系统要求对象的起始地址必须是 8 字节的整倍数, 若有些对象的对象头和示例数据内存设计不是 8 的倍数, 则需要利用占位符来进行填充.
对象的访问定位
Java 程序通过 reference 数据操作对上的具体对象, 主流的访问方式有两种: 句柄, 直接指针
句柄
Java 堆中可能划分出一块内存作为句柄池. reference 中存储对象的句柄地址, 句柄中包含对象的实例数据和类型数据的具体地址信息.
直接指针
Java 堆中对象的布局需要考虑如何放置类型数据的相关信息(如访问信息).reference 中存储的直接就是对象地址, 如果只访问对象本身, 就不要多一次间接访问的开销
优缺点
使用句柄访问, reference 数据只需关乎句柄地址, 当对象被回收或移动后只需改变句柄中的实例数据指针, 而 reference 本身不用修改
使用直接指针省去了一次指针定位的时间开销, 速度更快, 由于 Java 中对象的访问相当频繁, 所以效果可观.
HotSpot 使用直接指针的方式
来源: https://www.cnblogs.com/CodeMLB/p/12088837.html