java 对象与内存

java内存管理分为两个方面:内存分配和内存回收

不能随意挥霍java的内存分配,会造成java程序的运行效率低下:

不断分配内存使得系统中可用内存减少,从而降低程序运行性能。

大量已经分配内存的回收使得垃圾回收的负担加重,降低程序的运行性能。

一、. 实例变量和类变量的内存分配

java程序的变量大致分为成员变量和局部变量。局部变量分为3类:

形参:在方法中定义的局部变量,由方法调用者负责为其赋值,随方法的结束而消失

方法内的局部变量: 在方法内定义的局部变量,必须在方法内对其进行显示初始化。这种变量从初始化完成后开始生效,随方法的结束而消失。

代码块内的局部变量:在代码块中定义的局部变量,必须在代码块内对其进行显示初始化。这种类型的局部变量从初始化完成后开始生效,随代码块的结束而消失。

局部变量的作用时间很短,它们都被存储在方法的栈内存中。

类体内定义的变量是成员变量,如果定义成员变量没有使用static修饰,该成员变量又被称为实例变量,非静态变量;加了static的成员变量是类变量,静态变量。

static只能修饰类里定义的成员部分,包括成员变量、方法、内部类、初始化快、内部枚举类。

int    num1=num2-1;

int     num2=3;

这会出现非法前向引用,因为第一行中需要用到num2的值来对num1进行赋值,但num2在第二行才进行赋值。

int    num1=num2-1;

static int    num2=3;

这样就不会出错,因为num1是实例变量,而num2是类变量,类变量的初始化时机总是处于实例变量的初始化时机之前,所以正确。

使用static修饰的成员变量是类变量,属于该类本身;没有使用static的变量是实例变量,属于该类的实例。由于同一个JVM内每个类只对应一个class对象,因此同一个JVM内的一个类的类变量只需一块内存空间;但对于实例变量而言,该类每创一次实例,就需要为实例变量分配一块内存空间。也就是说,程序中有几个实例,实例变量就需要几块内存空间。

class person

{

string name;

int age;

static int eyenum;//类变量

}

public class fieldtest

{

public static void main (string [ ] args)

{

person.eyenum=2; //对类变量初始化,通过person

person p=new person();

p.name="猪八戒“;

p.age=300;

person  p2=new person();

p2.name="孙悟空”;

p2.age=500;

p2.eyenum=3;  }

}

java分配内存的情况如下:

当程序创建person的对象p时,系统不再为eyenum类变量分配内存空间,而只为person对象的实例变量执行初始化,因为实例变量才是属于person实例,而类变量是属于person类本身的。

当person类的eyenum类变量被修改之后,程序通过p,p2,person类访问eyenum类变量都将输出3.

2.实例变量的初始化时机

程序可以在3个地方对实例变量执行初始化:

定义实例变量时指定初始值;

非静态初始化块中对实例变量指定初始值;

构造器中对实例变量指定初始化;

前两种比第三种更早执行。但第一、二种的执行顺序与它们在源程序中的排列顺序相同。

class cat

{

string name;

int    age;

public cat (string  name, int age)

{      this.name=name;

this.age=age;

}

{   weight=2.0;}

double weight=2.3;

}

public class fieldtest

{

public static void main (string [ ] args)

{

cat  cat1=new cat("kitty",2);

cat cat2=new cat("niko",3);

}

}

程序执行的时候会先执行cat中的非静态初始化块(因为该类中没有类变量),再调用该cat类的构造器来初始化该cat的实例,weight的值是2.3而不是2。这是因为,初始化中指定初始值,定义weight时指定初始值,都属于对该实例变量执行的初始化操作,它们的执行顺序与它们在源代码中的排列顺序相同。所以,初始化块中对weight所指定的初始化的值每次都将被2.3所覆盖。

总结:定义实例变量时定义的初始值、初始化块中为实例变量指定的初始值、构造器中为实例变量指定的初始值,三者的作用完全类似,都用于对实例变量指定初始值。经过编译器处理之后,它们对应的赋值语句都被合并到构造器中,在合并过程中,构造器中的语句都在前面那些语句的后面,而前两种情况的执行顺序,和它们源代码中的顺序一样。

3. 类变量的初始化时机

程序可以在2个地方对类变量进行初始化:

定义类变量时指定初始值;

静态初始化块中对类变量指定初始值;

程序执行的时候,首先为所有类变量分配内存空间,再按源代码中的排列顺序执行静态初始化块中所指定的初始值和定义类变量时所指定的初始值。

二、父类构造器

当创建任何java对象时,程序总是会先依次调用每个父类非静态初始化块、父类构造器执行初始化,最后才调用本类的非静态初始化块、构造器执行初始化。

class creature

{

{

System.out.println("cresature的非静态初始化块");

}

public creature()

{

System.out.println("cresature的无参构造器");

}

public creature(String name)

{

this();

System.out.println("cresature的带有name参数的构造器,name参数:"+name);

}

}

class animal extends creature

{

{

System.out.println("animal的非静态初始化块");

}

public animal(String name)

{

super(name);

System.out.println("animal的带参数nmae的构造器");

}

public animal(String name, int age)

{

this(name);

System.out.println("animale的带两个参数的构造器");

}

}

class wolf extends animal

{

{

System.out.println("wolf的非静态初始化块");

}

public wolf()

{

super("灰太狼",3);

System.out.println("wolf的无参的构造器");

}

public wolf(double weight)

{

this();

System.out.println("wolf的带weight参数的构造器"+weight);

}

}

public class inittest {

public static void main(String[] args)

{

new wolf(5.6);

}

}

上面程序定义了三个类,都包含非静态初始化、构造器成分。其执行结果如下

cresature的非静态初始化块

cresature的无参构造器

cresature的带有name参数的构造器,name参数:灰太狼

animal的非静态初始化块

animal的带参数nmae的构造器

animale的带两个参数的构造器

wolf的非静态初始化块

wolf的无参的构造器

wolf的带weight参数的构造器5.6

只要在程序创建java对象的时候,系统总是先调用最顶层父类的初始化操作,包括初始化块和构造器,然后依次向下调用所以父类的初始化操作,最后执行本类的初始化操作返回本类的实例。至于调用父类的那儿一个构造器初始化,分为几种情况:

子类构造器执行体的第一行代码使用super()显示调用父类构造器

子类构造器执行体的第一行代码使用this()显示调用本类的重载构造器,系统根据this调用里传入的实参列表来确定本类的另一个构造器

子类中既没有super也没有this,则隐式调用父类中无参数构造器

super和this都只能在构造器中使用,且都必须在构造器中的第一行,一个构造器中只能使用其中一个,最多使用一次。

2.2 访问子类对象的实例变量

子类的方法可以访问父类的实例变量,这是因为子类继承了父类。父类的方法不能访问子类的实例变量,因为父类无法知道会被哪儿个子类继承。但是在一些极端的情况下,可能出现父类访问子类变量的情况。

构造器只是负责对java对象实例变量执行初始化,即赋值,在执行构造器代码之前,该对象所占的内存已经被分配下来,这些内存里值都默认是空值。当this在构造器中时,this代表正在初始化的java对象,

当变量的编译时类型和运行时类型不同时,通过该变量访问它引用的对象的实例变量时,该实例变量的值由声明该变量的类型决定;但通过该变量调用它引用的对象的实例方法,该方法行为将由它实际所引用的对象来决定。

2.3调用被子类重新的方法

一般情况下,父类不能调用子类的方法,但是有一种特殊情况,但子类方法重新父类的方法之后,父类表面上只是调用自己的方法,但实际是调用了子类的方法。

class animal

{

private String desc;

public animal()

{

this.desc=getDesc();

}

public String getDesc()

{

return"animal";       //2

}

public String toString()

{

return desc;

}

}

class wolf extends animal

{

private String name;

private double weight;

public wolf(String name, double weight)

{

this.name=name;             //3

this.weight=weight;

}

@Override

public String getDesc()

{

return "wolf[name="+name+",weight="+weight+"]";

}

}

public class inittest {

public static void main(String[] args)

{

System.out.println(new wolf("灰太狼",32.3));//1

}

}

上面的程序输出的结果是wolf[name=null,weight=0.0]  

子类重写了父类的getDesc()函数,程序执行的时候,首先是执行1部分程序,也就是调用构造器来初始化wolf对象,但是会先调用父类的无参数构造器先初始化,也就是第二部分的程序,先于3被执行,父类的无参构造器中的函数被子类重新,于是调用子类的方法,于是输出这样的结果。执行完2部分的程序之后,会执行3部分的程序,wolf对象会对name, weight进行赋值,但是desc实例变量的值是wolf[name=null,weight=0.0]

为了避免这种不希望看到的结果,应该避免在animal类 的构造器中调用被子类重写的方法,因此改为

class animal

{

private String desc;

public String getDesc()

{

return"animal";       //2

}

public String toString()

{

return  getDesc();

}

}

结果就会是你期待的wolf[name=灰太狼,weight=32.3]

2.3 父子实例的内存控制

class base

{

int count=2;

public void display()

{

System.out.println(this.count);

}

}

class derived extends base

{

int count=20;

@Override

public void display()

{

System.out.println(this.count);

}

}

public class inittest {

public static void main(String[] args)

{

base b=new base();
//1

System.out.println(b.count);

b.display();

derived d= new derived();

System.out.println(d.count);

d.display();

base bd=new derived();

System.out.println(bd.count);

bd.display();

base db2=d;

System.out.println(db2.count);

}

}

输出的结果为

2

2 // 输出结果毫无疑问,声明一个base变量b

20

20//  也是没有问题的

2

20//  声明了一个base变量,但是却将derived对象赋给变量。此时系统会自动进行向上转型来保证程序正确。直接通过db访问count实例变量,输出的将是base(声明时的类型)对象的count实例变量的值;如果通过db来调用display()方法,该方法将表现出derived(运行时的类型)对象的行为方法。

2// 此时db2和d都指向同一个对象,但是db2是base变量,所以通过它访问实例变量时,显示的是声明类型的行为,所以是2而不是20

但不管是d 变量,还是db,db2,只要它们实际指向一个derived对象,不管声明它们时用什么类型,当通过这些变量调用方法时,方法总是表现出derived对象的行为,而访问实例变量时,表现的是声明变量类型的行为

如果在子类重写了父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中。对于实例变量则不存在这样的现象,即使子类中有与父类相同的实例变量,这个实例变量也不会覆盖父类中的。因此继承成员变量和继承方法之间存在这样的差别。

2.3 父子类的类变量

由于类变量属于类本身,而实例变量属于对象,所以类变量不会那么的复杂。java允许通过对象来访问类变量,如果在子类中直接访问父类中定义的count类变量,可以直接使用父类名.count或者super.count,建议用前一种。

2.4 final修饰符

final可以修饰变量,被final修饰的变量被赋值初始值之后,不能对它重新赋值

final可以修饰方法,被final修饰的方法不能被重写

final可以修饰类,被修饰的类不能派生子类

对于普通实例变量,java程序可以对它执行默认的初始化,也就是将实例 变量的值指定为默认的初始值0或null;但对于final实例变量,则必须由程序员显示指定初始值。

只能在3个地方对final变量指定初始值;

定义final实例变量时指定初始值;

在非静态初始化块中为final实例变量指定初始值;

在构造器中为final实例变量指定的初始值。

对于final类变量而言,同样必须显示指定初始值,而且只能在2处初始化:

定义final类变量时

在静态初始化块中为final类变量赋值

对于一个final变量,不管它是类变量、实例变量,还是局部变量,只要定义该变量时使用了final修饰符修饰,并在定义该final类变量时指定了初始值,而且该初始值在编译时就确定下来,那么这个final变量本质上已经不再是变量,而是一个直接量。

final修饰符的一个重要用途就是定义“宏变量"。如果被赋的表达式只是基本的算术表达式或字符串连接运算,没有访问普通变量,调用方法,会将其看成宏变量处理。

但对于final实例变量而言只有在定义该变量时指定初始值才会有”宏变量“的效果。

public class inittest {

public static void main(String[] args)

{

String s1="赖yuppies";

String s2="赖"+"yuppies";

System.out.println(s1==s2);

String str1="赖";

String str2="yuppies";

String s3=str1+str2;

System.out.println(s1==s3);

}

}

输出的结果为true  false

s1是一个直接量,s2的值是两个字符串直接量进行连接运算,由于编译器可以在编译阶段就确定s2的值,所以系统会让s2直接指向字符串池中缓存中的”赖yuppies"字符串。str1,str2是两个变量,在编译的时候,s3的值不能确定,所以是false.

可以将str1,str2修饰为final类型。

如果程序需要在匿名内部类中使用局部变量,那么这个局部变量必须使用final修饰符修饰。

java要求所有被内部类访问的局部变量都使用final修饰的原因:对于局部变量而言,它的作用域停留在该方法中,当执行方法结束时,该局部变量也随之消失;但内部类则可能产生隐式的“闭包”,闭包将使得变量脱离它所在的方法继续存在。

时间: 2024-07-28 21:16:35

java 对象与内存的相关文章

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

源码分析:Java对象的内存分配

Java对象的分配,根据其过程,将其分为快速分配和慢速分配两种形式,其中快速分配使用无锁的指针碰撞技术在新生代的Eden区上进行分配,而慢速分配根据堆的实现方式.GC的实现方式.代的实现方式不同而具有不同的分配调用层次. 下面就以bytecodeInterpreter解释器对于new指令的解释出发,分析实例对象的内存分配过程: 一.快速分配 1.实例的创建首先需要知道该类型是否被加载和正确解析,根据字节码所指定的CONSTANT_Class_info常量池索引,获取对象的类型信息并调用is_un

java中的进程与线程及java对象的内存结构【转】

原文地址:http://rainforc.iteye.com/blog/2039501 1.实现线程的三种方式: 使用内核线程实现 内核线程(Kernel Thread, KLT)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操作调度器对线程进行调度,并负责将线程的任务映射到各个处理器上.程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),轻量级进程就是我们通常意义上所讲的线程,由于每个轻量