一、Java中内存分配策略:
在比较堆和栈的区别之前,我们先了解下Java的内存分配策略,按照编译原理的观点,程序运行时的内存分配有三种策略,分别是:静态的,栈式的,和堆式的。
(1)静态存储分配:是指在编译时,就能确定每个数据在运行时的存储空间,因而在编译时就可以给他们分配固定的内存空间。这种分配策略要求程序代码中不允许有可变数据结构的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算准确的存储空间需求。
(2)栈式存储分配:也可称为动态存储分配,是由一个类似于堆栈的运行栈来实现的。和静态存储分配相反,在栈式存储方案中,程序对数据区的需求在编译时是完全未知的,只有到运行的时候才能够知道,也就是规定在运行中进入一个程序模块时,必须知道该程序模块所需的数据区大小才能够为其分配内存。栈式存储分配按照先进后出的原则进行分配。
(3)堆式存储分配:静态存储分配要求在编译时能知道所有变量的存储要求,栈式存储分配要求在过程的入口处必须知道所有的存储要求,而堆式存储分配则专门负责在编译时或运行时模块入口处都无法确定存储要求的数据结构的内存分配,比如可变长度串和对象实例。堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放。
、JVM中的堆(heap)和栈(stack)的区别:
JVM是基于堆栈的虚拟机,堆和栈都是Java用来在内存中存放数据的地方。
1、功能和作用:
(1)栈,可以看成是方法的运行模型,所有方法的调用都是通过栈帧来进行的,JVM会为每个线程都分配一个栈区,JVM对栈只进行两种操作:以帧为单位的压栈和出栈操作。当线程进入一个Java方法函数的时候,就会在当前线程的栈里压入一个栈帧,用于保存当前线程的状态(参数、局部变量、中间计算过程和其他数据),当退出函数方法时,修改栈指针就可以把栈中的内容销毁。
(2)堆,唯一的目的就是用于存放对象实例,每个Java应用都唯一对应一个JVM实例,每个JVM实例都唯一对应一个堆,并由堆内存被应用所有的线程共享。
所以,从功能和作用来通俗的比较,堆主要用来存放对象的,栈主要是用来执行程序的。
2、性能与存储要求:
(1)栈的性能比堆要快,仅次于位于CPU中的寄存器。但是,在分配内存的时候,存放在栈中的数据大小与生存周期必须在编译时是确定的,缺乏灵活性。
(2)堆可以动态分配内存大小,编译器不必知道要从堆里分配多少存储空间,生存周期也不必事先告诉编译器,Java的垃圾收集器会自动收走这些不再使用的数据,因此可以得到更大的灵活性。但是,由于要在运行时动态分配内存和销毁对象时都需要占用时间,所以效率低。由于面向对象的多态性,堆内存分配是必不可少的,因为多态变量所需的存储空间只有在运行时创建了对象之后才能确定。当然,为达到这种灵活性,必然会付出一定的代价。
3、内存的分配与回收:
跟C/C++不同,Java中分配堆栈内存是由JVM自动分配和管理的。
Java中的数据类型有两种:一种是8个基本类型(即int, short, long, byte, float, double, boolean, char),一种是引用类型。
(1)函数中基本类型和对象的引用都是在栈内存中分配。当在一段代码块中定义一个变量时,由于这些变量大小可知,生存期可知,出于追求速度的原因,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间。
(2)对于引用类型:Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配。也就是说在建立一个对象时,从两个地方都分配内存,在堆中分配的内存实际用于建立这个对象,而在栈中分配的内存只是一个指向这个堆对象的引用而已。在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。
4、内存共享:
(1)栈数据的内存共享:
假设我们同时定义:int a = 3; int b = 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的值。
2)引用类型的内存共享:
String类型用String str = new String("abc")的形式来创建,也可以用String str = "abc"的形式来创建。前者是规范的类的创建过程,即在Java中,一切都是对象,而对象是类的实例,全部通过new()的形式来创建。那为什么在String str = "abc";中,并没有通过new()来创建实例,是不是违反了上述原则?其实没有:
关于String str = "abc"的内部工作。Java内部将此语句转化为以下几个步骤:
String str="abc"会先从字符串常量池(下文简称常量池)中查找,如果常量池中已经存在"abc",而"abc"必定指向堆区的某个String对象,那么直接将str指向这个String对象即可;如果常量池中不存在"abc",则在堆区new一个String对象,然后将"abc"放到常量池中,并将"abc"指向刚new好的String对象。
值得注意的是,一般String类中字符串值都是直接存值的。但像String str = "abc";这种场合下,其字符串值却是保存了一个指向存在栈中数据的引用! 为了更好地说明这个问题,我们可以通过以下的几个代码进行验证。
值得注意的是,一般String类中字符串值都是直接存值的。但像String str = "abc";这种场合下,其字符串值却是保存了一个指向存在栈中数据的引用! 为了更好地说明这个问题,我们可以通过以下的几个代码进行验证。
值得注意的是,一般String类中字符串值都是直接存值的。但像String str = "abc";这种场合下,其字符串值却是保存了一个指向存在栈中数据的引用! 为了更好地说明这个问题,我们可以通过以下的几个代码进行验证。
String str1 = "abc";
String str2 = "abc";
str1 = "bcd";
System.out.println(str1 + "," + str2); //bcd, abc
System.out.println(str1==str2); //false
这就是说,赋值的变化导致了类对象引用的变化,str1指向了另外一个新对象!而str2仍旧指向原来的对象。上例中,当我们将str1的值改为"bcd"时,JVM发现在栈中没有存放该值的地址,便开辟了这个地址,并创建了一个新的对象,其字符串的值指向这个地址。
事实上,String类被设计成为不可改变(immutable)的类。如果你要改变其值,可以,但JVM在运行时根据新值悄悄创建了一个新对象,然后将这个对象的地址返回给原来类的引用。这个创建过程虽说是完全自动进行的,但它毕竟占用了更多的时间。在对时间要求比较敏感的环境中,会带有一定的不良影响。 再修改原来代码:
String str1 = "abc";
String str2 = "abc";
str1 = "bcd";
String str3 = str1;
System.out.println(str3); //bcd
String str4 = "bcd";
System.out.println(str1 == str4); //true
str3 这个对象的引用直接指向str1所指向的对象(注意,str3并没有创建新对象)。当str1改完其值后,再创建一个String的引用str4,并指向因str1修改值而创建的新的对象。可以发现,这回str4也没有创建新的对象,从而再次实现栈中数据的共享。
我们再接着看以下的代码:
String str1 = new String("abc");
String str2 = "abc";
System.out.println(str1==str2); //false
创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。而在这同样是“abc”,按照上面说法应该只有一个对象,而为什么是两个对象呢。这里就是new String("abc")的不同之处了。
String str1 = "abc";
String str2 = new String("abc");
System.out.println(str1==str2); //false
new String("abc")相当于new String(String s1="abc"),即先要执行String s1="abc"(前面已经讲过了),然后再在堆区new一个String对象。所以String s=new String("abc")创建了1或2个对象,String s="abc"创建了0或1个对象。
创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。 以上两段代码说明,只要是用new()来新建对象的,都会在堆中创建,而且其字符串是单独存值的,即使与栈中的数据相同,也不会与栈中的数据共享。
综上:
(1)我们在使用诸如 String str = "abc" 的格式定义类时,对象可能并没有被创建。唯一可以肯定的是,指向 String类的引用被创建了,至于这个引用到底是否指向了一个新的对象,必须根据上下文来考虑,除非通过new()方法来显式地创建一个新的对象。
(2)使用 String str = "abc" 的方式,可以在一定程度上提高程序的运行速度,因为JVM会自动根据栈中数据的实际情况来决定是否有必要创建新对象,这是享元模式的思想。而对于String str = new String("abc");的代码,则一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担。
三、通过堆和栈的理解,思考为什么Java慢的原因?
在Java中,除了简单的基本数据类型,其它都是在堆中分配内存,这也是程序慢的原因之一。
如果没有GC,上面的说法就是成立的。堆不像栈是连续的空间,没法指望堆本身的内存分配能够象栈一样拥有传送带般的速度,因为谁会为你整理庞大的堆空间,让你几乎没有延迟的从堆中获取新的空间呢?
这个时候,GC站出来解决问题。GC清除内存垃圾,为堆腾出空间供程序使用,但GC同时也担负了另外一个重要的任务,就是要让Java中堆的内存分配和其他语言中堆栈的内存分配一样快。要达到这样的目的,就必须使堆的分配也能够做到像传送带一样,不用自己操心去找空闲空间。所以,GC除了负责清除Garbage外,还要负责整理堆中的对象,把它们转移到一个远离Garbage的纯净空间中无间隔的排列起来,就像堆栈中一样紧凑,这样Heap Pointer就可以方便的指向传送带的起始位置,或者说一个未使用的空间,为下一个需要分配内存的对象"指引方向"。因此可以这样说,垃圾收集影响了对象的创建速度。
那GC怎样在堆中找到所有存活的对象呢?前面说了,在建立一个对象时,在堆中分配实际建立这个对象的内存,而在栈中分配一个指向这个堆对象的引用,那么只要在栈(也有可能在静态存储区)找到这个引用,就可以跟踪到所有存活的对象。找到之后,GC将它们从堆的一个块中移到另一个块中,并将它们一个挨一个地排列起来,这样就可以在速度可以保证的情况下,可以任意分配的。
但是,GC的运行要占用一个线程,这本身就是一个降低程序运行性能的缺陷,更何况这个线程还要在堆中把内存翻来覆去的折腾。不仅如此,如上面所说,堆中存活的对象被搬移了位置,那么所有对这些对象的引用都要重新赋值,这些开销都会导致性能的降低。
附:有关String str="abc"和String str=new String("abc")原理相关
我们知道在java中,有8种基本类型(int short long byte double char boolean float),String类型并不是基本类型。在Java中提供了一个专门的String类,并且是被final声明的不可变的。
在jdk8以后,字符串常量池移到了堆空间中。
String str=new String("abc")的内存结构图
String str=new String("abc") 和String str2="abc"的区别
区别:String str2 = "abc"直接str1存储的是常量池中的地址值
String str = new String("abc") str存储的是在堆中的 new String()的地址值
new String()存放的是跟str2相同指向的常量池的地址值。
使用new String("abc")方式创建字符串对象时,会在堆内存中创建一个新的字符串对象,但同时也会在字符串常量池中检查是否已经存在相同值的字符串对象。如果字符串常量池中已经存在相同值的字符串对象,那么新创建的对象不会放入字符串常量池,而是直接指向已存在的对象;如果字符串常量池中不存在相同值的字符串对象,那么会在字符串常量池中创建一个新的字符串对象。
这种行为是为了节省内存,避免创建大量重复的字符串对象。通过这种机制,Java可以确保字符串常量池中不会存在重复的字符串对象,而且可以提高字符串对象的共享利用率,减少内存的占用。