[Java 内存]Java内存模型+String+Integer

本人大二学生党,对Java理解有所不足,敬请谅解。
[Java 内存]Java内存模型+String+Integer_第1张图片

1 内存模型

对于JVM里面的内存模型,可以用这个图来表示:(图片来源-JVM内存模型)
[Java 内存]Java内存模型+String+Integer_第2张图片

程序计数器:在计算机组成结构这门课里面,我们就学到CPU里面有个PC寄存器,这个寄存器主要指CPU当前运行的指令。
在这里,其实也是一样的,对于每一个线程,都有一个PCR,用来记录程序在当前线程执行的位置(准确的说是在Java代码中是:虚拟机字节码指令的地址;在native方法里面PC就是Undefined的)。当线程阻塞后然后再重新运行,就可以在PC记录的位置继续执行了。
线程之间的PC互不影响,所以称为线程私有的。同时,PC是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
虚拟机栈(VM Stack):线程私有,生命周期与线程一样。虚拟机栈描述的是Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。栈顶为当前执行的方法。
主要解释下局部变量表:主要存放各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型)。我们笼统的把Java内存分为Heap,Stack,这里就是Stack咯。
方法区(Method Area)线程共享的。主要存放类型信息,包括

  • 类的类型,限定符,类名等很多信息
  • 变量的变量名,类型,修饰符
  • 方法的方法名,参数,返回类型,修饰符
  • 类的静态变量
  • 常量池(等下会具体说明)

很多时候,对于习惯在HotSpot虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9等)来说是不存在永久代的概念的。即使是HotSpot虚拟机本身,根据官方发布的路线图信息,现在也有放弃永久代并“搬家”至Native Memory来实现方法区的规划了。

本地方法栈(Native Method Stacks),就是native方法咯。
堆(Heap),线程共享的。可以参加我的另一篇博客

首先看一下图(图片来源)

可以看到Java的Heap分为了2个部分(其实Permanent Generation常常被人称为方法区),下面一一介绍:

  • Eden(Young Generation):当程序中为一个对象分配内存的时候,这个对象的内存就会被分在这里。在Eden区域进一步被每个线程分割了,也就是说一个线程里面分配的内存都会在一起,而这个区域被称为 Thread Local Allocation Buffer(TLAB),有了TLAB,避免了昂贵的线程同步问题。当一个TLAB满了之后,内存就会被分配到CommonArea。(线程共享的资源是声明在Common Area里面的,在这里引申出一个问题:多线程对共享资源的访问与控制)。
  • Survivor(Young Generation):Survivor会被分为form to,当Eden触发GC之后就会触发Mark-and-Copy算法的GC。把Eden的存活的对象拷贝带Survivor1区域,当Eden再次满了之后,就会把Survivor1与Eden的存活的对象再次拷贝到Survivor2。注意以下几点:每个对象都一个age,每进行一次GC,age就会加一。当age达到一定的值就会被Old Generation区域。或者,Young Generation满了也会copy到Old Generation。(GC的问题可以参见我的其他文章 Java 垃圾回收)
//所有的类型信息都会在MethodArea里面进行存储
public class A
{
    int a = 0; //局部变量表里面
    static int b = 9;//Method Area的静态变量里面
    int sayHello(int word)//局部变量表里面
    {

    }
}

同时应该注意StackOverFlow与OutofMemory的区别

2 关于String

String这个很容易搞混淆咯。先看代码
首先应该了解一些基础知识:

  • a == b,比较的ab所引用的对象的地址,而不是内容
  • String a = "AAA",首先检查常量池中有没有"AAA"对象,有的话,返回其引用,没有的话创建一个然后返回引用。所以会创建0个或者1个对象
  • String b = new String("AAA");首先检查常量池中有没有"AAA"对象,没有的话就创建一个,然后每一次都在堆内存里面也创建一个"AAA"对象,返回堆内存的对象。。所以会创建2个或者1个对象
  • String一旦确立的,就是不可改变的。而常量池是在编译期间确立的存放在.class文件里面。
    可以看到String的构造函数:
public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

当把”AAA”传到构造器里面的时候,会先去常量池检查或者创建对象,然后再去放回堆内存里面的对象。

下面看几个例子熟悉一哈其他的属性:

    //String a = "AAA","AAA"对象在常量池咯
    //String b = new String("AAA"); "AAA"在堆内存里面
    //所以a的地址和b不一样啊
    @Test
    public void test1() {
        String a = "AAA";
        String b = new String("AAA");
        System.out.println(a == b); // false
    }
    //a = "DDD";a指向了"DDD",抛弃了"AAA"
    @Test
    public void test2() {
        String a = "AAA";
        String b = "AAA";
        System.out.println(a == b); // true
        a = "DDD";
        System.out.println(a == b); // false
        System.out.println(a); // DDD
        System.out.println(b); // DDD
    }
    //因为1会被转化为String类型的,而且是在编译阶段进行的(因为1已经确定了啊)
    @Test
    public void test3() {
        String a = "AA1";
        String b = "AA" + 1;
        System.out.println(a == b); // true
    }

    // i 是变量,在编译阶段不能确定,在运行阶段再能确定,所有不行咯
    @Test
    public void test4() {
        String a = "AA1";
        int i = 1;
        String b = "AA" + i;
        System.out.println(a == b); // false
    }

    //final会让i在编译阶段确定
    @Test
    public void test5() {
        String a = "AA1";
        final int i = 1;
        String b = "AA" + i;
        System.out.println(a == b); // true
    }
    //intern()方法动态扩展常量池
    @Test
    public void test6() {
        String a = "AAA";
        String b = new String("AAA");
        System.out.println(a == b.intern()); // true String#intern() 返回的是在常量池里面的地址
    }
 @Test
    public void test7() {
        String fre = "A";
        String a = "AA" + fre; //并没有把"AAA"加入到常量池里面
        String b = new String("AAA");
        String d = "AAA";

        //全为false
        System.out.println(a == b); //a b 是2个不同的堆对象
        System.out.println(a == b.intern()); // 返回的是在常量池里面的地址
        System.out.println(a == d); //a指向堆中 d指向常量池
        System.out.println(b == d);//b 指向堆中 d指向常量池
    }
 @Test
    public void test8() {
        final String fre = "A"; //fianl 使得fre在编译的期间是确定的
        String a = "AA" + fre; //将"AAA"添加到了常量池
        String b = new String("AAA");
        System.out.println(a == b); //a 指向堆中 b指向常量池
        System.out.println(a == b.intern()); // b.intern()返回的是在常量池里面的地址
    }

3 关于Integer

    //和String一样的
    @Test
    public void test() {
        Integer a = new Integer(1);
        Integer b = new Integer(1);

        System.out.println(a == b); //false
    }
    // 当基本类型包装类与基本类型值进行==运算时,包装类会自动拆箱。即比较的是基本类型值。
    // 具体实现上,是调用了Integer.intValue()方法实现拆箱。
    @Test
    public void test1() {
        int a = 1;
        Integer b = 1;
        Integer c = new Integer(1);

        System.out.println(a == b); //true
        System.out.println(a == c); //true
        System.out.println(c == b); //false
    }

对于这个我们反编译一哈可以看到这样

 @Test
    public void test1() {
        byte a = 1;
        Integer b = Integer.valueOf(1);
        Integer c = new Integer(1);
        System.out.println(a == b.intValue());
        System.out.println(a == c.intValue());
        System.out.println(c == b);
    }

出现了Integer.valueOf(1)这是啥?
Returns an {@code Integer} instance representing the specified {@code int} value,就是这个咯。

    // Integer a = 1; 会调用这个 Integer a = Integer.valueOf(1);
    // Integer已经默认创建了数值【-128-127】的Integer常量池
    // 在之间的值就直接在里面取,所以test2可以,test3就不行了
    @Test
    public void test2() {
        Integer a = 127;
        Integer b = 127;
        System.out.println(a == b); //true

    }

    @Test
    public void test3() {
        Integer a = 128;
        Integer b = 128;
        System.out.println(a == b); //false

    }
    // Java的数学计算是在内存栈里操作的
    // c1 + c2 会进行拆箱,比较还是基本类型
    @Test
    public void test4() {
        int a = 0;

        Integer b1 = 1000;
        Integer c1 = new Integer(1000);

        Integer b2 = 0;
        Integer c2 = new Integer(0);

        System.out.println(b1 == b1 + b2); //true
        System.out.println(c1 == c1 + c2); //true

        System.out.println(b1 == b1 + a); //true
        System.out.println(c1 == c1 + a); //true
    }

上面的主要是自动装箱与拆箱,装箱主要是把int基本类型变为Integer等包装类型Integer.value(),拆箱就是相反的咯(Integer.intValue())。

你可能感兴趣的:(java,jvm)