JAVA内存区域【一篇文章直接看懂】

Java内存区域与内存分配策略

对于JAVA程序员,不像C++一样需要为每个new操作去写del/free代码,但出现内存泄漏和溢出问题时,排查纠正成为了一个难题,因此需要对这部分的知识进行深入学习。

运行时数据区域

不同区域不同用途和销毁时间

程序计数器(Progaram Counter Register)

一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。

因为JVM多线程是通过线程轮转实现,因此,每个线程需要一个独立的计数器来独立存储(这类内存区域称为”线程私有”)

如果线程执行的是JAVA方法,那么计数器记录的是正在执行的虚拟机字节码指令的地址,如果是Native方法,那么为空

Java虚拟机栈(Java Virtual Machine Stack)

也是线程私有的,生命周期与线程相同

虚拟机栈描述的是Java方法执行的线程内存模型,每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

栈中大多数存的是局部变量表。表中存放了编译期可知的各种基本数据类型、对象引用。这些数据类型在表中以局部变量槽(clot)来表示,在编译期间就完成了分配大小(分配类型对应槽的数量)

64位的long和double就会占用两个

本地方法栈(Native Method Stacks)

与虚拟机栈类似,但是为虚拟机使用到的本地方法服务

Java堆(Java Heap)

是垃圾收集器管理的内存区域(最大的一块内存),被所有线程共享,用来存放对象实例,“几乎”所有的对象实例都在这里分配内存

JAVA日后可能会出现值类型的支持,所以是“几乎”

回收内存的角度来看:如今,基本是基于分代收集理论设置的,所以出现各种JAVA堆,但是如今HotSpot出现了不采用分代设计的新垃圾收集器,因此过去的方法不太准确。
分配内存的角度来看:JAVA堆中可以分出多个线程私有的分配缓冲区(TLAB,Thread Local Allocation Buffer)
JAVA堆物理可不连续,逻辑上连续,可设置为固定大小也可以设为可扩展的(主流为可扩展)

方法区(Method Area)

线程共享的区域,用于存储已加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据,别名为“非堆”(Non-Heap)。
JDK8前,将永久代(Permanent Generation)与方法区混为一谈,但永久代只是收集器的分代设计,导致了内存溢出的问题。
该区域的可以选择不实现垃圾收集,主要针对的目标是常量池的回收和对类型的卸载,但是因为条件非常苛刻故而很少使用
方法堆与JAVA堆存储逻辑相同,物理可不连续,逻辑上连续

运行时常量池(Runtime Constant Pool)

class文件中的常量池表(Constant Pool Table),它是方法区的一部分,用于存放编译期生成的各种字面量与符号引用,在类加载后存放到方法区的运行时常量池中,具有动态性,即运行期间也可以将新的常量放入池中

直接内存(Direct Memory)

为了提高部分场景的性能,避免在JAVA堆和Native堆中来回复制数据,引入基于通道与缓冲区的IO方式,通过JAVA堆里的DirectByteBuffer对象作为内存的引用进行操作。

HotSpot虚拟机对象

对象的创建

new Dog() =》new出一个对象

  1. 检查对应的参数是否能在常量池中定位到,进而检查是否被加载、解析、初始化过。若没有,则执行类加载(后续会详细描述)

  2. 通过检查后进行分配内存

    • 若内存是完整的:使用指针碰撞(Bump The Pointer)【使用过在一边,空闲的在一边,中间指针进行移动】
    • 若内存是不完整的,使用空闲列表(Free List),每次分配内存时维护这个表。

    **分配内存时,还需要考虑到:**并发情况下,快速移动指针是线程不安全的。

    • 对分配内存空间的动作进行同步处理,采用CAS+失败重试的方法保证原子性
    • 把内存分配的动作按照线程划分在不同的空间进行,每个线程在JAVA堆中先获得一块内存,称为本地线程分配缓冲(Thread Local ALlocation Buffer,TLAB)

    Serial\ParNew收集器使用指针碰撞,CMS使用空闲列表
    CAS:compare and swap,是乐观锁,CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。

  3. 内存分配后,将内存空间初始化为零值,如果使用了TLAB,可以在分配TLAB中直接进行。保证了对象的实例字段不赋初值就可以使用,访问带该类型对应的零值

  4. 对对象进行必要的设置,如对象属于哪个类、哈希码、元数据信息等存放在对象的对象头中

上述5个步骤结束后,虚拟机认为一个对象就已经产生了,但是JAVA程序中才完成构造函数这一步。一般来说,new会接着执行()方法,然后才会被构造出来。

对象的内存布局

在HotSpot虚拟机中,对象划分为:
对象头(Header)-----》实例数据(Instance Data)----》对齐补充(Padding)

对象头

  1. 用于存储对象自身运行时数据,如哈希码、GC分代年龄、锁状态等,也称为"Mark Word",有着动态定义的数据结构,根据对象的状态复用存储空间【如:对象未被同步锁锁定的状态下,32位的Mark Word的32个比特存储空间中的25存储哈希码,4个存储对象分代年龄,2个存储锁标志,1个规定为0】
  2. 类型指针:指向它的类型的元数据指针。通过这个确定哪个类的实例。如果是Java数组,对象头中还要有记录数组长度的数据。如果长度不确定,则无法通过元数据进行推断数组的大小

实例数据:存储对象真正的有效信息,即各种类型的字段内容。存储顺序会收到虚拟机分配策略参数和Java定义顺序影响,顺序为从大到小Long->bytes

对齐补充:不是必然存在的,起占位符的作用,因为自动内存管理系统要求对象的起始地址必须为8字节的整数倍,即任何对象的大小都必须是8字节的整数倍

对象的访问定位

java程序中会通过reference数据来操作栈上的具体对象。但reference只是定义为指向对象的引用,所以对象的访问方式也是由虚拟机决定的。

1. 句柄访问方式

在JAVA堆中划分出一块内存作为句柄池,存储对象的句柄(对象的实例数据与类型数据各自的具体地址信息)地址
使用句柄访问的最大好处就是reference存储的是稳定的地址,在对象被移动时(垃圾回收时很常见)只会改变句柄中的指针。

2. 直接指针访问

对象的内存布局需要考虑如何放置访问类型数据的相关信息,reference存储的直接就是对象地址。
速度快、HotSpot中主要使用该方法。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值