JVM内存模型和对象内存分配机制

目录


  • 线程程序计数器

  • 线程栈

    • 栈帧

  • 方法区

  • 对象创建流程

    • 类加载检查

    • 分配内存

    • 初始化

    • 设置对象头

    • 执行``方法

  • 内存分配流程

  • 对象栈中分配

  • 大对象直接进入老年代

  • 长期存活的对象进入老年代

  • 对象动态年龄判断

  • 老年代空间分配担保机制


运行时数据区内存模型

参考官方文档,运行时数据区主要由以下部分组成:

  • 线程程序计数器(The pc Register)
  • 线程栈(Stacks)
  • 堆(Heap)
  • 方法区(Method Area)
    • 运行时常量池(Run-Time Constant Pool)
  • 本地方法栈(Native Method Stacks)

图示:

JVM内存模型和对象内存分配机制

线程程序计数器

每一个Java线程都有自己的pc寄存器,如果不是native方法,则存储的值为JVM正在执行的字节码指令的地址,如果方法是native方法,则存储的值undefined

线程栈

JVM每个线程会分配一块内存区域,这块内存区域一般称为栈,不同线程不能共享栈内存空间,栈中存储了栈帧,栈帧随着方法的调用创建,随着方法调用结束而销毁。

栈帧

栈帧的内存空间是创建它的线程分配的,用于存储方法数据和方法过程的数据结构,栈中只能存储基本数据类型和引用类型数据。

栈帧由以下几个部分组成:

  • 局部变量表,方法的局部变量存储,通过索引访问。JVM使用局部变量来完成访问调用的参数传递,当调用非静态方法时,索引0位置局部变量存储的是该对象实例的引用(即this,这也是为什么静态方法中不能使用this的原因)。
  • 操作数栈,操作数栈可以类比计算机组成原理中的寄存器,JVM通过指令从局部变量表中加载常量、变量到操作数栈中,通过入栈、出栈操作通过指令进行运算,再把结果入栈。
  • 动态链接,一个方法如果调用另外一个方法、访问变量,需要通过符号引用表示,动态链接即把符号引用转换为直接引用。
  • 方法出口地址,当方法返回时,会恢复调用方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈,调整PC寄存器为执行方法调用后的指令地址。

JVM中,堆是各个线程共享的运行时内存区域,也是类和数组分配内存的区域,这些内存区域由GC管理,决定何时销毁。

方法区

方法区也是各个线程共享的内存区域,存储了类的信息,包括运行时常量池、方法的字节码内容等。

对象内存分配

对象创建流程

对象创建流程大致为类加载检查–>分配内存–>初始化零值–>设置对象头–>执行<init>方法。

图示:

JVM内存模型和对象内存分配机制

类加载检查

JVM创建对象前,会检查是否能定位到类的符号引用,并且检查符号引用的类是否已经被加载,如果没有,则先加载类

分配内存

划分内存给对象进行存储。

划分内存有两种方式:

  • 指针碰撞(默认使用),内存需要规整,即使用中的内存为一边,空虚内存为一边,分界线指针作为分界点,当需要分配时,向后挪动指针对象大小即可。
  • 空闲列表方式,内存不规整,JVM维护一个列表,用来记录空虚内存块,在分配时,找到一块足够大的内存用于分配。

这两种方法都存在并发问题,解决办法:

  • CAS方式
  • 本地线程分配缓冲(Thread Local Allocation Buffer, TLAB),每个线程会在堆中预先分配一块内存,JVM默认开启-XX:+UseTLAB

初始化

内存分配完成后,将分配的内存空间初始化为零值,这一步是为了保证在Java代码中可以不赋初始化就可以直接使用。

设置对象头

初始化零值后,需要设置对象头信息,比如元数据信息、哈希码、GC分代年龄、锁状态标志、线程持有的锁指针、类型指针(指向方法区类元数据的指针,通过这个指针来确定对象属于哪个类的实例)。

对象头很复杂,暂时不做过多深入,源码注释可以在markOop.hpp文件中找到:

#include "oops/oop.hpp"

// The markOop describes the header of an object.
//
// Note that the mark is not a real oop but just a word.
// It is placed in the oop hierarchy for historical reasons.
//
// Bit-format of an object header (most significant first, big endian layout below):
//
//  32 bits:
//  --------
//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
//             size:32 ------------------------------------------>| (CMS free block)
//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
//  size:64 ----------------------------------------------------->| (CMS free block)
//
//  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
//  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
//  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
//  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
//
//  - hash contains the identity hash value: largest value is
//    31 bits, see os::random().  Also, 64-bit vm's require
//    a hash value no bigger than 32 bits because they will not
//    properly generate a mask larger than that: see library_call.cpp
//    and c1_CodePatterns_sparc.cpp.
//
//  - the biased lock pattern is used to bias a lock toward a given
//    thread. When this pattern is set in the low three bits, the lock
//    is either biased toward a given thread or "anonymously" biased,
//    indicating that it is possible for it to be biased. When the
//    lock is biased toward a given thread, locking and unlocking can
//    be performed by that thread without using atomic operations.
//    When a lock's bias is revoked, it reverts back to the normal
//    locking scheme described below.
//
//    Note that we are overloading the meaning of the "unlocked" state
//    of the header. Because we steal a bit from the age we can
//    guarantee that the bias pattern will never be seen for a truly
//    unlocked object.
//
//    Note also that the biased state contains the age bits normally
//    contained in the object header. Large increases in scavenge
//    times were seen when these bits were absent and an arbitrary age
//    assigned to all biased objects, because they tended to consume a
//    significant fraction of the eden semispaces and were not
//    promoted promptly, causing an increase in the amount of copying
//    performed. The runtime system aligns all JavaThread* pointers to
//    a very large value (currently 128 bytes (32bVM) or 256 bytes (64bVM))
//    to make room for the age bits & the epoch bits (used in support of
//    biased locking), and for the CMS "freeness" bit in the 64bVM (+COOPs).
//
//    [JavaThread* | epoch | age | 1 | 01]       lock is biased toward given thread
//    [0           | epoch | age | 1 | 01]       lock is anonymously biased
//
//  - the two lock bits are used to describe three states: locked/unlocked and monitor.
//
//    [ptr             | 00]  locked             ptr points to real header on stack
//    [header      | 0 | 01]  unlocked           regular object header
//    [ptr             | 10]  monitor            inflated lock (header is wapped out)
//    [ptr             | 11]  marked             used by markSweep to mark an object
//                                               not valid at any other time
//
//    We assume that stack/thread pointers have the lowest two bits cleared.

32位对象头结构:

|-------------------------------------------------------|--------------------|
|                  Mark Word (32 bits)                  |       State        |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 |       Normal       |
|-------------------------------------------------------|--------------------|
|  thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 |       Biased       |
|-------------------------------------------------------|--------------------|
|               ptr_to_lock_record:30          | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
|               ptr_to_heavyweight_monitor:30  | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
|                                              | lock:2 |    Marked for GC   |
|-------------------------------------------------------|--------------------|

64位对象头结构:

|------------------------------------------------------------------------------|--------------------|
|                                  Mark Word (64 bits)                         |       State        |
|------------------------------------------------------------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |       Normal       |
|------------------------------------------------------------------------------|--------------------|
| thread:54 |       epoch:2        | unused:1 | age:4 | biased_lock:1 | lock:2 |       Biased       |
|------------------------------------------------------------------------------|--------------------|
|                       ptr_to_lock_record:62                         | lock:2 | Lightweight Locked |
|------------------------------------------------------------------------------|--------------------|
|                     ptr_to_heavyweight_monitor:62                   | lock:2 | Heavyweight Locked |
|------------------------------------------------------------------------------|--------------------|
|                                                                     | lock:2 |    Marked for GC   |
|------------------------------------------------------------------------------|--------------------|

执行<init>方法

属性赋值和执行构造方法。

内存分配流程

流程图:

JVM内存模型和对象内存分配机制

对象栈中分配

为了提升性能,通过逃逸分析确定对象不会被外部访问,则将该对象分配在栈上,对象内存空间随着栈帧出栈而销毁。

JVM开启逃逸分析-XX:+DoEscapteAnalysis,通过标量替换分配在栈上。Java的基本数据类型和引用类型就是标量,Java对象可以通过分解为标量,使其可以分配在栈内存中。

大对象直接进入老年代

通过设置JVM参数-XX:PretenureSizeThreshold设置大对象的大小,当对象大小超过这个值是,直接进入老年代,需要注意的是,这个参数只有在SerialParNew收集器下有效。

目的:避免大对象分配内存和GC时因为复制操作而降低效率。

长期存活的对象进入老年代

每个对象都会有一个对象年龄,存储在对象头中,当对象在Eden区域经历第一次Young GC后依然存活,并且能被Survivor容纳,则将被移动到Survivor区域,并将对象年龄设为1,之后每经过一次Young GC,年龄就增加1,直到增加到-XX:MaxTenuringThreshold值(默认15,CMS收集器默认6,不同收集器可能不同)后,晋升到老年代。

对象动态年龄判断

当Young GC后,如果当前Survivor区域中一批对象总大小超过了Survivor区域内存的50%(-XX:TargetSurvivorRatio),那么大于等于这批对象年龄最大值的对象,晋升到老年代。

目的:让长期存活的对象,尽早进入老年代

老年代空间分配担保机制

Young GC之前JVM会计算老年代剩余可用空间,如果可用空间不足以容纳年轻代所有对象,会查看是否配置-XX:-HandlePromotionFailure(1.8默认配置),如果有这个参数,会比较老年代剩余可用空间是否大于之前每一次Young GC后进入老年代的对象评价大小,如果小于或者没有这个参数,触发Full GC,Full GC后如果还是没有足够空间触发OOM

Young GC之后如果老年代无法容纳存活对象也会触发Full GC,Full GC之后如果还是空间不足,则也会发生OOM

参考文档

  • Oracle 官方文档
  • Java对象头分析及Synchronized锁


原文始发于微信公众号(erpang coding):JVM内存模型和对象内存分配机制

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/37334.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!