Java虚拟机所管理的内存将包括以下几个运行时数据区域 

Java内存区域划分-LMLPHP

  • 线程共享区:方法区、堆

  • 线程私有区:虚拟机栈、本地方法栈、程序计数器

 

程序计数器:

是一块较小的内存空间,可以看作当前线程所执行的字节码的行号指示器。

java多线程就是通过对线程的轮流切换并分配处理器的执行时间的方式来实现的,在任意时刻,一个处理器都只会执行某一个线程中的指令。所以每条线程为了切换后能恢复到正确的执行位置,每条线程都会有一个独立的程序计数器,各条线程之间互不影响,独立存储。

 

JAVA虚拟机栈:

线程私有,生命周期和线程一样。是描述java方法执行的内存模型。在每个方法执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。局部变量表包括了编译器可知的各种基本数据类型、对象引用和returnAddress类型(指向了一个字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量空间,其余的数据类型只占用1个。局部变量表所占用的内存在编译器就完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

 

本地方法栈:

与java虚拟机栈发挥的作用十分相似,区别是java虚拟机栈是为虚拟机执行java方法的,本地方法栈是为虚拟机执行native方法的。在本地方法栈中使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(如sun hotspot)直接把本地方法栈和虚拟机栈合二为一。

 

JAVA堆:

是java虚拟机所管理的内存中最大的一块。java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。所有的对象实例和数组都要在堆上分配。java堆是垃圾收集器管理的主要区域,因此很多时候也被称作GC堆。

从内存回收的角度来看,由于现在收集器基本都采用分代手机算法,所以java堆还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。

从内存分配的角度来看,线程共享的java堆中可能划分出多个线程私有的分配缓冲区。不论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步的划分目的是为了更好地回收内存和更快地分配内存。java堆可以处于物理上的不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

 

方法区:

方法区与java堆一样。是各个线程所共享的内存区域,它用于存储class二进制文件,包含了虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然java虚拟机规范把方法去描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与java堆区分开来。对于习惯在hotspot虚拟机上开发的开发者来说,更愿意把方法区叫做永久堆。本质上并不一样,仅仅是因为hotspot设计团队把GC分堆收集扩展至方法区,或者说使用永久堆来实现方法区而已,这样hotspot的垃圾收集器可以像管理java堆一样管理这部分内存,能够省去专门为方法去编写内存管理代码的工作。但是这样更容易出现内存溢出的问题。永久堆有上限。而其他不存在永久堆的虚拟机只要没有触碰到进程可用内存上限,就不会出现问题。在jdk7中,已经将放在永久堆的字符常量池移出。

运行时常量池是方法区的一部分,用于存放编译器生成的各种字面量和符号引用。运行时常量池相对于class文件常量池的另外一个重要特征是具备动态性。

需要特别注意的是:

方法区是线程安全的。由于所有的线程都共享方法区,所以,方法区里的数据访问必须被设计成线程安全的。例如,假如同时有两个线程都企图访问方法区中的同一个类,而这个类还没有被装入JVM,那么只允许一个线程去装载它,而其它线程必须等待 !

 

常量池:

常量池是方法区的一部分内存。常量池在编译期间就将一部分数据存放于该区域,包含基本数据类型如int、long等以final声明的常量值,和String字符串、特别注意的是对于方法运行期位于栈中的局部变量String常量的值可以通过 String.intern()方法将该值置入到常量池中。

下面我们来看一个例子:

声明一个类:

public class A {
    public final String tempString="world";//这里可以把final去掉,结果等同!!
    public final char[] charArray="Hello".toCharArray();
    public char[] getCharArray() {
        return charArray;
    }
    public String getTempString() {
        return tempString;
    }
}

创建测试类:

public class TestA {
    public static void main(String[] args) {
        A a1=new  A();
        A a2=new A();

        System.out.println(a1.charArray==a2.charArray);
        System.out.println(a1.tempString==a2.tempString);
    }
}

输出结果:

false
true

要想明白上面字符串对比为什么输出为true你必须知道:

Java内存区域划分-LMLPHP

该图片截自《深入理解Java虚拟机》

输出为true的原因:

那么为什么final类型的字符数组就不为true了呢??

申明(不管是通过new还是通过直接写一个数组)一个数组其实在Java中就等同创建了一个对象,即每次创建类的对象都会自动创建一个新的数组空间。

其中要注意的是:常量池中存储字符数组存储的是每个字符或者字符串。

 

静态域:

位于方法区的一块内存。存放类中以static声明的静态成员变量

 

全局变量与局部变量

局部变量:

在方法中声明的变量,每当程序调用方法时,系统都会为该方法建立一个方法栈,其所在方法中声明的变量就放在方法栈中,当方法结束系统会释放方法栈,其对应在该方法中声明的变量随着栈的销毁而结束,这就局部变量只能在方法中有效的原因。

在方法中生明的变量可以是基本类型的变量,也可以是引用类型的变量:

全局变量:

在类中声明的变量是成员变量,也叫全局变量,放在堆中的。

同样在类中声明的变量即可是基本类型的变量,也可是引用类型的变量:

 

堆、栈和方法区存储数据的区别

栈:为即时调用的方法开辟空间,存储局部变量值(基本数据类型),局部变量引用。当一段代码或者一个方法调用完毕后,栈中为这段代码所提供的基本数据类型或者对象的引用立即被释放;注意:局部变量必须手动初始化。


堆:存放引用类型的对象,即new出来的对象、数组值、类的非静态成员变量值(基本数据类型)、非静态成员变量引用。其中非静态成员变量在实例化时开辟空间初始化值。


方法区:存放class二进制文件。包含类信息、静态变量,常量池(String字符串和final修饰的常量值等),类的版本号等基本信息。静态成员变量是在方法区的静态域里面,而静态成员方法是在方法区的class二进制信息里面(.class文件和方法区里面的二进制信息不一样,读取.class文件按照虚拟机需要的格式存储在方法区,这种格式包括数据结构方面。)因为是共享的区域,所以如果静态成员变量的值或者常量值(String类型的值能够非修改)被修改了直接就会反应到其它类的对象中。

 

最后,我们用以上知识来回答几个问题

 

不同线程调用方法为什么是线程安全的?

任何方法每次被线程调用,都会在栈中开辟新的空间。同一方法的不同线程执行,方法与方法之间互不影响。全局变量因为是存在堆区的对象中,所以会互相干扰。

 

成员变量存储在哪儿?

静态成员变量存储在方法区,非静态成员变量存储在堆区。

 

为什么局部变量不能够static修饰?

局部变量存储在栈区,在方法调用时不能够自动初始化必须由程序员手动初始化,否则会报错,归根结底是由于static变量和局部变量存储的位置不一样。

 

10-07 13:12