Java 对象及其内存控制

9/6/2015来源:Java教程人气:1479

java 对象及其内存控制

作者:禅楼望月(http://www.cnblogs.com/yaoyinglong)

更新:其实这里有好多的变戏法,只要你理解了他们在JVM的中的实现机制,就豁然开朗了。有时间我会把这些变戏法的东西说明的。


Java 向程序员许下了美好的承诺:无需关心内存的回收,Java提供了优秀的垃圾回收机制来回收已经分配的内存。

所以初学者往往会肆无忌惮的挥霍Java内存,从而导致Java程序的运行效率下降,主要坏处为:

  1. 不断分配内存使得系统中可用内存减少,从而降低程序运行性能;
  2. (更重要的)大量已分配内存的回收使得垃圾回收的负担加重,降低程序的运行性能。
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修饰呢?原因是:对于普通的局部变量,它的作用于就停留在该方法内,该方法结束后该局部变量就消失了;但是内部类则可能产生隐式的“闭包”,闭包将使得局部变量脱离了它所在的方法继续存在。