1.运行时数据区域

  • 线程私有的:
    • 程序计数器
    • 虚拟机栈
    • 本地方法栈
  • 线程共享的:
    • 方法区
    • 直接内存
      image
      1.程序计数器
  • 程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理, 线程恢复等功能都需要依赖这个计时器来完成。
  • 程序计数器主要有两个作用:
    • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制。
    • 多线程下,用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到了哪里。
      2.Java虚拟机栈
  • 其生命周期和线程相同,即Java方法执行的内存模型,每次方法调用的数据都是通过栈传递的。
  • Java内存可以大概的分为堆内存heap和栈内存stack,其中栈就是虚拟机栈,或者是虚拟机栈中局部变量表部分。(实际上Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表,操作数栈,动态链表,方法出口信息)
  • 局部变量表主要存放了编译器可知的各数据类型,对象引用(reference类型,他可能是一个指向对象起始地址的引用指针,也可能是指向一个代码对象的句柄或其他与此对象相关的位置)
  • Java虚拟机栈会出现两种异常:StackOverFlowError和OutOfMemoryError
    • StackOverFlowError:若Java虚拟机栈的内存大小不允许动态扩展那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就会抛出StackOverFlowError。
    • OutOfMemoryError:若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法动态扩展了,就会跑出OutOfMemoryError异常。
  • Java虚拟机是线程私有的,每个线程都有自己的Java虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。
  • 方法、函数如何调用?
    • Java栈可用类比数据结构总的栈,Java栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入Java栈,每一个函数调用结束后,都会有一个栈帧被弹出。
      3.本地方法栈
  • 和虚拟机栈作用类似,区别是:虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务,在HotSpot虚拟机中和Java 虚拟机栈合二为一。
4.堆

Java虚拟机管理内存中最大的一块,Java堆里所有线程共享的一块内存区域,在虚拟机启动时创建。此区域主要存放对象实例。

1
Eden --> s0 --> s1 --> Tentired

上图所示Eden区,s0,s1区都是新生代,tentired区属于老生代,大部分情况下,对象都会首先在Eden区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入s0或者s1,并且对象的年龄还会加1,当它的年龄增加到一定程度(默认15岁),就会晋升为老生代中,对于老生代阈值,可通过参数-XX:MaxTenuringThreshold配置

5.方法区
  • 方法区与Java堆一样,是各个线程共享的内存区域,用于存放已被虚拟机加载的类信息,常量,静态变量,即是编译器编译后的代码等数据。还有一个别名Non-Heap(非堆)
  • 方法区也被称为永久代,其实方法区是Java虚拟机规范的定义,是一种规范。而永久代是一种实现,是HotSpot堆方法区的一种实现,其他虚拟机实现并没有永久代这种概念。
  • JDK1.8之前,可通过以下参数调节方法区的大小:

    1
    2
    -XX:PermSize=N // 方法区初始大小
    -XX:MaxPermSize=N // 方法区最大大小,超过这个值,将会跑出OutOfMemoryError异常
  • JDK1.8的时候,方法区被移除了,取而代之的是元空间,元空间使用的是直接内存

  • 常用参数
    1
    2
    -XX:MetaspaceSize=N // 设置Metaspace的初始代销
    -XX:MaxMetaspaceSize=N // 最大大小

与永久代很大的不同就是,如果不指定大小的话,随着类的创建,虚拟机会耗尽所有可用的系统内存。

6.运行时常量池

运行时常量池是方法区的一部分,Class文件中除了有类的版本,字段,方法等描述信息外,还有常量池信息。
JDK1.7及之后版本JVM将运行时常量池从方法区中移出来,在Java堆中开辟了一块区域存放运行时常量池

  • 常量池包含的内容
    • 字面量
      • 文本字符串
      • 被声明为final的常量值
      • 基本数据类型的值
      • 其他
    • 符号引用
      • 类和结构的完全限定名
      • 字段名称和描述符
      • 方法名称和描述符
        image
        7.直接内存
        直接内存并不是虚拟机运行时的数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁的使用

HotSpot虚拟机对象揭秘

1.对象的创建

image

1
类加载检查 -- 分配内存 -- 初始化零值 -- 设置对象头 -- 执行init方法

  • 1.1 类加载检查:虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类型的符号引用,并且检查这个符号引用代表的类是否已被加载过,解析和初始化过,如果没有,那必须先执行相应的类加载过程
  • 1.2 分配内存: 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。分配方式有“指针碰撞”和“空间列表”两种,选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
  • 1.3初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值,这一步操作保证了对象的实例在Java代码中可以不附初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
  • 1.4 设置对象头:初始化零值完成后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例,如何才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄等,这些信息存放在对象头中,另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
  • 1.5 执行init方法:在上述完成后,在虚拟机的角度看,新对象已经产生了,但是,Java程序来说,创建才刚开始,所有字段还为零,一般来说,执行new指令之后会接着执行init方法,把对象按照程序的意愿进行初始化,这样一个真正的对象才算完全产生出来。

2.对象的内存分布

在HotSpot中,对象在内存中的布局可以分为3块区域:对象头,实例数据和对齐填充

  • 对象头:包含两部分信息,第一部分用于存储对象自身的自身运行时数据(哈希码,GC分代年龄,锁状态表示等),第二部分是类型指针,即对象指向他的类元数据的指针,虚拟机通过这个指针确定这个对象是那个类的实例。
  • 实例数据部分:对象真正存储的有效信息
  • 对齐填充部分:不是必然存在,也没特别的含义,仅仅占位作用
    3.对象的方位定位
    建立对象就是为了使用对象,我们的Java程序通过栈上的reference数据来操作堆上的具体对象。对象的方位方式有虚拟机实现而定,目前主流方式有:使用句柄、直接指针