深入理解jvm阅读笔记-内存区域划分

前言

最近 在学习 jvm相关的知识
之前 一直看的一些博客 文章 没有静下心 看看 jvm相关的书籍
干脆买了一本 周志明大佬的jvm的书

内存区域划分

内存区域划分图

程序计数器
  • 线程独有的信息
  • 功能
    用来指示执行的字节码的行号 所以这个必须是线程独有信息不能共享
    字节码解释器通过改变计数器 来选取下一条的指令 去执行相关操作 如跳转、循环、异常、线程恢复等
  • 特殊说明
    jvm虚拟机规范没有对这个区域规定内存错误(oom)的情况
虚拟机栈
  • 线程独有信息
  • 功能
    线程栈帧 用来存储局部变量、操作数栈、动态链接、方法出口 每个方法调用执行完成 都对应一个栈帧 的入栈和出栈
    局部变量存储了 基础类型 和引用类型的引用指针或者句柄 和返回数据的地址(returnAddress)
  • 特殊说明
    64位的long和double 占用两个局部变量空间(slot)其余的都是占用一个
    局部变量需要的内存空间在编译的时期已经完成,当进入一个方法 局部变量的空间大小是确定的 运行期不会改变局部变量的内存大小
    当线程请求的栈深度大于虚拟机允许的深度抛出 StackOverflowError
    当虚拟机栈支持动态扩展 当扩展时候无法申请到足够的内存 抛出 OutOfMemoryError异常
本地方法栈
  • 线程独有信息
  • 功能
    线程栈帧 不过是本地方法的栈帧 而不是虚拟机的栈帧 保存的是本地方法的栈帧信息
  • 特殊说明
    当线程请求的栈深度大于虚拟机允许的深度抛出 StackOverflowError
    当虚拟机栈支持动态扩展 当扩展时候无法申请到足够的内存 抛出 OutOfMemoryError异常
java 堆 (gc 主要活动区 大致分为新生代、老年代)
  • 所有线程共享区域
  • 功能
    java 堆内存 java中几乎所有的对象杜存放在堆上
    java 垃圾收集主要就是收集 堆内存 (gc堆)
    根据现代垃圾收集器对于java堆内存的划分可细分为 新生代、老年代 或者更加细致 Eden、From Survivor、To Survivor 空间等
    从内存分配角度 线程共享的java堆可能划分出多个线程私有的分配缓存区 、
  • 特殊说明
    java堆可以在物理上不连续的内存空间内 只要逻辑是连续的 可以实现固定的大小也可以可以扩展的形式 通过-Xmx、-Xms控制堆内存的最小最大范围
    如果堆中没有内存完成是实例的分配并且堆无法再进行扩展 抛出 OutOfMemoryError
方法区
  • 所有线程共享区域
  • 功能
    存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码
    别名非堆(Non-Heap) 目的和 java 堆内存区分开
  • 特殊说明
    在HotSpot虚拟机中 方法区可以称之为永久代 但是在jdk7之后 HotSpot虚拟机也渐渐放弃永久代 改为Native Memory 在1.7中 String已经从常量池移除
    java虚拟机规范对于方法区限制较为宽松 除了跟java堆一样不需要连续的物理内存和可以选择固定还是可扩展大小 还可以选择不实现垃圾收集
    当方法区无法满足内存分配的需求的时候 抛出 OutOfMemoryError
    运行期常量池
  • 归属于方法区的一部分
  • 功能
    在编译期中 生成的各种字面量、符号引用等等 类加载后会直接加载进常量池
  • 特殊说明
    java虚拟机对class文件每个部分都有严格限制 但是对于常量池没有细节要求 一般只是保存字面量、符号引用还有翻译出来的直接引用
    java语言要求常量不一定只有编译的时候会产生 例如String.intern() 这种可以在运行期 产生新的常量到常量区
    由于存在运行期间 增加常量 那么当常量池无法申请到内存抛出 OutOfMemoryError 异常
直接内存
  • 不属于虚拟机运行的数据区
  • 不是java虚拟机规范定义的内存区域
  • 功能
    Channel和Buffer的io方式 可以只用Native函数 直接分配堆外内存 通过存储在java堆中的 DirectByteBuffer 作为这块内存的引用进行操作 避免在java堆和Native堆中来回复制数据
  • 特殊说明
    直接内存不受java虚拟机限制 当各个区域的内存和大于物理机的内存 那么也会抛出 OutOfMemoryError

HotSpot对象的创建和内存布局、对象访问定位

普通的java对象创建

当执行到new 指令 -> 检测能否在常量池中定位到一个类的符号引用 并且检测这个符合引用代表的类是否被加载、解析、初始化 如果没有先执行类加载
->执行完类加载检测 虚拟机为这个新对象分配内存。对象需要的内存大小在类加载后已经确定 为对象分配空间就是把一块确定大小的内存从堆中划分出来
->将分配到的内存初始化为0 不包括对象头 如果使用的TLAB 这个过程会在TLAB分配时候进行 保证对象的实例字段在java中不赋值即可使用 程序直接访问某些字段的零值
->虚拟机堆对象进行必要的设置 如对象是那个类的实例、如何才能找到类的元数据、对象哈希码、对象gc分代年龄等信息 这些信息在对象头中
->这个时候虚拟机认为对象已经初始化完毕 java程序认为对象还未创建完毕 继续执行init方法 ->对象初始化完毕

内存划分方法
  • 指针碰撞
    假设java堆内存绝对规整 所有用过的放在一边 空闲的放在另外一边 中间通过一个指针来作为指示器 那么内存划分只是把指针向空闲空间那边移动出来和对象大小相等的距离

  • 空闲列表
    虚拟机维护一个列表 记录那些内存可以用 在分配内存的时候 从列表中找出一块足够大的内存给对象 并且更新列表

根据gc是带有压缩规整功能 来选择 那种内存划分方法
Serial ParNew 带有压缩规整的 采用的是指针碰撞
CMS 采用空闲列表

指针分配内存存在的并发问题
  • 对分配内存空间的动作进行同步处理
    虚拟机采用cas配上失败重试方式保证更新操作都是原子性的操作
  • 本地线程分配缓冲 (Thread Local Allocation Buffer) TLAB
    把内存分配工作按照线程划分到不同的空间中进行
    每个线程预先在java堆中申请一块内存 那个线程要分配内存就在那个线程的TLAB上分配 只有当TLAB使用完毕分配新的TLAB的时候 才需要同步锁定
    虚拟机通过-XX:+/-UseTLAB参数设定
    对象内存布局
    HotSpot虚拟机中 对象在内存中存储的布局分为三块区域 对象头(header)、实例数据(instance data )、对齐填充(padding)
  • 对象头
    对象头分为两部分
    一: 存储对象自身运行时候的数据
    如hashCode gc分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
    这部分数据在32位和64为虚拟机中分别为32bit 64bit大小 官方称之为 “Mark Word”
    二: 类型指针
    指向类的元数据的指针 虚拟机通过这个指针 确定对象是那个类的实例
    不是所有的虚拟机实现都必须在对象数据上找到保留的类型指针
  • 实例数据
    存储对象真正有效的信息 至于存储顺序 受到虚拟机的分配策略参数和字段在java源码中定义的顺序影响
    HotSpot虚拟机默认分配策略 longs/double、ints、 shorts/chars 、bytes/booleans 、oops
  • 对齐填充
    HotSpot自动内存管理要求对象的起始地址必须是8字节的整数倍 当对象的实例数据部分不是8的整数倍的时候 需要这一部分区补齐占位
对象访问定位

java通过栈上的reference数据来操作堆上的具体对象 由于reference数据只是规定了一个指向对象的引用 没有定义如何去定位访问对象的具体位置
主流的实现方式有两种

  • 句柄
    在堆中划分句柄池 reference存储对象的句柄地址 句柄包含对象实例数据和类型数据的各自具体地址信息
    好处:reference中的数据是稳定的句柄地址 对象被移动只会改变句柄中的信息 不会改变句柄的地址 reference不需要变化
    坏处:增加了指针定位的开销
    java对象访问定位_句柄
  • 直接指针
    需要java堆对象布局考虑如何放置访问类型数据的相关信息 reference中直接存储堆对象的地址
    好处:直接访问对象 减少指针定位开销
    坏处:当对象内存地址发生变化 reference中数据也需要调整
    java对象访问定位_指针

HotSpot采用的是直接指针

常见区域内存溢出

  • java堆溢出
    一直new对象 当超出最大堆限制即溢出
    具体分析的时候 可以通过内存快照 分析到底是内存直接溢出导致还是因为内存泄漏导致的
    如果是堆大小限制了 通过-Xmx配置最大值
    如果是泄漏了 那么就只能找出那里泄漏了 然后修正了
  • 虚拟机栈和本地方法栈溢出
    当线程请求的栈深度大于虚拟机允许的最大深度 抛出StackOverflowError
    当虚拟机扩展的时候无法申请到足够的内存 抛出 OutOfMemoryError
    设置-Xss配置虚拟机栈和本地方法栈的大小
  • 方法区和常量池溢出
    一直新建String 并且使用.intern() 插入到方法区 就会造成 常量区溢出
    二是 不断新建新的类加载到方法区域也会溢出
  • 本机内存直接溢出
    DirectMemory 通过-XX:MaxDirectMemorySize指定 如果不指定 那么默认和java堆一样大小
    如果dump文件很小 程序中直接或者间接使用nio 那么 可能就是由于本机内存直接溢出

总结

java的jvm规范 定义了 内存中 线程独享信息(本地方法栈、虚拟机栈、程序计数器) 线程共享的信息(方法区、堆)
jvm中内存划分方式非为 指针碰撞、空间列表
jvm中对象的寻址有句柄和直接指针的方式
jvm中堆内存 大致分为 新生代和老年代
jvm中方法区 又称之为永生区 永久代 新的jvm已经在慢慢作去永生代了 jdk1.7开始

------ 本文结束 ------

版权声明
ming创作并维护,博客采用CC协议
本文首发于ming 博客( https://blog.xujiuming.com ),版权所有,转载请注明出处!