Java对象和它的内存管理

java中的内存管理分为两个方面:

内存分配:指创建java对象时JVM为该对象在堆空间中所分配的内存空间。

内存回收:指java 对象失去引用,变成垃圾时,JVM的垃圾回收机制自动清理该对象,并回收该对象所占用的内存。

虽然JVM 内置了垃圾回收机制,但仍可能导致内存泄露、资源泄露等,所以我们不能肆无忌惮的创建对象。此外,垃圾回收机制是由一个后台线程完成,也是很消耗性能的。

1.实例变量和类变量

java程序中的变量,大体可以分为 成员变量 和 局部变量 。其中局部变量可分为如下三类:

形参 :在方法名中定义的变量,有方法调用者负责为其赋值,随着方法的结束而消亡。 
方法内局部变量 :在方法内定义的变量,必须在方法内对其进行初始化。它从初始化完成后开始生效,随着方法结束而消亡。 
代码块内局部变量 :在代码块内定义的变量,必须在代码块内对其显示初始化。从初始化完成后生效,随着代码块的结束而消亡。 
局部变量的作用时间很短暂,他们被存在栈内存中。

类体内定义的变量为成员变量。如果使用 static 修饰,则为静态变量或者类变量,否则成为非静态变量或者实例变量。

static:

他的作用是将实例成员编程类成员。只能修饰在类里定义的成员部分,包括变量、方法、内部内(枚举与接口)、初始化块。不能用于修饰外部类、局部变量、局部内部类。

使用static修饰的成员变量是类类型,属于类本身,没有修饰的属于实例变量,属于该类的实例。在同一个JVM中,每个类可以创建多个java对象。同一个JVM中每个类只对应一个Class对象,类变量只占一块内存空间,但是实例变量,每次创建便会分配一块内存空间。

class Person{
	String name;
	int age;
	static int eyeNum;
	public void info(){
		 System.out.println("我的名字是:" + name  + ", 我的年龄是:" + age);
	}
}
public class FieldTest {
public static void main(String[] args) {
	// 类变量属于该类本身,只要该类初始化完成,
    // 程序即可使用类变量。
	Person.eyeNum=2;
	System.out.println("Person的eyeNum属性:"
            +Person.eyeNum);// 通过Person类访问eyeNum类变量
	//创建第一个Person对象
	// 通过p访问Person类的eyeNum类变量
	Person p=new Person();
	p.name="hoo";
	p.age=33;
	System.out.println("通过p变量访问eyeNum类变量:"
            +p.eyeNum);
	p.info();
	// 创建第二个Person对象
	Person p2 = new Person();
    p2.name = "po";
    p2.age = 50;
    p2.info();
    p2.eyeNum=4;// 通过p2修改Person类的eyeNum类变量
 // 分别通过p、p2和Person访问Person类的eyeNum类变量
    System.out.println("通过p变量访问eyeNum类变量:"
            + p.eyeNum);
        System.out.println("通过p2变量访问eyeNum类变量:"
            + p2.eyeNum);
        System.out.println("通过Person类访问eyeNum类变量:"
            + Person.eyeNum);
}
}

  代码中的内存分配如下:

当Person类初始化完成,类变量也随之初始化完成,不管再创建多少个Person对象,系统都不再为 eyeNum 分配内存,但会为 name 和age 分配内存并初始化。当eyeNum值改变后,通过每个Person对象访问eyeNum的值都随之改变。

1).实例变量的初始化

对于实例变量,它属于java对象本身,每次程序创建java对象时都会为其分配内存空间,并初始化。

实例变量初始化地方:

定义实例化变量时; 
非静态初始化块中; 
构造器中。 
其中前两种比第三种更早执行,而前两种的执行顺序与他们在程序中的排列顺序相同。它们三种作用完全类似,经过编译后都会提取到构造器中执行,且位于所有语句之前,定义变量赋值和初始化块赋值的顺序与他们在源代码中一致。

可以使用 javap 命令查看java编译器的机制:

用法: javap <options> <classes> 其中, 可能的选项包括: -help  --help  -?        输出此用法消息-version        
版本信息-v  -verbose          
  输出附加信息-l                  
    输出行号和本地变量表-public      
           仅显示公共类和成员-protected    
          显示受保护的/公共类和成员-package          
      显示程序包/受保护的/公共类              
        和成员 (默认)
-p  -private        
    显示所有类和成员-c          
      对代码进行反汇编-s                
      输出内部类型签名-sysinfo          
      显示正在处理的类的                  
            系统信息 (路径, 大小, 日期, MD5 散列)
-constants            
  显示最终常量-classpath <path>  
     指定查找用户类文件的位置-cp <path>          
    指定查找用户类文件的位置-bootclasspath <path>  
 覆盖引导类文件的位置

2).类变量的初始化

类变量属于java 类本身,每次运行时才会初始化。

类变量的初始化地方:

定义类变量时初始化; 
静态代码块中初始化 
如下代码,表面上看输出的是:17.2,17.2;但是实际上输出的是:-2.8,17.2

class Price{
	// 类成员是Price实例
	final static Price INSTANCE=new Price(2.8);
	// 再定义一个类变量。
	static double initPrice=20;
	// 定义该Price的currentPrice实例变量
	double currenPrice;
	public Price(double discount){
		// 根据静态变量计算实例变量
		currenPrice=initPrice-discount;
	}
}
public class FieldTest {
public static void main(String[] args) {
	// 通过Price的INSTANCE访问currentPrice实例变量
	System.out.println(Price.INSTANCE.currenPrice);
	Price p=new Price(2.8);// 显式创建Price实例
	System.out.println(p.currenPrice);// 通过先前创建的Price实例访问currentPrice实例变量
}
}

  

第一次使用Price 时,程序对其进行初始化,可分为两个阶段:

(1)系统为类变量分配内存空间;

(2)按初始化代码顺序对变量进行初始化。

这里的运行结果为:-2.8,17.2

说明:初始化第一阶段,系统先为 INSTANCE,initPrice两个类变量分配内存空间,他们的默认值为null和0.0,接着第二阶段依次为他们赋值。对 INSTANCE 赋值时要调用 Price(2.8),创建Price实例,为currentPrice赋值,此时,还未对 initPrice 赋值,就是用他的默认值0,则 currentPrice 值为-2.8,接着程序再次将 initPrice 赋值为20,但对于 currentPrice 实例变量已经不起作用了。

2.父类构造器

java中,创建对象时,首先会依次调用每个父类的非静态初始化块、构造器(总是先从Object开始),然后再使用本类的非静态初始化块和构造器进行初始化。在调用父类时可以用 super 进行 显示调用 ,也可以 隐式调用 。

在子类调用父类构造器时,有以下几种场景:

子类构造器第一行代码是用 super() 进行显示调用父类构造器,则根据super传入的参数调用相应的构造器; 
子类构造器第一行代码是用 this() 进行显示调用本类中重载的构造器,则根据传入this的参数调用相应的构造器; 
子类构造器中没有this和super,则在执行子类构造器前,隐式调用父类无参构造器。 
注:super和this都是显示调用构造器,只能在构造器中使用,且必须在第一行,只能使用它们其中之一,最多只能调用一次。

一般情况下,子类对象可以访问父类的实例变量,但父类不能访问子类的,因为父类不知道它会被哪个子类继承,子类又会添加怎样的方法。但在极端的情况下,父类可以访问子类变量的情况,如下实例代码:

class Base{
	private int i=2;
	public Base() {
		//this:运行时是Driver类型,编译时是Base 类型,这里是Driver对象
		this.display();
	}
	public void display(){
		System.out.println(i);
	}
}
//继承Base的Derived子类
class Derived extends Base{
	private int i=22;
	public Derived() {
		i=222;
	}
	public void display(){
		System.out.println(i);
	}
}
public class FieldTest {
public static void main(String[] args) {
	// 创建Derived的构造器创建实例
	new Derived();
}
}

  

上面的代码执行后,输出的并不是2、22或者222,而是 0 。在调用Derived 的构造器前会隐式调用Base的无参构造器,初始化 i= 2,此时如果输出 this.i 则为2,它访问的是Base 类中的实例变量,但是当调用 this.display() 时,表现的为Driver对象的行为,对于driver对象,它的变量i还未赋初始值,仅仅是为其开辟了内存空间,其值为0。

在java 中,构造器负责实例变量的初始化(即,赋初始值),在执行构造器前,该对象内存空间已经被分配了,他们在内存中存的事其类型所对应的默认值。

在上面的代码中,出现了变量的编译时类型与运行时类型不同。通过该变量访问他所引用的对象的实例变量时,该实例变量的值由申明该变量的类型决定的,当通过该变量调用它所引用的实例对象的实例方法时,该方法将由它实际所引用的对象来决定

当子类重写父类方法时,也会出现父类调用之类方法的情形,如下具体代码,通过上面的则很容易理解。

class Animal{
	private String desc;
	public Animal() {
		this.desc=getDesc();
	}
	public String getDesc() {
		return "Animal";
	}
	public String toString() {
		return  desc ;
	}

}
public class Wolf extends Animal{
	private String name;
	private double weight;
public Wolf(String name, double weight) {
		this.name = name;
		this.weight = weight;
	}
//重写父类的getDesc()方法
@Override
public String getDesc() {

	return "Wolf[name=" + name + " , weight="
            + weight + "]";  //输出:Wolf[name=null , weight=0.0]
}
public static void main(String[] args) {
	System.out.println(new Wolf("狼", 2.9));
}
}

  

3.父子实例的内存控制

java中的继承,在处理成员变量和方法时是不同的。如果子类重写了父类的方法,则完全覆盖父类的方法,并将其其移到子类中,但如果是完全同名的实例变量,则不会覆盖,不会从父类中移到子类中。所以,对于一个引用类型的变量,如果访问他所引用对象的实例变量时,该实例变量的值取决于申明该变量的类型,而调用方法时,则取决于它实际引用对象的类型。

在继承中,内存中子类实例保存有父类的变量的实例。

class Base{
	int count=2;
}
class Mid extends Base{
	int count=20;
}
public class Sub extends Mid{
	int count = 200;
public static void main(String[] args) {
	    // 创建一个Sub对象
		Sub s=new Sub();
		// 将Sub对象向上转型后赋为Mid、Base类型的变量
		Mid s2m=s;
		Base s2b=s;
		// 分别通过3个变量来访问count实例变量
		System.out.println(s.count);    //输出:200
        System.out.println(s2m.count);    //输出:20
        System.out.println(s2b.count);    //输出:2
}
}

  内存中的示意图:

在内存中只有一个Sub对象,并没有Mid和Base对象,但存在3个count的实例变量。

子类中会隐藏父类的变量可以通过super来获取,对于类变量,也可以通过super来访问。

4.final 修饰符

final 的修饰范围:

修饰变量,被赋初始值后不可重新赋值; 
修饰方法 ,不能被重写; 
修饰类,不能派生出子类。 
对于final 类型的变量,初始化可以在:定义时、非静态代码块和构造器中;对于final 类型的类变量,初始化可以在:定义时和静态代码块中。

当final类型的变量定义时就指定初始值,那么该该变量本质上是一个“宏变量”,编译器会把用到该变量的地方直接用其值替换。

如果在内部内中使用局部变量,必须将其指定为final类型的。普通的变量作用域就是该方法,随着方法的执行结束,局部变量也随之消失,但内部类可能产生隐式的“闭包”,使局部变量脱离它所在的方法继续存在。内部内可能扩大局部变量的作用域,如果内部内中访问的局部变量没有适用final修饰,则可以随意修改它的值,这样将会引起混乱,所以编译器要求被内部访问的局部变量必须使用final 修饰。

时间: 2024-08-02 12:14:50

Java对象和它的内存管理的相关文章

java 数组及数组得内存管理总结

一:一维数组的声明及初始化 数组变量属于引用类型,他的元素可以是引用类型,也可以是基本类型. int[] a=new int[3]; a[0]=1; a[1]=2; a[2]=3; int[] b={1,2,3}; int c[]={1,2,3}; // int d[5]; 错误表达方式 a的内存图: b与c引用的内存图与a一样.b与c引用只是a引用得简写. 1 public class Test1 { 2 3 public static void main(String[] args) { 4

java Vamei快速教程22 内存管理和垃圾回收

作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! 整个教程中已经不时的出现一些内存管理和垃圾回收的相关知识.这里进行一个小小的总结. Java是在JVM所虚拟出的内存环境中运行的.内存分为栈(stack)和堆(heap)两部分.我们将分别考察这两个区域. 栈 栈的基本概念参考纸上谈兵: 栈 (stack).许多语言利用栈数据结构来记录函数调用的次序和相关变量(参考Linux从程序到进程). 在Java中,JVM中的栈记录了线程的

Java性能剖析]Sun JVM内存管理和垃圾回收

内存管理和垃圾回收是JVM非常关键的点,对Java性能的剖析而言,了解内存管理和垃圾回收的基本策略非常重要.本篇对Sun JVM 6.0的内存管理和垃圾回收做大概的描述. 1.内存管理      在程序运行过程当中,会创建大量的对象,这些对象,大部分是短周期的对象,小部分是长周期的对象,对于短周期的对象,需要频繁地进行垃圾回收以保证无用对象尽早被释放掉,对于长周期对象,则不需要频率垃圾回收以确保无谓地垃圾扫描检测.为解决这种矛盾,Sun JVM的内存管理采用分代的策略.      1)年轻代(Y

new一个JAVA对象的时候,内存是怎么分配的?

new 对象的时候 在内存中 建立一个 内存区域 就是堆内存 用来存放对象的属性,当new完对象把对象的地址赋给对象的引用变量 这个时候 又在内存中建立一个区域 叫栈内存 用来存储 引用变量 引用变量存储对象的地址, 当对象没有被任何引用变量 引用的时候 就变成了 垃圾会被java的垃圾清除机制清除掉

【深入理解JVM】:Java对象的创建、内存布局、访问定位

对象的创建 一个简单的创建对象语句Clazz instance = new Clazz();包含的主要过程包括了类加载检查.对象分配内存.并发处理.内存空间初始化.对象设置.执行ini方法等. 主要流程如下: 1. 类加载检查 JVM遇到一条new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载.解析和初始化过.如果没有,那必须先执行相应的类的加载过程. 2. 对象分配内存 对象所需内存的大小在类加载完成后便完全确定(对象内存布局),

Java程序员必备技能内存管理机——垃圾标记

正文 1.怎么找到存活对象? 通过上篇文章我们知道,JVM创建对象时会通过某种方式从内存中划分一块区域进行分配.那么当我们服务器源源不断的接收请求的时候,就会频繁的需要进行内存分配的操作,但是我们服务器的内存确是非常有限的呢!所以对不再使用的内存进行回收再利用就成了JVM肩负的重任了! 那么,摆在JVM面前的问题来了,怎么判断哪些内存不再使用了?怎么合理.高效的进行回收操作?既然要回收,那第一步就是要找到需要回收的对象! 1.1.引用计数法 实现思路:给对象添加一个引用计数器,每当有一个地方引用

[java小笔记] 关于数组内存管理的理解

数组是大多数编程语言都提供的一种复合结构,如果程序需要多个类型相同的变量时,就可以考虑定义一个数组,java语言的数组变量时引用类型的变量,因此具有java引用变量的特性.在使用数组之前必须对数组对象进行初始化,当所有的数组元素都被分配了合适的内存空间,并指定了初始值是,数组初始化完成. 数组初始化分为 1.静态初始化:初始化时由程序员显式指定每个数组元素的初始值,由系统决定数组的长度. 语法形式: int[] y = {1,2,3,4,5}; 2.动态初始化:初始化时程序员只指定数组的长度,由

深入理解Java虚拟机(二)、Java对象的创建,内存布局和访问定位

对象的创建: Object obj = new Object(); 常量池中是否有Ljava.lang.Object

Objective-C MRC多个对象相互引用的内存管理

在MRC环境下,假定CTRoom对象是CTPerson的一个成员变量,那么修改CTRoom对象时应注意,代码如下: - (void) setRoom:(CTRoom *) room { //需判断新旧值是否相等,一旦相等则[_room release]等价于[room release],那么[room retain]将抛出异常. if(_room != room){ [_room release]; //释放旧值 _room = [room retain]; //retain新值 } } - (