Java 对象及其内存控制

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

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修饰呢?
原因是:对于普通的局部变量,它的作用于就停留在该方法内,该方法结束后该局部变量就消失了;但是内部类则可能产生隐式的“闭包”,闭包将使得局部变量脱离了它所在的方法继续存在。

时间: 2024-08-27 20:01:38

Java 对象及其内存控制的相关文章

JAVA对象与内存控制

1.1 实例变量和类变量 成员变量和局部变量: 局部变量分为三大类: 1)形参:在方法签名中定义的局部变量,由方法调用者为其赋值,随方法的结束而消亡. 2)方法内的局部变量:在方法内定义的局部变量,随方法的结束而消亡. 3)代码块内的局部变量:在代码块内定义的局部变量,随代码块的结束而消亡. 局部变量的作用时间很短,它们都是被存在方法的栈内存中.

Java对象的内存布局

Java对象的内存布局:对象头(Header),实例数据(Instance Data),对齐填充(Padding):另外:不同的环境结果可能有差异,我所在的环境是HotSpot虚拟机,64位Windows. 对象头 对象头在32位系统上占用8bytes,64位系统上占用16bytes. System.out.println("sizeOf(new Object()) = " + sizeOf(new Object())); sizeOf(new Object()) = 16 实例数据

java对象的内存布局(一):计算java对象占用的内存空间以及java object layout工具的使用

最近在学习java对象内存布局方面的一些知识,主要是想知道一个java对象到底占用多少内存空间,以及java对象在内存中到底是什么样子的.c/c++中的sizeof运算符能够方便地告诉我们一个变量占用的内存空间,但是在java中却没有直接提供这种机制.如果想获取java对象占用的内存大小,可以利用java的Instrumentation机制.java.lang.instrument.Instrumentation这个接口提供了getObjectSize(Object objectToSize),

Java对象的内存布局以及对象的访问定位

先来看看Java对象在内存中的布局 一 Java对象的内存布局 在HotSpot虚拟机中,对象在内存中的布局分为3个区域 对象头(Header) Mark Word(在32bit和64bit虚拟机上长度分别为32bit和64bit)存储对象自身的运行时数据,包括哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时 间戳等 类型指针 即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例.但是并不是所有类型虚拟机实现都必须在对象数据上保留类型指针,如果对象是一

记一次对java对象在内存中的分析

java 对象 占内存大小 计算方式 及 常用类型的占用 HotSpot的对齐方式为8字节对齐 ----计算公式:(对象头 + 实例数据 + padding) % 8等于0且0 <= padding < 8 Hotspot 机 中 普通对象32位 对象头 占 8个字节 引用类型 占 4字节64位 对象头 占 16个字节 引用类型 占 8字节 64位中 空对象数组 对象头 占 24 增加一个长度 增加 一个引用类型的长度 64位中是 8空的基本数据类型数组 对象头 占 24 增加一个长度 增加一

java对象占用内存大小计算方式

案例一: User public class User { } UserSizeTest public class UserSizeTest { static final Runtime runTime=Runtime.getRuntime(); public static void main(String[] args) { final int count = 100000; User[] us=new User[count]; long heap1 = 0; for (int i = -1;

Java对象在内存中的存储

Java对象在内存中的存储分3块区域 1.对象头(Header) 2.实例数据(Instance Data) 3.对齐填充(Padding) 一.对象头 哈希码.GC分代年龄.锁状态标志.线程持有的锁.偏向线程ID.偏向时间戳 类型指针 二.实例数据 各种类型的字段(父类继承下来的.子类自身定义的) 相同宽度的字段会被分配到一起 三.对齐填充 没有特别含义,仅仅起着占位符的作用(8字节的整数倍)

Java对象分配内存时的内存图

摘自高琪老师的JAVA教程. Java对象分配内存时的内存图

new Java对象占用内存分析

最近在读<深入理解Java虚拟机>,对Java对象的内存布局有了进一步的认识,于是脑子里自然而然就有一个很普通的问题,就是一个Java对象到底占用多大内存? 在网上搜到了一篇博客讲的非常好:http://yueyemaitian.iteye.com/blog/2033046,里面提供的这个类也非常实用: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35