(3) Java内存结构

这一部分在我看来讲的都是概念上的东西,实际上各个JVM的结构都不一样,实现很复杂,而且随着时间发展各个区域也不是一成不变的,事实上所谓的区域、结构划分完全是在逻辑上人为划分的,如果有兴趣的话可以查看《深入理解Java虚拟机》以及JVM的说明文档去查看具体内容。

“Java的内存”,即Java的运行时数据区,也就是Java的内存结构(Java Structure),但注意区分Java的内存模型(Java Memory Model,JMM),JMM现在我还没仔细去研究过,有兴趣的同学可以自行百度查看下,如果以后有时间,我也会研究,然后记下自己的理解。

Java的内存结构,可以分为以下几个部分:堆、栈、非堆以及其他。

1.堆:存放Java对象,所有的对象(包括数组,但Class对象除外)数据实际存放地方。堆是程序级别,每一个Java程序共享一个堆(所以存在多线程访问堆内存同步问题)。

2.栈:存的是引用(如果是基本类型,则存的是值),引用(直接或间接)指向堆中的对象。栈是线程级别,每一个线程有各自的栈。栈又分为两种,一是Java方法栈,一是本地方法栈(有的JVM这两者是合在一起的,不过这里还是讨论逻辑上)。另外,每个线程都有各自的程序计数器,也是栈格式的。

(另外,比较疑惑的是,栈是不是堆得一部分呢?用的时候从堆分配出来?还是一开始就分出一部分内存用于栈?新开线程从这一部分分配出来?不过不管怎么样,逻辑上可以看成两个部分)

3.非堆:方法区、常量池、静态变量、即时编译后的代码等(貌似jdk1.7常量池又放在堆中了,逻辑上还是单独拿出来好了),堆与非堆的区别是,堆是供给程序使用,而非堆是供给JVM使用的。(严格来说,栈也是非堆?),因为主要是方法区,这部分又常常被称为方法区,又因为这部分数据不会(应该说很少会)被GC(GC,垃圾对象回收,详见下一节),所以也称为永久代。

        jdk8好像有把非堆全部移除的,取而代之的是一个叫元空间,不过这里不详细谈,有兴趣可以参考:Java 8: 从永久代(PermGen)到元空间(Metaspace)(炎黄)

4.其他:存放JVM本身代码。

5.另外说一点,直接内存(OS管理的内存一部分),这不是JVM内存的一部分,但在NIO中,引入了一种基于通道和缓冲区的IO方式,Java程序可以通过本地方法的形式直接分配这部分内存,以DirectByteBuffer对象作为引用,间接操作使用。

其具体结构图可以大体概括如下:

堆内存又分为:Old区+ Young区,Young区又划分为:Eden + Survivor,Survivor又划分为:From + To (From 、To 大小相等,这些大小都能手动设置),如下图所示:

0).为什么要这样划分?因为性能,每次GC可以按照不同区域GC,加快垃圾回收速度。不过也因此,如果有更好的GC算法,可能划分就不一样了。

1).新建的对象存放到Eden区,From/To存放经过一次及以上GC的对象,若经过n次(可设置,通常为0,这里的0意思是GC检查时是0移到From/To区,并且该值+1,不是0移到Old区),对象每次GC若能存活(每次GC,Eden区清空),移到From/To区,以后都是从From到To再到From跳来跳去,经过n次,变成“老对象”就会被移到Old区。

2).堆是共享空间,每次分配空间要加锁。但是有的JVM会为每个线程分配一个TLAB空间,这样就不用加锁(不过这种仅适用于小对象,大的还是直接分配在堆上)。

3).堆在物理上是可以分散的,只要在逻辑上连续的就可以,大小可以是固定的,也可以是可扩展的,主流的JVM采用的都是可扩展的,可以用-Xmx -Xms控制(详见JVM优化一节)。

4).Old区、Eden区的内存并不一定会被填满,相反,一般情况下,不可能被填满,而是达到一定的值就会启动GC(详见GC一节)。

0.程序计数器

又称PC寄存器(Program Counter Register),较小的一块内存,用来存放下一条指令(当前指令正在执行引擎中执行,在指令寄存器中?),如果当前执行的是本地方法,则PC寄存器不存任何信息(本地方法执行不通过Java的执行引擎)。另外,如何去取下一条指令就是PC寄存器的工作,像跳转、分支、循环、异常处理等都依赖于PC寄存器来完成。PC寄存器是唯一一个不会有OutOfMemoryError情况的区域

1.Java方法栈

程序主要的工作地方,每个线程私有,生命周期同线程,另外,一个方法其实就是一个帧,主线程main启动,就会创建主线程的栈,同时将第一个帧(main方法)压入栈中,每创建一个方法就将一个新的帧压入,帧用于存储局部变量表、操作栈、动态链接等信息。方法结束,帧出栈,相对应的内存消除(包括局部变量指向的堆中的对象也会被下一次GC)。当调用本地方法,则工作转到本地栈。

当进入一个方法的时候,其所需要的内存大小是确定的了,不会再改变。如果线程申请的栈深度大于JVM所允许的深度,则会抛出栈溢出StackOverflowError;若是JVM栈可以动态扩展(栈不是固定长度,需要的时候可以自动增加深度,当前JVM基本上都支持这种),但是当扩展时无法申请得到足够的内存同样会抛出OutOfMemoryError异常错误。

另外说一点,局部变量中的对象引用并一定是直接指向实例对象,具体的实现是由JVM决定的,主要有两种实现方式:

(1).直接指向实例对象,由实例对象提供指向对应Class对象的引用。

(2).指向一个句柄池(Java堆开辟出来的),句柄池存放着实例对象地址以及对应Class对象地址。

具体实现及优缺点,可参考《深入理解Java虚拟机》一书。

2.本地方法栈

用于本地方法调用,其他的同Java方法栈,所以有的JVM把两者合二为一。

非堆

也是程序共享区域,从这一点可以看出,JVM规范把他描述为堆的逻辑一部分是有一定的道理,虽然它有个非堆(Non Heap)名称。主要用来存储加载的类的信息、常量、静态变量等,有的称为这一部分为永久代,是因为GC不会处理这一部分,但有的JVM的GC算法也是会涉及到这个部分的(JVM规范对这一点并没有作要求),毕竟这一部分也会出现内存溢出异常的可能,比如,过多的加载类。

类的信息:包括所有类相关的信息:类变量、类字段、类的方法(包括静态方法和非静态方法,具体包括方法的修饰词、返回类型、参数列表等等)、类常量等,这一部分的实际结构很复杂,有兴趣的可以自行查阅。


总结:

0.一句话:栈线程私有,存放局部变量;堆程序共享,存放对象实例数据;非堆主要存放类信息(Class对象)。这是Java内存最主要的三块内存,而直接内存,如果没用到NIO是不会操作到的。

1.数据存放地方

局部变量:栈中,包括基本类型(存放的是值)、对象引用、返回地址;

类变量:方法区(非堆),类变量算是类信息一部分;

字符串和基本类型常量:常量池(事实上,常量池已被放到堆中,不过我们姑且将常量池单独在逻辑上拿出来);

Class对象:方法区(非堆);

new对象:引用放到栈中,对象数据放在堆中;

这些存放地方可能因为Java不断发展改变而不一样,但是逻辑上大概是这个意思,还有细节地方可能还有所不同(比如Eden可能还存放其他信息),如果想追究具体是什么情况的话,可以查看最新的jdk说明文档。

本节主要讲的是JVM运行时数据区,JVM内存各个区域的划分以及作用。


版权声明:本文为a327369238原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。