java创建对象的过程详解(从内存角度分析)

简介: java对象的创建操作其实我在《JVM系列之类的加载机制》一文曾经提到过,包含两个过程:类的初始化和实例化。

java对象的创建操作其实我在《JVM系列之类的加载机制》一文曾经提到过,包含两个过程:类的初始化和实例化。为此为了理解的深入,我们还需要再来看一下类的生命周期。一张图表示:

v2-a5e9c3ff9d5ba684204ae3fef6f61716_1440w.jpg

从上面我们可以看到,对象的创建其实包含了初始化和使用两个阶段。有了这个印象之后,我们就能开始今天的文章了。先给出这篇文章的大致脉络:

首先,介绍一下java中对象的创建基本知识然后,介绍一下对象初始化的顺序接下来,介绍一下创建对象的几种方式最后,进行一个总结。(从内存角度去分析:重点)重点


一、基本知识


我们知道,一个对象的创建过程包含两个过程:初始化和实例化


我们在使用一个对象时,JVM首先会检查相关类型是否已经加载并初始化,如果没有,则JVM立即进行加载并调用类构造器完成类的初始化。在类初始化过程中或初始化完毕后,根据具体情况才会去对类进行实例化。


实例化时候,java虚拟机就会为其分配内存来存放自己及其从父类继承过来的实例变量。在为这些实例变量分配内存的同时,这些实例变量先会被赋予默认值(零值)。在内存分配完成之后,Java虚拟机才会对新创建的对象赋予我们程序员给定的值。


小结:创建一个对象包含下面两个过程:


1、类构造器完成类初始化(分配内存、赋予默认值)

2、类实例化(赋予给定值)


二、类初始化


下面我们直接给出一个例子看一下java是如何初始化的。我们知道一个类中,往往包含静态变量、静态代码块、变量、普通方法、构造方法等信息。那么他们是如何初始化的呢?

public class InitialTest {
    //静态变量
    public static String staticField = "静态变量";
    //变量 
    public String field = "==普通变量==";
    // 静态代码块
    static {
        System.out.println( staticField );
        System.out.println( "静态初始化块" );
    }
    // 初始化块
    {
        System.out.println( field );
        System.out.println( "==初始化块==" );
    }
    // 构造方法
    public InitialOrderTest(){
        System.out.println( "构造器" );
    }
    public static void main( String[] args ){
        new InitialOrderTest();
    }
}

输出:

v2-af8b4fb6e57d9ea3d5038702bb23d08f_1440w.jpg

上面这个例子比较简单,我们再来看看带有父类和接口的情况。

第一步:定义一个父类

class FatherClass {
    // 静态变量 
    public static String parent_StaticField = "父----静态变量";
    // 变量
    public String parent_Field = "父类----普通变量";
    // 静态初始化块
    static {
        System.out.println(parent_StaticField);
        System.out.println("父类------静态初始化块");
    }
    // 初始化块 
    {
        System.out.println(parent_Field);
        System.out.println("父类-----初始化块");
    }
    // 构造器 
    public FatherClass() {
        System.out.println("父类--构造器");
    }
}

第二步:定义一个子类实现

public class SubSon extends FatherClass  {
    // 静态变量
    public static String son_StaticField = "子类--静态变量";
    // 变量 
    public String son_Field = "子类--变量";
    // 静态初始化块
    static {
        System.out.println(son_StaticField);
        System.out.println("子类--静态初始化块");
    }
    // 初始化块 
    {
        System.out.println(son_Field);
        System.out.println("子类--初始化块");
    }
    // 构造器
    public SubSon(){
        System.out.println( "子类--构造器" );
    }
    public static void main(String[] args) {
        System.out.println("子类-----main方法");
        new SubSon();
    }
}

第三步:看结果

v2-4fc37eb2f508f631fd3b752484604635_1440w.jpg

小结,类的初始化顺序,这样看确实不好记,不过没有继承关系的我们都能很好的看到。带继承关系的,使用网上一张图来表示

v2-7b8871318d9fdd74ff83c9e9530f2f6f_1440w.jpg

OK,类的初始化中的知识点基本上就是初始化的顺序。


三、创建对象的几种方式


其实对象的初始化就是在创建对象的时候由jvm完成的。对于创建对象,主要是研究创建对象的几种方式。下面一一的解答.这里给出6种方式,面试的时候足够你zhuangbility。


  1. 使用new关键字
  2. Class对象的newInstance()方法
  3. 构造函数对象的newInstance()方法
  4. 对象反序列化
  5. Object对象的clone()方法
  6. 使用Unsafe类创建对象
  7. 最后再揭晓。。。

OK,先认识一个,下面一个一个看。


(1)使用new关键字


Test t1 = new Test();
Test t2 = new Test("java的架构师技术栈");

(2)class的newInstance()方法

public static void main(String[] args) throws Exception {
    String className = "com.fdd.Test";
    Class clasz = Class.forName(className);
    Test t = (Test) clasz.newInstance();
}

首先我们通过Class.forName()动态的加载类的Class对象,

然后通过newInstance()方法获得Test类的对象


(3)构造函数的newInstance()方法

public static void main(String[] args) throws Exception {
   Constructor<Test> constructor;
   try {
        constructor = Test.class.getConstructor();
       Test t = constructor.newInstance();
   } catch (Exception){
        e.printStackTrace();
   }
}

类Constructor也有newInstance方法,这一点和Class有点像。从它的名字可以看出它与Class的不同,Class是通过类来创建对象,而Constructor则是通过构造器。


(4)序列化

public static void main(String[] args) throws Exception {
     String filePath = "sample.txt";//序列化的路径
     Test t1 = new Test("java的架构师技术栈");
     try {
        //t1开始序列化
        FileOutputStream fileOutputStream = new FileOutputStream(filePath);
        ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream);
        outputStream.writeObject(t1);
        outputStream.flush();
        outputStream.close();
        //t2开始反序列化
        FileInputStream fileInputStream = new FileInputStream(filePath);
        ObjectInputStream inputStream = new ObjectInputStream(fileInputStream);
        Test t2 = (Test) inputStream.readObject();
        inputStream.close();
        System.out.println(t2.getName());
     } catch (Exception ee) {
        ee.printStackTrace();
     }
}

首先我们要对Test实现Serializable接口。然后开始序列化数据。最后得到反序列化的对象。


(5)clone方式


Object对象中存在clone方法,它的作用是创建一个对象的副本。

public static void main(String[] args) throws Exception {
    Test t1 = new Test("java的架构师技术栈");
    Test t2 = (Test) t1.clone();
    System.out.println(t2.getName());
}

(6)使用Unsafe类创建对象


Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。Oracle正在计划从Java 9中去掉Unsafe类,如果真是如此影响就太大了。

我们无法直接创建Unsafe对象。这里我们使用反射方法得到

private static Unsafe getUnsafe() {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            Unsafe unsafe = (Unsafe) field.get(null);
            return unsafe;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
  }

拿到这个对象后,调用其中的native方法allocateInstance 创建一个对象实例

Object event = unsafe.allocateInstance(Test.class);


四、总结


我们想要创建一个对象。基本上就是java虚拟机分配内存的过程。因此我们可以先回顾一下java程序的执行过程。给一张网上的图,写的很清晰

v2-143bbbdc6b100e1d8b712783574bcb10_1440w.jpg


一个例子去解释:(摘自我之前的文章《java8的内存结构》)

public class Person{
    int age;
    String name;
    public void walk() {
        System.out.println("我正在走路。。。。");
    }
}

然后我们测试一下:

public class Test {
    public static void main(String[] args) {
        Person person = new Person();
        person.name = "java的架构师技术栈";
        person.age = 18;
        person.walk();
    }
}

我们分析一下这个过程


第一步,JVM去方法区寻找Test类的代码信息,如果有直接调用,没有的话使用类的加载机制把类加载进来。同时把静态变量、静态方法、常量加载进来。这里加载的是(“冯冬冬的IT技术栈”,“冯XX”);这是因为字符串是常量,age中的18是基本类型。


第二步,jvm进入main方法,看到Person person=new Person()。首先分析Person这个类,同样的寻找Person类的代码信息,有就加载,没有的话类加载机制加载进来。同时也加载静态变量、静态方法、常量(“我正在走路。。。”)


第三步,jvm接下来看到了person,person在main方法内部,因而是局部变量,存放在栈空间中。


第四步,jvm接下来看到了new Person()。new出的对象(实例),存放在堆空间中。


第五步,jvm接下来看到了“=”,把new Person的地址告诉person变量,person通过四字节的地址(十六进制),引用该实例。 是不是有点晕,别着急,画个图看一下。

v2-03499693ec42e4f0afc936d84a804f71_1440w.jpg

第六步,jvm看到person.name = “冯冬冬的IT技术栈”;person通过引用new Person实例的name属性,该name属性通过地址指向常量池的"冯冬冬的IT技术栈"。


第七步,jvm看到person.age = 18; person的age属性是基本数据类型,直接赋值。


第八步,jvm看到person.walk(); 调用实例的方法时,并不会在实例对象中生成一个新的方法,而是通过地址指向方法区中类信息的方法。走到这一步再看看图怎么变化的

v2-88ee44c059183129097989194444b021_1440w.jpg

大多数人基本上都能看懂。创建一个对象的过程基本上就是这。

如有问题还请批评指正。谢谢

相关文章
|
11月前
|
设计模式 网络协议 数据可视化
Java 设计模式之状态模式:让对象的行为随状态优雅变化
状态模式通过封装对象的状态,使行为随状态变化而改变。以订单为例,将待支付、已支付等状态独立成类,消除冗长条件判断,提升代码可维护性与扩展性,适用于状态多、转换复杂的场景。
1186 157
|
安全 Java 应用服务中间件
Spring Boot + Java 21:内存减少 60%,启动速度提高 30% — 零代码
通过调整三个JVM和Spring Boot配置开关,无需重写代码即可显著优化Java应用性能:内存减少60%,启动速度提升30%。适用于所有在JVM上运行API的生产团队,低成本实现高效能。
1301 3
|
安全 Java 编译器
new出来的对象,不一定在堆上?聊聊Java虚拟机的优化技术:逃逸分析
逃逸分析是一种静态程序分析技术,用于判断对象的可见性与生命周期。它帮助即时编译器优化内存使用、降低同步开销。根据对象是否逃逸出方法或线程,分析结果分为未逃逸、方法逃逸和线程逃逸三种。基于分析结果,编译器可进行同步锁消除、标量替换和栈上分配等优化,从而提升程序性能。尽管逃逸分析计算复杂度较高,但其在热点代码中的应用为Java虚拟机带来了显著的优化效果。
409 4
|
存储 缓存 Java
Java数组全解析:一维、多维与内存模型
本文深入解析Java数组的内存布局与操作技巧,涵盖一维及多维数组的声明、初始化、内存模型,以及数组常见陷阱和性能优化。通过图文结合的方式帮助开发者彻底理解数组本质,并提供Arrays工具类的实用方法与面试高频问题解析,助你掌握数组核心知识,避免常见错误。
|
11月前
|
Java 大数据 Go
从混沌到秩序:Java共享内存模型如何通过显式约束驯服并发?
并发编程旨在混乱中建立秩序。本文对比Java共享内存模型与Golang消息传递模型,剖析显式同步与隐式因果的哲学差异,揭示happens-before等机制如何保障内存可见性与数据一致性,展现两大范式的深层分野。(238字)
433 4
|
11月前
|
存储 缓存 Java
【深入浅出】揭秘Java内存模型(JMM):并发编程的基石
本文深入解析Java内存模型(JMM),揭示synchronized与volatile的底层原理,剖析主内存与工作内存、可见性、有序性等核心概念,助你理解并发编程三大难题及Happens-Before、内存屏障等解决方案,掌握多线程编程基石。
|
11月前
|
存储 Java Go
【Java】(3)8种基本数据类型的分析、数据类型转换规则、转义字符的列举
牢记类型转换规则在脑海中将编译和运行两个阶段分开,这是两个不同的阶段,不要弄混!
437 2
|
11月前
|
设计模式 缓存 Java
【JUC】(4)从JMM内存模型的角度来分析CAS并发性问题
本篇文章将从JMM内存模型的角度来分析CAS并发性问题; 内容包含:介绍JMM、CAS、balking犹豫模式、二次检查锁、指令重排问题
297 1
|
11月前
|
Java Go 开发工具
【Java】(9)抽象类、接口、内部的运用与作用分析,枚举类型的使用
抽象类必须使用abstract修饰符来修饰,抽象方法也必须使用abstract修饰符来修饰,抽象方法不能有方法体。抽象类不能被实例化,无法使用new关键字来调用抽象类的构造器创建抽象类的实例。抽象类可以包含成员变量、方法(普通方法和抽象方法都可以)、构造器、初始化块、内部类(接 口、枚举)5种成分。抽象类的构造器不能用于创建实例,主要是用于被其子类调用。抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类abstract static不能同时修饰一个方法。
410 1
|
缓存 监控 Kubernetes
Java虚拟机内存溢出(Java Heap Space)问题处理方案
综上所述, 解决Java Heap Space溢出需从多角度综合施策; 包括但不限于配置调整、代码审查与优化以及系统设计层面改进; 同样也不能忽视运行期监控与预警设置之重要性; 及早发现潜在风险点并采取相应补救手段至关重要.
1133 17