您的位置:首页 > 编程语言 > Java开发

java内存分配

2015-07-13 21:15 501 查看
1、概述

Java源代码被编译器编译成class文件。而并不是底层操作系统可以直接执行的二进制指令(比如Windows OS的.exe文件)。因此,我们需要有一种平台可以解释class文件并运行它。而做到这一点的正是Java 虚拟机(JVM)。JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。实际上,JVM是一种解释执行class文件的规范技术。各个提供商都可以根据规范,在不同的底层平台上实现不同的JVM。

一个运行时的Java虚拟机实例的天职是:负责运行一个java程序。当启动一个Java程序时,一个虚拟机实例也就诞生了。当该程序关闭退出,这个虚拟机实例也就随之消亡。如果同一台计算机上同时运行三个Java程序,将得到三个Java虚拟机实例。每个Java程序都运行于它自己的Java虚拟机实例中。

下面是JVM实现的基本结构框图。其中类装载子系统、运行时数据区、执行引擎等是JVM的必须要解决的几大问题。



垃圾回收器(Garbage Collection):负责回收堆内存(Heap)中没有被使用的对象,即这些对象已经没有被引用了。

类装载子系统(Classloader Sub-System):除了要定位和导入二进制class文件外,还必须负责验证被导入类的正确性,为类变量分配并初始化内存,以及帮助解析符号引用。

执行引擎(Execution Engine):负责执行那些包含在被装载类的方法中的指令。运行Java的每一个线程都是一个独立的虚拟机执行引擎的实例。从线程生命周期的开始到结束,他要么在执行字节码,要么在执行本地方法。一个线程可能通过解释或者使用芯片级指令直接执行字节码,或者间接通过JIT执行编译过的本地代码。

运行时数据区(Java Memory Allocation Area):又叫虚拟机内存或者Java内存,虚拟机运行时需要从整个计算机内存划分一块内存区域存储许多东西。例如:字节码、从已装载的class文件中得到的其他信息、程序创建的对象、传递给方法的参数,返回值、局部变量等等。

2、运行时数据区

运行时数据区即是java内存,而且数据区要存储的东西比较多,如果不对这块内存区域进行划分管理,会显得比较杂乱无章。程序喜欢有规律的东西,最讨厌杂乱无章的东西。 根据存储数据的不同,java内存通常被划分为5个区域:程序计数器(Program Count Register)、本地方法栈(Native Stack)、方法区(Methon Area)、栈(Stack)、堆(Heap)。

2.1 程序计数器(线程私有)

程序计数器(Program Count Register):又叫程序寄存器。JVM支持多个线程同时运行,当每一个新线程被创建时,它都将得到它自己的PC寄存器(程序计数器)。如果线程正在执行的是一个Java方法(非native),那么PC寄存器的值将总是指向下一条将被执行的指令,如果方法是 native的,程序计数器寄存器的值不会被定义。 JVM的程序计数器寄存器的宽度足够保证可以持有一个返回地址或者native的指针。

2.2 栈(线程私有)

JVM为每个新创建的线程都分配一个栈。也就是说,对于一个Java程序来说,它的运行就是通过对栈的操作来完成的。栈以帧为单位保存线程的状态。JVM对栈只进行两种操作:以帧为单位的压栈和出栈操作。我们知道,某个线程正在执行的方法称为此线程的当前方法。我们可能不知道,当前方法使用的帧称为当前帧。当线程激活一个Java方法,JVM就会在线程的Java堆栈里新压入一个帧,这个帧自然成为了当前帧。在此方法执行期间,这个帧将用来保存参数、局部变量、中间计算过程和其他数据。从Java的这种分配机制来看,堆栈又可以这样理解:栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有先进后出的特性。

当线程调用某个方法时,JVM会根据方法区中该方法的字节码组建一个栈帧。并将该栈帧压入Java栈中,方法执行完毕时,JVM会弹出该栈帧并释放掉。

注意,Java栈中的数据是线程私有的,一个线程是无法访问另一个线程的Java栈的数据。这也就是为什么多线程编程时,两个相同线程执行同一方法时,对方法内的局部变量时不需要数据同步的原因。

2.2.1栈帧

栈帧有三部分构成:局部变量区、操作数栈和帧数据区。在编译器编译Java代码时,就已经在字节码中为每个方法都设置好了局部变量区和操作数栈的数据和大小。并在JVM首次加载方法所属的Class文件时,就将这些数据放进了方法区。因此在线程调用方法时,只需要根据方法区中的局部变量区和操作数栈的大小来分配一个新的栈帧的内存大小,并堆入Java栈。

局部变量区: 用来存放方法中的所有局部变量值,包括传递的参数。这些数据会被组织成以一个字长(32bit或64bit)为单位的数组结构(以索引0开始)中。其中类型为int, float, reference(引用类型,记录对象在堆中地址)和returnAddress(一种JVM内部使用的基本类型)的值占用1个字长,而byte, char和shot会扩大成1个字长存储,long,double则使用2个字长。

操作数栈: 用来在执行指令的时候存储和使用中间结果数据。

帧数据区: 常量池的解析,正常方法返回以及异常派发机制的信息数据都存储在其中。

2.3 本地方法栈

本地方法栈与栈发挥作用非常相似,他们之间区别:

栈:为JVM执行java方法服务

本地方法栈:为JVM使用到的Native方法服务

2.4方法区--永久代(线程共享)

当虚拟机装载一个class文件时,它会从这个class文件包含的二进制数据中解析类型信息,然后把这些类型信息(包括类信息、常量、静态变量等)放到方法区中,该内存区域被所有线程共享。本地方法区存在一块特殊的内存区域,叫常量池(Constant
Pool)。

下面我们具体看看需要存储哪些信息?

●该类型的全限定名。如java.io.FileOutputStream

●该类型的直接超类的全限定名。如java.io.OutputStream

●该类型是类类型还是接口类型。

●该类型的访问修饰符(public、abstract、final)。

●任何直接超接口的全限定名的有序列表。如java.io.Closeable, java.io.Flushable。

●该类型的常量池。比如所有类型、方法和字段的符号。基本数据类型的直接数值等。

●字段信息。对类型中声明的每个字段,方法区中必须保存下面的信息。除此之外,这些字段在类或者接口中的声明顺序也必须保存。

字段名

字段的类型

字段的修饰符(public, private, protected, static, final, volatile, transient的某个子集)

●方法信息。和字段一样保存方法的相关信息。

方法名

方法的返回类型

方法的参数的数量和类型

方法的修饰符

方法的字节码

操作数栈和栈帧中局部变量的大小

异常表

●类静态变量。这里要注意:类中的静态变量时存放在方法区中的。并不是存放在堆中某一个该类型的对象中的。 也就是我们常说的“类静态变量属于类,而不属于对象”这句话的由来了。

●指向ClassLoader类的引用。任何类都需要被类装载器装入内存。如果是被用户自定义类装载器装载的,那么JVM必须在类型信息中存储对该装载器对象的引用。

●指向Class类的引用。对于每一个被装载的类型,虚拟机都会相应的为它创建一个java.lang.Class类的实例,而且虚拟机还必须以某种方式把这个实例和存储在方法区中的类型信息关联起来。 这就使得我们可以在程序运行时查看某个加载进内存的类的当前状态信息。也就是反射机制实现的根本。

●方法表。 为了能快速定位到类型中的某个方法。JVM对每个装载的类型都会建立一个方法表,用于存储该类型对象可以调用的方法的直接引用,这些方法就包括从超类中继承来的。而这张表与Java动态绑定机制( 参见《java动态绑定机制实现多态 》 )的实现是密切相关的。

2.5 堆(线程共享,虚拟机启动时创建)

Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域。在此区域的唯一目的就是存放对象实例,java 程序在运行时创建的所有类型对象和数组都存储在堆中。堆中对象存储的是该对象以及对象所有超类的实例数据(但不是静态数据)。JVM会根据new指令在堆中开辟一个确定类型的对象内存空间。但是堆中开辟对象的空间并没有任何人工指令可以回收,而是通过JVM的垃圾回收器负责回收。几乎所有的对象实例都是在这里分配内存,但是这个对象的引用却是在栈(Stack)中分配。因此,执行String
s = new String("s")时,需要从两个地方分配内存:在堆中为String对象分配内存,在栈中为引用(这个堆对象的内存地址,即指针)分配内存,如下图所示。



在堆中分配二维数组的实例:



堆中对象还必须有指向方法区中的类信息数据(见上面方法区)。为什么需要这个信息呢?因为当程序在运行时需要对象转型,那么JVM必须检查当前对象所属类型及父类的信息。以判断转型是否是合法的,而这一点也是instanceof操作符实现的基础。

当然,上述只是JVM的规范,具体堆的实现是由JVM设计者来决定。下面两幅图就直观的表现出了堆对象的不同实现结构:



java堆是垃圾收集器管理的主要区域,因此又称为“GC 堆”(Garbage Collectioned Heap)。现在的垃圾收集器基本都是采用的分代收集算法,所以Java堆还可以细分为:新生代(Young Generation)和老年代(Old Generation),如下图所示。

分代收集算法的思想:

第一种说法,用较高的频率对年轻的对象(young generation)进行扫描和回收,这种叫做minor collection,而对老对象(old generation)的检查回收频率要低很多,称为major collection。这样就不需要每次GC都将内存中所有对象都检查一遍,以便让出更多的系统资源供应用系统使用;

另一种说法,在分配对象遇到内存不足时,先对新生代进行GC(Young GC);当新生代GC之后仍无法满足内存空间分配需求时, 才会对整个堆空间以及方法区进行GC(Full GC)。



新生代(Young Generation)又分为:Eden区和Survivor区,Survivor区有分为From Space和To Space。Eden区是对象最初分配到的地方;默认情况下,From Space和To Space的区域大小相等。JVM进行Minor GC时,将Eden中还存活的对象拷贝到Survivor区中,还会将Survivor区中还存活的对象拷贝到Tenured区中。在这种GC模式下,JVM为了提升GC效率,
将Survivor区分为From Space和To Space,这样就可以将对象回收和对象晋升分离开来。新生代的大小设置有2个相关参数:

老年代(Old Generation): 当 OLD 区空间不够时, JVM 会在 OLD 区进行 major collection ;完全垃圾收集后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现"Out of memory错误" 。

2.6 常量池

虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集和,包括直接常量(string,integer和floating point常量)和对其他类型,字段和方法的符号引用。对于String常量,它的值是在常量池中的。而JVM中的常量池在内存当中是以表的形式存在的,对于String类型,有一张固定长度的CONSTANT_String_info表用来存储文字字符串值,注意:该表只存储文字字符串值,不存储符号引用。

3、JVM运行程序过程解析

通过编译器将下面的代码编译成edu/hr/jvm/Test.class 和 edu/hr/jvm/bean/Act.class。然后开始启动JVM:

//源代码 Test.java
package edu.hr.jvm;

import edu.hr.jvm.bean;
public class Test{
public static void main(String[] args){
Act act=new Act();
act.doMathForever();
}
}

//源代码 Act.java
package edu.hr.jvm.bean;

public class Act{
public void doMathForever(){
int i=0;
i+=1;
i*=2;

}
}
(1) 首先OS会创建一个JVM实例(进行必要的初始化工作,比如初始启动类装载器,初始运行时内存数据区等)。

(2) 然后通过自定义类装载器 加载Test.class。并提取Test.class字节码中的信息存放在方法区中(具体的信息在上面已经讲过)。下图展示了方法区中的Test类信息,其中在常量池中有一个符号引用"Act"(注意:这个引用目前还没有真正的类信息的内存地址)。



(3) 接着JVM开始从Test类的main字节码处开始解释执行。在运行之前,会在Java栈中组建一个main方法的栈帧 。如上图Java栈所示。JVM需要运行任何方法前,通过在Java栈中压入一个帧栈。在这个帧栈的内存区域中进行计算。

(4) 现在可以开始执行main方法的第一条指令——JVM需要为常量池的第一项的类(符号引用Act)分配内存空间。但是Act类此时还没有加载进JVM(因为常量池目前只有一个"Act"的符号引用)。

(5) JVM加载进Act.class,并提取Act类信息放入方法区中。见上图方法区所示,然后以一个直接指向方法区Act类信息的直接引用替换开始在常量池中的符号引用"Act",这个过程就是常量池解析 。以后就可以直接访问Act的类信息了。

(6) 此时JVM可以根据方法区中的Act类信息,在堆中开辟一个Act类对象 act。见上图堆所示。

(7) 接着开始执行main方法中的第二条指令调用doMathForever。这个可以通过堆中act对象所指的方法表中查找,然后定位到方法区中的Act类信息中的doMathForever方法字节码。在运行之前,仍然要组建一个doMathForever栈帧压入Java栈,如上图所示。(注意:JVM会根据方法区中doMathForever的字节码来创建栈帧的局部变量区和操作数栈的大小)

(8) 接下来JVM开始解释运行Act.doMathForever字节码的内容了。

参考来源:

JVM 常量池理解

JVM的常量池

Java 虚拟机体系结构

java内存分配和String类型的深度解析

Java 内存分配全面浅析
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: