Rumah  >  Artikel  >  Java  >  探寻Java内存分配

探寻Java内存分配

一个新手
一个新手asal
2017-09-06 10:28:121258semak imbas

引子

这两天有个同事抓耳挠腮地纠结:Java到底是值传递还是引用传递。百思不得其姐,他将这个问题抛给大家一起讨论。于是,有的人说传值,有的人说传引用;不管哪方都觉得自己的理解是正确无误的。我觉得:要回答这个问题不妨先搁置这个问题,先往这个问题的上游走走——Java内存分配。一提到内存分配,我想不少人的脑海里都会浮现一句话:引用放在栈里,对象放在堆里,栈指向堆。嗯哼,这句话听上去没有错;但是我们继续追问一下:这个栈是什么栈?是龙门客栈么?非也!它其实是Java虚拟机栈。呃,到了此处,好学的童鞋忍不住要追问了:啥是Java虚拟机栈呢?不急,我们一起来瞅瞅。


JVM的生命周期

我们知道:每个Java程序都运行于在Java虚拟机上;也就是说:一个运行时的Java虚拟机负责运行一个Java程序。当启动一个Java程序时,一个虚拟机实例也就随之诞生了;当该程序执行完毕后这个虚拟机实例也就随之消亡。例如:在一台计算机上同时运行五个Java程序,那么系统将提供五个Java虚拟机实例;每个Java程序独自运行于它自己所对应的Java虚拟机实例中。

Java虚拟机中有两种线程,即:守护线程与非守护线程。守护线程通常由虚拟机自身使用,比如执行垃圾收集的线程。非守护线程,通常指的是我们自己的线程。当程序中所有的非守护线程都终止时,虚拟机实例将自动退出。


JVM运行时数据区

既然Java虚拟机负责执行Java程序,那我们就先来看看Java虚拟机体系结构,请参见下图:


这里写图片描述

在这里可以看到:class文件由类加载器载入JVM中运行。此处,我们重点关注蓝色线框中JVM的Runtime Data Areas(运行时数据区),它表示JVM在运行期间对内存空间的划分和分配。在该数据区内分为以下几个主要区域:Method Area(方法区),Heap(堆),Java Stacks(Java 栈),Program Counter Register(程序计数器),Native Method Stack(本地方法栈),现对各区域的主要作用及其特点作如下详细介绍。

Method Area(方法区)

Method Area(方法区)是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、编译器编译后的代码等数据。根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError(OOM)异常。为进一步了解Method Area(方法区),我们来看在该区域内包含了哪些具体组成部分。

(1) 运行时常量池

Class文件中除了有类的版本、字段、方法、接口等描述等与类紧密相关的信息之外,还有一个常量池用于存放编译期生成的各种字面量和符号引用;该常量池将在类加载后被存放到方法区的运行时常量池中。换句话说:在运行时常量池中存放了该类使用的常量的一个有序集合,它在java程序的动态连接中起着非常重要的作用。在该集合中包括直接常量(string,integer和,floating point等)和对其他类型、字段和方法的符号引用。外界可通过索引访问运行时常量池中的数据项,这一点和访问数组非常类似。当然,运行时常量池是方法区的一部分,它也会受到方法区内存的限制,当运行时常量池无法再申请到内存时也会抛出OutOfMemoryError(OOM)异常。

(2) 类型信息

在该部分中包括:

  • 类型的完全限定名

  • 类型的直接超类的全限定名

  • 类型是类类型还是接口类型

  • 类型的访问修饰符(public、abstract、final等)

  • 直接超接口的全限定名的有序列表

(3) 字段信息

字段信息用于描述该类中声明的所有字段(局部变量除外),它包含以下具体信息:

  • 字段名

  • 字段类型

  • 字段的修饰符

  • 字段的顺序

(4) 方法信息

方法信息用于描述该类中声明的所有方法,它包含以下具体信息:

  • 方法名

  • 方法的返回类型

  • 方法输入参数的个数,类型,顺序

  • 方法的修饰符

  • 操作数栈

  • 在帧栈中的局部变量区的大小

(5) 类变量

该部分用于存放类中static修饰的变量。

(6) 指向类加载器的引用

类由类加载器加载,JVM会在方法区保留指向该类加载器的引用。

(7) 指向Class实例的引用

在类被加载器加载的过程中,虚拟机会创建一个代表该类的Class对象,与此同时JVM会在方法区保留指向该Class的引用。

Program Counter Register(程序计数器)

Program Counter Register(程序计数器)在Runtime Data Areas(运行时数据区)只占据非常小的内存空间,它用于存储下一条即将执行的字节码指令的地址。

Java Stacks(Java 栈)

Java Stacks(Java 栈)亦称为虚拟机栈(VM Stack),也就是我们通常说的栈。它用于描述的Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。Java Stacks(Java 栈)的生命周期与线程相同;当一个线程执行完毕那么该栈亦被清空。

Native Method Stack(本地方法栈)

Native Method Stack(本地方法栈)与Java Stacks(Java 栈)非常类似,它用于存储调用本地方法(C/C++)所涉及到的局部变量表、操作栈等信息。

Heap(堆)

Heap(堆)在虚拟机启动时创建,用于存放对象实例,几乎所有的对象实例都在这里分配内存。所以,Heap(堆)是Java 虚拟机所管理的内存中最大的一块,也是垃圾回收器管理的重点区域。

小结

在此对JVM运行时数据区做一个小结:

  • Method Area(方法区)和Heap(堆)是被所有线程共享的内存区域。

  • Java Stacks(Java 栈)和Program Counter Register(程序计数器)以及Native Method Stack(本地方法栈)是各线程私有的内存区域。

  • 创建一个对象,该对象的引用存放于Java Stacks(Java 栈)中,真正的对象实例存放于Heap(堆)中。这也是大家常说的:栈指向堆。

  • 除了刚才提到的JVM运行时数据区所涉及到的内存以外,我们还需要关注直接内存(Direct Memory)。请注意:直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError(OOM)异常出现。比如,在使用NIO时它可以使用Native 函数库直接分配堆外内存,然后通过存储在Java 堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。类似的操作,可避免了在Java 堆和Native 堆中来回复制数据,从而提高性能。


Java调用方法时的参数传递机制

在调用Java方法传递参数的时候,到底是传值还是传引用呢?面对众多的争论,我们还是来瞅瞅代码,毕竟代码不会说谎。我们先来看一个非常简单的例子:交换两个int类型的数据,代码如下:

package cn.com;/**
 */public class TestMemory {

    public static void main(String[] args) {
        TestMemory testMemory=new TestMemory();        
        int number1=9527;        
        int number2=1314;
        System.out.println("main方法中,数据交换前:number1="+number1+" , number2="+number2);
        testMemory.swapData(number1, number2);
        System.out.println("main方法中,数据交换后:number1="+number1+" , number2="+number2);
    }    private void swapData(int a,int b) {
        System.out.println("swapData方法中,数据交换前:a="+a+" , b="+b);        
        int temp=a;
        a=b;
        b=temp;
        System.out.println("swapData方法中,数据交换后:a="+a+" , b="+b);
    }

}

我们在main方法中声明的两个变量number1=9527 , number2=1314;然后将这两个数作为参数传递给了方法swapData(int a,int b),并在该方法内交换数据。至于代码本身无需再过多的解释了;不过,请思考输出的结果是什么?在您考虑之后,请参见如下打印信息:

main方法中,数据交换前:number1=9527 , number2=1314
 swapData方法中,数据交换前:a=9527 , b=1314
 swapData方法中,数据交换后:a=1314 , b=9527
 main方法中,数据交换后:number1=9527 , number2=1314

嗯哼,这和你想的一样么?为什么会是这样呢?还记得刚才讨论Java Stacks(Java 栈)时说的么:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。结合示例的代码:main( )方法在一个栈帧中,swapData( )在另外一个栈帧中;两者彼此独立互不干扰。在main( )中调用swapData( )传入参数时它的本质是:将实际参数值的副本(复制品)传入其它方法内而参数本身不会受到任何影响。也就是说,这number1和number2这两个变量仍然存在于main( )方法所对应的栈帧中,但number1和number2这两个变量的副本(即int a和int b)存在于swapData( )方法所对应的栈帧中。故,在swapData( )中交换数据,对于main( )是没有任何影响的。这就是Java中调用方法时的传值机制——值传递。

嗯哼,刚才这个例子是关于基本类型的参数传递。Java对于引用类型的参数传递一样采用了值传递的方式。我们在刚才的示例中稍加改造。首先,我们创建一个类,该类有两个变量number1和number2,请看代码:

package cn.com;/**
 */public class DataObject {

    private int number1;    
    private int number2;    
    public int getNumber1() {        
    return number1;
    }    public void setNumber1(int number1) {        
            this.number1 = number1;
    }    public int getNumber2() {       
            return number2;
    }    public void setNumber2(int number2) {        
            this.number2 = number2;
    }

}

好了,现在我们来测试交换DataObject类对象中的两个数据:

package cn.com;/**
  */public class TestMemory {

    public static void main(String[] args) {
        TestMemory testMemory=new TestMemory();
        DataObject dataObject=new DataObject();
        dataObject.setNumber1(9527);
        dataObject.setNumber2(1314);
        System.out.println("main方法中,数据交换前:number1="+dataObject.getNumber1()+" , number2="+dataObject.getNumber2());
        testMemory.swapData(dataObject);
        System.out.println("main方法中,数据交换后:number1="+dataObject.getNumber1()+" , number2="+dataObject.getNumber2());
    }    private void swapData(DataObject dataObject) {
        System.out.println("swapData方法中,数据交换前:number1="+dataObject.getNumber1()+" , number2="+dataObject.getNumber2());        
        int temp=dataObject.getNumber1();
        dataObject.setNumber1(dataObject.getNumber2());
        dataObject.setNumber2(temp);
        System.out.println("swapData方法中,数据交换后:number1="+dataObject.getNumber1()+" , number2="+dataObject.getNumber2());

    }

}

简单地描述一下代码:在main( )中定义一个DataObject类的对象并为其number1和number2赋值;然后调用swapData(DataObject dataObject)方法,在该方法中交换数据。请思考输出的结果是什么?在您考虑之后,请参见如下打印信息:

main方法中,数据交换前:number1=9527 , number2=1314
 swapData方法中,数据交换前:number1=9527 , number2=1314
 swapData方法中,数据交换后:number1=1314 , number2=9527
 main方法中,数据交换后:number1=1314 , number2=9527

嗯哼,为什么是这样呢?我们通过DataObject dataObject=new DataObject();创建一个对象;该对象的引用dataObject存放于栈中,而该对象的真正的实例存放于堆中。在main( )中调用swapData( )方法传入dataObject作为参数时仍然传递的是值,只不过稍微特殊点的是:该值指向了堆中的实例对象。好了,再结合栈帧来梳理一遍:main( )方法存在于与之对应的栈帧中,在该栈帧中有一个变量dataObject它指向了堆内存中的真正的实例对象。swapData( )收到main( )传递过来的变量dataObject时将其存放于其本身对应的栈帧中,但是该变量依然指向堆内存中的真正的实例对象。也就是说:main( )方法中的dataObject和swapData( )方法中的dataObject指向了堆中的同一个实例对象!所以,在swapData( )中交换了数据之后,在main( )会体现交换后的变化。在此,我们可以进一步的验证:在该swapData( )方法的最后一行添加一句代码dataObject=null ;我们发现打印信息并没有任何变化。因为这句代码仅仅使得swapData( )所对应的栈帧中的dataObject不再指向堆内存中的实例对象但不会影响main( )所对应的栈帧中的dataObject依然指向堆内存中的实例对象。

通过这两个示例,我们进一步验证了:Java中调用方法时的传递机制——值传递。当然,有的人说:基础类型传值,对象类型传引用。其实,这也没有什么错,只不过是表述方式不同罢了;只要明白其中的道理就行。如果,有些童鞋非纠缠着个别字眼不放,那我只好说:PHP是世界上最好的语言。

Atas ialah kandungan terperinci 探寻Java内存分配. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn