文档章节

深入JVM系列(一)之内存模型与内存分配

郑加威
 郑加威
发布于 2017/04/17 11:45
字数 5921
阅读 353
收藏 1

一、JVM内存区域划分

大多数 JVM 将内存区域划分为 Method Area(Non-Heap),Heap,Program Counter RegisterJava Method Stack,Native Method StackDirect Memomry(注意 Directory Memory 并不属于 JVM 管理的内存区域)。前三者一般译为:方法区、堆、程序计数器。但不同的资料和书籍上对于后三者的中文译名不尽相同,这里将它们分别译作:Java 方法栈、原生方法栈和直接内存区。对于不同的 JVM,内存区域划分可能会有所差异,比如 Hot Spot 就将 Java 方法栈和原生方法栈合二为一,我们可以统称为方法栈(Method Stack)

首先我们熟悉一下一个一般性的 Java 程序的工作过程。一个 Java 源程序文件,会被编译为字节码文件(以 class 为扩展名),然后告知 JVM 程序的运行入口,再被 JVM 通过字节码解释器加载运行。那么程序开始运行后,都是如何涉及到各内存区域的呢?

概括地说来,JVM 每遇到一个线程,就为其分配一个程序计数器、Java 方法栈和原生方法栈。当线程终止时,两者所占用的内存空间也会被释放掉。栈中存储的是栈帧,可以说每个栈帧对应一个“运行现场”。在每个“运行现场”中,如果出现了一个局部对象,则它的实例数据被保存在堆中,而类数据被保存在方法区。

 

二、指令、方法与属性

 

在讲各部分之前,我们首先要搞清楚的是什么是数据以及什么是指令。然后要搞清楚对象的方法和对象的属性分别保存在哪里。

1)方法本身是指令的操作码部分,保存在Stack中;

2)方法内部变量作为指令的操作数部分,跟在指令的操作码之后,保存在Stack中(实际上是简单类型保存在Stack中,对象类型在Stack中保存地址,在Heap 中保存值);上述的指令操作码和指令操作数构成了完整的Java 指令。

3)对象实例包括其属性值作为数据,保存在数据区Heap 中。

非静态的对象属性作为对象实例的一部分保存在Heap 中,而对象实例必须通过Stack中保存的地址指针才能访问到。因此能否访问到对象实例以及它的非静态属性值完全取决于能否获得对象实例在Stack中的地址指针。

 

非静态方法和静态方法的区别:

非静态方法有一个和静态方法很重大的不同:非静态方法有一个隐含的传入参数,该参数是JVM给它的,和我们怎么写代码无关,这个隐含的参数就是对象实例在Stack中的地址指针。因此非静态方法(在Stack中的指令代码)总是可以找到自己的专用数据(在Heap 中的对象属性值)。当然非静态方法也必须获得该隐含参数,因此非静态方法在调用前,必须先new一个对象实例,获得Stack中的地址指针,否则JVM将无法将隐含参数传给非静态方法。

静态方法无此隐含参数,因此也不需要new对象,只要class文件被ClassLoader load进入JVM的Stack,该静态方法即可被调用。当然此时静态方法是存取不到Heap 中的对象属性的。

 

总结一下该过程:当一个class文件被ClassLoader load进入JVM后,方法指令保存在Stack中,此时Heap 区没有数据。然后程序技术器开始执行指令,如果是静态方法,直接依次执行指令代码,当然此时指令代码是不能访问Heap 数据区的;如果是非静态方法,由于隐含参数没有值,会报错。因此在非静态方法执行前,要先new对象,在Heap 中分配数据,并把Stack中的地址指针交给非静态方法,这样程序技术器依次执行指令,而指令代码此时能够访问到Heap 数据区了。

 

静态属性和动态属性:

前面提到对象实例以及动态属性都是保存在Heap 中的,而Heap 必须通过Stack中的地址指针才能够被指令(类的方法)访问到。因此可以推断出:静态属性是保存在Stack中的,而不同于动态属性保存在Heap 中。正因为都是在Stack中,而Stack中指令和数据都是定长的,因此很容易算出偏移量,也因此不管什么指令(类的方法),都可以访问到类的静态属性。也正因为静态属性被保存在Stack中,所以具有了全局属性。

在JVM中,静态属性保存在Stack指令内存区,动态属性保存在Heap数据内存区。

 

三、Stack 栈

 

Stack(栈)是JVM的内存指令区。Stack管理很简单,push一定长度字节的数据或者指令,Stack指针压栈相应的字节位移;pop一定字节长度数据或者指令,Stack指针弹栈。Stack的速度很快,管理很简单,并且每次操作的数据或者指令字节长度是已知的。所以Java 基本数据类型,Java 指令代码,常量都保存在Stack中。

栈也叫栈内存,是 Java 程序的运行区,是在线程创建时创建,它的生命期是跟随线程的生命

,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束,该栈就 Over。

那么栈中存的是那些数据呢?又什么是格式呢?

 

栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是

一个有关方法(Method)和运行期数据的数据集,当一个方法 A 被调用时就产生了一个栈帧 F1,并

被压入到栈中,A 方法又调用了 B 方法,于是产生栈帧 F2 也被压入栈,执行完毕后,先弹出 F2

栈帧,再弹出 F1 栈帧,遵循“先进后出”原则。

 

那栈帧中到底存在着什么数据呢?栈帧中主要保存 3 类数据:本地变量(Local Variables),

包括输入参数和输出参数以及方法内的变量;栈操作(Operand Stack),记录出栈、入栈的操作;

栈帧数据(Frame Data),包括类文件、方法等等。光说比较枯燥,我们画个图来理解一下 Java

栈,如下图所示:

四、Heap 堆

 

Heap(堆)是JVM的内存数据区。Heap 的管理很复杂,每次分配不定长的内存空间,专门用来保存对象的实例。在Heap 中分配一定的内存来保存对象实例,实际上也只是保存对象实例的属性值,属性的类型和对象本身的类型标记等,并不保存对象的方法(方法是指令,保存在Stack中),在Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。而对象实例在Heap 中分配好以后,需要在Stack中保存一个4字节的Heap 内存地址,用来定位该对象实例在Heap 中的位置,便于找到该对象实例。

Java中堆是由所有的线程共享的一块内存区域。

4.1 Generation

JVM堆一般又可以分为以下三部分:

Perm

Perm代主要保存class,method,filed对象,这部门的空间一般不会溢出,除非一次性加载了很多的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到java.lang.OutOfMemoryError : PermGen space 的错误,造成这个错误的很大原因就有可能是每次都重新部署,但是重新部署后,类的class没有被卸载掉,这样就造成了大量的class对象保存在了perm中,这种情况下,一般重新启动应用服务器可以解决问题。

 

Tenured

Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区,一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间。

 

Young

Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用,在Young区间变满的时候,minor GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到Tenured区间。

 

4.2 Sizing the Generations

 

JVM提供了相应的参数来对内存大小进行配置。正如上面描述,JVM中堆被分为了3个大的区间,同时JVM也提供了一些选项对Young,Tenured的大小进行控制。

Total Heap

-Xms :指定了JVM初始启动以后初始化内存

-Xmx:指定JVM堆得最大内存,在JVM启动以后,会分配-Xmx参数指定大小的内存给JVM,但是不一定全部使用,JVM会根据-Xms参数来调节真正用于JVM的内存

-Xmx -Xms之差就是三个Virtual空间的大小

 

Young Generation

-XX:NewRatio=8意味着tenured 和 young的比值8:1,这样eden+2*survivor=1/9

堆内存

-XX:SurvivorRatio=32意味着eden和一个survivor的比值是32:1,这样一个Survivor就占Young区的1/34.

-Xmn 参数设置了年轻代的大小

 

Perm Generation

-XX:PermSize=16M -XX:MaxPermSize=64M

Thread Stack

-XX:Xss=128K

 

五、The pc Register 程序计数器寄存器

 

JVM支持多个线程同时运行。每个JVM都有自己的程序计数器。在任何一个点,每个JVM线程执行单个方法的代码,这个方法是线程的当前方法。如果方法不是native的,程序计数器寄存器包含了当前执行的JVM指令的地址,如果方法是 native的,程序计数器寄存器的值不会被定义。 JVM的程序计数器寄存器的宽度足够保证可以持有一个返回地址或者native的指针。

 

六、Method Area 方法区

 

Object Class Data(类定义数据) 是存储在方法区的。除此之外,常量、静态变量、JIT 编译后的代码也都在方法区。正因为方法区所存储的数据与堆有一种类比关系,所以它还被称为 Non-Heap。方法区也可以是内存不连续的区域组成的,并且可设置为固定大小,也可以设置为可扩展的,这点与堆一样。

方法区内部有一个非常重要的区域,叫做运行时常量池(Runtime Constant Pool,简称 RCP)。在字节码文件中有常量池(Constant Pool Table),用于存储编译器产生的字面量和符号引用。每个字节码文件中的常量池在类被加载后,都会存储到方法区中。值得注意的是,运行时产生的新常量也可以被放入常量池中,比如 String 类中的 intern() 方法产生的常量。

 

6.1 常量池 (constant pool)

 

常量池指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。除了包含代码中所定义的各种基本类型(如int、long等等)和对象型(如String及数组)的常量值(final)还包含一些以文本形式出现的符号引用,比如:

◆类和接口的全限定名;

◆字段的名称和描述符;

◆方法和名称和描述符。

虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集和,包括直接常量(string,integer和 floating point常量)和对其他类型,字段和方法的符号引用。

对于String常量,它的值是在常量池中的。而JVM中的常量池在内存当中是以表的形式存在的, 对于String类型,有一张固定长度的CONSTANT_String_info表用来存储文字字符串值,注意:该表只存储文字字符串值,不存储符号引 用。说到这里,对常量池中的字符串值的存储位置应该有一个比较明了的理解了。

在程序执行的时候,常量池 会储存在Method Area,而不是堆中。

 

七、Java Method Stack Java 方法栈 与 Native Method Stack 原生方法栈

 

Java 方法栈也是线程私有的,每个 Java 方法栈都是由一个个栈帧组成的,每个栈帧是一个方法运行期的基础数据结构,它存储着局部变量表、操作数栈、动态链接、方法出口等信息。当线程调用调用了一个 Java 方法时,一个栈帧就被压入(push)到相应的 Java 方法栈。当线程从一个 Java 方法返回时,相应的 Java 方法栈就弹出(pop)一个栈帧。

其中要详细介绍的是局部变量表,它保存者各种基本数据类型和对象引用(Object reference)。基本数据类型包括 boolean、byte、char、short、int、long、float、double。对象引用,本质就是一个地址(也可以说是一个“指针”),该地址是堆中的一个地址,通过这个地址可以找到相应的 Object(注意是“找到”,原因会在下面解释)。而这个地址找到相应 Object 的方式有两种。一种是该地址存储着 Pointer to Object Instance Data 和 Pointer to Object Class Data,另一种是该地址存储着 Object Instance Data,其中又包含有 Pointer to Object Class Data。

如下两图所示:

第一种方式,Java 方法栈中有 Handler Pool 和 Instance Pool。无论哪种方式,Object Class Data 都是存储在方法区的,Object Instance Data 都是存储在堆中的。


图1 句柄方式


图2 直接方式

原生方法栈与 Java 方法栈相类似,这里不再赘述。

 

八、JVM运行原理 例子

 

以上都是纯理论,我们举个例子来说明 JVM 的运行原理,我们来写一个简单的类,代码如下:

public class JVMShowcase {  
//静态类常量,  
public final static String ClASS_CONST = "I'm a Const";  
//私有实例变量  
private int instanceVar=15;  
public static void main(String[] args) {  
//调用静态方法  
runStaticMethod();  
//调用非静态方法  
JVMShowcase showcase=new JVMShowcase();  
showcase.runNonStaticMethod(100);  
}  
//常规静态方法  
public static String runStaticMethod(){  
return ClASS_CONST;  
}  
//非静态方法  
public int runNonStaticMethod(int parameter){  
int methodVar=this.instanceVar * parameter;  
return methodVar;  
}  
}  

这个类没有任何意义,不用猜测这个类是做什么用,只是写一个比较典型的类,然后我们来看看 JVM 是如何运行的,也就是输入 java JVMShow 后,我们来看 JVM 是如何处理的:

       向操作系统申请空闲内存。JVM 对操作系统说“给我 64M 空闲内存”,于是第 1 步,JVM 向操作系统申请空闲内存作系统就查找自己的内存分配表,找了段 64M 的内存写上“Java 占用”标签,然后把内存段的起始地址和终止地址给 JVM,JVM 准备加载类文件。

       分配内存内存。第 2 步,JVM 分配内存。JVM 获得到 64M 内存,就开始得瑟了,首先给 heap 分个内存,并且是按照 heap 的三种不同类型分好的,然后给栈内存也分配好。

       文件。第 3 步,检查和分析 class 文件。若发现有错误即返回错误。

       加载类。第 4 步,加载类。由于没有指定加载器,JVM 默认使用 bootstrap 加载器,就把 rt.jar 下的所有类都加载到了堆类存的永久存储区,JVMShow 也被加载到内存中。

我们来看看栈内存,如下图:

       执行引擎执行方法。第 5 步,执行引擎执行 main 方法。执行引擎启动一个线程,开始执行 main 方法,在 main 执行完毕前,方法区如下图所示:

在 Method Area 加入了 CLASS_CONST 常量,它是在第一次被访问时产生的。堆内存中有两个对象 object 和 showcase 对象,如下图所示:

为什么会有 Object 对象呢?是因为它是 JVMShowcase 的父类,JVM 是先初始化父类,然后再

初始化子类,甭管有多少个父类都初始化。在栈内存中有三个栈帧,如下图所示:

于此同时,还创建了一个程序计数器指向下一条要执行的语句。

   

       释放内存。运第 6 步,释放内存。运行结束,JVM 向操作系统发送消息,说“内存用完了,我还给你”

行结束。

 

九、JVM 相关问题

 

问:堆和栈有什么区别堆和栈有什么区别有什么

答:堆是存放对象的,但是对象内的临时变量是存在栈内存中,如例子中的 methodVar 是在运行期存放到栈中的。

栈是跟随线程的,有线程就有栈,堆是跟随 JVM 的,有 JVM 就有堆内存。

 

问:堆内存中到底存在着什么东西?堆内存中到底存在着什么东西?

答:对象,包括对象变量以及对象方法。

 

问:类变量和实例变量有什么区别?类变量和实例变量有什么区别?有什么区别

答:静态变量是类变量,非静态变量是实例变量,直白的说,有 static 修饰的变量是静态变量,没有 static 修饰的变量是实例变量。静态变量存在方法区中,实例变量存在堆内存中。

 

问:我听说类变量是在 JVM 启动时就初始化好的,和你这说的不同呀!

答:那你是道听途说,信我的,没错。

 

问:Java 的方法(函数)到底是传值,还是传址值还是传址?

答:都不是,是以传值的方式传递地址,具体的说原生数据类型传递的值,引用类型传递的地址。对于原始数据类型,JVM 的处理方法是从 Method Area 或 Heap 中拷贝到 Stack,然后运行 frame中的方法,运行完毕后再把变量指拷贝回去。

 

问:为什么会产生 OutOfMemory 产生?

答:一句话:Heap 内存中没有足够的可用内存了。
这句话要好好理解,不是说 Heap 没有内存了,是说新申请内存的对象大于 Heap 空闲内存,比如现在 Heap 还空闲 1M,但是新申请的内存需要 1.1M,于是就会报 OutOfMemory 了,可能以后的对象申请的内存都只要 0.9M,于是就只出现一次 OutOfMemory,GC 也正常了,看起来像偶发事件,就是这么回事。 但如果此时 GC 没有回收就会产生挂起情况,系统不响应了。

 

问:我产生的对象不多呀,为什么还会产生 OutOfMemory?我产生的对象不多呀,?

答:你继承层次忒多了,Heap 中 产生的对象是先产生 父类,然后才产生子类,明白不?

 

问:OutOfMemory 错误分几种?

答:分两种,分别是“OutOfMemoryError:java heap size”和”OutOfMemoryError: PermGen

space”,两种都是内存溢出,heap size 是说申请不到新的内存了,这个很常见,检查应用或调整

堆内存大小。

“PermGen space”是因为永久存储区满了,这个也很常见,一般在热发布的环境中出现,是

因为每次发布应用系统都不重启,久而久之永久存储区中的死对象太多导致新对象无法申请内存,

一般重新启动一下即可。

 

问:为什么会产生 StackOverflowError??

答:因为一个线程把 Stack 内存全部耗尽了,一般是递归函数造成的。

 

问:一个机器上可以看多个 JVM 吗?JVM 之间可以互访吗?

答:可以多个 JVM,只要机器承受得了。JVM 之间是不可以互访,你不能在 A-JVM 中访问B-JVM 的 Heap 内存,这是不可能的。在以前老版本的 JVM 中,会出现 A-JVM Crack 后影响到B-JVM,现在版本非常少见。

 

问:为什么 Java 要采用垃圾回收机制,而不采用 C/C++的显式内存管理?

答:为了简单,内存管理不是每个程序员都能折腾好的。

 

问:为什么你没有详细介绍垃圾回收机制?

答:垃圾回收机制每个 JVM 都不同,JVM Specification 只是定义了要自动释放内存,也就是说它只定义了垃圾回收的抽象方法,具体怎么实现各个厂商都不同,算法各异,这东西实在没必要深入。

 

问:JVM 中到底哪些区域是共享的?哪些是私有的?

答:Heap 和 Method Area 是共享的,其他都是私有的,

 

问:什么是 JIT,你怎么没说?,你怎么没说?

答:JIT 是指 Just In Time,有的文档把 JIT 作为 JVM 的一个部件来介绍,有的是作为执行引擎的一部分来介绍,这都能理解。Java 刚诞生的时候是一个解释性语言,别嘘,即使编译成了字节码(byte code)也是针对 JVM 的,它需要再次翻译成原生代码(native code)才能被机器执行,于是效率的担忧就提出来了。Sun 为了解决该问题提出了一套新的机制,好,你想编译成原生代码,没问题,我在 JVM 上提供一个工具,把字节码编译成原生码,下次你来访问的时候直接访问原生码就成了,于是 JIT 就诞生了,就这么回事。

 

问:JVM 还有哪些部分是你没有提到的?

答:JVM 是一个异常复杂的东西,写一本砖头书都不为过,还有几个要说明的:

常量池(constant pool)按照顺序存放程序中的常量,:并且进行索引编号的区域。比如 int i =100,这个 100 就放在常量池中。

安全管理器(Security Manager):提供 Java 运行期的安全控制,防止恶意攻击,比如指定读取文件,写入文件权限,网络访问,创建进程等等,Class Loader 在 Security Manager 认证通过后才能加载 class 文件的。

方法索引表(Methods table),记录的是每个 method 的地址信息,Stack 和 Heap 中的地址指针其实是指向 Methods table 地址。

 

问:为什么不建议在程序中显式的生命 System.gc()?

答:因为显式声明是做堆内存全扫描,也就是 Full GC,是需要停止所有的活动的(Stop The World Collection),你的应用能承受这个吗?

 

问:JVM 有哪些调整参数?

答:非常多,自己去找,堆内存、栈内存的大小都可以定义,甚至是堆内存的三个部分、新生代的各个比例都能调整。

 

 

 

© 著作权归作者所有

郑加威
粉丝 175
博文 183
码字总数 387300
作品 0
杭州
架构师
私信 提问

暂无文章

堆”和“栈

C++作为一款C语言的升级版本,具有非常强大的功能。它不但能够支持各种程序设计风格,而且还具有C语言的所有功能。我们在这里为大家介绍的是其中一个比较重要的内容,C++内存区域的基本介绍。...

SibylY
10分钟前
1
0
总结:Https

一、介绍 简单理解,https即在http协议的基础上,增加了SSL协议,保障数据传输的安全性。 它由以前的http—–>tcp,改为http——>SSL—–>tcp;https采用了共享密钥加密+公开密钥加密的方式 ...

浮躁的码农
13分钟前
1
0
数据库表与表之间的一对一、一对多、多对多关系

表1 foreign key 表2 多对一:表 1 的多条记录对应表 2 的一条记录 利用foreign key的原理我们可以制作两张表的多对多,一对一关系 多对多: 表1的多条记录可以对应表2的一条记录 表2的多条记...

Garphy
43分钟前
6
0
MySQL 表崩溃修复

MySQL日志报错 2019-10-19 13:41:51 19916 [ERROR] /usr/local/mysql/bin/mysqld: Table './initread_hss/user_info' is marked as crashed and should be repaired2019-10-19 13:41:51 1......

雁南飞丶
53分钟前
6
0
Error和Exception

1.Error类和Exception类都是继承Throwable类 2.Error(错误)是系统中的错误,程序员是不能改变的和处理的,是在程序编译时出现的错误,只能通过修改程序才能修正。一般是指与虚拟机相关的问...

大瑞清_liurq
今天
4
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部