JAVA对象的创建过程介绍

171 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

对象创建

  1. 对象创建的方式:

    new关键字、class的newInstance方法,Constructor类的newInstance方法等;

  2. 对象创建的流程:

graph LR
A[new ClassA]  --> B[检查当前类是否加载]
    B --> |NO| D[加载类] -->E  
    B --> |YES| E[内存分配]
    E --> F[初始化] --> G[设置对象头] --> H[init初始化]
   
  • 当虚拟机接收到创建对象的指令,例如new关键字时候,会先检查这个指令的参数在常量池是否已经加载过,如果还没加载则必须先加载;
  • 内存分配,当类被加载完成之后,创建的对应的实例对象所需要的内存大小就已经被确定了下来;内存分配就是为这个新生对象在堆中划分一块装在它的内存;

​ 一般给对象分配内存有两种方式:

​ 1.指针碰撞

​ 假设堆内存是规整的,一边存放的是已经使用的内存,另外一边是未使用的,中间有一根内存指针作为分界点,这种方式在内存分配的时候只需要将指针移动跟这个新生对象相对应的内存大小即可;

​ 2.空闲列表

​ 很多情况下Java堆中的内存并不是规整的,已使用的和空 闲的内存混杂的堆在一块,指针碰撞已经不能满足内存分配了,这个和时候虚拟机就需要维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新记录到列表;

​ 高并发场景下,在分配内存的过程中肯定会遇到并发安全问题,一般通过两种方式保证线程安全;

​ 1、CAS+失败重试保证线程原子操作

​ 2、TLAB (本地线程缓冲) :每个线程在 Java 堆 中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB),当线程要分配内存时,就在哪该线程的 TLAB 上分配。

​ 可以通过通过-XX:+/-UserTLAB参 数来设定虚拟机是否使 用TLAB

  • 初始化:就是给新生对像初始值,这里并不是直接赋予我们希望的值,也就是零值;
  • 设置对象头:

​ 这里先来说一下在HotSpot虚拟机中实例对象的组成部分:分别是对象头,对象体和对齐填充

1、对象头:包含三个部分

  • mark_word:

    主要用于存储实例在运行时的状态和数据,笔记哈希码、线程id、锁标志、GC的 分代年龄等

  • klass Pointer(类对象指针)

    是用来存放此对象的元数据(InstanceKlass)的 地址,通过它可以确定当前这个对象是哪个Class;

  • Array Length(数组长度)

    用来标记数组的长度,数组类型的对像才有,需要额外存储;

2、对像体

​ 存放着对像的成员变量,包括父类的成员属性值;

3、对齐填充

​ 是用来保证 Java 对象在所占内存字节数为 8 的倍数;

​ HotSpot 的内存管理要求对象起始地址必须是 8 字节的整数倍。对象头本身是 8 的 倍数,当对象的实例变量数据不是 8 的倍数,便需要填充不足数据来保证 ;

  • 执行init方法:也就是赋值操作,把对象属性赋值我们期望的值;

👆【关于对象头的mark_word阶段示意图】

​ 32位Mark Word结构图

锁状态 25bit 4 bit 1 bit 2 bit
23bit 2bit biased偏向标志位 lock 锁状态
无锁 对象的 hashCode(25bit) 分代年龄 0 01
偏向锁 线程 ID(23bit) epoch(2bit) 分代年龄 1 01
轻量级锁 指向栈帧中的锁记录指针(30bit) 00
重量级锁 指向重量级锁监视器的指针(30bit) 10
GC标记 11

【逃逸分析与栈上分配】

  • 逃逸分析:JVM可以通过开启参数-XX:+DoEscapeAnalysis,来优化jvm的对象内存分配,

​ 通常情况下java对像都是在堆中分配内存空间,但当开启了逃逸分析后,系统会分析当前需要分配内存的对象是否会被外部引用,比如在方法中声明得我对象,如果只在本方法内部使用,那么在许可的情况下完全没必要在堆上分配内存,因为方法在加载的时候,会被加载到线程栈的栈帧,是线程独享的不存再并发问题。

​ 因此,这种情况下的对象的创建,就可以引入栈上分配的概念,即在栈内存足够的情况下将对像创建所需的空间存放到栈帧上;栈帧还具有后进先出的特性,也能大大减少Gc对这种对象的回收带来的压力,提升系统性能;

  • 栈上分配:

    ​ 栈空间相对于堆空间是非常小的,跟多情况下栈中没有足够的连续的内存可供创建的对象使用,所以这个时候,就需要用到另外一种技术------标量替换;

    ​ 如果说逃逸分析确定该对象不会北外部访问到,对象可以被进一步分解时,jvm会将该 对象成员变量分解成替换这些成员的变量的更小的单位变量,而不会创建该对象,这些代替的成员变量在栈帧上分配空间,如此一来就解决了因为没有足够的连续的内存空间而无法创建对象的局面;


【堆空间上内存的分配】

​ java对像并不是一开始创建分配了内存就完了的,在JVM内部还要经过相当多的流程,jvm的内存模型的堆空间是存放主力对象的地方,也就是说大多数的对象还是在这里被分配在这里;

堆空间的分配

  • 年轻代 约1/3

    ​ 存放着的都是些生命周期比较短的对象,在这块区域MinorGC相对会比较频繁,不再被使用的对象将会被垃圾收集器清理掉;

    年轻代也分成Eden区和Survivor区,默认内存大小比例为8:1:1

    • Eden :最初的新生毒对像分配在这。等待经历MinorGC,是否进入S区还是直接被干掉;
    • Survivor :S区域有两块,每次经历MinorGC,就是使用复制清除算法来回挪;熬过了这两个区域也就可以升级到老年代了;
  • 老年代 约2/3

​ 存放的就是经历过各种MinorGC还没被清理的对像或者一字儿比较大的对像例如数组;

在这块区域等待着这些对像的就是FullGC,当然如果这块区域在经历过fullgc之后还没有足够的内存存放准备进入的对象则会"OOM" 内存溢出;

image-20220603145937421.png

👆[关于内存溢出的情况简单分析]

​ 理论上来说,Java是有自动垃圾回收机制,它会自动清理那些不再被使用的对象,自动从内存中清除;但是,Java也还是存在着内存泄漏的情况,因为标记java对象是否可被回收的理论依据是可达性分析算法;

​ 因此,导致内存泄露的原因可能是编码的不合理导致某些长生命周期的 对象长期引用着短生命周期对象,这种情况就很可能发生内存泄露,因为这个时候短生命周期对象已经不会再被系统使用了,但是因为 长生命周期对象持有它的引用而导致不能被回收,这种情况累计到一定程度后,就发生了内存泄露;

另外还有栈的内存溢出,栈的生命周期是伴随着线程的,线程在执行方法的时候,会为每个方法在栈内创建栈帧,用来存储一些信息。

​ 但是栈的内存资源非常有限,虽然说栈帧会随着方法的执行结束而出栈,但是如果遇到递归调用,不加控制的恶性调用,不断的请求栈内存,当请求栈深度超过了栈本身的最大深度,则StackOverflowError报错出现;