编程易筋经:内存解析之实例讲解

一、引言


        作为一名优秀的编程人员我们不能会敲代码,我们还必须要懂得那些代码在计算机中是如何一步步执行的。只有这样我们才能写出经久不衰,经得起考验,更优秀的代码,开发出更好的软件。我们都知道任何代码的执行都是在内存中进行的,其实也就是对内存单元的操作。不论任何语言,对内存的操作都是它们的核心,最根本的东西,所以这是我们学通任何一门语言必须掌握的东西。曾有人对内存解析在编程语言学习中的重要性,做过这样一个比喻:内存解析就好比武功秘籍中的易筋经,只要学会了它,再去学习任何武功,都会变的轻而易举。由此可见内存解析在编程语言中的重要性。下面我们来详细的解析一下。


       要想学懂内存解析,我们得先来看看计算机中的内存结构布局。一张图胜过千言万语,我们先来一张图Look,Look。





           这是一个Java程序代码在计算机内存中的完整的执行过程。从图中我们可以看到,计算机的内存分为四个区域,分别是堆(heap)区栈(stack)区数据区(data segment)代码区(code segment)。具体在Java语言中它们都是用来干嘛的呢?请仔细看这幅图。



二、内存块划分


1. 堆


         用来存放new出来的东西,如 new出来的类对象,数组,会放到这里。在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。引用变量就相当于是为数组或对象起的一个别名,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放。而数组和对象本身在堆中分配,即使程序运行到使用 new 产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放。数组和对象在没有引用变量指向它的时候,就会变为垃圾,不能再被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)。这也是 Java 比较占内存的原因。


2. 栈 


       用来存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中。比如,存放局部变量,方法的形参,临时的返回值等。当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。 


3.数据区


     用来存放静态变量,常量,字符串等。


4. 代码区


    当然用来存放程序代码的了,比如调用对象的方法体时,必须得到这个区域来调用。


       分析到这里相信大家,都已经明白了。而对于这张图的程序执行过程相信大家都能看得懂,在这里就不赘述了。下面我们通过举几个具体的实例来详细解析。



三、举例说明


1. 简单变量初始化


public class Test {
	public static void main(String []are) {

			int a = 5;
			double b = 5.0;
			String c = "内存分析";
						
			System.out.println(a + b);
			System.out.println(c);
		
							
	}
}

   我们来详细的分析一下这个小程序在内存中的执行情况。分析任何程序,我们必须先从Main方法开始分析。详细过程见下图 :


程序执行内存解析图 :



解析:先从Main方法开始分析)

    首先main方法在栈中建立一int类型内存单元 a 存放5,再建立一double型的内存单元b存放5.0,再建立一引用型内存单元c,在data segment区建立一字符串对象存放“内存分析”,然后c指向该字符串对象,再然后在栈中建立一临时内存单元来存放a+b的和,最后分别输出到屏幕。mian方法执行完,java的垃圾回收器把内存清理干净。


2. 类的实例化过程


class Point {
	double x;
	double y;
	double z;
	
	public Point(double _x, double _y, double _z) {
		x = _x;
		y = _y;
		z = _z;
	}
	
	public void setX(double _x) {
		x = _x;
	}
	
	public void setY(double _y) {
		y = _y;
	}
	
	public void setZ(double _z) {
		z = _z;
	}
	
	public double getDistance(Point p) {
		return (p.x-x)*(p.x-x) + (p.y-y)*(p.y-y) + (p.z-z)*(p.z-z);
	}
}

public class TestPoint {
	public static void main(String []arge) {
		Point p = new Point(1.0,2.0,3.0);
		Point p1 = new Point(0.0,0.0,0.0);
		System.out.println(p.getDistance(p1));
		
	}
}



程序执行内存解析图 :



解析:同样先从Main方法开始分析


1. 在栈中创建一Point类型引用变量 P 

2. 在堆中创建一Point类型的对象(创建对象的过程实质是调用类的构造方法的过程)

a. 在栈中分别创建Point构造方法的形参变量_x,_y,_z 并把实参传过来的值放入相应的形参内存单元中。

b. 在堆中创建一Point类型对象框架。

c. 初始化对象,把_x,_y,_z 的值分别拷贝到相应的对象成员变量x,y,z 的内存单元中。(构造方法调用完成_x,_y,_z这三个变量的内存    单元被释放。)

3. 引用变量P指向该对象。(当main方法执行完毕,p变量的内存单元被释放,指向的对象就会变为垃圾,由Java垃圾回收器回收。)


 创建P1对象同理.......



3. 方法调用


abstract class Person {
	private String name;
	
	Person(String name) {
		this.name = name;
	}
	abstract public void sleep();
	public String getName() {return name;}
}

class Student extends Person {
	private int sid;
	
	Student(String name,int sid) {
		super(name);
		this.sid = sid;
	}
	public void sleep() {
		System.out.println(this.getName() + " is sleeping");
	}
	public void study() {
		System.out.println(this.getName() + " is studying");
	}
	public void sing() {
		System.out.println(this.getName() + " is singing");
	}
	
}

public class Test {
	public static void main(String []are) {
		Person p = new Student("小美",123456);
		p.sleep();
		Student s =(Student)p;
		s.sing(); 
		s.study();
		
	}
}



程序执行内存解析图 :



解析:同样先从Main方法开始分析


1. 在栈中创建一Person类型引用变量 P 。


2. 在堆中创建一Student类型的对象(创建对象的过程实质是调用类的构造方法的过程,在这里Student类继承了Person类,经过了2次调用构造方法)

a.在栈中分别创建Student类构造方法的形参变量namesid并把实参传过来的值放入相应的形参内存单元中。

  因为形参name是一个字符串类型,给它赋的值是一字符串常量,和基本类型变量赋值有点不同,不能把小美这个字符串常量直接放到name内存单元中。而是把小美这个常量放到数据区(datasegment)中,让name变量指向它,即name变量中存放的是小美这个字符串常量的地址,这时name就成了"小美"这个字符串常量的引用。

b.在堆中创建一Student类型对象框架。

  因为Student类继承了Person类,所以在Student类实例化的对象中包含了一个Person类的对象,如上图所示。

c.初始化对象,首先把形参name指向的"小美"字符串常量的地址拷贝给Student对象的成员变量name(实质是拷贝给Person类对象的成员变量name,即指向"小美"字符串常量,然后把形参sid的值拷贝到Student类对象成员变量sid的内存单元中。(对象初始化完毕,形参name和sid的内存单元被释放)


3.  引用变量 P 指向 Student 类对象(实质是指向 Student 类对象中的 Person 对象)。

因为变量PPerson类型的引用,所以它能看到的只是Student类对象中的Person类对象部分,其他部分它是看不到的,无权限操作。所以,Student子类所特有的方法sing()和study(),P变量是无权访问的,必须把P对象向下转型为Student类型,才可以访问sing()和study()方法。


4. 调用 sleep ()方法。
因为sleep()是Person类中的放法,所以P对象可以直接到代码区(Codesegment)中调用sleep()的方法体执行。(方法的方法体是被存储在代码区的,只有被调用是执行,如上图)
5. 对象转型。
a.先在栈中创建一引用变量S,用来存储p对象转型后的对象地址。
b.把P对象由Person类型向下转为Student类型,其实就是扩大p对象的访问权限,使它指向整个Student类型的对象而非只Person类型。
c.让引用变量S指向Student类型对象。
6. 通过S引用变量到堆中找到创建的Student类对象,通过Student类对象再到代码区中找到 sing ()方法及 study ()方法顺序执行。

 


  • 9
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 36
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 36
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值