Java对象及其内存管理

Java内存分配分为两种:

  • 内存分配:特指创建Java对象时JVM为该对象在堆内存中分配的内存空间。
  • 内存回收:当Java对象失去引用,编程垃圾时,JVM的垃圾回收机制自动清除该对象,并且回收该对象所占用的内存。

由于JVM的垃圾回收机制由一条后台线程完成,本身也是非常消耗性能的。

实例变量和类变量

Java程序的变量大体可以分为成员变量和局部变量。其中局部变量又可以分为三种:

  • 形参:在方法签名中定义的局部变量,由方法调用者负责为其赋值,随方法的结束而消亡。
  • 方法内的局部变量:初始化时必须在方法内部对其显示赋值,生命周期与方法的相同。
  • 代码块内的局部变量:必须在代码块内对其显示初始化,生命周期与代码块的相同。

局部变量的作用时间很短,它们被存储在栈内存中。

类内定义的变量被称作成员变量,如果该成员变量定义是没有使用 static 修饰,该成员变量又被称为非静态变量或者实例变量;如果使用了 static 修饰,该成员变量又被称为静态变量或者类变量。

  • 对于static而言,从词意上看是“静态”的意思;从Java程序的角度来看,static是一个标志,static的作用就是将实例变量转换为类变量。

  • static只能修饰类里定义的成员部分,包括成员变量、方法、内部类(枚举或者接口)、代码块。如果没有 static 修改这些成员,这些成员属于该类的实例;如果使用 static 修饰,则这些成员属于该类本身。

一般Java没有要求定义变量要有先后顺序,但要保证前向引用的正确性,即在使用一个变量是必须确保该变量已经被定义。

int a = b + 12;
int b;

因此在编译上面程序时提醒“非法前向引用”。
但是如果一个是类变量一个是实例变量,则实例变量总是可以应用类变量:

int num = num2 + 2;
static int num2 = 2;

因为static修饰的成员变量属于类,类变量会随着类的初始化得到初始化。而实例变量是随着对象的初始化而初始化。在初始化一个对象之前,肯定会初始化该对象所属的类。

实例变量和类变量的属性

在同一个JVM中,每个类只对应一个 Class 对象,但每个类可以创建对个 Java 对象。由于同一个JVM中每个类对应一个 Class 对象,因此同一个 JVM 内的一个类的类变量只需一块内存空间。但是对于实例变量而言,该类每创建一个实例对象,就需要为实例变量分配一块内存空间,即堆内存中有几个实例对象,实例变量就需要几块内存空间。

  • 所有的类都是 Class 类的实例。每个类初始完成后,系统就会为该类创建一个对应的 Class 实例,程序可以通过反射来获取某个类对应的 Class 实例,例如要获取 Person 类对应的 Class 实例,通过 Person.class 或者 Class.forName(“Person”);

实例变量的初始化时机

对应实例变量而言,它属于java对象。

  • 从程序的运行角度来看,每次程序创建Java对象时就需要为实例变量分配内存空间,并对实例变量执行初始化。
  • 从语法角度来看,程序可以在三个地方对实例变量执行初始化:
    1. 定义实例变量是指定初始值
    2. 非静态代码块中对实例变量指定初始值
    3. 构造函数中对实例变量指定初始值

其中前两中种方式比第三种更早执行。但前两种方式的执行顺序与它们在代码中的顺序有关。

class Cat{
    String name;
    int age;
    // 静态代码块属于类,该类初始时就会执行
    static{
        System.out.println("执行静态代码块");
    }

    public Cat(String name,int age){
        System.out.println("执行构造函数");
        this.name = name;
        this.age = age;
    }
    /**
    * 定义实例变量时指定初始值和在非静态代码块中指定初始值的执行优先级是相同的。与它们在代码中的顺序有关。
    * 可能会有如果非静态代码块先执行的话,weight = 2.0; 该句代码没有指定数据类型,编译不会报错吗的疑虑?
    * 其实在编译时,编译器会想将实例变量提出:
    *    即先从 double weight = 2.3; 提出 double weight;
    *    然后执行 weight = 2.0;
    *    最后执行 weight = 2.3;
    */

    {
        System.out.println("执行非静态代码块");
        weight = 2.0;
    }

    double weight = 2.3;

    public String toString(){
        return "Cat[ name="+name+",age="+age+",weight="+weight+"]";
    }
}

public class InitTest{
    public static void main(String[] args){
        Cat cat = new Cat("Kitty",20);
        System.out.println(cat.toString());
    }
}

执行结果为:

---------- java ----------
执行静态代码块
执行非静态代码块
执行构造函数
Cat[ name=Kitty,age=20,weight=2.3]

输出完成 (耗时 0 秒) - 正常终止

如何证明在编译的时候JVM会提出实例变量:其实jdk提供了javap 工具可以查看编译的机制。语法如下;

javap <options> <classes>...

该工具有如下常用操作:

  • -c :分解方法代码,也就是显示每个方法具体的字节码
  • -l :用于指定显示行号和局部变量列表
  • -public|protected|package|private :用于指定显示哪种级别的类成员,分别对应java的四种访问控制权限
  • -verbose :用于指定显示更进一步的详细信息

对于上面的程序,执行如下语句:

javap -c Cat

输出的结果为:

Compiled from "InitTest.java"
class Cat {

  java.lang.String name;
  int age;
 double weight;

 public Cat(java.lang.String, int);
Code:
   0: aload_0
   1: invokespecial #1    // Method java/lang/Object."<init>":()V
   4: getstatic     #2    // Field java/lang/System.out:Ljava/io/PrintStream;
   7: ldc           #3    // String 执行非静态代码块
   9: invokevirtual #4    // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  12: aload_0
  13: ldc2_w        #5    // double 2.0d
  16: putfield      #7    // Field weight:D
  19: aload_0
  20: ldc2_w        #8    // double 2.3d
  23: putfield      #7    // Field weight:D
  26: getstatic     #2    // Field java/lang/System.out:Ljava/io/PrintStream;
  29: ldc           #10   // String 执行构造函数
  31: invokevirtual #4    // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  34: aload_0
  35: aload_1
  36: putfield      #11   // Field name:Ljava/lang/String;
  39: aload_0
  40: iload_2
  41: putfield      #12   // Field age:I
  44: return

  public java.lang.String toString();
Code:
   0: new           #13     // class java/lang/StringBuilder
   3: dup
   4: invokespecial #14     // Method java/lang/StringBuilder."<init>":()V
   7: ldc           #15     // String Cat[ name=
   9: invokevirtual #16     // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  12: aload_0
  13: getfield      #11    // Field name:Ljava/lang/String;
  16: invokevirtual #16    // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  19: ldc           #17    // String ,age=
  21: invokevirtual #16    // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  24: aload_0
  25: getfield      #12    // Field age:I
  28: invokevirtual #18    // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  31: ldc           #19    // String ,weight=
  33: invokevirtual #16    // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  36: aload_0
  37: getfield      #7     // Field weight:D
  40: invokevirtual #20    // Method java/lang/StringBuilder.append:(D)Ljava/lang/StringBuilder;
  43: ldc           #21    // String ]
  45: invokevirtual #16    // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  48: invokevirtual #22    // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  51: areturn

  static {};
Code:
   0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
   3: ldc           #23                 // String 执行静态代码块
   5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
   8: return
}

从执行结果可以发现,在编译时,JVM会把类中的实例变量提取出来,然后统一在构造函数中初始化,初始化顺序是,先是定义实例变量时指定初始值、在非静态代码块中指定初始值,后是在构造函数中指定初始值。而定义实例变量时指定初始值和在非静态代码块中指定初始值的执行顺序与它们在代码中的顺序有关。由此也可知构造函数只是负责对Java对象实例变量执行初始化(即赋初始值),而在构造函数执行之前,该对象所占的内存已经被分配出来了,也就是这些实例变量都分配了内存空间,且被赋了默认值。

类变量的初始化时机

对应类变量,它属于Java类本身。

  • 从程序运行的角度:在同一JVM中,每个Java类只初始化一次,因此只有在每次运行Java程序时,才会初始化Java类,才会为该类的类变量分配内存空间,并执行初始化。
  • 从语法角度:程序可以在两个地方对类变量执行初始化。
    1. 定义类变量是指定初始值
    2. 静态代码块中对类变量指定初始值

这两种方式与它们在代码中的顺序有关

class StaticDemo {

    static int b;

    static {
        b = 4;
    }
    static int a = 2;
}

使用javap工具查看编译情况:

javap -c StaticDemo

执行结果为:

E:\Java\test>javap -c StaticDemo
Compiled from "StaticDemo.java"
class StaticDemo {
  static int b;
  static int a;

  StaticDemo();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  static {};
    Code:
       0: iconst_4
       1: putstatic     #2                  // Field b:I
       4: iconst_2
       5: putstatic     #3                  // Field a:I
       8: return
}

从执行结果可以发现,在编译时,JVM将定义类变量先提取出来,然后统一在静态代码块中初始化。初始化的顺序与代码顺序相同。即不论在定义类变量是初始值还是在静态代码块中初始值,系统默认都会在静态代码块中初始值。而静态代码块先于构造函数执行。

class Price {
    // 类变量是 Price 的实例
    final static Price INSTANCE = new Price(2.8);
    // 定义类变量并赋初始值
    static double initPrice = 20;
    double currentPrice;
    public Price(double discount){
        currentPrice = initPrice - discount;
    }
}

public class StaticInitTest{
    public static void main(String[] args){
        /* 通过Price的INSTANCE访问currentPrice
         *    执行 Price.INSTANCE 后就会调用构造函数,执行代码,此时 initPrice 还没与执行,默认值为0
         *
         */
        System.out.println(Price.INSTANCE.currentPrice);// -2.8
        /* 通过Price类的实例来访问
         *    执行 Price p = new Price(2.8); 时,系统已经初始化完成所有类变量,initPrice 已经被赋值为20
         * 
         */
        Price p = new Price(2.8);
        System.out.println(p.currentPrice); // 17.2
    }
}

父类构造器

当创建任何Java对象时,程序总会先依次调用父类的非静态代码块、构造函数执行初始化,然后才调用本类的非静态代码块、构造函数。

当调用某个类的构造器来创建Java对象时,系统总会调用父类的非静态代码块进行初始化。这个调用时隐式执行的,而且父类的非静态代码块总会执行。接着会调用父类的一个或多个构造函数,这个调用既可以通过 super 显示调用,也可以隐式调用。当父类的非静态代码块、构造函数执行完成后,系统后调用该类本身的非静态代码块 、构造函数,最后返回本类的实例。

至于调用父类的哪个构造函数,有如下几种情况:

  • 子类构造函数中第一行代码使用 super() 函数显示调用父类的构造函数,根据 super() 函数的参数调用父类对应的构造函数。
  • 子类构造函数的第一行代码使用 this() 函数显示本身的其他构造函数,有其他构造函数显示调用父类的对应构造函数
  • 子类构造函数既没有调用 super() 没有调用 this() ,则系统会隐式调用父类的无参构造函数
class  Parent{
    private int i = 2;
    {
        System.out.println("Parent的非静态代码块执行了");
    }

    public Parent(){
        /**
        * 子类与父类中的同名实例变量不会被认为是重写,虽然名字相同,但分别属于不同的类
        * 此处输出 2 是因为在程序运行时JVM会为 Sub 分配两块栈内存,
        *    一块用来存储 Parent类的实例变量
        *   一块用来存储 Sub类的实例变量,
        * this的编译时类型和运行时类型不一样
        * 通过this访问它所引用的对象的实例变量是,该实例变量的值由声明该变量的类型决定
        * 通过this访问它所引用的对象的实例方法时,该方法的行文将由它实际引用的对象来决定
        * 此处this的类型是 Parent,但实际的应用对象时 Sub。
        * 怎么理解呢?this在构造函数中,则this代表该类的Java对象,则类型就是该类。
        *           但是此处Parent的构造函数时在 Sub的构造函数中调用的,则this的实际引用对象是Sub的实例对象
        */
        System.out.println("Parent构造函数执行了"+this.i);
        /**
        * 在编译期,this代表当前类的实例对象,执行 this.display() 方法,因为当前类有 display 方法编译通过
        * 在运行期,this代表的实例对象会根据具体的引用而不同,比如此处this在运行期代表的是子类 sub 的
        *  实例对象。
        */
        System.out.println("****parent****  "+this);// 此处的this是 sub 类的对象
        this.display();// 调用子类的的 display() 
        // 无法通过编译,因为this的编译类型是 parent,但是parent中没有 show方法
        //this.show();
    }

    public void display(){
        System.out.println("****parent****  "+ i);
    }
}

class Sub extends Parent{
    private int i = 22;
    {
        System.out.println("Sub的非静态代码块执行了");
    }

    public Sub(){
        // 隐式调用了父类的无参构造函数
        System.out.println("Sub构造函数执行了"+this.i);
        i = 222;
        this.display();
    }
    public void display(){
        System.out.println("+++++sub display i +++++  "+ i);
        System.out.println("+++++sub display a +++++  "+ i);
    }
    public void show(){
        System.out.println("****sub show****  "+ i);
    }
}

public class ParentTest{
    public static void main(String[] args){
        new Sub();
    }
}    

执行结果为:

---------- java ----------
Parent的非静态代码块执行了2
Parent构造函数执行了
****parent****  Sub@4f57011e
+++++sub display i +++++  0
+++++sub display a +++++  0
Sub的非静态代码块执行了22
Sub构造函数执行了
+++++sub display+++++  222

输出完成 (耗时 0 秒) - 正常终止

访问子类对象的实例变量和重写父类的方法

子类的方法可以访问父类的实例变量(访问权限允许时),这是因为子类继承父类就会获得父类的成员变量和方法;但是父类方法无法访问子类的实例变量,因为在父类实例化时子类还不存在,故父类无法知道子类有哪些成员变量和方法。

但是在极端的情况下也会存在父类访问子类的变量的情况。在上面的例子中,程序运行到Parent类的构造函数时,执行 this.display() 会调用 Sub类的 display 方法。因为当 this 在构造函数时,this 代表正在执行初始化的Java对象。那么此时的this应该是 Parent 类的实例对象,但是 Parent 的构造函数是在子类的构造函数中执行的,故此时 this 的实际引用对象应该是 Sub 类的实例对象。

通过上面的例子可以看到一个变量的编译时类型和运行时类型会不一样。若不一样时,通过该变量访问它所引用的对象的实例变量时,该实例变量的值由声明该变量的类型决定;通过该变量访问它所引用的对象的实例方法时,该方法的行文将由它实际引用的对象来决定。(具体原因看父子实例的内存控制)

  • 如果父类构造函数调用了子类重写的方法,且通过子类构造函数来创建子类对象,调用了这个父类的构造函数,就会导致子类的重写方法在子类的构造函数之前执行,会出现子类的重写方法访问不到子类的实例变量的情形。

父子实例的内存控制

继承成员变量和继承方法的区别

在访问权限允许的情况下,子类可以获得父类的全部方法,但对于成员变量则不会继承。

class Animal {
    public String name;
    public void info(){
        System.out.println(name);
    }
}

public class Wolf extends Animal{
    private double weight;
}

上面程序中,Wolf继承自Animal类,因此它会获得Animal类中声明的成员变量和方法。使用javap工具分析 Wolf 类:

javap -c -private Wolf

得到的结果为:

E:\Java\test>javap -c -private Wolf
Compiled from "Wolf.java"
public class Wolf extends Animal {
  private double weight;

  public Wolf();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method Animal."<init>":()V
       4: return

  public void info();
    Code:
       0: aload_0
       1: invokespecial #2                  // Method Animal.info:()V
       4: return
}

可以看出,Wolf继承Animal类时,编译器会将Animal类中的 void info() 方法转移到Wolf类中,即Wolf类中默认会有一个 void info() 方法。当然也可以显示声明该方法,则子类会重写父类的方法(父类该方法中的代码逻辑失效)。而Animal类中的成员变量则没有转移到Wolf类中。

  • 只有当子类声明为 public 而父类不是使用 public 修饰时才可以通过 javap 工具看到编译器将父类的方法转移到了子类。

如果子类重写了父类的方法,就意味着子类的方法完全覆盖了父类里的同名方法,编译器将不会再把父类的方法转移到子类。对于实例变量则不存在这种现象,即使子类中定义了与父类完全同名的实例变量,这个实例变量也不可能覆盖父类中定义的实例变量。

因为继承成员变量和继承方法之间存在这样的差别,所以对于一个引用类型的变量而言,当通过该变量访问它所引用的对象的实例变量时,该实例变量的值取决于声明该变量时的类型;当通过该变量调用它所引用的对象的方法时,该方法行为取决于它所实际引用的对象的类型。

内存中子类实例

当程序创建一个类对象时,系统不仅会为该类中定义的实例变量分配内存,也会为其父类中定义的实例变量分配内存,即使子类定义了与父类同名的实例变量。

如果子类中定义了与父类同名的实例变量,那么通过子类引用对象的变量访问实例变量永远得到的是子类的实例变量的值,为了在子类访问父类中定义的实例变量,可以使用 super. 作为限定来修饰这些实例变量和实例方法。

父、子类的类变量

final修饰符

final修饰符的功能:

  • final可以修饰变量,被final修饰的变量被赋初始值后,不能对它重新赋值
  • final可以修饰方法,被final修饰的方法不能被重写。
  • final可以修饰类,被final修饰的类不能派生子类。

final修饰变量

被final修饰的实例变量必须显示赋初始值,且只能在三个地方指定初始值:

  • 定义final实例变量是指定初始值
  • 在非静态代码块中为final实例变量指定初始值
  • 在构造函数中为final实例变量指定初始值

对于普通的实例变量可以在定义时赋初始值,亦可以让系统赋默认值。被final修饰的实例变量已被称为常量,必须显示赋初始值

class FinalInstanceVariableTest{
    final int var1 = 666;
    final int var2 ;
    {
        var2 = 777;
    }
    final int var3;
    public FinalInstanceVariableTest(){
        var3 = 888;
    }
}

使用 javap 工具查看编译情况:

javap -c FinalInstanceVariableTest

执行结果为:

E:\Java\test>javap -c FinalInstanceVariableTest
Compiled from "FinalInstanceVariableTest.java"
class FinalInstanceVariableTest {
  final int var1;

  final int var2;

  final int var3;

  public FinalInstanceVariableTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: sipush        666
       8: putfield      #2                  // Field var1:I
      11: aload_0
      12: sipush        777
      15: putfield      #3                  // Field var2:I
      18: aload_0
      19: sipush        888
      22: putfield      #4                  // Field var3:I
      25: return
}

从结果可以看出,三种初始值的方式都会被抽取到构造函数中执行。

  • final实例变量必须显示赋初始值,且本质final实例变量只能在构造函数中被赋初始值。final实例变量的三种赋初始值方式的执行顺序与普通实例变量的相同。

被final修饰的类变量也必须显示地指定初始值,而且final类变量只能在俩个地方指定初始值:

  1. 定义final类变量时指定初始值
  2. 在静态代码块中为final类变量指定初始值

    class FinalClassVariableTest {
        // 编译时无法确定 a 的准确取值
        final static int a = "final类变量".length();
        final static int b ;
        static{
            b = "final实例变量".length();
        }
    }
    

使用 javap 查看编译情况:

javap -c class FinalClassVariableTest

执行结果为:

E:\Java\test>javap -c FinalClassVariableTest
Compiled from "FinalClassVariableTest.java"
class FinalClassVariableTest {
  static final int a;

  static final int b;

  FinalClassVariableTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  static {};
    Code:
       0: ldc           #2                  // String final类变量
       2: invokevirtual #3                  // Method java/lang/String.length:()I
       5: putstatic     #4                  // Field a:I
       8: ldc           #5                  // String final实例变量
      10: invokevirtual #3                  // Method java/lang/String.length:()I
      13: putstatic     #6                  // Field b:I
      16: return
}

从结果可以看出,两种初始值的方式都会被抽取到静态代码块中执行。而执行顺序与代码顺序相关。被final修饰的类变量同样在赋值后不能修改。

  • final修饰符的另外一个重要用途是定义“宏变量”。在定义final变量时就显示为变量指定初始值,而且在初始值在编译的时候能够确定下来,那么这个final变量本质上就是一个“宏变量”,编译器会把程序中所有用到该变量的地方直接替换成该变量的值。

    class FinalClassVariableTest {
        final static int a = 44;
        final static int b ;
        static{
            b = 66;
        }
    }
    

使用 javap 查看编译情况:

javap -c class FinalClassVariableTest

执行结果为:

E:\Java\test>javap -c FinalClassVariableTest
Compiled from "FinalClassVariableTest.java"
class FinalClassVariableTest {
  static final int a;

  static final int b;

  FinalClassVariableTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  static {};
    Code:
       0: bipush        66
       2: putstatic     #2                  // Field b:I
       5: return
}

通过该程序不难发现,当使用final修饰类变量时,如果定义该final类变量时指定了初始值,而且该初始值在编译时就可以确定下来,系统将不会该静态代码块中对该final类变量赋初始值,而是在类定义中直接使用该初始值代替该final变量。

同理对用final修饰的实例变量,如果在定义该实例变量时就指定了初始值,且该初始值能够在编译时明确确定,虽然系统会在构造函数中对其初始赋值,但是该实例变量也会被系统当做“宏变量”。

对于一个使用final修饰的变量而言,如果定义该final变量时就指定了初始值,而且这个初始值在编译的时候就能够确定下来,那么这final变量将不再是一个变量,系统将其当成一个“宏变量”处理。也就是说,所有出现该变量的地方,系统直接把他当成对应的值处理。
该变量一般被称为直接量。

final方法不能被重写

final类不能派生子类