JAVA 虚拟机深入研究(三)——Java内存区域

时间:2022-12-27 12:38:56

 

JAVA 虚拟机深入研究(一)——关于Java的一些历史

 

JAVA 虚拟机深入研究(二)——JVM虚拟机发展以及一些Java的新东西

 

JAVA 虚拟机深入研究(三)——Java内存区域

 

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的围城,城外的人想进去,城里的人想出来。

      Java运行的时候会把内存分为若干个,他们各有各的用途,每块区域的创建和销毁都是相对独立的,有的跟虚拟机一起混,有的则抱着用户的大腿同生共死。

     按照第七版的《Java虚拟机规范》规定,JVM所管理的内存包括以下几个区域。

    JAVA 虚拟机深入研究(三)——Java内存区域

 

1.1 程序计数器(Program Counter Register)

     这块内存区比较小,按照概念模型(各种虚拟机可能不按照概念模型,而采用自己的更高效的方式来搞定),字节码解释器就通过这个计数器来寻去下一个字节码指令,分支,循环,跳转,异常处理,线程回复等都需要这个东西来完成。

     Java虚拟机多线程采用时间片轮转的方式,所以同一时刻,一个内核只能执行一个线程中的指令。因此,为了当获取到时间片之后线程可以正常恢复执行,每个线程都需要有自己的线程计数器,这种内存称为“线程私有”的内存。

    如果线程执行的是Java方法,这个计数器就存储正在执行的字节码指令地址,如果线程执行的是Native方法,那么这个计数器就是空的(undefined)。

    这个内存区域是唯一一个在Java虚拟机规范中没有规定任何OOM(OutOfMemoryError)情况的区域。

1.2 Java虚拟机栈(Java Virtual Machine Stacks)

    跟计数器一样,这个区域也是线程私有的,跟线程同生共死。这个区域描述了Java方法执行的内存模型:没个方法执行的时候创建一个栈帧(回头再讲),存储局部变量表,操作数栈,动态链接,方法出口等信息。一个方法开始执行,栈帧就会在虚拟机栈中入栈,方法执行完成后出栈。Java虚拟机规范中规定,如果线程请求的栈深大鱼虚拟机允许的深度,就跑出*Error,如果虚拟机栈允许动态扩展(多数都能,也可以固定),那就扩展,如果扩展的时候发现内存不足,跑出OutOfMemoryError。

    有的人会把Java的内存草草的分为两部分,一部分叫堆,另一部分叫栈,这个分法太粗糙,实际上要复杂很多,不过其中的栈指的就是这个虚拟机栈里面所说的局部变量表。

      局部变量表存储了基本数据类型(八种),returnAddress类型(指向一个字节码指令地址)和引用类型,引用类型包含两种种,第一可能是指向对象起始位置的引用指针,第二可能是指向一个代表对象句柄或者其他与这个对象相关的位置。

     除了两个64位的数据类型(long,double)占用两个局部变量空间(Slot),其余的都只占用一个。局部变量表的内存空间在编译器就已经分配完成了,运行时不会改变局部变量表的大小。

1.3 本地方法栈(Native Method Stack)

      跟虚拟机栈功能类似,区别在于这个栈服务于Native方法,而虚拟机栈服务于Java方法(字节码)。规范中没有对这个区域的实现做出明确的要求,所以相应的这个区域的实现就显得比较*,像HotSpot就很干脆的直接和虚拟机栈合二为一。

1.4 Java堆(Java Heap)

     多数的应用中堆都是虚拟机中最大的一块内存,按照虚拟机规范要求,所有的对象实例以及数组都要在堆上分配,随着JIT编译器的发展以及逃逸分析技术的成熟,栈上分配、标量替换优化技术就会带来一些新的变化,所有的对象都分配在对上就不是那么绝对了。

     Java的堆是垃圾回收的大头,因此有时候会称为GC堆。由于现在多数GC都采用的是分代垃圾回收算法,所以对堆还可以细分,比如新生代,老年代。要是再详细一点,就有Eden空间,From Survivor,To Survivor等(前面提到过)。

      规范中并没有要求堆在内存上的物理连续性,只要保证逻辑连续即可,设计实现的时候可以固定大小,也可以扩展,现在主流的都可以扩展(利用-Xmx和-Xms控制),如果碰到扩展时内存不够的情况,就会抛出OutOfMemoryError。

1.5 方法区(Method Area)

      这个区域也是线程共享的区域,用于存储已经被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。规范中对这个区域的描述是,方法区是堆的一部分,但是它有个名字叫做非堆(Non-Heap),目的是用于区分堆。

      有些在HotSpot上搞事情的人喜欢吧方法区和永久代(Permanent Generation)画等号,但是从本质上说这两个不是一回事,只不过HopSpot虚拟机的团队选择用永久代来实现方法区而已,省掉了为方法区单独编写内存回收代码的工作。

      原则上,方法区的实现不受规范约束,但是从设计角度说,这个设计并不是好设计,由于方法区占用了永久代,导致永久代更容易出现内存溢出问题(永久代有最大内存先知 -XX:MaxPermSize),有一些方法,比如String.intern(),会因为这个原因而导致不同的虚拟机出现不一样的表现,目前JDK1.7中字符串常量池已经从永久代移出了。

      方法区无法满足内存分配需求时,将会抛出OutOfMemoryError。

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

     这个部分是方法区的一部分。Class文件里除了有版本,字段,方法,接口等描述信息之外,还有一个是常量池(Constant Pool Table),用于存放在编译期生成的各种字面量和符号引用,并在类加载后进入方法区的常量池。

     Java虚拟机规范对于Class的格式有严格要求,唯独对这个运行时常量池没有细节要求,不同的提供商可以按照自己的需要来实现这块区域。

     这个运行时常量池具备动态性,Java并没要求常量只能在编译期产生,运行时也可以进入常量池,典型应用就是String.intern().

     由于常量池就在方法区内部,因此也受到内存的限制,一旦申请不到内存就OutOfMemoryError。

1.7 直接内存(Direct Memory)

      Java虚拟机规范中并没有对这个部分作出规范。

      JDK 1.4中新加入了NIO,利用它可以使用Native函数库直接分配内存,然后通过一个存在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样,在一些场景中可以显著地提高性能,可以避免在Java堆中和Native堆中来回复制数据。