运行时内存模型,分为线程私有和共享数据区两大类,其中线程私有的数据区包含程序计数器、虚拟机栈、本地方法区,所有线程共享的数据区包含Java堆、方法区,在方法区内有一个常量池。java运行时的内存模型图,如下:
从图中,可知内存分为线程私有和共享两大类:
(1)线程私有区,包含以下3类:
程序计数器,记录正在执行的虚拟机字节码的地址;
虚拟机栈:方法执行的内存区,每个方法执行时会在虚拟机栈中创建栈帧;
本地方法栈:虚拟机的Native方法执行的内存区;
(2)线程共享区,包含以下2类
Java堆:对象分配内存的区域;
方法区:存放类信息、常量、静态变量、编译器编译后的代码等数据;
常量池:存放编译器生成的各种字面量和符号引用,是方法区的一部分。
楼主提到的Java栈,一般而言是指图中的虚拟机栈,在代码中的方法调用过程中,往往需要从一个方法跳转到另一个方法,执行完再返回,那么在跳转之前需要在当前方法的基本信息压入栈中保存再跳转。
三、关于寄存器的问题
对于java最常用的虚拟机,sun公司提供的hotspot虚拟机,是基于栈的虚拟机而对于android的虚拟机,则采用google提供的dalvik,art两种虚拟机,在android 5.0以后便默认采用art虚拟机,这是基于寄存器的虚拟机。 楼主问的是jvm(即java vm),这是基于栈的虚拟机。那么关于虚拟机栈,这块内存的内容,我们再进一步详细分析,如下图:
可以看到,在虚拟机栈有一帧帧的 栈帧组成,而栈帧包含局部变量表,操作栈等子项,那么线程在运行的时候,代码在运行时,是通过程序计数器不断执行下一条指令。真正指令运算等操作时通过控制操作栈的操作数入栈和出栈,将操作数在局部变量表和操作栈之间转移。
看看字节码就明白了源代码:
1 public class test{
2 public static void main(String[] args){
3 int i = 234
4 i = i++
5 System.out.println(i)
6 }
7 }
编译之后的字节码:
public static void main(java.lang.String[])
0: sipush 234
3: istore_1
4: iload_1
5: iinc1, 1
8: istore_1
9: getstatic #2//Field java/lang/System.out:Ljava/io/PrintStream
12: iload_1
13: invokevirtual #3//Method java/io/PrintStream.println:(I)V
16: return
我们分析一下字节码
0: sipush 234 //将常量234压入操作数栈
3: istore_1 //将操作数栈中的数值存到局部变量区1号位置上
4: iload_1 //将局部变量区1号位置上的值压入操作数栈
5: iinc1, 1 //将局部变量区1号位置上的数值增1
8: istore_1 //将操作数栈中的数值存到局部变量区1号位置上
现在很明显了:它先把i压入栈,然后把i(在原来的位置上)加1,然后又把栈上的旧值写回i。这就导致了i被原来的旧值给覆盖了,所以值没有变化。
类似的,你后一个问题可以用同样的方法去分析,我就不写了
一般来说内存泄漏有两种情况。一种情况,在堆中的分配的内存,在没有将其释放掉的时候,就将所有能访问这块内存的方式都删掉(如指针重新赋值);另一种情况则是在内存对象明明已经不需要的时候,还仍然保留着这块内存和它的访问方式(引用)。第一种情况,在Java中已经由于垃圾回收机制的引入,得到了很好的解决。所以,Java中的内存泄漏,主要指的是第二种情况。可能光说概念太抽象了,大家可以看一下这样的例子:
1 Vector v=new Vector(10)
2 for (int i=1i<100i++){
3 Object o=new Object()
4 v.add(o)
5 o=null
6 }
在这个例子中,代码栈中存在Vector对象的引用v和Object对象的引用o。在For循环中,我们不断的生成新的对象,然后将其添加到Vector对象中,之后将o引用置空。问题是当o引用被置空后,如果发生GC,我们创建的Object对象是否能够被GC回收呢?答案是否定的。因为,GC在跟踪代码栈中的引用时,会发现v引用,而继续往下跟踪,就会发现v引用指向的内存空间中又存在指向Object对象的引用。也就是说尽管o引用已经被置空,但是Object对象仍然存在其他的引用,是可以被访问到的,所以GC无法将其释放掉。如果在此循环之后,Object对象对程序已经没有任何作用,那么我们就认为此Java程序发生了内存泄漏。
尽管对于C/C++中的内存泄露情况来说,Java内存泄露导致的破坏性小,除了少数情况会出现程序崩溃的情况外,大多数情况下程序仍然能正常运行。但是,在移动设备对于内存和CPU都有较严格的限制的情况下,Java的内存溢出会导致程序效率低下、占用大量不需要的内存等问题。这将导致整个机器性能变差,严重的也会引起抛出OutOfMemoryError,导致程序崩溃。
一般情况下内存泄漏的避免
在不涉及复杂数据结构的一般情况下,Java的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度。我们有时也将其称为“对象游离”。
例如:
1 public class FileSearch{
2
3 private byte[] content
4 private File mFile
5
6 public FileSearch(File file){
7 mFile = file
8 }
9
10 public boolean hasString(String str){
11 int size = getFileSize(mFile)
12 content = new byte[size]
13 loadFile(mFile, content)
14
15 String s = new String(content)
16 return s.contains(str)
17 }
18 }
在这段代码中,FileSearch类中有一个函数hasString,用来判断文档中是否含有指定的字符串。流程是先将mFile加载到内存中,然后进行判断。但是,这里的问题是,将content声明为了实例变量,而不是本地变量。于是,在此函数返回之后,内存中仍然存在整个文件的数据。而很明显,这些数据我们后续是不再需要的,这就造成了内存的无故浪费。
要避免这种情况下的内存泄露,要求我们以C/C++的内存管理思维来管理自己分配的内存。第一,是在声明对象引用之前,明确内存对象的有效作用域。在一个函数内有效的内存对象,应该声明为local变量,与类实例生命周期相同的要声明为实例变量……以此类推。第二,在内存对象不再需要时,记得手动将其引用置空。
复杂数据结构中的内存泄露问题
在实际的项目中,我们经常用到一些较为复杂的数据结构用于缓存程序运行过程中需要的数据信息。有时,由于数据结构过于复杂,或者我们存在一些特殊的需求(例如,在内存允许的情况下,尽可能多的缓存信息来提高程序的运行速度等情况),我们很难对数据结构中数据的生命周期作出明确的界定。这个时候,我们可以使用Java中一种特殊的机制来达到防止内存泄露的目的。
之前我们介绍过,Java的GC机制是建立在跟踪内存的引用机制上的。而在此之前,我们所使用的引用都只是定义一个“Object o”这样形式的。事实上,这只是Java引用机制中的一种默认情况,除此之外,还有其他的一些引用方式。通过使用这些特殊的引用机制,配合GC机制,就可以达到一些我们需要的效果。