JVM是如何创建对象的?了解 JDK8 下 Java 对象的内存分配和创建过程

本文详细介绍了JVM在JDK8中如何创建对象和分配内存,包括堆内存的结构、对象创建的步骤、内存分配方式、内存布局以及字段重排列等。讨论了新生代、老年代、永久代的概念,以及TLAB的作用和对象在内存中的分布。此外,还阐述了Java对象创建的不同方式,如new关键字、反射、Constructor.newInstance等,并分析了对象创建的过程。最后,探讨了内存对齐、字段重排列以及字节序等概念。
摘要由CSDN通过智能技术生成

正文

快要讲解 Java 垃圾回收机制了,在此之前我们有必要了解一下 Java 对象的内存分配和创建过程。

JDK8内存区域

Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。如下图所示:

Java 对象分配内存主要与堆有关,所以此处只介绍一下堆内存。

堆是 JVM 内存管理的最大的一块区域,此内存区域的唯一目的就是存放对象的实例,所有对象实例与数组都要在堆上分配内存。它也是垃圾收集器的主要管理区域。java 堆可以处于物理上不连续的空间,只要逻辑上是连续的即可。线程共享的区域。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将抛出 OutOfMemoryError 异常。

为了支持垃圾收集,堆被分为三个部分:

  1. 年轻代 : 常常又被划分为Eden区和Survivor(From Survivor To Survivor)区(Eden空间、From Survivor空间、To Survivor空间(空间分配比例是8:1:1)
  2. 老年代
  3. 永久代 (jdk 8已移除永久代)

img

(1)、堆是 JVM 中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了 new 对象的开销是比较大的。

(2)、Sun Hotspot JVM 为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间 TLAB(Thread Local Allocation Buffer),其大小由 JVM 根据运行的情况计算而得,在 TLAB 上分配对象时不需要加锁,因此 JVM 在给线程的对象分配内存时会尽量的在 TLAB 上分配,在这种情况下 JVM 中分配对象内存的性能和 C 基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配。

(3)、TLAB 仅作用于新生代的 Eden Space,因此在编写 Java 程序时,通常多个小的对象比大的对象分配起来更加高效。

(4)、所有新创建的 Object 都将会存储在新生代 Yong Generation 中。如果 Young Generation 的数据在一次或多次 GC 后存活下来,那么将被转移到 OldGeneration。新的 Object 总是创建在 Eden Space。

值得注意的是,我们说 TLAB 是线程独享的,但是只是在“分配”这个动作上是线程独占的,至于在读取、垃圾回收等动作上都是线程共享的。而且在使用上也没有什么区别。

img

换言之,虽然每个线程在初始化时都会去堆内存中申请一块 TLAB,并不是说这个 TLAB 区域的内存其他线程就完全无法访问了,其他线程的读取还是可以的,只不过无法在这个区域中分配内存而已。

Java对象创建方式

在 Java 程序中,我们拥有多种新建对象的方式。

最简单的方式就是使用new关键字。

User user = new User();

除此以外,还可以使用反射机制创建对象:

User user = User.class.newInstance();

或者使用 Constructor 类的 newInstance:

Constructor<User> constructor = User.class.getConstructor();
User user = constructor.newInstance();

除此之外还可以使用 clone 方法、反序列化以及 Unsafe 类的方式,Object.clone 方法和反序列化通过直接复制已有的数据,来初始化新建对象的实例字段。Unsafe.allocateInstance 方法则没有初始化实例字段,而 new 语句和反射机制,则是通过调用构造器来初始化实例字段。想深入这三种方式的朋友,推荐阅读 java创建对象的五种方式

new关键字

以 new 语句为例,它编译而成的字节码将包含用来请求内存的 new 指令,以及用来调用构造器的 invokespecial 指令。

public class SubUser {
      

  public static void main(String[] args) {
      
    SubUser subUser = new SubUser();
  }
}

// 解析字节码文件
  public com.msdn.java.hotspot.object.SubUser();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1 // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2 // class com/msdn/java/hotspot/object/SubUser
         3: dup
         4: invokespecial #3 // Method "<init>":()V
         7: astore_1
         8: return

提到构造器,就不得不提到 Java 对构造器的诸多约束。首先,如果一个类没有定义任何构造器的话, Java 编译器会自动添加一个无参数的构造器。

Object 类是一切 Java 类的父类,对于普通的 Java 类,即便不声明,也是默认继承了Object 类。正如上面案例所示,SubUser 没有显式声明构造方法,Java 编译器会自动添加对父类构造器的调用。但是,如果父类没有无参数构造器,那么子类的构造器则需要显式地调用父类带参数的构造器。

显式调用又可分为两种,一是直接使用“super”关键字调用父类构造器,二是使用“this”关键字调用同一个类中的其他构造器,具体可查看下述示例代码。无论是直接的显式调用,还是间接的显式调用,都需要作为构造器的第一条语句,以便优先初始化继承而来的父类字段。(不过这可以通过字节码注入来绕开,我们之前讲重载时有介绍到)

public class Cutomer {
      

  private String name;
  private int age;

  public Cutomer(String name, int age) {
      
    this.name = name;
    this.age = age;
  }
}

public class SubUser extends Cutomer {
      

  private long id;

  public SubUser(String name, int age) {
      
    super(name, age);
  }

  public SubUser(String name, int age, long id) {
      
// super(name, age);
    this(name, age);
    this.id = id;
  }
}

总而言之,当我们调用一个构造器时

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值