Java进阶---对象与内存控制(一)
Java提供了优秀的垃圾回收机制来回收已经分配的内存,但是这并不是意味着我们在编程程序的过程中,就可以肆无忌惮地挥霍Java程序的内存分配,这样做会造成程序的运行效率低下,直接影响程序的整体用户体验。
Java的内存管理机制分为内存分配和内存回收机制。内存分配机制在Java对象创建时为该对象在堆内存中分配指定的内存空间。内存回收指的是当该Java对象失去引用,变成垃圾时,内存回收机制会自动清理该对象,并回收该对象所占用的内存。
Java的内存回收机制由一条后台线程完成,本身也是非常消耗性能的,因此如果肆无忌惮地创建对象,让系统分配内存,那这些分配的内存都将由垃圾回收机制进行会回收,这样不但会使系统中可用内存减少,降低程序运行性能,而且使得垃圾回收的负担加重,降低程序的运行性能。
1、实例变量和类变量
Java程序的变量大体可分为成员变量和局部变量,局部变量可分为3类:
>形参:在方法签名中定义的局部变量,由方法调用者为其赋值,随方法的结束而消亡;
>方法内的局部变量:必须在方法内对其进行显示初始化,从初始化完成后开始生效,随方法的结束而消亡;
>代码块内的局部变量:必须在代码块内对其进行显示初始化,从初始化完成后开始生效,随代码块的结束而消亡。
由上可知,局部变量是指在方法签名、方法内和代码块内定义的变量。被存储在方法的栈内存中。
成员变量是指定义在类体内的变量,存储在类的栈中。如果定义该成员变量时没有使用static修饰,该成员变量又被称为非静态变量或实例变量,否则就被称为静态变量或类变量。
1)、实例变量和类变量的属性
类变量属于该类本身,而实例变量属于该类的实例。在同一个JVM内,每个类只对应一个Class对象,但每个类可以创建多个Java对象。
由于同一个JVM内每个类只对应一个Class对象,因此用一个JVM内的一个类的类变量只需一块内存空间;但对于实例变量而言,该类每创建一次实例,就需要为实例变量分配一块内存空间。也就是说程序中有几个实例,实例变量就需要几块内存空间。
通过下面的程序可以很好地理解实例变量和类变量:
class NoFive{ String name; int id; static int age; public void output(){ System.out.println("姓名:" + name + ",学号:" + id); } } public class test01 { public static void main(String[] args) { //类变量属于该类本身,只要该类初始化完成,程序即可使用类变量 NoFive.age = 22; //通过类访问类变量 System.out.println("NoFive的年龄age是:" + NoFive.age); NoFive noFive = new NoFive(); noFive.name = "小武灵灵"; noFive.id = 41009160; noFive.age = 22; //通过noFive访问NoFive类的age类变量 System.out.println("通过noFive变量访问age类变量:" + noFive.age); noFive.output(); NoFive noFive2 = new NoFive(); noFive2.name = "灵灵小武"; noFive2.id = 6190014; noFive2.age = 23; noFive2.output(); //通过noFive2修改NoFive类的age类变量 noFive2.age = 21; //分别通过noFive、noFive2和NoFive访问NoFive类的age类变量 System.out.println("通过noFive变量访问age类变量:" + noFive.age); System.out.println("通过noFive2变量访问age类变量:" + noFive2.age); System.out.println("通过NoFive类访问age类变量 :" + NoFive.age); } }
2)、实例变量的初始化时机
程序运行中,每次创建Java对象都会为实例变量分配内存空间,并对实例变量执行初始化。在代码中,程序可以在3个地方对实例变量执行初始化:
l 定义实例变量时指定初始值;
l 非静态初始化块中对实例变量指定初始值;
l 构造器中对实例变量指定的初始化。
其中前两种方式比最后一种方式更早执行,但前两种方式的执行顺序与它们在源程序中的排列顺序相同。例如:
class NoFive{ { name = “小武灵灵”; } String name = “灵灵小武”; public void output(){System.out.println(name);} }
上面程序output方法输出的name值应该为”灵灵小武”.
3)、类变量的初始化时机
在程序中,JVM对一个Java类只初始化一次,因此Java程序每运行一次,系统只为类变量分配一次内存空间,执行一次初始化。在代码中,程序可以在2个地方对类变量执行初始化:
l 定义类变量时指定初始值;
l 静态初始化块中对类变量指定初始值。
这两种方式的执行顺序与它们在源程序中的排列顺序相同。下面是一个例子:
public class test01 { //定义时指定初始值 static String name = "小武"; //通过静态初始化块为id类变量指定初始值 static{ System.out.println("静态初始化块"); id = 6190014; } static int id = 41009160; public static void main(String[] args) { System.out.println("name类变量的值:" + test01.name); System.out.println("id类变量的值:" + test01.id); } }
上面程序输出结果为“小武”和“41009160”。程序执行的顺序为:系统先为所有类变量分配内存空间,再按源代码中的排列顺序执行静态初始化块中所指定的初始值和定义类变量时所指定的初始值。
下面借用一个典型的例子来更好地理解类变量的初始化过程。首先定义了Price类,该Price类里有一个静态的initPrice变量,用于代表初始价格。每次创建Price实例时,系统会以initPrice为基础,减去当前打折价格(由discount参数代表)即得到该Price的currentPrice变量值。
class Price{ //类成员是Price实例 final static Price INSTANCE = new Price(2.8); //再定义一个类变量 static double initPrice = 20; //定义该Price的currentPrice实例变量 double currentPrice; public Price(double discount) { //根据静态变量计算实例变量 currentPrice = initPrice - discount; } } public class test01 { public static void main(String[] args) { //通过Price的INSTANCE访问currentPrice实例变量 System.out.println(Price.INSTANCE.currentPrice); //显示创建Price实例 Price p = new Price(2.8); //通过显示创建的Price实例访问currentPrice实例变量 System.out.println(p.currentPrice); } }
表面上看,程序输出两个Price的currentPrice都应该返回17.2(20-2.8),但实际上运行后输出结果为-2.8和17.2。
如果仅仅停留在代码表面来看这个问题,很难得到正确结果,下面将从内存角度来分析这个程序。
首先看PriceTest类中的主函数,执行System.out.println(Price.INSTANCE.currentPrice);时程序会第一次用到Price类,这个时候会对Price类进行初始化,初始化过程为:
A. 为Price的两个类变量(INSTANCE和initPrice)分配内存空间,此时INSTANCE和initPrice的值为默认值null和0.0;
B. 按照初始化代码(定义时指定初始值和初始化块中执行初始值)的排列顺序对类变量执行初始化:
a) 对INSTANCE执行初始化:创建Price实例用到Price类的带参数构造器,执行其中的currentPrice=initPrice-discount。因为此时的initPrice=0.0,所以currentPrice=-2.8;
b) 对initPrice执行初始化:initPrice=20;
C. 这个时候主函数main()中System.out.println(Price.INSTANCE.currentPrice);执行完毕,出书结果为-2.8。
D. 之后执行Price p = new Price(2.8);这行代码,会先执行ABC过程,此时currentPrice=-2.8,initPrice=20。然后调用Price类的带参构造器执行currentPrice = initPrice – discount,得到currentPrice=17.2。