如果敌人让你生气,那说明你还没有胜他的把握,如果朋友让你生气,那说明你仍然在意他的友情

java内存的基本概念

       每运行一个java程序会产生一个java进程,每个java进程可能包含一个或者多个线程,每一个Java进程对应唯一一个JVM实例,每一个JVM实例唯一对应一个堆,每一个线程有一个自己私有的栈。进程所创建的所有类的实例(也就是对象)或数组(指的是数组的本身,不是引用)都放在堆中,并由该进程所有的线程共享。Java中分配堆内存是自动初始化的,即为一个对象分配内存的时候,会初始化这个对象中变量。虽然Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在栈中分配,也就是说在建立一个对象时在堆和栈中都分配内存,在堆中分配的内存实际存放这个被创建的对象的本身,而在栈中分配的内存只是存放指向这个堆对象的引用而已。局部变量 new 出来时,在栈空间和堆空间中分配空间,当局部变量生命周期结束后,栈空间立刻被回收,堆空间区域等待GC回收。

java内存的具体划分

Java内存分配主要包括以下几个区域:

1. 堆:

          存储的全部是对象,每个对象都包含一个与之对应的class的信息(class的目的是得到操作指令) ,jvm只有一个堆区(heap),且被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身和数组本身。堆内存用来存放由new创建的对象和数组。 在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。

         在堆中产生了一个数组或对象后,还可以 在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。引用变量就相当于是为数组或者对象起的一个名称。 引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放。

       而数组和对象本身在堆中分配,即使程序运行到使用 new 产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍 然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)。这也是 Java 比较占内存的原因。 

2. 栈:

         实际上,栈中的变量指向堆内存中的变量,这就是Java中的指针!

         每个线程包含一个栈区,栈中只保存基础数据类型本身和自定义对象的引用,每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问,栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。

         栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量数据(int, short, long, byte, float, double, boolean, char)和对象句柄(引用)。     

3. 静态域(方法区):

        被所有的线程共享,方法区包含所有的class(class是指类的原始代码,要创建一个类的对象,首先要把该类的代码加载到方法区中,并且初始化)和static变量。 方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。 方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据 (重点)。相对而言,垃圾收集行为在这个区域比较少出现,但并非数据进了方法区就永久的存在了,这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

      

4. 常量池:

       常量池指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。除了包含代码中所定义的各种基本类型(如int、long等等)和对象型(如String及数组)的常量值(final)还包含一些以文本形式出现的符号引用,比如: 类和接口的全限定名,字段的名称和描述符, 方法和名称和描述符。

  虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集和,包括直接常量(string,integer和 floating point常量)和对其他类型,字段和方法的符号引用。

      对于String常量,它的值是在常量池中的。而JVM中的常量池在内存当中是以表的形式存在的, 对于String类型,有一张固定长度的CONSTANT_String_info表用来存储文字字符串值,注意:该表只存储文字字符串值,不存储符号引 用。说到这里,对常量池中的字符串值的存储位置应该有一个比较明了的理解了。

  在程序执行的时候,常量池会储存在Method Area,而不是堆中。

5.程序计数器:

        可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作就是通过改变程序计数器的值来选择下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都要依赖这个计数器来完成。

public class demo {
    public static void main(String[] args) {
        String h="hello";
        String h1="hello";
        Test test = new Test("hello");
        Test test1= new Test("hello");
    }
}
class Test{
    String name;

    public Test(String name) {
        this.name = name;
    }
}

下图是上面这段代码在Java中发生的一些变化。(需要注意的是,在new的时候如果我们通过字符创new一个对象,它会先在常量池中找看有没有自己所需要的对象,如果找到了就引用,如果没有则在常量池中创建一个此字符串对象,然后堆中再创建一个常量池中此”china”对象的拷贝对象。)

如果我们再new一个对象

new Test("hello,java")  的时候,会产生几个对象???

在这里是两个。一般情况下,会产生一个或者两个,就看常量池中有没有这个字符串对象。

String常量池问题的几个例子

例1:

        String q="qq";
        String s="q";
        String m="q"+q;
        System.out.println((q==m));

返回结果是:false

分析:

      JVM对于字符串引用,由于在字符串的"+"连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即"a" + bb无法被编译器优化,只有在程序运行期来动态分配并将连接后的新地址赋给b。所以上面程序的结果也就为false。

例2:

        String q="qq";
        final String s="q";
        String m="q"+s;
        System.out.println((q==m));

返回结果:true

分析:

      例2和例1中唯一不同的是s字符串加了final修饰,对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。所以此时的"q"+s和"q" + "q"效果是一样的。故上面程序的结果为true。

例3:

        String q="qq";
        final String s=getq();
        String m="q"+s;
        System.out.println((q==m));
    }
    private static String getq() {
        return "q";
    }

返回结果:false

分析:

JVM对于字符串引用s,它的值在编译期无法确定,只有在程序运行期调用方法后,将方法的返回值和"q"来动态连接并分配地址为m,故上面程序的结果为false。

总之:

        字符串是一个特殊包装类,其引用是存放在栈里的,而对象内容必须根据创建方式不同定(常量池和堆).有的是编译期就已经创建好,存放在字符串常 量池中,而有的是运行时才被创建.使用new关键字,存放在堆中。