JVM的内存结构学习笔记

JVM的内存结构

相关知识:

运行时数据区

线程独有

线程独有的数据区,生命周期同自己所属的线程一致。线程创建时,会分配自己的程序计数器、java虚拟机栈、本地方法栈。线程销毁时,他们也会被销毁。

程序计数器
  • 如果当前执行java方法,那么记录当前正在执行的虚拟机字节码指令地址

  • 如果是本地方法,那么则为null

  • 并且是唯一一个不会OOM(OutOfMemoryError)的区域

java虚拟机栈
  • 线程每次执行java方法,会向栈中压入一个栈帧

    • 栈帧存放着局部变量表、操作数栈、动态连接、方法出口等信息

      • 局部变量表存放着

        • 各种Java虚拟机基本数据类型

        • 对象引用

        • returnAddress类型(指向了一条字节码指令的地址)

      • 局部变量表中以局部变量槽(slot)为存储单元(int一个slot,long两个slot),每个slot实际大小由虚拟机规定。局部变量表所需内存大小在编译期间确定,方法运行时不会改变大小。

  • 如果栈帧数量超过规定数量,就会抛出StackOverflowError异常

  • 如果栈动态扩容无法申请到足够的内存,会抛出OutOfMemoryError异常(Hotspot虚拟机不允许栈动态扩容)

本地方法栈
  • 与java虚拟机栈类似(异常抛出机制一样),只不过本地方法栈执行的本地方法

  • 虚拟机可以自行实现本地方法栈中方法使用的语言、使用方式数据结构甚至在Hotspot中本地方法栈和java虚拟机栈合二为一

堆中的线程分配缓冲区(Thread Local Allocation Buffer,TLAB)
  • 线程启动时从堆的 Eden 区申请一块 TLAB(默认大小约 1% Eden)

  • 通过线程本地分配降低同步开销,尤其在高并发场景下显著提高内存分配效率

线程共有
  • 存放对象的实例和数组(注意区分,栈中存放对象的引用)

    • ps:但是现在逃逸分析技术不断升级,栈上分配和标量替换的优化手段出现变化,对象实例不一定只划分在堆上

  • Java堆是垃圾收集器管理的内存区域,在虚拟机具体实现上,可能会对堆进行分区(以hotspot采用分代的垃圾回收器为例)

    • 新生代

      • Eden区

      • 两个Survivor区(from区、to区)

    • 老年代

  • 可扩展也可不扩展(hotspot是可扩展的,并且可以通过参数-Xmx(堆的最大大小)-Xms(堆的初始大小)设定)

    • 如果在Java堆中没有内存完成实例分配,并且堆也无法再 扩展时,Java虚拟机将会抛出OutOfMemoryError异常。

  • 堆内存空间在逻辑上连续,在物理上可以不连续(但是大对象(典型的如数组对象),大多数虚拟机会分配给他连续内存)

方法区
  • 方法区被描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap)

  • 用于存储已被虚拟机加载的类型信息常量静态变量即时编译器编译后的代码缓存等数据

  • hotspot对他的实现根据jdk版本不同,分为三个阶段

    • jdk6及以前,方法区由永久代实现,占用本地内存

    • jdk7,把原本放在永久代的字符串常量池(StringTable,被移入堆中)、静态变量等移出,方法区依旧由永久代实现,占用jvm内存

    • jdk8及以后,方法区由元空间实现,并且把jdk7中永久代还剩余的内容(主要是类型信息)全部移到元空间中,占用本地内存

  • 如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。

运行时常量池(区别于字符串常量池)
  • 属于元空间的一部分

  • 运行时常量池相对于Class文件常量池(这个是字节码文件里面的内容,编译时期的)的另外一个重要特征是具备动态性,例如

    • 反射动态加载类

    • String的intern()方法

  • 当类加载时,类文件中的字符串字面量(如 "hello")会存入运行时常量池,并触发 JVM 在字符串常量池中创建对应的 String 对象(jvm执行到对应的字节码才会创建)。运行时常量池中的条目会引用字符串常量池中的对象。

  • 当常量池无法再申请到内存 时会抛出OutOfMemoryError异常

字符串常量池(jdk7及以后属于堆)
  • 一张全局哈希表,维护所有字符串

  • jdk6及以前,需要full gc才会进行垃圾回收;jdk7及以后,minor gc就会进行垃圾回收(hotspot存放在堆的永久代中)

  • "a"+"b"会被直接优化成"ab"放入字符串常量池,但如果是用"+"号拼接两个new出来字符串对象(在堆中),底层会调用StringBuilder的append()和toString()方法,这种方式得到的字符串也是在堆中

    s1=new String("a")+new String("b") //s1在堆中 
    s2 = "a"+"b" //s2在字符串常量池中
  • String的intern()方法

    • jdk6时:这个字符对象尝试放入字符串常量池,如果有则并不会放入,如果没有则将字符对象复制一份放入串池,会把串池中的对象返回

    • jdk8时:将这个字符对象尝试放入字符串常量池池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回

    • 两者最大的区别就是

      s1=new String("a")+new String("b") 
      s=s1.intern() s2 = "a"+"b" 
      System.out.println( s2 == s1);//jdk6:false jdk8:true 
      System.out.println( s2 == s);//true
  • 并且是懒加载,只有执行到对应行才会生成对应的字符串对象。在类加载的时候,只会将字符串字面量加载到运行时常量池。

直接内存
  • 并不是虚拟机运行时数据区的一部分,直接内存的分配更不会受到Java堆大小的限制

  • 在jdk4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区 (Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作。

  • 动态扩展时触及本机内存上限或者规定上限,就会出现OutOfMemoryError异常。

HotSpot虚拟机的对象

对象概述

对象=对象头(Header) + 实例数据(Instance Data)+ 对齐填充(Padding)

对象头
  • Mark Word

    • 根据操作系统的位数分为32位和64位

    • 以32位为例

      • 25个比特用于存储对象哈希码,

      • 4个比特用于存储对象分代年龄,

      • 2个比特用于存储锁标志位,

      • 1个比特固定为0(biased_lock,如果为1,表示偏向锁)

  • 对象类型指针

    • 指向对象所属类(class对象,如果是数组的话,还会包含数组长度信息)

实例数据
  • 优先初始化父类继承的字段

  • 自身字段优先初始化长类型的字段(如:long,double-->int-->byte)

对齐填充
  • 将对象的大小填充至8字节的整数倍

对象创建流程(以new为例)
MyObject o=new MyObject()
  1. 检查运行时常量池是否具有当前对象所属类的符号引用

  2. 检查类是否已经被加载过(没有的话就进行类的加载)

  3. 执行new指令

    1. 分配给新生对象一块空内存

      1. 判断当前线程的TLAB是否还有空间分配内存(有的话就直接分配,无并发问题)

      2. 否则,使用CAS的方法在堆上的新生代的Eden区公共区域分配一块内存(TLAB也在Eden区)

        • 分配的方式取决于垃圾回收的策略

          • 使用Serial、ParNew、G1这种使用标记-整理的垃圾回收机制,则采用指针碰撞(Bump The Pointer),用一个指针分割使用区未使用区,分配内存的时候移动指针就行。

          • 使用CMS这种使用标记-清除的垃圾回收机制,则采用空闲列表(Free List),使用一个列表将空闲区域的起始地址和大小串起来,分配内存,挑选一块大小合适的内存就行(但其实如果空闲内存块很大,该空闲内存块内部分配内存也可以采用指针碰撞)。

    2. 初始化对象的字段(Instance Data)

      1. 优先初始化父类继承的字段

      2. 自身字段优先初始化长类型的字段(如:long,double)

    3. 根据是否启用偏向锁,设置对象头信息(Header)

    4. 将对象引用(这个o)指向该对象

  4. 执行java程序的构造函数

对象的访问

本地方法栈的栈帧中存放着本地变量表,其中就包括对象引用,但是访问到对象实例数据和对象类型数据有两种方法。

句柄访问

在堆中维护一个句柄池,句柄池中每个句柄包含指向对象实例数据和对象类型数据的指针。而本地变量表的对象引用(reference)指向这个句柄。

  • 优点:在垃圾回收后,对象会被移动,此时指针会改变。此时只需要修改句柄池的指针,无需修改本地变量表的对象引用(reference)。

  • 缺点:每次访问对象实例数据,需要两次内存寻址(reference-->句柄-->对象实例)。

直接访问(HotSpot使用的方式)

本地变量表的对象引用直接指向对象实例数据,而对象类型数据的指针存放在对象实例数据中。

  • 优点:访问对象实例数据只需要一次内存寻址(reference-->对象实例)。

  • 缺点:垃圾回收时,需要修改每个reference。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值