类的初始化:
第一次使用某个类,例如Person类,系统通常会在第一次使用Person类时加载这个类并初始化这个类。在类的准备阶段,系统将会为该类的类变量分配内存空间,并指定默认初始值。当Person类初始化完成后,系统内存中的存储如下所示:
当Person类初始化完成后,系统将在堆内存中为Person类分配一块内存区(当Person类初始化完成后,系统会为Person类创建一个类对象),在这块内存区里包含了保存eyeNum类变量的内存,并设置eyeNum的默认初始值:0。
系统如果接着创建了一个Person对象,并把这个Person对象赋给p1变量(栈内存中),Person对象里包含了名为name的实例变量,实例变量是在创建实例时分配内存空间并指定初始值的。当创建了第一个Person对象后,系统内存中的存储如下所示。
类变量eyeNum类变量并不属于Person对象,它是属于Person类的,所以创建第一个Person对象时并不需要为eyeNum类变量分配内存,系统只是为name实例变量分配了内存空间,并指定默认初始值:null。
如果接着执行var p2=new Person();代码创建第二个Person对象,此时因为Person类已经存在于堆内存中了,所以不再需要对Person类进行初始化。创建第二个Person对象与创建第一个Person对象并没有什么不同。
当程序执行p1.name=“张三”;代码时,将为p1的name实例变量赋 值,也就是让堆内存中的name指向"张三"字符串。执行完成后,两个Person对象在内存中的存储如下图所示。
name实例变量是属于单个Person实例的,因此修改第一个Person对象的name实例变量时仅仅与该对象有关,与Person类和其他Person对象没有任何关系。同样,修改第二个Person对象的name实例变量时,也与Person类和其他Person对象无关。
直到执行p1.eyeNum=2;代码时,此时通过Person对象来修改Person的类变量,Person对象根本没有保存eyeNum这个变量,通过p1访问的eyeNum类变量,其实还是Person类的eyeNum类变量。因此,此时修改的是Person类的eyeNum类变量。修改成功后,内存中的存储如图所示。
当通过p1来访问类变量时,实际上访问的是Person类的eyeNum类变量。事实上,所有的Person实例访问eyeNum类变量时都将访问到Person类的eyeNum类变量,也就是上图中灰色覆盖的区域。换句话说,不管通过哪个Person实例来访问eyeNum类变量,本质其实还是通过Person类来访问eyeNum类变量时,它们所访问的是同一块内存。基于这个理由,当程序需要访问 类变量时,尽量使用类作为主调,而不要使用对象作为主调,这样可以避免程序产生歧义,提高程序的可读性。
注意:
Java语法中允许通过对象来访问类成员(包括类变量、方法) 可以说完全是一个缺陷,聪明的开发者应该学会避开这个陷阱,而不是天天在这个陷阱旁边绕来绕去!
局部变量的初始化和内存中的运行机制:
局部变量定义后,必须经过显式初始化后才能使用,系统不会为局部变量执行初始化。这意味着定义局部变量后,系统并未为这个变量分配内存空间,直到等到程序为这个变量赋初始值时,系统才会为局部变量分配内存,并将初始值保存到这块内存中。与成员变量不同,局部变量不属于任何类或实例,因此它总是保存在其所在方法的栈内存中。如果局部变量是基本类型的变量,则直接把这个变量的值保存在该变量对应的内存中;如果局部变量是一个引用类型的变量,则这个变量里存放的是地址,通过该地址引用到该变量实际引用的对象或数组。栈内存中的变量无须系统垃圾回收,往往随方法或代码块的运行结束而结束。因此,局部变量的作用域是从初始化该变量开始,直到该方法或该代码块运行完成而结束。因为局部变量只保存基本类型的值或者对象的引用,因此局部变量所占的内存区通常比较小。