首页 > 代码库 > Java 对象及其内存控制
Java 对象及其内存控制
作者:禅楼望月(http://www.cnblogs.com/yaoyinglong)
Java 向程序员许下了美好的承诺:无需关心内存的回收,Java提供了优秀的垃圾回收机制来回收已经分配的内存。
所以初学者往往会肆无忌惮的挥霍Java内存,从而导致Java程序的运行效率下降,主要坏处为:
- 不断分配内存使得系统中可用内存减少,从而降低程序运行性能;
- (更重要的)大量已分配内存的回收使得垃圾回收的负担加重,降低程序的运行性能。
1 前向应用
这说明Java中定义实例成员变量时,必须采用合法的前向引用。同样两个类成员变量也必须采用合法的前向引用:
但是,如果一个是实例成员变量,一个是类成员变量,则实例成员变量总是可以引用类成员变量:
这是因为:类成员变量初始化时机总是在实例成员变量初始化时机之前。
2、静态成员可实例成员
使用static修饰的静态变量属于类本身,而实例变量数据类的实例。在同一JVM中,每个类只对应一个Class对象,因此同一JVM内的一个类的类变量只需一块内存空间,但每个类可以创建多个Java对象,因此JVM必须为每个Java对象的实例变量分配一块内存空间。
Java允许通过类来访问类成员变量,也允许类实例访问类成员变量,(Java这样设计是不合理的)
但是java设计者,却在类实例访问类成员变量时,底层依然转换为类来访问类成员变量。怎么证明呢?
通过反编译来看看:
JVM在底层使用someTh对象所对应的引用类型来调用静态成员,这就给程序员造成了一定的错觉,以为调用的是自己对象的东西,但是改变静态成员的值,在其他的对象的中会体现出来,这个很危险:
在一个类实例中修改了类成员变量的值,在另一个类实例中却体现出来了。
3 实例变量的初始化时机
从语法角度看,我们可以在如下3个地方对实例变量执行初始化:
①定义实例变量是指定初始值
②非静态初始化块中对实例变量指定初始值
③构造器中对实例变量指定初始值。
其中①和②比③更早执行,①和②那个更早执行,就看那个在代码中出现的更早。如:
由此可见类实例变量只能放在构造器中初始化,但是作为程序员编程时,可以放在定义处,也可以放在非静态块中,但是结果都是一样的,JVM会把它们抽取出来放在构造函数中。
4 类变量初始化时机
从语法角度看,可以在如下两个地方对类变量初始化:
①定义类变量时初始化;
②静态初始化块中对类变量指定初始值。
这两种方式的执行顺序和它们在源代码中出现的顺序相同:
由此可见,类变量只能在静态块中被初始化,但是作为程序员编程来说,可以放在定义处也可以放在静态快中,结果都是一样的,JVM会把它们收取出来都放进静态快中。
5 父类构造器
看如下代码:
这里便引发了一个疑问:在这里Sub类还没有被创建(因为调用display的时候父类的构造函数还没有走完,怎么会走子类的构造函数),怎么能调用它的方法呢?
诶!难道类实例不是由构造器创建的吗?
很多书籍中都是这样说的:类实例是由构造器创建。
其实,这句话是完全错误的。实际的情况是构造器只是负责对Java对象实例变量执行初始化(即赋初始值),在执行构造器代码之前,该对象所占的内存已经被分配下来了。这些内存里的值都是各个类型的默认值。
所以上面代码在执行new Sub();的时候系统已经为Sub对象分配了内存空间(两块内存空间,一块用于存放Sub的i另一块用于存放Base的i(这一块内存,子类和父类共用,改变任何一个另一个会跟着动),原因是子类不能完全覆盖父类的成员变量)
注意:
对象是由new关键字创建的,在执行new……的时候,一个Java对象已经建成了,只是它的变量还没有初始化,构造函数的功能就是对这些变量进行初始化。没有运行完构造函数Java对象的方法是可以被调用的,因为它和一般Java对象没有任何的区别。
再来看一段代码:
是否会感觉到this指代有点混乱呢?
但是从打印出来的结果来看,this确实指代的是Sub,但是我们也知道,当this在构造器中this指的是正在被初始化Java对象。怎么理解呢?从源代码看,此时this位于Base构造器中,但是这些代码实际放在Sub()构造器内执行,是Sub()构造器隐式调用了Base()构造器的代码。由此可见,this指的是Sub而不是Base。现在问题又来了,既然this指的是Sub,那么,为什么System.out.println("I come from "+this.getClass()+" -->"+this.i);执行结果却为2?这是因为,虽然,this实际指向的是Sub对象,但是当在Base构造器中时,它的编译类型为Base。所以会输出2.
因此我们可以得出如下结论:
当变量(a)编译时类型和运行是类型不同时,通过该变量(a)访问它引用的对象的实例变量时,该实例变量的值是由声明该变量(a)的类型决定。但当通过该变量调用它引用的对象的实例方法时,该方法行为将由该变量(a)实际所引用的对象来决定。
6 父子实例的内存控制
由上图可知:
1、变量d2b和d实际指向同一个对象,但是访问他们的实例变量时却输出不同的值,这表明d2b和d变量所指向的java对象中包含了两块内存,更别存放着值为2的count实例变量和值为20的count实例变量。
2、不管d、db、d2b,只要它们指向一个Sub对象,不管声明它们使用什么类型,当通过这些变量来调用时,方法的行为总是表现出它们实际类型的行为。但如果通过这些变量来访问它们所指对象的实例变量,这些实例变量的值总是表现出声明这些变量所用的类型的行为。由此可见Java继承在处理成员变量和方法时,是有区别的。
但是,还是可以通过super来调用父类中被覆盖的方法。
我们再来看一下这段代码:
//父类public class Base { private int x=10; public int getX() { return x; } public void setX(int x) { this.x = x; } }
//子类public class Sub extends Base { public Sub() { this.setX(20); } }
//测试 public static void main(String[] args) { Base b=new Base(); System.out.println("我是父类:"+b.getX()); //-->10 Base base=new Sub(); System.out.println("我是父类:"+base.getX()); //-->20 Sub s=new Sub(); System.out.println("我是子类:"+s.getX()); //-->20 System.out.println("我是父类:"+base.getX()); //-->20 }
用javap工具查看:
由此可见子类继承了父类的实例变量,内存中值为父类中的变量申请了空间,并没有为子类中该变量开辟内存空间。有人可能说你这里的实例变量x是private,其实即是public也是一样的,不信的话可以试试。
那么,我们在子类中调用setX方法其实,设置的是父类中的实例变量x。因为这个方法是从父类继承过来的。由此也可以得出父类中一般不要设置静态全局变量,这样会有线程安全的问题。
所以在子类中使用super的意思是,使用自己对象里面保存的从父类继承下来的那个方法。
由此可见,super本身并没有引用任何对象,它只能算作一个标记。它的作用仅限于在子类中(不是子类的对象)调用在父类中定义的,被隐藏了的实例变量,或者在子类中定义的,被覆盖的方法。
注意:虽然说这是父类中的方法和变量。其实和父类没有一点关系了。只是在调用上有点区别,其他的和类自己的方法没什么区别。
7 父子类的类变量
记住:Java允许通过实例对象来调用类的静态变量
其他的和实例变量一样。
8 final
final修饰的变量
final修饰的变量必须显示的指定初始值(普通变量系统会为其设置默认值),而且只能在以下3个地方制定初始值:
对于一个final变量而言,不管它是类变量、实例变量还是局部变量,只要该变量被final修饰,并且被赋予的初始值(必须的),那么该在类编译的时候就被确定了,那么,这个final变量就不再是变量了,而是相当于一个直接量。
内部类中的局部变量
如果程序需要在内部类中使用局部变量,那么这个局部变量必须由final修饰。
但是为什么内部类中要访问的局部变量都必须使用final修饰呢?
原因是:对于普通的局部变量,它的作用于就停留在该方法内,该方法结束后该局部变量就消失了;但是内部类则可能产生隐式的“闭包”,闭包将使得局部变量脱离了它所在的方法继续存在。
Java 对象及其内存控制