[Java 内存]Java内存组成

版权声明:转载说明出处 https://blog.csdn.net/HaveFerrair/article/details/50951521

本人大二学生党,对Java理解有所不足,敬请谅解。
这里写图片描述

动机

写了这么多年的Java代码,总要了解一些底层的知识吧!(才2年)了解Java内存是由那些数据区域组成的,可以解决一些莫名其妙的错误。而且使得程序更好更快的运行。

内存数据区域

对于JVM里面的内存数据区域,可以用这个图来表示:(图片来源-JVM内存模型
这里写图片描述
注意上图:蓝色为所有线程共享的数据区,紫色为线程的私有区。

1)程序计数器:在计算机组成结构这门课里面,我们就学到CPU里面有个PC寄存器,这个寄存器主要指CPU当前运行的指令。
在这里,其实也是一样的,对于每一个线程,都有一个PCR,用来记录程序在当前线程执行的位置。当线程阻塞后然后再重新运行,就可以在PC记录的位置继续执行了。
线程之间的PC互不影响,所以称为线程私有的。同时,PC是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
2)虚拟机栈(VM Stack):线程私有,生命周期与线程一样。虚拟机栈描述的是Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。栈顶为当前执行的方法。

主要解释下局部变量表:主要存放各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型)。我们笼统的把Java内存分为Heap,Stack,这里就是Stack咯。

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

int a = 3;
int b = 3;

3)本地方法栈(Native Method Stacks),就是native方法咯。
4)方法区(Method Area)线程共享的。主要存放类型信息,包括

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

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

5)堆(Heap),接下来详细介绍一哈Heap。线程共享的。可以动态分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的。可以参加我的另一篇博客
首先看一下图(图片来源
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的区别

关于String

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

  • a == b,比较的ab所引用的对象的地址(即在栈里面的地址),而不是内容
  • String a = "AAA",首先检查常量池中有没有"AAA"对象,有的话,返回其引用,没有的话创建一个然后返回一个常量池对象的引用。所以会创建0个或者1个对象
  • String b = new String("AAA");首先检查常量池中有没有"AAA"对象,没有的话就创建一个,然后每一次都在堆内存里面也创建一个"AAA"对象,返回一个堆内存的对象的引用。。所以会创建2个或者1个对象
  • String一旦确立的,就是不可改变的。而常量池是在编译期间确立的存放在.class文件里面。
    可以看到String的构造函数:
  • 对于一个String的实例对象a,a.intern()返回的是一个常量池对象的引用
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); // AAA
    }
    //因为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指向常量池
        System.out.println(d == b.intern()); //true
    }
 @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()返回的是在常量池里面的地址
    }

关于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
    }

对于Integer小结一哈:

  • Interger的常量池只有[-128,127],所以test中,c == b为false,因为b为常量池对象引用,而c为堆内存对象的引用。
    这个在Java8源码里面可以看到:

    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];
    
        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;
    
            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);
    
            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }
    
        private IntegerCache() {}
    }
  • Integer a = 1; 会调用这个 Integer a = Integer.valueOf(1)即装箱

  • intInterger比较:Integer会调用Integer.intvalue()实现拆箱,然后再比较。而IntergerInterger比较,会比较堆内存对象引用或者常量池对象。
  • Java的数学计算是在内存栈里操作的

拓展(staticfinal

首先要了解Java代码的执行过程,可以看这里

static 的修饰的变量和方法,实际上是指定了这些变量和方法在内存中的”固定位置”-static storage,可以理解为所有实例对象共有的内存空间。static 变量有点类似于 C 中的全局变量的概念;静态表示的是内存的共享,就是它的每一个实例都指向同一个内存地址。把 static 拿来,就是告诉 JVM 它是静态的,它的引用(含间接引用)都是指向同一个位置,在那个地方,你把它改了,它就不会变成原样,你把它清理了,它就不会回来了。 那静态变量与方法是在什么时候初始化的呢?对于两种不同的类属性,static 属性与 instance 属性,初始化的时机是不同的。instance 属性在创建实例的时候初始化,static属性在类加载,也就是第一次用到这个类的时候初始化,对于后来的实例的创建,不再次进行初始化。

final 只对引用的”值”(也即它所指向的那个对象的内存地址)有效,它迫使引用只能指向初始指向的那个对象,改变它的指向会导致编译期错误。至于它所指向的对象的变化,final 是不负责的。

最后附上一张极客学院的Java各种内存操作时间效率

执行效率

本人邮箱:1906362072@qq.com 欢迎一起学习

展开阅读全文

没有更多推荐了,返回首页