Java中的变量的内存分配以及传值机制

来源:互联网 发布:windows loader 病毒 编辑:程序博客网 时间:2024/05/18 03:12

本文记录了java中的变量的内存调用情况,以及传递机制,如有疑惑或错误之处可评论或邮箱联系博主:xuwang.me@gmail.com

Java中的变量类型在这里博主将其分为基本类型和引用类型以及一个特殊的变量类型String:

  • 基本类型:int、float、double、char、boolean,long,short,byte
  • 引用类型:数组、Map、List等
  • 特殊类型:String(特殊的引用类型)

要想弄清楚java中的变量在内存中是如何存储的,我们首先要弄清计算机中的内存情况。在计算机内存中主要来自四个地方:

  • heap segment(堆区):一种通用性的内存池,用于存放所有的JAVA对象,以及常量池。堆不同于栈的好处是:编译器不需要知道要从堆里分配多少存储区域,也不必知道存储的数据在堆里存活多长时间。堆中的对象的由垃圾回收器负责回收,因此大小和生命周期不需要确定。因此,在堆里分配存储有很大的灵活性。当你需要创建一个对象的时候,只需要new写一行简单的代码,当执行这行代码时,会自动在堆里进行存储分配。当然,为这种灵活性必须要付出相应的代码。用堆进行存储分配比用堆栈进行存储存储需要更多的时间。
  • stack segment(栈区):栈区主要存放Java程序运行时所需的局部变量、方法的参数、对象的引用以及中间运算结果等数据
  • codesegment(代码区):代码区主要存放Java的代码;
  • data segment(静态数据区):静态数据区主要存放静态变量及全局变量;

其在内存中分配情况如下图所示:
内存使用情况

注意:这里还有一个常量池的概念需要注意,常量池存在于堆中,堆区

这里我们主要关心栈,堆和常量池,对于栈和常量池中的对象可以共享,对于堆中的对象不可以共享。
栈中的数据大小和生命周期是可以确定的,当没有引用指向数据时,这个数据就会消失。堆中的对象的由垃圾回收器负责回收,因此大小和生命周期不需要确定,具有很大的灵活性。

1. 特殊的引用类型String

首先我们对特殊引用类型String做出讲解
String类具有以下特性:

  1. String类是final的,不可被继承。
  2. Java运行时会维护一个String Pool(String池),JavaDoc中将其翻译为“字符串缓冲区”。String池用来存放运行时中产生的各种字符串,并且池中的字符串的内容不重复。而一般对象不存在这个缓冲池,并且创建的对象仅仅存在于方法的堆区。String池中的String对象,可以被共享使用,因此它提高了效率。由于String类是final的,它的值一经创建就不可改变,因此我们不用担心String对象共享而带来程序的混乱。字符串池由String类维护,我们可以调用intern()方法来访问字符串池。
  3. 创建字符串的方式很多,归纳起来有三类:
    其一,使用new关键字创建字符串,比如String s1 = new String(“wang”);
    其二,直接指定。比如String s2 = “wang”;
    其三,使用串联生成新的字符串。比如String s3 = “wa” + “ng”;

对于字符串:其对象的引用都是存储在栈中的,如果是编译期已经创建好(直接用双引号定义的)的就存储在常量池中,如果是运行期(new出来的)才能确定的就存储在堆中。对于equals相等的字符串,在常量池中永远只有一份,在堆中有多份。
String 例子:

/** * Created by xuwang on 2017/9/24. */public class TestAddress {    public static void main(String args[]) {        String str1 = "hello world";        String str2 = new String("hello world");        String str3 = "hello world";        String str4 = new String("hello world");        System.out.println(str1==str2);        System.out.println(str1==str3);        System.out.println(str2==str4);    }}

程序运行结果:
这里写图片描述

在class文件中有一部分 来存储编译期间生成的字面常量以及符号引用,这部分叫做class文件常量池,在运行期间对应着方法区的运行时常量池。

因此在上述代码中,String str1 = “hello world”;和String str3 = “hello world”; 都在编译期间生成了 字面常量和符号引用,运行期间字面常量”hello world”被存储在运行时常量池(当然只保存了一份)。通过这种方式来将String对象跟引用绑定的话,JVM执行引擎会先在运行时常量池查找是否存在相同的字面常量,如果存在,则直接将引用指向已经存在的字面常量;否则在运行时常量池开辟一个空间来存储该字面常量,并将引用指向该字面常量。其引用存储在栈区。

众所周知,通过new关键字来生成对象是在堆区进行的,而在堆区进行对象生成的过程是不会去检测该对象是否已经存在的。因此通过new来创建对象,创建出的一定是不同的对象,即使字符串的内容是相同的。其引用存储在栈区。

2 . 基本类型以及特殊引用类型String的内存分配

2.1. String

由于String类型的特殊机制,其特性和基本类型几乎一样。我们通过一段代码来进行展示:

/** * Created by xuwang on 2017/9/24. */public class TestAddress {    public static void ma0in(String args[]) {        String str1 = "hello world";        String str2 = "hello world";        String str3 = new String("hello world");        System.out.println("str1: " + System.identityHashCode(str1));        System.out.println("str2: " + System.identityHashCode(str2));        System.out.println("hello world: " + System.identityHashCode("hello world"));        System.out.println("str3: " + System.identityHashCode(str3));        str1 = "hello";        str3 = "hello world";        String str4 = new String("hello world");        System.out.println("-------->>>>>changed");        System.out.println("str1: " + System.identityHashCode(str1));        System.out.println("str3: " + System.identityHashCode(str3));        System.out.println("str4: " + System.identityHashCode(str4));    }}

程序运行结果:
这里写图片描述

注:identityHashCode是根据内存地址获得的hash值

我们可以发现:通过“”号直接定义的字符串在编译期间生成了字面常量,运行期间字面常量”hello world”被存储在运行时常量池(当然只保存了一份)。之后再通过“”号直接定义“hello world”时,都会首先去常量池中寻找,若存在,则共享该对象,将引用直接指向该对象,故改变前的str1、str2、以及直接定义的“hello world”的Hash值都一样,均为856419764。
在改变后,将str1改变值为“hello”,其地址发生了变化,为1265094477;同时将str3赋值为“hello world”,这时,由于常量池中“hello world”对象还存在,str3的hash值和改变前的str1、str2、以及直接定义的“hello world”的Hash值一样,为856419764;对于str4,由于是new产生的,直接在堆区重新开辟一个存储空间,并将str4引用指向它。

2.2. 基本类型
在这里我们以double为例,通过下面一段程序进行说明:

import java.util.ArrayList;/** * Created by xuwang on 2017/9/27. */public class TestAdressDouble {    public static void main(String args[]){        Double doubleNum1 = 2.1;        Double doubleNum2 = 2.1;        System.out.println("doubleNum1: " + System.identityHashCode(doubleNum1));        System.out.println("doubleNum1: " + System.identityHashCode(doubleNum1));        System.out.println("----------->>>>changed: ");        doubleNum1 = 1.2;        System.out.println("doubleNum1: " + System.identityHashCode(doubleNum1));        ArrayList<Double> arrayList = new ArrayList<>();        arrayList.add(doubleNum1);        doubleNum1 = 1.3;        arrayList.add(doubleNum1);        System.out.println(arrayList.get(0) + ":  " + System.identityHashCode(arrayList.get(0)));        System.out.println(arrayList.get(1) + ":  " + System.identityHashCode(arrayList.get(1)));    }

程序运行结果:
这里写图片描述0

我们可以发现:double类型的变量是不可变的,且double类型在声明且定义变量时,会首先在栈中搜索是否有该变量的字面值存在,如果有则
指向这个地址,如果没有则重新开辟一块存储空间。doubleNum2在进行定义的过程中,首先在栈中进行搜索,发现doubleNum1所在区域的字面值和其相等,故doubleNum2将引用指向doubleNum1的地址。且doubleNum在后期进行改值,改为1.2的过程中,地址发生了变化,说明了double类型的变量是不可变的。
同时,在将doubleNum1加入List中时,对其进行改值,其在list中的值不会变化,说明了其实不可变的,两次的地址也不一样更说明了这一点。

原创粉丝点击