Java 内存分配详解(五)

Java 内存分配详解(五)

栈(stack):

主要保存基本类型(或者叫内置类型)(char、byte、short、int、long、float、double、boolean)和对象的引用,数据在栈内可以共享,速度仅次于寄存器(register),快于堆。 

堆(heap):

用于存储对象和数组

常量池:

常量池的划分
Class 文件常量池
运行时常量池
字符串常量池


1. Class 文件常量池
Class 文件常量池指的是编译生成的 class 字节码文件,其结构中有一项是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

AppMain类的加载过程:系统收到了我们发出的指令,启动了一个Java虚拟机进程,这个进程首先从classpath中找到AppMain.class文件,读取这个文件中的二进制数据,然后把Appmain类的类信息存放到运行时数据区的方法区中。

什么是字面量和符号引用?

字面量是指字符串字面量和声明为 final 的(基本数据类型)常量值,这些字符串字面量除了类中所有双引号括起来的字符串(包括方法体内的),还包括所有用到的类名、方法的名字和这些类与方法的字符串描述、字段(成员变量)的名称和描述符;方法内的常量值由栈分配,所以并不算是字面量。
符号引用,就是指指向 UTF-8 表中向这些字面量的引用,包括类和接口的全限定名(包括包路径的完整名)、字段的名称和描述符、方法的名称和描述符。只不过是以一组符号来描述所引用的目标,和内存并无关,所以称为符号引用,直接指向内存中某一地址的引用称为直接引用。


2. 运行时常量池(Run-time Constant Pool)
运行时常量池是方法区的一部分,是一块内存区域。Class 文件常量池将在类加载后进入方法区的运行时常量池中存放。

一个类加载到 JVM 中后对应一个运行时常量池,运行时常量池相对于 Class 文件常量池来说具备动态性,Class 文件常量只是一个静态存储结构,里面的引用都是符号引用。而运行时常量池可以在运行期间将符号引用解析为直接引用。

运行时常量池就是用来索引和查找字段和方法名称和描述符的。给定任意一个方法或字段的索引,通过这个索引最终可得到该方法或字段所属的类型信息和名称及描述符信息。

3. 字符串常量池(Interned Strings)
在 JDK1.6(含)之前也是方法区的一部分,并且其中存放的是字符串的对象。
在 JDK1.7(含)之后,是在堆内存之中,存储的是字符串对象的引用,字符串实例是在堆中。
字符串常量池是全局的,JVM 中独此一份,因此也称为全局字符串常量池。

Integer i1 = 127;    常量池里面没有的话就将127放入常量池
Integer i2 = 127;    首先去常量池里面查找是否有127 ,结果查找到了,就不用再创建了

String s1 = new String("aaa");  首先会查找常量池,如果没有"aaa"则在常量池里面创建"aaa",然后拷贝一份到堆里面,然后再在栈里面保存 s1这个引用指向堆里面的"aaa"对象。
String s2 = new String("aaa");  首先会查找常量池,找到了"aaa"就直接引用,然后拷贝一份到堆里面,然后再在栈里面保存 s2这个引用指向堆里面的"aaa"对象。

方法区:

Java方法区和堆一样,方法区是一块所有线程共享的内存区域,他保存系统的类信息。   比如类的字段、方法、常量池等。方法区的大小决定系统可以保存多少个类。

如果系统定义太多的类,导致方法区溢出。虚拟机同样会抛出内存溢出的错误。方法区可以理解   为永久区。

String str = new String("abc");问一共创造了几个对象???

String str=new String("abc");   紧接着这段代码之后的往往是这个问题,那就是这行代码究竟创建了几个String对象呢?

相信大家对这道题并不陌生,答案也是众所周知的,2个。

接下来我们就从这道题展开,一起回顾一下与创建String对象相关的一些JAVA知识。  

我们可以把上面这行代码分成String str、=、"abc"和new String()四部分来看待。String str只是定义了一个名为str的String类型的变量,因此它并没有创建对象;=是对变量str进行初始化,将某个对象的引用(或者叫句柄)赋值给它,显然也没有创建对象;现在只剩下new String("abc")了。那么,new String("abc")为什么又能被看成"abc"和new String()呢?

我们来看一下被我们调用了的String的构造器:  

public String(String original) {  //other code ...  }   大家都知道,我们常用的创建一个类的实例(对象)的方法有以下两种:

一、使用new创建对象。 

二、调用Class类的newInstance方法,利用反射机制创建对象。

我们正是使用new调用了String类的上面那个构造器方法创建了一个对象,并将它的引用赋值给了str变量。同时我们注意到,被调用的构造器方法接受的参数也是一个String对象,这个对象正是"abc"。由此我们又要引入另外一种创建String对象的方式的讨论——引号内包含文本。

这种方式是String特有的,并且它与new的方式存在很大区别。  

String str="abc";   毫无疑问,这行代码创建了一个String对象。  

String a="abc";  String b="abc";  这行代码只创建了一个String对象。  

String a="ab"+"cd";  这行代码创建了三个String对象。  

说到这里,我们就需要引入对字符串池相关知识的回顾了。  

在JAVA虚拟机(JVM)中存在着一个字符串池,其中保存着很多String对象,并且可以被共享使用,因此它提高了效率。由于String类是final的,它的值一经创建就不可改变,因此我们不用担心String对象共享而带来程序的混乱。字符串池由String类维护,我们可以调用intern()方法来访问字符串池。  

我们再回头看看String a="abc";,这行代码被执行的时候,JAVA虚拟机首先在字符串池中查找是否已经存在了值为"abc"的这么一个对象,它的判断依据是String类equals(Object obj)方法的返回值。如果有,则不再创建新的对象,直接返回已存在对象的引用;如果没有,则先创建这个对象,然后把它加入到字符串池中,再将它的引用返回。因此,我们不难理解前面三个例子中头两个例子为什么是这个答案了。

只有使用引号包含文本的方式创建的String对象之间使用“+”连接产生的新对象才会被加入字符串池中。对于所有包含new方式新建对象(包括null)的“+”连接表达式,它所产生的新对象都不会被加入字符串池中,对此我们不再赘述。因此我们提倡大家用引号包含文本的方式来创建String对象以提高效率,实际上这也是我们在编程中常采用的。

String 类和常量池

1 String 对象的两种创建方式:

String str1 = "abcd";
String str2 = new String("abcd");
System.out.println(str1==str2);  //false
这两种不同的创建方法是有差别的,第一种方式是在常量池中拿对象第二种方式是直接在堆内存空间创建一个新的对象。

注意:只要使用new方法,就需要创建新的对象。

2 String 类型的常量池比较特殊。它的主要使用方法有两种:
一,直接使用双引号声明出来的 String 对象会直接存储在常量池中。
二,如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方String.intern() 是一个 Native 方法,它的作用是:如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,则在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用。
String s1 = new String("计算机");
String s2 = s1.intern();
String s3 = "计算机";
System.out.println(s2);//计算机
System.out.println(s1 == s2);//false,因为一个是堆内存中的String对象一个是常量池中的String对象,
System.out.println(s3 == s2);//true,因为两个都是常量池中的String对象

3 String 字符串拼接

String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";//常量池中的对象
String str4 = str1 + str2; //在堆上创建的新的对象     
String str5 = "string";//常量池中的对象
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false

尽量避免多个字符串拼接,因为这样会重新创建对象。如果需要改变字符串的花,可以使用 StringBuilder 或者 StringBuffer。

8种基本类型的包装类和常量池

Java 基本类型的包装类的大部分都实现了常量池技术,即Byte,Short,Integer,Long,Character,Boolean;这6种包装类默认创建了数值[-128,127]的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。
两种浮点数类型的包装类 Float,Double 并没有实现常量池技术

        Integer i1 = 33;
        Integer i2 = 33;
        System.out.println(i1 == i2);// 输出true
        Integer i11 = 333;
        Integer i22 = 333;
        System.out.println(i11 == i22);// 输出false
        Double i3 = 1.2;
        Double i4 = 1.2;
        System.out.println(i3 == i4);// 输出false

Integer 缓存源代码:

/**
*此方法将始终缓存-128到127(包括端点)范围内的值,并可以缓存此范围之外的其他值。
*/
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

应用场景: 1. Integer i1=40;Java 在编译的时候会直接将代码封装成Integer i1=Integer.valueOf(40);,从而使用常量池中的对象。 2. Integer i1 = new Integer(40);这种情况下会创建新的对象。

Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);//输出false

Integer比较更丰富的一个例子:   Intefer 操作 + 时会自动拆箱为 int

  Integer i1 = 40;
  Integer i2 = 40;
  Integer i3 = 0;
  Integer i4 = new Integer(40);
  Integer i5 = new Integer(40);
  Integer i6 = new Integer(0);

  System.out.println("i1=i2   " + (i1 == i2));    //true
  System.out.println("i1=i2+i3   " + (i1 == i2 + i3));  //true
  System.out.println("i1=i4   " + (i1 == i4));  //false
  System.out.println("i4=i5   " + (i4 == i5));  //false
  System.out.println("i4=i5+i6   " + (i4 == i5 + i6));   //true  因为+这个操作符不适用于Integer对象,首先i5和i6进行自动拆箱操作,进行数值相加,即i4 == 40。
  System.out.println("40=i5+i6   " + (40 == i5 + i6));  //true  因为Integer对象无法与数值进行直接比较,所以i5和i6自动拆箱转为int值40,最终这条语句转为40 == 40进行数值比较。

原文地址java栈、堆、常量池、方法区-云海天教程

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值