精华内容
下载资源
问答
  • Java 虚拟机内存分配机制

    千次阅读 2017-09-15 15:34:45
    Java 虚拟机内存分配机制 内存区域划分 对于大多数的程序员来说,Java 内存比较流行的说法便是堆和栈,这其实是非常粗略的一种划分,这种划分的“堆”对应内存模型的 Java 堆,“栈”是指虚拟机栈,然而 Java ...

    Java 虚拟机内存分配机制

    内存区域划分

    对于大多数的程序员来说,Java 内存比较流行的说法便是堆和栈,这其实是非常粗略的一种划分,这种划分的“堆”对应内存模型的 Java 堆,“栈”是指虚拟机栈,然而 Java 内存模型远比这更复杂,想深入了解 Java 的内存,还是有必要明白整个内存区域分。

    了解 Java GC 机制,必须先清楚在 JVM 中内存区域的划分。 在 Java 运行时的数据区里,由 JVM 管理的内存区域分为下图几个模块:

    JVM 内存划分

    程序计数器(Program Counter Register)

    程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。 字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。

    每个程序计数器只用来记录一个线程的行号,所以它是线程私有(一个线程就有一个程序计数器)的。

    如果程序执行的是一个 Java 方法,则计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地( native,由 C 语言编写完成)方法,则计数器的值为 Undefined,由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,因此,程序计数器也是所有JVM内存区域中唯一一个没有定义 OutOfMemoryError 的区域。

    虚拟机栈(JVM Stack)

    一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作数栈、动态链接、方法出口等,当方法被调用时,栈帧在 JVM 栈中入栈,当方法执行完成时,栈帧出栈。

    局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。 在局部变量表中,只有 long 和 double 类型会占用 2 个局部变量空间(Slot,对于32位机器,一个 Slot 就是 32 个 bit),其它都是 1 个 Slot。 需要注意的是,局部变量表是在编译时就已经确定好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。

    虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出 StatckOverFlowError(栈溢出);不过多数 Java 虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的),所以线程可以一直申请栈,直到内存不足,此时,会抛出 OutOfMemoryError(内存溢出)。

    每个线程对应着一个虚拟机栈,因此虚拟机栈也是线程私有的。

    本地方法栈(Native Method Statck)

    本地方法栈在作用,运行机制,异常类型等方面都与虚拟机栈相同,唯一的区别是:虚拟机栈是执行 Java 方法的,而本地方法栈是用来执行 native 方法的,在很多虚拟机中(如:Sun 的 JDK 默认的 HotSpot 虚拟机),会将本地方法栈与虚拟机栈放在一起使用。

    本地方法栈也是线程私有的。

    堆区(Heap)

    堆区是理解 Java GC 机制最重要的区域,没有之一。 在 JVM 所管理的内存中,堆区是最大的一块,堆区也是 Java GC 机制所管理的主要内存区域,堆区由所有线程共享,在虚拟机启动时创建。 堆区的存在是为了存储对象实例,原则上讲,所有的对象都在堆区上分配内存(不过现代技术里,也不是这么绝对的,也有栈上直接分配的)。

    一般的,根据 Java 虚拟机规范规定,堆内存需要在逻辑上是连续的(在物理上不需要),在实现时,可以是固定大小的,也可以是可扩展的,目前主流的虚拟机都是可扩展的。 如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出 OutOfMemoryError:Java heap space 异常。

    关于堆区的内容还有很多,将在下面“内存分配机制”中详细介绍。

    方法区(Method Area)

    在 Java 虚拟机规范中,将方法区作为堆的一个逻辑部分来对待,但事实上,方法区并不是堆(Non-Heap);另外,不少人的博客中,将 Java GC 的分代收集机制分为 3 个代:青年代,老年代,永久代,这些作者将方法区定义为“永久代”,这是因为,对于之前的 HotSpot Java 虚拟机的实现方式中,将分代收集的思想扩展到了方法区,并将方法区设计成了永久代。 不过,除 HotSpot 之外的多数虚拟机,并不将方法区当做永久代,HotSpot 本身,也计划取消永久代。 本文中,由于主要使用 Oracle JDK6.0,因此仍将使用永久代一词。

    方法区是各个线程共享的区域,用于存储已经被虚拟机加载的类信息(即加载类时需要加载的信息,包括版本、field、方法、接口等信息)、final 常量、静态变量、编译器即时编译的代码等。

    方法区在物理上也不需要是连续的,可以选择固定大小或可扩展大小,并且方法区比堆还多了一个限制:可以选择是否执行垃圾收集。 一般的,方法区上执行的垃圾收集是很少的,这也是方法区被称为永久代的原因之一(HotSpot),但这也不代表着在方法区上完全没有垃圾收集,其上的垃圾收集主要是针对常量池的内存回收和对已加载类的卸载。

    在方法区上进行垃圾收集,条件苛刻而且相当困难,效果也不令人满意,所以一般不做太多考虑,可以留作以后进一步深入研究时使用。

    在方法区上定义了 OutOfMemoryError:PermGen space 异常,在内存不足时抛出。

    • 运行时常量池(Runtime Constant Pool)

    方法区的一部分,用于存储编译期就生成的字面常量、符号引用、翻译出来的直接引用(符号引用就是编码是用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址,将在类链接阶段完成翻译);运行时常量池除了存储编译期常量外,也可以存储在运行时间产生的常量(比如 String 类的 intern() 方法,作用是 String 维护了一个常量池,如果调用的字符 “abc” 已经在常量池中,则返回池中的字符串地址,否则,新建一个常量加入池中,并返回地址)。

    直接内存(Direct Memory)

    直接内存并不是 JVM 管理的内存,可以这样理解,直接内存,就是 JVM 以外的机器内存。

    比如:你有 4G 的内存,JVM占用了1G,则其余的 3G 就是直接内存,JDK 中有一种基于通道(Channel)和缓冲区(Buffer)的内存分配方式,将由 C 语言实现的 native 函数库分配在直接内存中,用存储在 JVM 堆中的 DirectByteBuffer 来引用。 由于直接内存收到本机器内存的限制,所以也可能出现 OutOfMemoryError 的异常。

    内存分配机制

    以下面代码为例,来分析,Java 的实例对象在内存中的空间分配。

    //JVM 启动时将 Person.class 放入方法区
    public class Person {
    
    	//new Person 创建实例后,name 引用放入堆区,name 对象放入常量池
        private String name;
    
    	//new Person 创建实例后,age = 0 放入堆区
        private int age;
    
    	//Person 方法放入方法区,方法内代码作为 Code 属性放入方法区
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
    	//toString 方法放入方法区,方法内代码作为 Code 属性放入方法区
        @Override
        public String toString() {
            return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
        }
    }
    
    //JVM 启动时将 Test.class 放入方法区
    public class Test {
    
    	//main 方法放入方法区,方法内代码作为 Code 属性放入方法区
        public static void main(String[] args) {
    
            //person1 是引用放入虚拟机栈区,new 关键字开辟堆内存 Person 自定义对象放入堆区
            Person person1 = new Person("张三", 18);
            Person person2 = new Person("李四", 20);
    
            //通过 person 引用创建 toString() 方法栈帧
            person1.toString();
            person2.toString();
        }
    }
    
    1. 首先 JVM 会将 Test.class, Person.class 加载到方法区,找到有 main() 方法的类开始执行。

    这里写图片描述

    如上图所示,JVM 找到 main() 方法入口,创建 main() 方法的栈帧放入虚拟机栈,开始执行 main() 方法。

    Person person1 = new Person("张三", 18);
    

    执行到这句代码时,JVM 会先创建 Person

    实例放入堆区,person2 也同理。

    1. 创建完 Person 两个实例,main() 方法中的 person1,person2 会指向堆区中的 0x001,0x002(这里的内存地址仅作为示范)。紧接着会调用 Person 的构造函数进行赋值,如下图:

    这里写图片描述

    如上图所示,新创建的的 Person 实例中的 name, age 开始都是默认值。 调用构造函数之后进行赋值,name 是 String 引用类型,会在常量池中创建并将地址赋值给 name,age 是基本数据类型将直接保存数值。

    注:Java 中基本类型的包装类的大部分都实现了常量池技术,这些类是 Byte, Short, Integer, Long, Character, Boolean,另外两种浮点数类型的包装类则没有实现。

    基本数据类型 包装类 (是否实现了常量池技术)
    byte Byte 是
    boolean Boolean 是
    short Short 是
    char Character 是
    int Integer 是
    long Long 是
    float Float 否
    double Double 否
    1. Person 实例初始化完后,执行到 toString() 方法,同 main() 方法一样 JVM 会创建一个 toString() 的栈帧放入虚拟机栈中,执行完之后返回一个值。

    这里写图片描述

    参考资料

    《深入理解 Java 虚拟机》

    更多文章

    https://github.com/jeanboydev/Android-ReadTheFuckingSourceCode

    我的公众号

    欢迎你「扫一扫」下面的二维码,关注我的公众号,可以接受最新的文章推送,有丰厚的抽奖活动和福利等着你哦!?

    qrcode_android_besos_black_512.png

    如果你有什么疑问或者问题,可以 点击这里 提交 issue,也可以发邮件给我 jeanboy@foxmail.com

    同时欢迎你 Android技术进阶:386463747 来一起交流学习,群里有很多大牛和学习资料,相信一定能帮助到你!

    展开全文
  • Java对象与内存分配

    千次阅读 2017-11-07 16:39:37
    Java对象与其内存分配Java中一切都是对象,对象是Java运行的单元,知道对象是如何存在的、什么时候是可见的,才知道怎样运用对象来完成相应的操作。 Java运行时对象和变量等都是在内存中,可以根据内存中的数据来...

    ##Java对象与内存分配
    Java中一切都是对象,对象是Java运行的单元,知道对象是如何存在的、什么时候是可见的,才知道怎样运用对象来完成相应的操作。
    Java运行时对象和变量等都是在内存中,可以根据内存中的数据来判断这些对象的可见性。下面了解一下Java对象在内存中的分配。
    内存主要分为:程序计数器、虚拟机栈、堆、方法区、本地方法栈。
    程序计数器可以看作是当前线程所执行的字节码的行号指示器。它是线程私有的。
    虚拟机栈也是线程私有的,描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
    本地方法栈同虚拟机栈非常相似,,为虚拟机使用到的Native方法服务。也是线程私有的。
    Java堆是所有线程共享的,是Java虚拟机所管理的内存中最大的一块,在虚拟机启动时创建。几乎所有的对象实例以及数组都要在堆上分配内存。Java堆是垃圾收集器管理的主要区域,因此也称作“GC堆”。Java堆在计算机实际内存中可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
    方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
    运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
    C语言可以直接操控电脑的内存,所以C语言在操作数据时要手动分配内存给数据,在数据用完后要及时释放内存,以免发生内存溢出问题。Java是用C语言写的,它对内存的分配和回收机制已经处理了,所以不需要程序员再去手动的处理内存的申请与释放操作。
    Java虽然有回收机制(GC),也不能肆无忌惮的创建对象。建的多了GC频繁的回收也会影响效率,也会导致内存溢出。了解Java对象的内存分配有助于服务性能的提高。

    Java变量分为局部变量和成员变量。局部变量主要是:a、形参,即方法的参数;b、方法中的变量;c、代码块中的变量。成员变量主要是非静态变量即实例变量和静态变量即类变量。
    变量的定义和初始化的内存分配的时机是有先后顺序的,如下:
    a、类变量和静态代码块为同一级,优先分配内存,其次是实例变量和代码块这一级别,最后是为构造器初始化分配内存;
    b、Java要求定义处在内存分配中同一级别的变量时,必须采用合法的前向引导。通俗的讲就是类变量和静态代码块定义和初始化的变量要有先后顺序,实例变量和代码块定义和初始化要有先后顺序;
    c、内存分配中同一级别的变量,先按顺序定义,都定义好后再按顺序初始化。
    下面举例说明:

    public class MemoryTest {     //第一步
        int surplus = num - count;          //a //第六步
        static int count = 0;     //第二步
        int max;  //第七步
        {  //第八步
            System.out.println("代码块初始化前surplus=" + surplus);
            count = 2;
            surplus = num - count;
            persons = 1;                  //b
            int var = 3;
            System.out.println("代码块初始化后surplus=" + surplus);
        }
        static int num = 5;     //第三步
        int persons = 2; //第九步
        {//第十步
            max = count -persons + 1;     //c
        }
    
        public static void main(String[] args){  //第四步
            MemoryTest memory = new MemoryTest();  //第五步
            System.out.println(memory.persons);
        }
    }
    

    执行结果是

    代码块初始化前surplus=5
    代码块初始化后surplus=3
    2
    

    这个例子很好的说明了变量的内存分配和分配时机。这里的变量存在实例变量、类变量、代码块中的局部变量。这里对代码分析下内存分配和分配时机.
    在第六步实例变量surplus初始化时用到了类变量num和count,num和count的定义和初始化是在后面代码,之所以实例变量可以操作类变量num和count是因为类变量是属于类本身的,就是在加载MemoryTest类时就对类变量进行定义和初始化.
    类初始化时先将count和num定义到栈内存中,并分配内存空间,此时两个变量的默认值都是0.再根据初始化代码进行初始化.如图:
    这里写图片描述
    在第五步创建类MemoryTest对象new MemoryTest()时会在堆栈中为实例memory分配内存并赋默认值null,再在堆内存中创建对象。下面用图来分析MemoryTest对象的创建过程。
    首先,在堆内存中分配空间,将对象的实例变量逐个定义,类型为int型,默认值都是0,如图:
    这里写图片描述
    其次,根据代码和变量的先后顺序为变量初始化,实例变量和非静态代码块按顺序执行,如第六步,
    这里写图片描述
    在执行第八步代码块时,会对persons赋值,虽然定义persons的代码是在代码块后,但对象定义变量是在初始化和代码块之前执行,所以在代码块中为persons赋值不会报错。
    这里写图片描述
    第八步代码块中定义的局部变量var的作用域仅是代码块,代码块结束后var也随记被销毁,空间被回收。
    这里写图片描述
    这里需要注意一个问题,在第八步代码块中,persons只能被赋值,不能作为变量用于其他语句的计算或引用等操作,原因是persons是在第九步定义被赋值,代码现在执行在第八步,我们说实例变量和代码块是顺序执行的,还未执行到第九步,也就是persons还未结束定义初始化,所以不能操作persons变量,但在第九步初始化结束,persons变量就可以使用了。
    这里写图片描述
    MemoryTest对象创建完之后memory指向对象地址,调用对象的persons实例变量。
    这里写图片描述
    main方法执行完之后,堆栈中的memory将被销毁,空间回收,此时MemoryTest对象将没有实例指向它,它也会被GC回收。

    展开全文
  • java数组内存分配内存结构详解

    万次阅读 多人点赞 2018-08-12 19:51:49
    Java 数组是静态的 Java 语言是典型的静态语言,因此 Java 数组是静态的,即当数组被初始化之后...所谓初始化,即创建实际的数组对象,也就是在内存中为数组对象分配内存空间,并为每个数组 元素指定初始值。 数...

    引自:https://m.2cto.com/kf/201611/561021.html

    Java 数组是静态的

    Java 语言是典型的静态语言,因此 Java 数组是静态的,即当数组被初始化之后,该数组 所占的内存空间、数组长度都是不可变的。Java 程序中的数组必须经过初始化才可使用。所谓初始化,即创建实际的数组对象,也就是在内存中为数组对象分配内存空间,并为每个数组 元素指定初始值。

    数组的初始化有以下两种方式。

    • 静态初始化:初始化时由程序员显式指定每个数组元素的初始值,由系统决定数组长度。
    • 动态初始化:初始化时程序员只指定数组长度,由系统为数组元素分配初始值。

    不管采用哪种方式初始化Java 数组,一旦初始化完成,该数组的长度就不可改变,Java 语言允许通过数组的length 属性来访问数组的长度。示例如下。

    
     
    1. public class ArrayTest
    2. {
    3. public static void main(String[] args)
    4. {
    5. // 采用静态初始化方式初始化第一个数组
    6. String[] books = new String[]
    7. { "1", "2", "3", "4"
    8. };
    9. // 采用静态初始化的简化形式初始化第二个数组
    10. String[] names =
    11. {
    12. "孙悟空",
    13. "猪八戒",
    14. "白骨精"
    15. };
    16. // 采用动态初始化的语法初始化第三个数组
    17. String[] strArr = new String[5];
    18. // 访问三个数组的长度
    19. System.out.println("第一个数组的长度:" + books.length);
    20. System.out.println("第二个数组的长度:" + names.length);
    21. System.out.println("第三个数组的长度:" + strArr.length);
    22. }
    23. }

    上面程序中的粗体字代码声明并初始化了三个数组。这三个数组的长度将会始终不变,程 序输出三个数组的长度依次为4 、3 、5 。

    前面已经指出,Java 语言的数组变量是引用类型的变量,books、names 、strArr 这三个变量,以及各自引用的数组在内存中的分配示意图如图1.1 所示。

    \

    从图1.1可以看出,对于静态初始化方式而言,程序员无须指定数组长度,指定该数组的 数组元素,由系统来决定该数组的长度即可。例如 books 数组,为它指定了四个数组元素,它 的长度就是4 ;对于names 数组,为它指定了三个元素,它的长度就是3 。

    执行动态初始化时,程序员只需指定数组的长度,即为每个数组元素指定所需的内存空间, 系统将负责为这些数组元素分配初始值。指定初始值时,系统将按如下规则分配初始值。

    • 数组元素的类型是基本类型中的整数类型(byte 、short、int 和long ),则数组元素的值是0 。
    • 数组元素的类型是基本类型中的浮点类型(float 、double ),则数组元素的值是0.0。
    • 数组元素的类型是基本类型中的字符类型(char ),则数组元素的值是'\u0000'。
    • 数组元素的类型是基本类型中的布尔类型(boolean),则数组元素的值是false 。
    • 数组元素的类型是引用类型(类、接口和数组),则数组元素的值是null 。

      Java 数组是静态的,一旦数组初始化完成,数组元素的内存空间分配即结束,程序只能改变数组元素的值,而无法改变数组的长度。

    需要指出的是,Java 的数组变量是一种引用类型的变量,数组变量并不是数组本身,它 只是指向堆内存中的数组对象。因此,可以改变一个数组变量所引用的数组,这样可以造成数 组长度可变的假象。假设,在上面程序的后面增加如下几行。

    
     
    1. // 让books 数组变量、strArr 数组变量指向names 所引用的数组
    2. books = names;
    3. strArr = names;
    4. System.out.println("--------------");
    5. System.out.println("books 数组的长度:" + books.length);
    6. System.out.println("strArr 数组的长度:" + strArr.length);
    7. // 改变books 数组变量所引用的数组的第二个元素值
    8. books[1] = "唐僧";
    9. System.out.println("names 数组的第二个元素是:" + books[1]);

    上面程序中粗体字代码将让books 数组变量、strArr 数组变量都指向names 数组变量所引 用的数组,这样做的结果就是books、strArr、names 这三个变量引用同一个数组对象。此时, 三个引用变量和数组对象在内存中的分配示意图如图1.2 所示。

    \

    从图1.2可以看出,此时 strArr、names 和books 数组变量实际上引用了同一个数组对象。 因此,当访问 books 数组、strArr 数组的长度时,将看到输出 3。这很容易造成一个假象:books 数组的长度从4 变成了3。实际上,数组对象本身的长度并没有发生改变,只是 books 数组变 量发生了改变。books 数组变量原本指向图 1.2下面的数组,当执行了books = names;语句之后,books 数组将改为指向图1.2 中间的数组,而原来books 变量所引用的数组的长度依然是4 。

    从图1.2 还可以看出,原来 books 变量所引用的数组的长度依然是 4 ,但不再有任何引用 变量引用该数组,因此它将会变成垃圾,等着垃圾回收机制来回收。此时,程序使用books、 names 和strArr 这三个变量时,将会访问同一个数组对象,因此把 books 数组的第二个元素赋 值为“唐僧”时,names 数组的第二个元素的值也会随之改变。

    与Java 这种静态语言不同的是,JavaScript 这种动态语言的数组长度是可以动态改变的,示例如下。

    
     
    1. <script type="text/javascript">
    2. var arr = [];
    3. document.writeln("arr的长度是:" + arr.length + "
      ");
    4. // 为arr 数组的两个数组元素赋值
    5. arr[2] = 6;
    6. arr[4] = "孙悟空";
    7. // 再次访问arr 数组的长度
    8. document.writeln("arr的长度是:" + arr.length + "
      ");
    9. </script>

    上面是一个简单的JavaScript 程序。它先定义了一个名为 arr的空数组,因为它不包含任 何数组元素,所以它的长度是0 。接着,为 arr数组的第三个、第五个元素赋值,该数组的长 度也自动变为5 。这就是JavaScript 里动态数组和Java 里静态数组的区别。

    基本类型数组的初始化

    对于基本类型数组而言,数组元素的值直接存储在对应的数组元素中,因此基本类型 数组的初始化比较简单:程序直接先为数组分配内存空间,再将数组元素的值存入对应内 存里。

    下面程序采用静态初始化方式初始化了一个基本类型的数组对象。

    
     
    1. public class PrimitiveArrayTest
    2. {
    3. public static void main(String[] args)
    4. {
    5. // 定义一个int[] 类型的数组变量
    6. int[] iArr;
    7. // 静态初始化数组,数组长度为4
    8. iArr = new int[]{2 , 5 , -12 , 20};
    9. }
    10. }

    上面代码的执行过程代表了基本类型数组初始化的典型过程。下面将结合示意图详细介绍这段代码的执行过程。

    执行第一行代码int[] iArr;时,仅定义一个数组变量,此时内存中的存储示意图如图1.4所示。

    \

    执行了int[] iArr; 代码后,仅在 main 方法栈中定义了一个 iArr 数组变量,它是一个引用类 型的变量,并未指向任何有效的内存,没有真正指向实际的数组对象。此时还不能使用该数组 对象。

    当执行iArr = new int[]{2,5,-12,20}; 静态初始化后,系统会根据程序员指定的数组元素来决 定数组的长度。此时指定了四个数组元素,系统将创建一个长度为4 的数组对象,一旦该数组 对象创建成功,该数组的长度将不可改变,程序只能改变数组元素的值。此时内存中的存储示 意图如图1.5 所示。

    静态初始化完成后,iArr 数组变量引用的数组所占用的内存空间被固定下来,程序员只能 改变各数组元素内的值。既不能移动该数组所占用的内存空间,也不能扩大该数组对象所占用 的内存,或缩减该数组对象所占用的内存。

    \

    对于程序运行过程中的变量,可以将它们形容为具体的瓶子——瓶子可以存储 水,而变量用于存储值,也就是数据。对于强类型语言如Java ,它有一个要求: 怎样的瓶子只能装怎样的水,也就是说,指定类型的变量只能存储指定类型的值。

    所有局部变量都是放在栈内存里保存的,不管其是基本类型的变量,还 是引用类型的变量,都是存储在各自的方法栈内存中的;但引用类型的变量所引用的对象(包 括数组、普通的Java 对象)则总是存储在堆内存中。

    对于Java 语言而言,堆内存中的对象(不管是数组对象,还是普通的 Java 对象)通常不 允许直接访问,为了访问堆内存中的对象,通常只能通过引用变量。这也是很容易混淆的地方。 例如,iArr 本质上只是main 栈区的引用变量,但使用 iArr.length 、iArr[2] 时,系统将会自动变 为访问堆内存中的数组对象。

    对于很多Java 程序员而言,他们最容易混淆的是:引用类型的变量何时只是栈内存中的 变量本身,何时又变为引用实际的Java 对象。其实规则很简单:引用变量本质上只是一个指 针,只要程序通过引用变量访问属性,或者通过引用变量来调用方法,该引用变量就会由它所 引用的对象代替。

    
     
    1. public class PrimitiveArrayTest2
    2. {
    3. public static void main(String[] args)
    4. {
    5. // 定义一个int[] 类型的数组变量
    6. int[] iArr = null;
    7. // 只要不访问iArr 的属性和方法,程序完全可以使用该数组变量
    8. System.out.println(iArr); //①
    9. // 动态初始化数组,数组长度为5
    10. iArr = new int[5];
    11. // 只有当iArr 指向有效的数组对象后,下面才可访问iArr 的属性
    12. System.out.println(iArr.length); //②
    13. }
    14. }

    上面程序中两行粗体字代码两次访问iArr 变量。对于①行代码而言,虽然此时的iArr 数 组变量并未引用到有效的数组对象,但程序在①行代码处并不会出现任何问题,因为此时并未 通过iArr 访问属性或调用方法,因此程序只是访问iArr 引用变量本身,并不会去访问iArr 所 引用的数组对象。对于②行代码而言,此时程序通过iArr 访问了length 属性,程序将自动变 为访问iArr 所引用的数组对象,这就要求iArr 必须引用一个有效的对象。

    有过一些编程经验,应该经常看到一个Runtime 异常: NullPointerException (空指针异常)。当通过引用变量来访问实例属性,或者调 用非静态方法时,如果该引用变量还未引用一个有效的对象,程序就会引发 NullPointerException 运行时异常。

    引用类型数组的初始化

    引用类型数组的数组元素依然是引用类型的,因此数组元素里存储的还是引用,它指向另一块内存,这块内存里存储了该引用变量所引用的对象(包括数组和Java 对象)。

    为了说明引用类型数组的运行过程,下面程序先定义一个Person 类,然后定义一个 Person[]数组,并动态初始化该Person[]数组,再显式地为数组的不同数组元素指定值。该程序代码如下。

    
     
    1. class Person
    2. {
    3. // 年龄
    4. public int age;
    5. // 身高
    6. public double height;
    7. // 定义一个info 方法
    8. public void info()
    9. {
    10. System.out.println("我的年龄是:" + age
    11. + ",我的身高是:" + height);
    12. }
    13. }
    14. public class ReferenceArrayTest
    15. {
    16. public static void main(String[] args)
    17. {
    18. // 定义一个students 数组变量,其类型是Person[]
    19. Person[] students;
    20. // 执行动态初始化
    21. students = new Person[2];
    22. System.out.println("students所引用的数组的长度是:"
    23. + students.length); //①
    24. // 创建一个Person 实例,并将这个Person 实例赋给zhang 变量
    25. Person zhang = new Person();
    26. // 为zhang 所引用的Person 对象的属性赋值
    27. zhang.age = 15;
    28. zhang.height = 158;
    29. // 创建一个Person 实例,并将这个Person 实例赋给lee 变量
    30. Person lee = new Person();
    31. // 为lee 所引用的Person 对象的属性赋值
    32. lee.age = 16;
    33. lee.height = 161;
    34. // 将zhang 变量的值赋给第一个数组元素
    35. students[0] = zhang;
    36. // 将lee 变量的值赋给第二个数组元素
    37. students[1] = lee;
    38. // 下面两行代码的结果完全一样,
    39. // 因为lee 和students[1]指向的是同一个Person 实例
    40. lee.info();
    41. students[1].info();
    42. }
    43. }

    上面代码的执行过程代表了引用类型数组的初始化的典型过程。下面将结合示意图详细介绍这段代码的执行过程。

    执行Person[] students;代码时,这行代码仅仅在栈内存中定义了一个引用变量,也就是一个指针,这个指针并未指向任何有效的内存区。此时内存中的存储示意图如图1.6 所示。

    \

    在图1.6中的栈内存中定义了一个 students 变量,它仅仅是一个空引用,并未指向任何有 效的内存,直到执行初始化,本程序对 students 数组执行动态初始化。动态初始化由系统为数 组元素分配默认的初始值null ,即每个数组元素的值都是 null 。执行动态初始化后的存储示意 图如图1.7 所示。

    从图1.7 中可以看出,students 数组的两个数组元素都是引用,而且这两个引用并未指 向任何有效的内存,因此,每个数组元素的值都是 null 。此时,程序可以通过students 来 访问它所引用的数组的属性,因此在①行代码处通过 students 访问了该数组的长度,此时 将输出2 。

    students 数组是引用类型的数组,因此 students[0] 、students[1] 两个数组元素相当于两个引 用类型的变量。如果程序只是直接输出这两个引用类型的变量,那么程序完全正常。但程序依 然不能通过students[0] 、students[1] 来调用属性或方法,因此它们还未指向任何有效的内存区, 所以这两个连续的Person 变量(students 数组的数组元素)还不能被使用。

    \

    接着,程序定义了zhang 和lee 两个引用变量,并让它们指向堆内存中的两个Person 对象,此时的zhang、lee 两个引用变量存储在 main 方法栈区中,而两个 Person 对象则存储在堆内存中。此时的内存存储示意图如图1.8 所示。

    \

    对于zhang、lee 两个引用变量来说,它们可以指向任何有效的Person 对象,而students[0] 、 students[1] 也可以指向任何有效的Person 对象。从本质上来看,zhang、lee、students[0] 、students[1] 能够存储的内容完全相同。接着,程序执行students[0] = zhang;和students[1] = lee; 两行代码, 也就是让zhang 和students[0] 指向同一个 Person 对象,让 lee 和students[1] 指向同一个Person 对象。此时的内存存储示意图如图1.9 所示。

    \

    从图1.9 中可以看出,此时 zhang 和students[0] 指向同一个内存区,而且它们都是引用类 型的变量,因此通过 zhang 和students[0] 来访问Person 实例的属性和方法的效果完全一样。不 论修改students[0] 所指向的 Person 实例的属性,还是修改 zhang 变量所指向的 Person 实例的 属性,所修改的其实是同一个内存区,所以必然互相影响。同理,lee 和students[1] 也是引用 到同一个Person 对象,也有相同的效果。

    前面已经提到,对于引用类型的数组而言,它的数组元素其实就是一个引用类型的变量, 因此可以指向任何有效的内存——此处“有效”的意思是指强类型的约束。比如,对 Person[] 类型的数组而言,它的每个数组元素都相当于Person 类型的变量,因此它的数组元素只能指 向Person 对象。

    展开全文
  • Java数组及其内存分配

    千次阅读 2016-08-30 10:22:25
    几乎所有的程序设计语言都支持数组。Java也不例外。当我们需要多个类型相同的变量的时候,...1.数组的初始化方式及其内存分配 对于Java数组的初始化,有以下两种方式,这也是面试中经常考到的经典题目: 静态初始

    几乎所有的程序设计语言都支持数组。Java也不例外。当我们需要多个类型相同的变量的时候,就考虑定义一个数组。在Java中,数组变量是引用类型的变量,同时因为Java是典型的静态语言,因此它的数组也是静态的,所以想要使用就必须先初始化为数组对象的元素分配空间)。

    1.数组的初始化方式及其内存分配

    对于Java数组的初始化,有以下两种方式,这也是面试中经常考到的经典题目:

    1. 静态初始化:初始化时由程序员显式指定每个数组元素的初始值,由系统决定数组长度,如:
      1 //只是指定初始值,并没有指定数组的长度,但是系统为自动决定该数组的长度为4
      2 String[] computers = {"Dell", "Lenovo", "Apple", "Acer"};  //①
      3 //只是指定初始值,并没有指定数组的长度,但是系统为自动决定该数组的长度为3
      4 String[] names = new String[]{"多啦A梦", "大雄", "静香"};  //②
    2. 动态初始化:初始化时由程序员显示的指定数组的长度,由系统为数据每个元素分配初始值,如:
      1 //只是指定了数组的长度,并没有显示的为数组指定初始值,但是系统会默认给数组数组元素分配初始值为null
      2 String[] cars = new String[4];  //③

       

    前面提到,因为Java数组变量是引用类型的变量,所以上述几行初始化语句执行后,三个数组在内存中的分配情况如下图所示:

    Java数组及其内存分配

    由上图可知,静态初始化方式,程序员虽然没有指定数组长度,但是系统已经自动帮我们给分配了,而动态初始化方式,程序员虽然没有显示的指定初始化值,但是因为Java数组是引用类型的变量,所以系统也为每个元素分配了初始化值null,当然不同类型的初始化值也是不一样的,假设是基本类型int类型,那么为系统分配的初始化值也是对应的默认值0。

    对于多维数组,假设有这么一段代码:

    1 int[][] nums = new int[2][2];2 nums[0][1] = 2;

    那么他在内存中的分配情况如下:

    Java数组及其内存分配

    由上图可知,并没有真正的多维数组,它的本质其实是一维数组。




    展开全文
  • Java 堆栈内存分配

    千次阅读 2017-10-12 16:37:24
    很多人在Java的书籍中看到过很多关于堆和栈内存的教程以及参考说明, 但是很难解释什么是程序的堆内存以及栈内存一: Java 堆内存空间Java程序运行时使用java Heap 内存为对象以及JRE类分配内存, 不论我们在何时创建...
  • Java内存分配

    千次阅读 2016-03-25 22:30:37
    而对于Java程序员来说,JVM自动进行内存管理,程序员不再需要为每一个new操作去写配对的delete/free代码,不容易出现内存泄露和内存溢出问题。 但是,正因为JVM帮我们管理了内存,一旦出现内存泄露或溢出问题,如果...
  • 再探Java内存分配

    千次阅读 多人点赞 2017-09-01 20:56:54
    我觉得:要回答这个问题不妨先搁置这个问题,先往这个问题的上游走走——Java内存分配。一提到内存分配,我想不少人的脑海里都会浮现一句话:引用放在栈里,对象放在堆里,栈指向堆。嗯哼,这句话听上去没有错;但是...
  • Java 内存分配全面浅析

    千次阅读 2016-05-13 11:56:05
    本文将由浅入深详细介绍Java内存分配的原理,以帮助新手更轻松的学习Java。这类文章网上有很多,但大多比较零碎。本文从认知过程角度出发,将带给读者一个系统的介绍。 进入正题前首先要知道的是Java程序运行在...
  • Java虚拟机内存分配策略

    千次阅读 2015-08-22 14:14:29
    1.JVM内存分配分区Java内存简单分为堆内存和非对内存 其中堆内存又可以分为新生代和老年代 新生代分为1个Eden区和2个Survivor区,如下图: 2.JVM垃圾回收种类分为2种,MinorGC和FullGC. MinorGC针对新生代的...
  • java内存分配分析

    万次阅读 多人点赞 2013-09-02 16:40:46
    java内存分配分析 本文将由浅入深详细介绍Java内存分配的原理,以帮助新手更轻松的学习Java。这类文章网上有很多,但大多比较零碎。本文从认知过程角度出发,将带给读者一个系统的介绍。 进入正题前首先要知道的是...
  • java内存管理 对象的分配与释放 分配: 程序员通过new为每个对象申请内存空间(基本类型除外下面有介绍,注意局部变量和对象的实例变量的区别)所有对象都在堆中分配空间; 释放: 对象的释放是由垃圾回收机制决定和...
  • java 内存分配 final关键字

    千次阅读 2014-02-24 21:50:23
    1. java内存分配 java程序在运行时,内存结构分为:方法区(method),栈内存(stack),堆内存(heap),本地方法栈(java中的jni调用)等。 jvm为每一个运行的线程分配一个堆栈(方法栈),堆栈以帧为...
  • 浅谈Java内存分配策略

    千次阅读 2016-09-11 20:43:51
    Java 内存分配策略 Java 程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、栈区和堆区。 静态存储区(方法区):...
  • Java内存管理-内存分配与回收

    千次阅读 2017-12-29 17:48:00
    Java内存管理-内存分配与回收
  • Java中ArrayList内存分配问题

    千次阅读 2015-10-08 21:42:56
    在深入学习Java语言时,当了解到对象在内存中的分配情况时,于是对动态数组ArrayList等集合类是如何分配内存空间的产生了兴趣,于是结合百度网友的回答问题,给大家一个分享咯。感谢网友 java软件工程师 文思海辉...
  • java数组内存分配

    千次阅读 2020-07-04 17:40:39
    声明一个长度为3的int类型数组:int[] arr=new int[3] 栈内存中存放局部变量,定义在方法中的变量,...数组在初始化时会为存储空间分配默认值。 整数:0 浮点数:0.0 字符:空字符 布尔:false 引用数据类型:null ...
  • java内存分配分析/栈内存、堆内存

    千次阅读 2015-09-06 21:58:07
    java内存分配分析 本文将由浅入深详细介绍Java内存分配的原理,以帮助新手更轻松的学习Java。这类文章网上有很多,但大多比较零碎。本文从认知过程角度出发,将带给读者一个系统的介绍。 进入正题前首先要知道的是...
  • java new 内存分配

    千次阅读 2017-05-16 14:40:41
    就在栈中为这个变量分配内存空间,当超过变量的作用域后(比如,在函数A中调用函数B,在函数B中定义变量a,变量a的作用域只是函数B,在函数B运行完以后,变量a会自动被销毁。分配给它的内存会被回收),
  • Java内存分配全面浅析

    万次阅读 多人点赞 2012-06-11 09:46:31
    本文将由浅入深详细介绍Java内存分配的原理,以帮助新手更轻松的学习Java。这类文章网上有很多,但大多比较零碎。本文从认知过程角度出发,将带给读者一个系统的介绍。  进入正题前首先要知道的是Java程序运行在...
  • Java核心-内存分配原理详解

    千次阅读 2013-11-22 17:54:59
    java内存分配与管理是java的核心技术之一,一般java内存分配时会涉及到以下区域: 寄存器:程序中无法控制(补充:c里是是可以通过register关键字将数据分配在寄存器上的)栈:存放基本类型的数据和对
  • Java内存分配与回收原理

    千次阅读 2017-04-25 13:37:48
     由于java虚拟机内部是自动分配和回收内存,因此,大部分同学的直观感受是内存是系统自动处理的,程序员无需关注内存问题,其实这是一种错误的观点。  虽然JVM有垃圾回收机制,但并不表示不会出现内存泄露等问题...
  • java内存分配初探

    千次阅读 2007-09-20 18:38:00
    在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配,当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间...
  • Java继承的内存分配

    千次阅读 多人点赞 2017-02-10 14:56:13
    今天,复习的是继承的内存分配。我们知道,Java内存可以初略分为堆、栈、方法区。 package sort; class Person{ public int age; public String name; public Person(){ System.out.println("父类"); say...
  • Java内存分配和管理

    千次阅读 2018-01-14 22:46:13
    Java内存分配和管理 Java内存分配时涉及的区域: 寄存器:在程序中无法控制; 栈:存放基本类型的数据和对象的引用,但是对象本身不存放在栈中,而是存放在堆中; 堆:存放用new产生的数据; 静态域:存放在...
  • Java内存分配与管理

    千次阅读 2016-11-24 12:02:50
    Java内存分配与管理是Java的核心技术之一,一般Java内存分配时会涉及到以下区域: 1.栈区:由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。  2.堆区:由程序员分配...
  • Java内存分配浅析1

    千次阅读 2017-07-01 15:54:07
    本文将由浅入深详细介绍Java内存分配的原理,以帮助新手更轻松的学习Java。这类文章网上有很多,但大多比较零碎。本文从认知过程角度出发,将带给读者一个系统的介绍。进入正题前首先要知道的是Java程序运行在JVM...
  • Java内存划分和分配

    千次阅读 2018-10-18 15:03:41
    在了解Java每个内存区域的功能之后,进一步分析Java对象如何的创建和对象的内存分配,以及如何访问对象中的内存。最后学习一下Java内存的分代划分和内存分配Java内存区域划分 首先通过一张图来看一下Java虚拟机...
  • Java深入 - Java 内存分配和回收机制

    万次阅读 多人点赞 2014-06-13 14:55:45
    Java的GC机制是自动进行的,和c语言有些区别需要程序员自己保证内存的使用和回收。

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 493,485
精华内容 197,394
关键字:

java无法分配内存

java 订阅