一、引言
作为一名优秀的编程人员我们不仅只会敲代码,我们还必须需要懂得那些代码在计算机中的是如何一步步的执行的。只有这样我们才能写出经久不衰,经得起考验,更优秀的代码,开发出更好的软件。我们都知道任何代码的执行都是在内存中进行的,其实也就是对内存单元的操作。不论任何语言,对内存的操作都是它们的核心,最根本的东西,所以是我们学通任何一门语言必须会的东西。曾有人对内存解析在编程语言学习中的重要性,做过这样一个比喻:内存解析就好比武功秘籍中的易筋经,只要学会了它,再去学习任何武功,都会变的轻而易举。由此可见内存解析在编程语言中的重要性。下面我们来详细的解析一下。
要想学懂内存解析,我们得先来看看计算机中的内存结构布局。一张图胜过千言万语,我们先来一张图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类型的对象(创建对象的过程实质是调用类的构造方法的过程)
3. 引用变量P指向该对象。a. 在栈中分别创建Point构造方法的形参变量_x,_y,_z 并 把实参传过来的值放入相应的形参内存单元中。
b. 在堆中创建一Point类型对象框架。
c. 初始化对象,把_x,_y,_z 的值分别拷贝到相应的对象成员变量x,y,z 的内存单元中。
创建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类构造方法的形参变量name,sid并把实参传过来的值放入相应的形参内存单元中。
因为形参name是一个字符串类型,给它赋的值是一字符串常量,和基本类型变量赋值有点不同,不能把“小美”这个字符串常量直接放到name内存单元中。而是把“小美”这个常量放到数据区(datasegment)中,让name变量指向它,即name变量中存放的是”小美”这个字符串常量的地址,这时name就成了"小美"这个字符串常量的引用。
b.在堆中创建一Student类型对象框架。
因为Student类继承了Person类,所以在Student类实例化的对象中包含了一个Person类的对象,如上图所示。
3. 引用变量P指向Student类对象(实质是指向Student类对象中的Person对象)。c.初始化对象,首先把形参name指向的"小美"字符串常量的地址拷贝给Student对象的成员变量name(实质是拷贝给Person类对象的成员变量name,即指向"小美"字符串常量,然后把形参sid的值拷贝到Student类对象成员变量sid的内存单元中。
4. 调用sleep()方法。因为变量P是Person类型的引用,所以它能看到的只是Student类对象中的Person类对象部分,其他部分它是看不到的,无权限操作。所以,Student子类所特有的方法sing()和study(),P变量是无权访问的,必须把P对象向下转型为Student类型,才可以访问sing()和study()方法。
因为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()方法顺序执行。