【摘要】本文针对在java教学中经常出现的一些疑难问题,进行了探讨。并且结合数据在内存中存储的方式给出了更为合理的解释,并在教学中进行了实验,收到了良好的教学效果。也为java教学提供了新的思路和方法,从而进一步推动了java教学的发展。
【关键字】java;堆内存;栈内存;
,java语言作为一种面向对象的编程语言由于其优越的平台无关性已经越来越受到企业的青睐。为了满足企业对于高效、实用代码开发的需求,我们学校在今年也开设了java语言课,但在教学过程中,其主要原因就是学生在理解java数据的表示形式以及数据结构时遇到了一定的困难。我在数据存储方式上对这些问题进行了探讨和研究。并最终解决了这些疑问。
一 问题的引入
日前,在java教学中遇到这样的一个例题:
问题一:
String str1 = "cdef";
String str2 = "cdef";
System.out.println(str1==str2);
答案为true
问题二:
String str1 =new String ("cdef");
String str2 =new String ("cdef");
System.out.println(str1==str2);
答案为false
问题三:
String s1 = "ja";
String s2 = "va";
String s3 = "java";
String s4 = s1 + s2;
System.out.println(s3 == s4);
答案为false
对于以上这几个问题,学生疑惑很大,初看起来问题1和问题2中的答案应该是一样,问题3中的答案也好像应该是true。但是,计算机运行后的结果显然给我们理解的大相径庭,问题出在哪呢。为此,我们不得不研究一下数据在内存的存储形式。
二 问题的解决过程
1、内存的分配策略
一般情况下,程序运行时的内存分配有三种方式:分别是静态的、栈式的和堆式的。静态存储分配是指在程序代码编译时就能确定每个数据目标在运行时刻的存储空间需求,因而在编译时就可以给他们分配固定的内存空间。这种分配策略要求程序代码中不允许有可变数据结构(比如可变数组)的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算准确的存储空间需求。
栈式存储分配也称做动态存储分配,是由一个栈来实现的,这和静态存储分配恰好相反。在栈式存储方案中,程序对数据区的需求在编译时是完全未知的,只有到运行的时候才能够知道,但是规定在运行中进入一个程序模块时,必须知道该程序模块所需的总的数据区大小才能够为其分配内存。这和数据结构所熟知的栈一样,栈式存储分配按照先进后出的原则进行分配。
而堆式存储分配则专门负责在程序模块编译时或运行时模块入口处都无法确定存储要求的数据结构的内存分配,比如可变长度串和对象实例。堆由大片的可利用存储空间或空闲存储空间组成,堆中的内存可以按照任意顺序进行空间分配和释放。那么在java中内存是怎么分配的呢?
2、java中内存分配方法
Java 把内存划分成两种:一种是栈内存,另一种是堆内存。
在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配,当在一段代码块定义一个变量时,Java 就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java 会自动释放掉为该变量分配的内存空间,该内存空间可以立即被另作它用。
堆内存用来存放由 new 创建的对象和数组,在堆中分配的内存,由 Java 虚拟机的自动垃圾回收器来管理。在堆中产生了一个数组或者对象之后,还可以在栈中定义一个特殊的变量,让栈中的这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或者对象,引用变量就相当于是为数组或者对象起的一个名称。引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放。而数组和对象本身在堆中分配,即使程序运行到使用 new 产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)。这也是 Java 比较占内存的原因。实际上,栈中的变量指向堆内存中的变量,这就是 Java 中的指针!
由此我们就可以很清楚的知道问题1中
Stringstr1="cdef";
Stringstr2="cdef";
System.out.println(str1==str2);//trueString str1 = "abc";
String str2 = "abc";
System.out.println(str1==str2); //true
可以看出str1和str2是指向同一个对象的.
而问题2中由于
Stringstr1=newString("abc");
Stringstr2=newString("abc");
System.out.println(str1==str2);//false
用new的方式是生成不同的对象,每一次生成一个.因此用第二种方式创建多个'abc'字符串,在内存中其实只存在一个对象而已.这种写法有利与节省内存空间.同时它可以在一定程度上提高程序的运行速度,因为JVM会自动根据栈中数据的实际情况来决定是否有必要创建新对象.而对于String str = new String("abc")的代码,则一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担.
2.4 java内存的优化
在Java中,除了基本的八种数据类型的数据结构,其它凡是用new初始化的都是在堆中分配内存(所以说Java的一切都是对象),这也是我们用java工作时感到java有点慢的原因之一。因此我们可以想到,上面的说法是基本成立的,堆不象栈是连续的空间,没有办法指望堆本身的内存分配能够象堆栈一样拥有很高的传送速度,因为,堆空间需要一个为其整理内存的工具,让使用者能在很短时间从堆中获取新的空间。
在java中,Garbage Collector(空间收集器)能在很大程度上解决问题。由此我们都知道GC是用来清除内存垃圾,为堆腾出空间供程序使用,另外同时GC也担负了另外一个重要的任务,就是要让Java中堆的内存分配和其他语言中堆栈的内存分配一样快。因为速度的问题是java最需要解决的问题。要达到这样的目的,就必须使堆的分配也能够做到详栈一样,有连续的存储空间不用自己操心去找空闲空间。这样,GC除了负责清除Garbage外,同时还要负责整理堆中的无关的对象,把它们转移到一个远离Garbage的纯净空间中连续的排列起来,就象堆栈中一样紧凑,这样Heap 指针就可以方便的指向连续空间的起始位置,或者指向一个连续的未分配的空间,为下一个需要分配内存的对象"指引方向"。因此可以这样说,无用空间的收集影响了对象的创建速度,在很大程度上影响了java中堆空间的利用。
那GC怎样在堆中找到所有存活的对象呢?前面说了,在建立一个对象时,在堆中分配实际建立这个对象的内存,而在堆栈中分配一个指向这个堆对象的引用(相当于c++中的指针),那么只要在堆栈(也有可能在静态存储区)找到这个引用,就可以由此找到其他对象。找到之后,GC将它们从一个堆的块中移到另外一个堆的块中,并 将它们一个挨一个的排列起来,就象我们上面说的那样,模拟出了一个栈的结构,但又不是先进后出的分配,而是可以任意分配的,在速度又像我们说熟知的栈一样快,这样不是很好嘛!