Java内存区域与内存溢出异常(二)

11 篇文章 0 订阅

前言

读书笔记《深入理解java虚拟机》,这个系列得慢慢更新,因为每个知识都是未接触过的。

对象的创建的流程

类加载

虚拟机在遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用(类名),并且检查这个符号引用的类是否以及被加载、解析和初始化过。如果没有就必须先执行类加载的过程。

为新生对象分配内存

对象所需要的内存大小在类加载之后便可以完全确认,为对象分配空间的任务等同于把一块确定大小的内存从java堆中划分出来。
分配内存的方式:
- 指针碰撞:如果java堆里面的内存是决定规整的,所有用过的内存都放在一边,空闲的内存放在另外一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是吧那个指针向空闲空间那边摞动一段与对象大小相等的距离,这种分配方式成为“指针碰撞”。
- 空闲列表:如果java堆中的内存不规整,已经使用的内存与空闲的内存相互交错,那么虚拟机就必须维护一个列表,记录哪些内存是可用的,在分配的时候从列表中找一块足够大的空间划分给对象实例,并更新列表上的记录。
- 堆内存是否规整决定了使用哪种分配方式,而堆内存是否规整又由垃圾收集器决定。这些后面再说咯。

分配内存中存在的问题

对象创建在虚拟机中是非常频繁的行为,仅仅修改一个指针所指向的位置,在并发的情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来的即修改,对象B又同时使用了原来的指针来分配内存。
解决方式:
- 对分配内存空间的动作进行同步处理------实际上虚拟机采用CAS配上失败重试的方式来保证更新操作的原子性。(CAS:compare and swap 比较再交换,是一种无锁算法,cas有三个操作数,内存值v、旧的预期值a、修改的新值b,a与v不想等才修改v为b)
- 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中都预先分配一小块内存,称为本地线程分配缓存,分配内存就在本地线程分配缓存上分配。本地线程分配缓存不足就分配新的内存,只有此时才需要同步锁定。
内存空间初始化
虚拟机对分配到的内存空间都初始化为0(不包括对象头后面会写到对象头)如果使用本地线程分配缓存,这一流程会提前到本地线程分配缓存分配时进行。这一步操作其实就是保证了对象的实例字段都有初始化值null、0等

对对象进行必要的设置

例如这个对象是哪个类的实例、如何才能找得到类的元数据信息、对象的哈希码、对象的gc分代年龄信息。这些信息存放在对象头中

最后

上述步骤完成之后。一个新的对象已经产生了,但从java程序的角度来看,这才刚刚开始,构造方法还没有执行呢。

对象的内存布局

对象在内存中存储的布局分为三个区域:对象头(header)、实例数据(Instance Data)和对其填充(Padding)

  • 对象头(header)
    包括两个部分信息:
    第一部分:
    用于存储对象自身运行时数据(Mark word),例如哈希吗(hashcode)、GC分代年龄、锁状态标志、线程持有锁、偏向锁线程ID、偏向时间戳等。这部份数据在32位和64位的虚拟机中分别为32bit和64bit。它会根据对象的状态复用自己的内存空间。
    在这里插入图片描述
    第二部分:
    类型指针(Klass Word),即对象指向它的类元数据(类对象)的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有虚拟机实现都必须在对象数据上保留类型指针,即查找对象的元数据信息不一定要经过对象本身,如果对象是一个java数组,那么对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通java对象的元数据信息确定java对象的大小。但是从数组的元数据中却无法确认数组的大小。
  • 实例部分
    实例部分是对象真正存储有效信息,也是程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来,这部分的存储顺序会收到虚拟机分配策略参数(FieldsAllocationStyle)和字段在java源码中定义的顺序影响
  • 对齐填充
    并不是必然存在的,没有特别的含义,仅仅起着占位符的作用,由于虚拟机自动内存管理系统要求对象起始地址必须是8字节的整背书,换句话说,就是对象的大小必须是8字节的整倍数,而对象头部分正好是8字节的倍数64、32,因此对象实例数据部分未对齐的情况下需要通过对齐填充来补全。
    在这里插入图片描述
    最后用图片的方式呈现出来,此图片传转于https://blog.csdn.net/scdn_cp/article/details/86491792

对象的访问定位

java程序需要通过栈上的reference数据(引用类型)来操作对上的具体对象。
主流的对象访问方式有两种

  • 使用句柄 图来自《深入理解jvm》
    java堆会划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息
    在这里插入图片描述

  • 直接指针 图来自《深入理解jvm》
    reference中存储的就直接是对象地址,堆中的对象包括了实例数据以及一个指向对象类型数据的指针。
    在这里插入图片描述

这两种访问方式各有优势:
使用句柄防伪最大的好处就是reference中存储的是稳定的句柄地址,在对象被移动时,只会改变句柄中的实例数据指针,而reference本身不需要改变。
使用直接指针访问的好处就是速度快,节省了一次指针定位的时间开销。由于对象的访问在java中很频繁,所有这样的节省非常有必要

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值