java面试题JavaJava

Java面试相关(二)--堆和栈

2016-08-04  本文已影响3465人  androidjp

引言:看了网上一些作品,没有特别清晰的一个结构,所以,这里本人整理一下Java的堆栈相关知识。Java 中的堆和栈 Java把内存划分成两种:一种是栈内存,一种是堆内存。至于“方法区”(静态存储区),可以理解为:主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。总的来说:堆和栈针对非静态数据,而方法区针对静态数据。

一、堆内存和栈内存

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

补充: 在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。
引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。
引用变量是普通变量,定义时在栈中分配内存,引用变量在程序运行到作用域外释放。而数组&对象本身在堆中分配,即使程序运行到使用new产生数组和对象的语句所在地代码块之外,数组和对象本身占用的堆内存也不会被释放,<u>数组和对象在没有引用变量指向它的时候,才变成垃圾,不能再被使用,但是仍然占着内存,在随后的一个不确定的时间被垃圾回收器释放掉。这个也是java比较占内存的主要原因</u>。
这里可以理解为:String s1 = new String("abc");这里面: "abc"表示栈中的一个存储空间中的一个数据,new String("abc")表示存在于堆中的一个对象,这个对象的值为‘abc’,String s1则表示栈中定义的一个取了new String("abc")在堆中的首地址的一个特殊变量,也就是:s1成了引用变量,相当于一个别名。

二、Java数据存储和JVM内存分区

就速度来说,有如下关系:
寄存器 < 堆栈 < 堆 < 其他

补充:大家也许听说过“数据区”或者“运行时数据区”这个名词,这里,我们说JVM是驱动Java程序运行的基础,而它有三个分区:堆、栈、方法区,实际上,JVM的三个方法区就是包含于 JVM的运行时数据区中的三大块。于是,“数据区”与上述的分区的关系就明朗了。

三、Java的两种数据类型:

  1. 基本类型(primitive types), 共有8种,即int, short, long, byte, float, double, boolean, char(注意,
    并没有string的基本类型)。这种类型的定义是通过诸如int a = 3; long b = 255L;的形式来定义的,称为自动变量。【自动变量存的是字面值,不是类的实例(即不是类的引用),这里并没有类的存在,如int a=3;这里a只是指向int类型(不是类)的引用,指向字面值3,此时,由于这些字面值的数据大小可知并且生存期可知(他们在程序内某个固定代码块中,代码块退出,他们就消失),为了追求速度,于是存在中】
  2. 包装类,如Integer, String, Double等将相应的基本数据类型包装起来的类。这些类数据全部存在于中,Java用new()语句来显式地告诉编译器,在运行时才根据需要动态创建,因此比较灵活,但缺点是要占用更多的时间。

四、代码示例说明

用一些例子来理解哪些数据属于栈内存,哪些数据属于堆内存:

示例一:对于字面值和字面值引用
//.....
int a = 1; //a属于字面值 1 的引用
//.....
int b = 1;//b属于字面值 1 的引用

执行上面的代码是这样的一个过程:

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

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

示例二:由new String()开始解释

代码一:

String str1 = "abc"; 
String str2 = "abc"; 
System.out.println(str1==str2); //true 

代码二:

String str1 =new String ("abc"); 
String str2 =new String ("abc"); 
System.out.println(str1==str2); // false 

从代码一和代码二分析:
String是一个特殊的包装类数据。可以用:
①String str = new String("abc");
②String str = "abc";
两种的形式来创建,第一种是用new()来新建对象的,它会在存放于堆中。每调用一次就会创建一个新的对象。
而第二种是先在栈中创建一个对String类的对象引用变量str,然后查找栈中有没有存放"abc",如果没有,则将"abc"存放进栈,并令str指向”abc”,如果已经有”abc” 则直接令str指向“abc”。

代码三:

String s1 = "ja"; 
String s2 = "va"; 
String s3 = "java"; 
String s4 = s1 + s2; 
System.out.println(s3 == s4);//false 
System.out.println(s3.equals(s4));//true 

从代码三分析:
比较类里面的数值是否相等时,用equals()方法;当测试两个包装类的引用是否指向同一个对象时,用==。

示例三:程序代码运行过程分析
public class Test {///运行时,JVM把TestB的类信息全部放入方法区
    public static void main(String[] args){//main方法本身是静态方法,放入方法区
        ///obj1 和 obj2都是对象引用,所以放到栈区,这个‘new Sample("xxx")’是自定义对象应该放到堆区
        Obj obj1 = new Obj("A");
        Obj obj2 = new Obj("A");
        obj1.printName();
        obj2.printName();
        //  这里,两个实例中的size成员都是int(基本类型),所以,这个“3”最终存在于栈区(而不是堆区),并供obj1和obj2共用。
        obj1.size = 3;
        obj2.size = 3;
        int A = 4;
        int B = 4;
        System.out.println(obj1.getName()==obj2.getName());
        System.out.println(obj1 == obj2);
        System.out.println(A == B);
    }
}
/**
 * 自定义类:Obj
 * 运行时,JVM把Obj的类信息全部放入方法区
 */
class Obj{
    private String name;//new出一个Obj实例后,‘name’这个引用放入了栈区,而给‘name’的赋值的是字面值"A"而不是一个newString("A"),则这个"A"会存在栈中,所以,obj1.name和obj2.name共用这个栈中的"A"
    public int size;//虽然size是基本数据类型的对象,但是它是跟随这Obj类初始化加载的,所以上面obj1和obj2两个对象的size指向的地址不同,由于此时赋予给他们的“3”在两个不同存储位置。
    public Obj(String name) {
        this.name = name;
    }

    public String getName(){
        return this.name;
    }

    public void printName(){///printName方法本身放入方法区中
        System.out.println(this.name);
    }
}

整体图解:



输出截图:


看过本人上一篇文章Java面试相关(一)-- Java类加载全过程的朋友应该比较清楚JVM在启动程序执行代码时对类的加载过程,这里可以简单看看上述代码的注释说明类的加载时机。这里重点配合上述代码注释说说过程中的数据存储分区情况:

五、扩展:Java内存分配策略

按照编译原理的观点,程序运行时的内存分配有三种策略,分别是静态的,栈式的,和堆式的。
静态存储分配是指在编译时就能确定每个数据目标在运行时刻的存储空间需求,因而在编译时就可以给他们分配固定的内存空间.这种分配策略要求程序代码中不允许有可变数据结构(比如可变数组)的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算准确的存储空间需求。
栈式存储分配也可称为动态存储分配,是由一个类似于堆栈的运行栈来实现的.和静态存储分配相反,在栈式存储方案中,程序对数据区的需求在编译时是完全未知的,只有到运行的时候才能够知道,但是规定在运行中进入一个程序模块时,必须知道该程序模块所需的数据区大小才能够为其分配内存.和我们在数据结构所熟知的栈一样,栈式存储分配按照先进后出的原则进行分配。
 静态存储分配要求在编译时能知道所有变量的存储要求,栈式存储分配要求在过程的入口处必须知道所有的存储要求,而堆式存储分配则专门负责在编译时或运行时模块入口处都无法确定存储要求的数据结构的内存分配,比如可变长度串和对象实例.堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放。

参考文章


https://github.com/GeniusVJR/LearningNotes/blob/master/Part1/Android/Android%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E6%80%BB%E7%BB%93.md

上一篇 下一篇

猜你喜欢

热点阅读