NO.11 对象是怎样炼成的 | Java敲黑板系列

开场白

老铁 :Java是一门完全面向对象的语言,在我们的编程过程中,对象无处不在,为此我们对Java对象已经司空见惯。但是,对于我们经常使用的技术,我们真的理解对象吗?

其中,一个常见的对象认识误区就是,我们常会低估JVM创建对象所带来的成本。实际上,JVM在创建一个对象时,其空间、时间成本比我们通常意识到的要高得多。

对象在内存中构建过程

对象构建并不是我们通常所理解的只是进行内存分配、初始化成员变量等简单内容。真实的构建过程包括了很多步骤,通过对这些步骤的深入理解,可以指导我们编程过程中精简对象并减少对象数量,提升JVM的内存利用率与运行效率。以下是对象在内存中构建过程:

敲黑板

  1. 从堆上分配内存,该内存用于存放对象的全部实例变量,以及父类方法地址信息。
  2. 对象的实例变量被初始化为相应的缺省值。如int类型为0;double类型为0.0;对象类型为null;布尔类型为false。
  3. 按照继承深度,从浅到深依次调用父类的构造函数,这个一直持续到调用java.lang.Object构造函数位置。(Object类是所有Java类的祖先类)为此,最顶层的父类(Object类)构造函数最先执行,本对象的构造函数最后执行。
  4. 在上述所有构造函数执行之前,先执行本类的实例变量的初始化赋值与初始化区块语句,然后再执行构造函数的语句。

对象分类

从对JVM构造对象的复杂度出发,我们可以将对象分为以下两类:
轻型对象:继承深度较浅,并且不包含许多其他对象。
重型对象:轻型对象以外的其他对象。
根据上述不同类型对象的定义,并结合上述对象在内存中的构建过程可知,重型对象构造步骤更加复杂,所耗费的堆内存也更大。

轻型对象构造过程

轻型对象应用举例,如代码1所示:

class Light{
    private int count; //实例变量
    private boolean isDone = true; //实例变量;初始化

    //构造函数
    public Light(int count){
        this.count = count;
    }   
    //....
}

下面我们结合下述语句来说明轻型对象是如何进行构建的:

 Light light = new Light(100);
  1. 从堆中分配内存,用来存放Light class的实例变量,以及父类方法地址信息;
  2. 实例变量count、isDone分别初始化为相应缺省值0,false;
  3. 调用构造函数,传入数值100;
  4. 构造函数调用其父类Object类的构造函数;
  5. Object构造函数执行返回后,对Light类的实例变量执行初始化操作,将isDone赋值为true;
  6. 将this.count赋值为100;Light构造函数执行完成并返回;
  7. 对象引用light执行在heap中刚刚建立完成的Light对象;
  8. 打完收工。

重型对象构造过程

重型对象应用举例,如代码2所示:

import java.awt.Color;
import java.awt.Point;

class animal{
    private String name;//实例变量
    public animal(String n){
    this.name =n;
    }
}

class bird extends animal{
    private boolean canFly = true;//实例变量;初始化
    public bird(String n,boolean b){
    super(n); //显式调用父类
    this.canFly = b;
    }
}

class parrot extends bird{
    private Point location;//实例变量
    private Color color;//实例变量
    public parrot(String n, boolean b){
    super(n,b);//显式调用父类
    this.location = new Point(1,1);//新生成Point对象
    this.color = new Color(0,255,255);//新生成Color对象
    }
}

下面我们结合下述语句来说明轻型对象是如何进行构建的:

parrot p = new parrot(“air”, true);

首先,parrot类继承了bird,bird类继承了animal,animal继承了Object类,可见parrot类的继承深度为3;此外,parrot类还包括了Color类对象、Point对象,为此parrot类完全符合重型对象的定义。下面,我们来看看重型对象是如何产生的:

  • 从堆中分配内存,用来存放p的实例变量(location、color)、bird类的实例变量(canFly)、以及animal类的实例变量(name),以及父类方法地址信息;
  • 分别对上述实例变量初始化为相应缺省值。对象引用location、color、name被初
  • 始化为null;canFly被初始化为false;
  • 调用parrot构造函数,传入“air”、true;
  • parrot构造函数调用父类bird的构造函数;
  • bird构造函数调用父类animal的构造函数;
  • animal的构造函数调用父类Object的构造函数;
  • Object构造函数返回后,animal构造函数将其实例变量name赋值为“air”,然后返回;
  • bird构造函数进行实例变量初始化,将canFly赋值为true,然后返回;
  • parrot构造函数本体开始执行,建立一个Point对象与一个Color对象。此时,分别针对这两个对象分别有从上述的步骤1开始重复执行全部过程(这也是为什么不要包含太多类对象的原因);
  • 对象引用p指向堆内存中创建的parrot对象;
  • 再次打完收工。

怎么办?

如上所述,创建一个重型对象需要更多的步骤;为此,重型对象比建立轻型对象的性能相差很多。当一个类符合如下特征的时候,我们就要小心我们是否已经上了重型对象的“道”了:
- 构造函数中包括了大量的代码;
- 类定义中包含了大量的类对象;
- 太深的继承层次。

敲黑板
每一个被创建出来的对象,也是垃圾回收器跟踪和可能释放的目标。所以,不仅仅建立对象需要付出代价,垃圾回收器管理与回收这些对象同样也需要付出代价。
通过性能评测,确定是因为重型对象而造成的性能瓶颈,可以采用以下方法进行优化:

  1. 对类进行“瘦身”。重新设计这个class,将这个class分解为多个轻型class,并使得性能需求最高的代码只使用轻型class;
  2. 使用延迟初始化技术;
  3. 尽可能复用对象。

转载自公众号:代码荣耀
图1

猜你喜欢

转载自blog.csdn.net/maijia0754/article/details/80571223