Java内存区域

jdk1.8运行时数据区域?

主要分成:

线程私有的

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

线程共享的

  • 方法区
  • 直接内存

Java虚拟机对这些运行时数据区域的规定是相当宽松的,以堆为例:堆可以是连续空间,也可以不连续。堆的大小可以固定,也可以在运行的时候按需扩展。

什么是程序计数器?

程序计数器是一块较小的空间,可以看作是当前线程所执行的字节码的行号计数器。字节码解释器工作的时候通过改变这个计数器的值来选取下一条所需执行的字节码指令。

程序计数器是唯一不会出现OutOfMemoryError的内存区域,随着线程创建而创建,线程消亡而消亡。

什么是Java虚拟机栈?⭐⭐⭐⭐

和程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同,随着线程创建而创建,线程消亡而消亡。

除了一些本地方法是通过本地方法栈实现的,其它所有方法调用都是通过栈实现的。方法调用的数据通过栈传递,称为压栈。每个方法调用结束之后,都有一个栈帧弹出。栈由一个个栈帧组成,每个栈帧有:

  • 局部变量表:存储编译时可知的各种数据类型(基本类型)、对象引用。
  • 操作数栈:存放执行过程中产生的中间计算结果。
  • 动态链接:主要服务一个方法需要调用其他方法的场景。Class 文件的常量池里保存有大量的符号引用比如方法引用的符号引用。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用,这个过程也被称为 动态连接
  • 方法返回地址:方法调用完后返回的地址。

Java方法返回有两种方式:一种是return,直接返回,一种是抛出异常。两种方式都会导致栈帧被弹出。

如果一直压栈,会导致栈空间过深,导致StackOverFolowError

除了 StackOverFlowError 错误之外,栈还可能会出现OutOfMemoryError错误,这是因为如果栈的内存大小可以动态扩展, 那么当虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

本地方法栈和虚拟机栈类似,但是是为本地方法服务的。

堆区内存?⭐⭐⭐⭐

Java虚拟机最大的一块内存。所有线程共享。在虚拟机启动时创建。

唯一目的就是存放对象实例,几乎所有对象实例以及数组都是分配在这里。

为什么说是几乎呢?随着JIT编译器的发展和逃逸分析逐渐成熟,有些方法的对象引用没有被返回或者没有被外部使用,那么对象就可以直接在栈上分配内存。

堆也被称为GC堆,是垃圾收集器管理的主要区域。

堆通常被分为:

  • 新生代内存(Young Generation)
  • 老年代(Old Generation)
  • 元空间(Jdk8)或者永久代(Jdk8之前)

新生代内存区域又可以分为Eden(伊甸园)、s1、s2区域。

大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。不过,设置的值应该在 0-15,否则会爆出以下错误

MaxTenuringThreshold of 20 is invalid; must be between 0 and 15

为什么年龄只能是0-15?

因为记录年龄的区域,在对象头中大小通常是4,所以最大可以标识的值是15。

堆是最容易出现OutOfMemoryError错误的区域了,比如:

  • java.lang.OutOfMemoryError: GC Overhead Limit Exceeded:当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
  • java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误。(和配置的最大堆内存有关,且受制于物理内存大小。最大堆内存可通过-Xmx参数配置,若没有特别配置,将会使用默认值。
方法区?⭐⭐⭐

方法区是线程共享的区域。

当虚拟机需要某个类时,需要读取解析Class文件获取相关信息。将信息存到方法区。方法区会存储已经被虚拟机加载的类信息、字段信息、方法信息、常量、静态变量、计时编译器编译后的代码缓存等

永久代和原空间可以看作是方法去的两种实现方式,并且,永久代是JDK1.8之前的实现,元空间是JDK1.8之后的实现。

运行时常量池?

Class文件中除了有类的版本、字段、方法、接口等描述信息之外,还有用于存放编译期生成的各种字面常量和符号引用的常量池表。

字面量是源代码中的固定值的表示法,即通过字面我们就能知道其值的含义。字面量包括整数、浮点数和字符串字面量。常见的符号引用包括类符号引用、字段符号引用、方法符号引用、接口方法符号。

运行时常量池也是方法区的一部分

对象创建的过程?⭐⭐⭐⭐
  1. 类加载检查:虚拟机遇到一条new的时候,就会去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,然后通过这个符号引用检测类是否被加载过、解析过、初始化过。如果没有,就进行类加载。

  2. 分配内存:为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定。分配方式有指针碰撞空闲列表,选择哪种分配方式取决于Java堆区是否规整。

    指针碰撞

    适用于:堆内存规整(没有内存碎片)。

    原理:用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。

    空闲列表

    适用场合:堆内存不规整(存在内存碎片)。

    原理:虚拟机维护一个列表,记录哪些内存可用,分配的时候,找一块足够大的内存分配给对象,最后更新列表记录。

    内存分配存在的并发问题:

    在创建对象的时候,是需要注意线程安全问题的。虚拟机通过两种方式来保证线程安全:

    • CAS+失败重试。
    • TLAB:为每个线程提前在Eden分配一块区域,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或者TLAB的内存以及用完的时候,再采用上述CAS进行内存分配。
  3. 初始化零值:虚拟机在分配完内存之后,会把内存空间都初始化成0值(除了对象头)。保证对象实例字段不赋值也可以直接适用。

  4. 设置对象头:比如这个对象是哪个类的实例、如何找到类的元数据信息、对象的哈希码、对象的GC分代年龄等。

  5. 执行init方法:从虚拟机看,新的对象已经产生了。但是从Java看,对象创建才刚开始。<init>方法没有执行,所有字段都是零。所以一般来说,执行new指令之后才会接着执行<init>方法,把对象按照程序员的意愿进行初始化。

总结:类加载->分配内存->初始化零值->设置对象头->执行init方法。

对象的内存布局?⭐⭐⭐⭐

大致分成:

  • 对象头:标记字段(Mark Word)和类型指针,标记字段存储自身运行数据,比如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。类型指针主要是:对象指向它的类元数据的指针,通过这个指针来确定对象是哪个类的实例。
  • 实例数据:就是程序中定义各种类型的字段内容。
  • 对齐填充:不是必须存在,就是占位,用于内存对齐。内存地址必须是8字节的整数倍,所以当实例数据没有对齐的时候,就用对齐填充来补全。
对象是如何访问定位的?

对象是通过栈上的reference数据来操作堆上的具体对象。对象的访问方式由具体的虚拟机实现,目前主流的方式有:句柄、直接指针

句柄:Java堆中会划分出一块内存作为句柄池,reference中存储的对象就是句柄地址,而句柄中包含了对象实例数据和对象类型数据各自的具体地址信息。

直接指针:reference中存储的就是对象的地址。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值