Android 进阶 堆栈详解(续)

1. 堆栈简介

 

堆(heap)与栈(stack)都是Java用来在Ram中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。

 

 

 

2. 堆栈各自优缺点

 

   堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。

 

   的优势是,存取速度比堆要快,仅次于直接位于CPU中的寄存器。缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。另外,栈数据在多个线程或者多个栈之间是不可以共享的,但是在栈内部多个值相等的变量是可以指向一个地址的。

  

 

 

3.堆栈举例说明

 

java的基本类型(int long float double...)定义是通过诸如int a= 3; long b = 255L;的形式来定义的,称为自动变量。存的是字面值,不是类的实例,即不是类的引用,这里并没有类的存在。

 

如int a= 3; 这里的a是一个指向int类型的引用,指向3这个字面值。这些字面值的数据,由于大小可知,生存期可知(这些字面值固定定义在某个程序块里面,程序块退出后,字段值就消失了),出于追求速度的原因,就存在于栈中。

 

编译器先处理int a= 3;首先它会在栈中创建一个变量为a的内存空间,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理int b= 3;在创建完b的引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。

 

特别注意的是,这种字面值的引用与类对象的引用不同。假定两个类对象的引用同时指向一个对象,如果一个对象引用变量修改了这个对象的内部状态,那么另一个对象引用变量也即刻反映出这个变化。相反,通过字面值的引用来修改其值,不会导致另一个指向此字面值的引用的值也跟着改变的情况。如上例,我们定义完a与b的值后,再令a=4;那么,b不会等于4,还是等于3。在编译器内部,遇到a=4;时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。

 

另一种是包装类数据,(如Integer,String, Double等将相应的基本数据类型包装起来的类。这些类数据全部存在于【堆】中),Java用new()语句来显示地告诉编译器,在运行时才根据需要动态创建,因此比较灵活,但缺点是要占用更多的时间。 

 

String是一个特殊的包装类数据。即可以用String str = new String(“abc”);的形式来创建,也可以用Stringstr = “abc”;的形式来创建。前者是规范的类的创建过程,即在Java中,一切都是对象,而对象是类的实例,全部通过new()的形式来创建。

 

 

 

 

4.Java内存区域

Android 进阶 堆栈详解(续)_第1张图片

 

 

Java 内存区域中比较重要也是经常被提到的几部分是:程序计数器,栈(Stack),堆(Heap)方法区,它们都定义在被称作运行时数据区(Runing Data Area)的区域中。

其中程序计数器与栈(Stack)是随线程启动而生,线程结束而灭的,也就属于线程私有。而堆(Heap)和方法区是由JVM启动时创建切被所有线程共享的。

 

四部分分别介绍

 

1.程序计数器

 

我们知道所有程序代码在最底层都是要被转化为机器指令才能够被电脑识别并执行。当前程序不在是执行1+1或简单的打印字符串(实际上执行打印字符串相对于1+1已经是比较复杂的功能了,包含多个指令),而是要完成更加复杂的功能时,其所包含的指令也就更多,那么为了按逻辑顺序执行这些指令,就需要一个计数器来帮助一条条提取指令或指出下一条指令的位置。这里说的是CPU中的程序计数器的概念,换成JVM 程序计数器,其所承担的任务是基本一样的,只需要将指令换成字节码行号即可。

 

JVM的字节码解释器通过改变程序计数器的当前值,来提取下一条需要执行字节码的行号(既被编译为二进制的java代码)。所有的完成分支,循环,跳转,异常处理,线程恢复等的功能都是依赖它来完成的。

另外,由于Java的多线程特性(通过轮流切换并分配CPU的使用权,CPU在同一时间也只会执行一个线程中指令),为了在线程切换后能够恢复到正确位置,所以每个线程都有自己的独立的 程序计数器,这就是所谓的线程私有。

 

 

2.栈(Stack)

 

这里说的栈(Stack)是指虚拟机栈 (在jvm规范中,栈分为虚拟机栈也就是执行Java 方法时用到的,还有本地方法栈,两者区别在与本地方法栈为执行Native方法服务,Sun公司的jvm 称为HotSpot,它将虚拟机栈与本地方法栈合二唯一了)。

 

相比程序计数器,栈(Stack)的出镜率更高,也离开发人员更近。在栈(Stack)中包含了用于Java方法执行时所需要的局部变量表,操作数,动态链接以及方法出口等信息。由于操作数,我们常说的栈其实只是其中的局部变量表(至少我这个级别的coder是这样),这主要是因为操作数,动态链接以及方法出口等与我们关系并不大,这也就造成了局部变量表成为栈(Stack)的代名词。

之所以局部变量表被我们常常提到,是因为他的作用。局部变量表是用于存放编译期间就已知的基本数据类型以及对象引用(地址指针或对象句柄)。

 

 

 

以上代码中的 int a 与 User user 都是存放在局部变量表中。其中 a 是基本数据类型,它指向了一个值为1的内存空间,该内存空间的大小有类型 int 限定。user 则存放的是 new User() 这个对象在堆(Heap)中的内存位置。另外,以上代码之所以要声明一个方法体,是为了强调局部变量概念,如果 a 和 use 是全局变量,那么它们则在堆内存中属于对象,而如果他们是加上关键static的静态变量或者加上final的常量,则会在方法区的常量池中属于类。

栈是线程私有的,随线程结束而空间被回收。栈的大小是有限制的,当出现线程请求深度大于栈的最大深度则会出现StackOverFlow异常;而当Stack需要动态扩展而内存没有足够空间时则会出现OutOfMemoryError异常。

 

 

 

3.堆(Heap)

 

堆的唯一作用就是存放所有对象实例与数组的。它是线程共享的,是Jvm所有管理的内存中最大的一块,也是垃圾收集器GC管理的主要区域。

为了GC更好的进行内存回收,大部分GC使用了分代收集算法,它将堆划分了新生代与老年代。其中新生代又被划分为Eden空间、From Survivor空间和To Survivor空间(Eden是伊甸园,Survivor是幸存者,新创建的对象会优先在Eden中,当第一次GC清理时未被清理的对象则会移动到Survivor空间。这部分会在将来讨论GC时细说)。

堆的空间大小也是有限制的。我们可以通过参数-Xmx与-Xms来制定最大与最小空间。当超出-Xmx所设置的量时,则出现OutOfMemoryError:heapSpace 异常。

 

 

 

4.方法区

 

方法区用于存储虚拟机启动时加载的类信息,常量和静态变量。与堆一样,方法区也是线程共享的。与其他版本的JVM不同,SUN 公司的Hotspot 虚拟机将方法区*划分为永久代(Permanent Generation)。

大多数JAVA开发人员,尤其是会使用很多三方JAR的WEB开发人员都应该见过OutOfMemoryError:PermGen space 这个异常信息。出现原因便是方法区发生了内存溢出。虽然GC在方法区也会进行回收,但其目的是卸载类和回收常量池,大部分情况下,回收效果都不是很好。方法区的溢出在使用如Spring或Hibernate(这些框架会在运行时通过CGLib字节码技术来增强原有类),或者有大量JSP文件,又或者OSGI等会有大量动态生成的类被加载到方法区的应用中更容易出现。 在不能精简加载的jar和应用本身包含的类或常量,静态变量的情况下,可通过参数-XX:PermSize和-XX:MaxPermSize参数来设置方法区大小,来减少OutOfMemoryError:PermGen space异常的出现。

你可能感兴趣的:(Android,进阶)