2 对象与内存控制

本节要点

  • 变量初始化细节
  • 构造器
  • 父、子实例的实例变量的内存分配机制
  • 父、子类的类变量的内存分配机制
  • Final 方法注意点

Java 内存管理包括两方面:内存分配和内存回收。
内存分配特指创建 Java 对象时 JVM 为该对象在堆内存中所分配的内存空间;
内存回收指当Java 对象失去引用,JVM 的 GC 机制会自动清理该对象,回收该对象所占用的内存。

虽然 JVM 内置了垃圾回收机制,回收失去引用的 Java 对象所占用的内存,但 Java 程序依然会有内存泄露。

JVM 的垃圾回收机制由一条后台线程完成,本身也是非常消耗性能的,因此若肆无忌惮的创建对象,让系统分配内存,会产生两个坏处:

1) 不断分配内存使得系统可用内存减少,从而降低程序运行性能;

2) 大量已分配内存的回收使得垃圾回收的负担加重,降低程序运行性能

2.1 实例变量和类变量

Java 程序的变量大题可分为成员变量和局部变量,其中局部变量可分为如下3类:

1) 形参:在方法签名中定义的局部变量,由方法调用者负责赋值,随方法的结束而消亡

2) 方法内的局部变量:在方法内定义的局部变量,必须在方法内对其进行显式初始化,随方法的结束而消亡

3) 代码块内的局部变量:在代码块内定义的局部变量,必须在代码块内对其进行显式初始化,随代码块的结束而消亡

局部变量的作用时间都很短暂,都被存储在方法的栈内存中。

实例变量的初始化时机

从程序运行的角度来看,每次创建 Java 对象都会为实例变量分配内存空间,并对实例变量执行初始化。

从语法角度来看,可在3个地方对实例变量执行初始化:

1) 定义实例变量时直接指定初始值

2) 非静态初始化块中指定初始值

3) 在构造器中指定初始值

其中前两种方式比第3种更早执行,第1、2种方式的执行顺序与它们在程序中的排列顺序相同。

使用 javap 命令分析字节码文件可以看出,类经过编译器处理之后,初始化块消失,构造器里包含了初始化块里的语句,而且在类中定义的实例变量不再有初始值,为该变量指定的初始值的代码也被提取到了构造器里面。

在赋值语句合并到构造器的过程中,定义变量语句转换得到的赋值语句、初始化块里的语句转换得到的赋值语句,总是位于构造器的所有语句之前,合并后,这两种赋值语句的顺序保持它们在源代码中的顺序

类变量的初始化时机

从程序运行的角度来看,JVM 对一个 Java 类只初始化一次,因此 Java 程序每运行一次,系统只为类变量分配一次内存空间,执行一次初始化。

从语法角度来看,程序可在2个地方对类变量执行初始化:

1) 定义类变量时指定初始值;

2) 静态初始化块中对类变量指定初始值

这两种方式的执行顺序与它们在源程序中排列顺序相同。

2.2 构造器

父类构造器

当创建任何 Java 对象时,程序总会先一次调用每个父类非静态初始化块、父类构造器(总是从 Object 开始)执行初始化,最后才调用本类的非静态初始化块、构造器执行初始化。

2.3 父、子实例的实例变量的内存分配机制

如果子类重写了父类方法,意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中;

对于实例变量则不存在这样的现象,即使子类中定义了与父类完全同名的实例变量,这个实例变量依然不可能覆盖父类中定义的实例变量。

所以,当变量的编译时类型和运行时类型不同时,通过该变量访问它引用的对象的实例变量时,该实例变量的值由声明该变量的类型决定;但通过该变量调用它引用的对象的实例方法时,该方法行为将由它实际所引用的对象来决定。

通过代码及内存分配来说明:

// 声明并创建一个 Sub (继承 Mid 类)对象
Sub s = new Sub();            // 含有count 属性
System.out.println(s.count);  // 200

// 将 Sub 对象向上转型后赋为 Mid、Base 类型变量
Mid s2m = s;                   // 含有count 属性  
System.out.println(s2m.count); // 20

Base s2b = s;                  // 含有count 属性 
System.out.println(s2b.count); // 2

Sub 对象在内存中到底如何存储?

对于 Sub 对象,它有3个不同的 count 实例变量,意味着需用3块内存保存它们。

当创建 Sub 对象之后,对象在内存中的存储如下左图,但 Sub 类只定义了一个 count 实例变量,另外的两个是 Mid、Base 定义的,因此尝试按右图来看内存分配:

这里写图片描述

实际上,系统中只有一个 Sub 对象,而且这个对象持有3个count 实例变量,因为通过 s、s2m、s2b 变量访问 count 时,可以分别输出 200、20、2 这些值。

注意:系统内存中并不存在 Mid 和 Base 两个对象,程序内存中只有一个 Sub 对象,该对象不仅保存了在本类中定义的实例变量,还保存了其所有父类所定义的全部实例变量。

结论:当创建一个子类对象时,系统为该类中定义的实例变量分配内存,也为其父类中定义的所有实例变量分配内存,即使子类定义了与父类中同名的实例变量。

2.4 父、子类的类变量的内存分配机制

类变量属于类本身,在类初始化阶段完成初始化。没有实例变量的内存分配那么复杂。

2.5 Final 方法注意点

1)final 可修饰变量,被 final 修饰的变量被赋初始值后,不能对该变量重新赋值

2) final 可修饰方法,被 final 修饰的方法不能被重写

3) final 可修饰类,被 final 修饰的类不能派生子类,即不能被继承

private 和 final 同时修饰某个方法没有太大意义,但是 java 语法中是允许的。

final 修饰变量

被 final 修饰的实例变量必须显式指定初始值,而且只能在如下3个位置指定初始值:

1) 定义 final 实例变量时直接指定初始值

2) 在非静态初始化块中为 final 实例变量指定初始值

3) 在构造器中为 final 实例变量指定初始值

经过编译器处理,这3种方式都会被抽取到构造器中赋初始值。

对于 final 类变量,同样必须显式的指定初始值,而且只能在2个地方指定:

1) 定义 final 类变量时直接指定初始值

2) 在静态初始化块中为 final 类变量指定初始值

对于 final 变量,系统会将其当成“宏变量”处理,因为该变量的值在编译时就确定了。其本质上已经不再是变量,而是相当于一个直接量。

如果被赋的表达式只是基本的算术表达式或字符串连接运算,没有访问普通变量(final String s = str1 + “a”)、调用方法(final Stirng s = “0”+String.valueOf(9);),java 编译器同样会将这种 final 变量当成“宏变量”处理。



参考资料:
疯狂Java:突破程序员基本功的16课-对象与内存控制

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值