2.1 内存结构
- 方法区
- 栈
- 堆
- 本地方法栈
- 程序计数器
2.2 直接内存
2.3 对象创建过程
- 检查类加载
- 分配内存地址(CAS / TLAB)
- 地址数据清零
- 设置对象头
- 执行构造方法 <init>()等
2.4 对象内存布局
- 对象头(MarkWord + KlassWord + (数组))
- 实例数据(父类数据 + 本类数据)
- padding
2.5 对象的访问方式
- 句柄访问
- 直接指针访问(Hotspot使用)
2.1 内存结构
根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域
1.方法区 | 共享 |
|
2.虚拟机栈 | 线程私有 |
|
3.虚拟机堆 | 共享 |
|
4.本地方法栈 | 线程私有 |
|
5.程序计数器 | 线程私有 |
|
什么是本地方法:
- 指用非Java语言实现的方法,它们通过Java本地接口(JavaNativeInterface)与Java代码交互。本地方法使用native关键字标识。
- 常见的本地方法
- System 类:
- System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length) 用于高效地复制数组。
- System.currentTimeMillis() 获取当前时间的毫秒值。
- System.nanoTime() 获取高精度的时间,用于性能度量。 Object 类: Object.clone() 创建并返回对象的一个副本。
- Thread 类:
- Thread.start0() 启动一个新线程。
- Thread.sleep(long millis) 使当前线程休眠指定的毫秒数。
- Thread.currentThread() 返回当前正在执行的线程对象的引用。
- Runtime 类:
- Runtime.freeMemory() 返回JVM空闲内存量。
- Runtime.totalMemory() 返回JVM总内存量。
- Runtime.gc() 请求垃圾收集器执行垃圾回收。
- FileInputStream 和 FileOutputStream 类:
- FileInputStream.open(String name) 打开指定名称的文件以供读取。
- FileOutputStream.write(int b, boolean append) 将指定的字节写入文件输出流。
- Unsafe 类 (sun.misc.Unsafe):
- Unsafe.allocateMemory(long bytes) 分配一块给定大小的内存。
- Unsafe.freeMemory(long address) 释放指定内存地址的内存。
- System 类:
2.2 直接内存
- 直接内存的定义与位置:直接内存(Direct Memory)不是JVM堆内存的一部分,它是在Java堆外分配的内存,直接向操作系统申请的内存空间
- 由来:在JDK 1.4中新加入了NIO类,引入了一种基于通道(Channel)与缓冲区 (Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。
- DirectByteBuffer中的unsafe.allocateMemory(size)是个一个native方法,这个方法分配的是堆外内存,通过C的malloc来进行分配的。分配的内存是系统本地的内存
- 优点:在一些场景中显著提高性能,因为避免在Java堆和Native堆中来回复制数据。
- 受到本机总内存的限制,也会有OOM
2.3 对象创建过程
1.类加载检查,如果没有则进行类加载
当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
2.为对象在堆中分配内存
对象所需的内存大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来
主要使用的两种分配方式(这两种策略被用于不同的场景和垃圾收集器中)
- 指针碰撞(Bump The Pointer)
- 内存是规整的,所有用过的内存块被放在一起,而空闲的内存放在另一边。对象分配时,直接根据需要分配的大小移动指针:指针之前的部分是已分配的内存,指针之后的部分是未分配的内存
- 适用场景:垃圾收集器实现的新生代区域,如使用Serial、ParNew等带压缩整理过程的收集器时
- 优点:①分配速度快,只需要修改一次指针。②实现简单,不需要遍历整个堆来找到足够大的空间
- 局限性:需要与压缩(Compaction)或复制(Copying)的垃圾回收算法结合使用,以避免内存碎片化
- 空闲列表(Free List)
- 内存不规整,维护了一个空闲内存块的列表,每个空闲内存块都记录了自己的大小和位置。当分配内存时,系统会遍历这个列表,找到一个足够大的空闲块,然后从中划分出所需的内存给对象使用,并更新列表中的空闲块信息
- 适用场景:适用老年代,如CMS这种基于清除 (Sweep)算法的收集器
- 局限性:分配速度相对较慢,因为需要遍历空闲列表来找到合适的内存块
如何确保多线程下的线程安全(多个线程可能会抢同一块地址):两种解决方案
- CAS
- 对分配内存空间的动作进行同步处理——采用CAS配上失败重试的方式保证更新操作的原子性
- 效率低
- TLAB(Thread Local Allocation Buffer):浅析java中的TLAB
- 另一种是把内存按线程划分成不同区域,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定分配到Eden区域
- Java中JVM使用的方法
- 线程初始化时会申请一块Buffer,先在TLAB上分配,如果Buffer容量不足,再用CAS在Eden上分配
- 可以通过-XX:+/-UseTLAB参数来设定是否使用TLAB
3.分配到的内存空间(但不包括对象头)都初始化为零值
- 所以对于基本类型,默认值是0
- 如果使用TLAB,这项工作会提前至TLAB分配时完成
4.设置对象头
初始化零值完成之后,JVM将设置对象头的信息。对象头信息包括这个对象是哪个类的实例(KlassWord)、如何才能找到类的元数据信息、对象的哈希码(实际上对象的哈希码会延后到真正调用Object::hashCode()方法时才计算)、对象的GC分代年龄,是否启用偏向锁等信息。
5.调用构造函数<init>方法
执行new指令所对应的字节码,即执行<init>()方法。这个方法中将包括Java代码所设定的初始化,比如你可能会在一个对象的构造函数中设置初始值或者通过方法调用来进行初始化操作。
对象创建过程中可能存在的问题
- 内存分配并发问题:见上文中的TLAB解决方案
- 初始化不完全问题:指对象初始化未完全完成就被其他线程可见,这通常是由于Java内存模型中允许的重排序操作所导致。为了解决这个问题,JVM在对象头的构造过程中会插入必要的内存屏障,以禁止特定类型的处理器重排序
2.4 对象的内存布局
对象的组成:对象头、实例数据、填充对齐。详细见Java并发编程3——JMM / synchronized / volatile / Monitor / 加锁算法_violate monitor_JYY_JYY_的博客-CSDN博客
- 对象头(主要两部分:Mark Word + Klass Word)
- Mark Word:用于存储对象自身的运行时数据。包括哈希码、GC分代年龄、锁状态标志位、线程持有的锁、偏向线程ID、偏向时间戳等
- Klass Word:类型指针,指向它的类型元数据的指针
- 数组长度(只有数组对象才有)
- 实例数据:包括父类的字段内容,本类的属性信息及字段内容等
- 分配顺序规则
- 先分配父类
- 默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs)
- 分配顺序规则
- 对齐填充(padding):确保任何对象的大小必须是8字节的整数倍,管理方便
2.5 对象的访问方式
- 句柄访问(Handle Access)
- 对象指向句柄池,再根据句柄池指向具体数据的地址。通过一个中介层实现了对象的间接访问。
- 优点:对象被移动时(如GC整理)只需要改指针
- 缺点:寻址时多一次指针查找
- 直接指针访问(Direct Pointer Access)(HotSpot使用)
- 直接指向具体数据,如果要访问对象类型则需再一次指针
- 优点:对于访问具体数据,只需要一次指针寻址
- 缺点:移动麻烦
2.6 虚拟机参数
堆大小 | 堆最小值:-Xms 堆最大值:-Xmx |
栈大小 | -Xss |
常量池 | -XX:PermSize -XX:MaxPermSize |
Reference
TLAB: