@cxm-2016
2016-12-04T22:05:28.000000Z
字数 4084
阅读 4737
JVM知识
版本:7
作者:陈小默
声明:禁止商业,禁止转载
对于有过C++编程经验的人来说,Java无疑是一款干净、便捷、清爽的编程语言。本章为理论课,主要从操作系统层面介绍Java中是如何管理内存的。另外会有一门实践课,用来体会Java的内存管理。
首先我们来回顾一下学校里学过的计算机组成原理,计算机的五大组件为:
我的电脑只有512M,可是我运行了10个128M的程序,我的内存是如何存放这些程序的呢?
小插曲:作为一个用过电脑的人都一定听说过32位、64位什么的,它们的区别是什么?回答这个问题之前问你自己,你在使用XP系统的时候听说过8G内存吗?是因为过去技术不发达才没有8G的内存?当然不是!我们知道计算机组成中有一个相当重要的组成部分,就是地址总线。在32位计算机中地址总线的宽度只有32位,那么它的寻址范围是0x0000 0000~0xFFFF FFFF
能访问到的内存空间大小为 位,也就是4G,当我们给32位计算机增加内存条后并没有什么卵用,因为总线根本访问不到这些数据。而x64架构的内存地址总线寻址范围是,就是说64位的地址总线理论上可以访问容量为ZB级别的内存。并且64位的寄存器长度也升级为64位,也就是说曾经计算一个long类型的加法运算需要4个寄存器,而现在只需要两个。[1]
转回正题。在过去的编程经验中我们知道我们对内存的访问都是被指定的,而不能去主动指定我要操作哪一块内存。对于Java程序员甚至都不需要编写任何和内存相关的代码。通常情况下,为我们的程序分配内存的都是操作系统。操作系统用来保证每个进程拥有独立的内存空间。
注意:这里独立的内存空间指的是逻辑上独立而非物理上独立。操作系统只保证在某一时刻一块内存空间只属于一个进程独占。
当我们的程序设计的越来越大,就会出现前言中提到的问题,我们的程序比物理内存还要大,它是如何加载的呢?
为了解决这个这个问题,操作系统引入了虚拟内存的概念。虚拟内存在Windows系统中以页面的方式实现,在Linux中以swap区方式实现。
操作系统会将一个不活动的进程移动到一个磁盘文件中,也就是页面或者swap区中。而将真正的物理内存交给正在运行的进程。
我买了一台拥有4G内存的计算机,那么我可以真正可以一次性使用这4G的内存吗?
在我们的计算机中第一个程序一定是操作系统,以Linux为例,其内核的主要作用是[2]:
系统内核拥有相当重要的使命,为了保证操作系统的稳定性,操作系统会将物理内存划分开来,维持两个互不干扰的空间,也就是内核空间和用户空间。一般情况下在当前Windows32位操作系统中默认内核空间与用户空间的比例是1:1(2GB内核空间,2GB用户空间),而在32位Linux系统中默认比例是1:3(1GB内核空间,3GB用户空间)。[3]
所以安全系数较高的操作(访问网络、操作硬件资源等)都需要在内核中完成,再将结果复制到用户空间。
Java程序既然也是一个进程,那么它都是如何分配内存的呢?
Java程序是基于JVM运行的,所以我们的Java程序并不需要也不可以直接操作内存。这一切都被JVM通过一定的方式转换成了符合JVM规范的内存操作。
Java中的堆是用来存放对象的内存区域,堆的大小是程序运行时就计算好的,不能在运行时再向操作系统申请内存。同样的,程序不用的内存也不会退还给操作系用。JVM通过一个垃圾回收器(GC,Garbage Collection)回收无效对象占用的内存。
线程是一种系统资源,每一个线程都维护着自己的线程控制块,在这里存放着线程信息供操作系统调用。并且每一个线程都会拥有属于自己的调用栈,这个栈的大小通常在256KB~756KB之间。为了保证程序的运行效率,一般分配CPU核心数量的线程,超出的话程序的效率反而会降低。
第三节说过我们的内存被划分为了内核空间和用户空间,操作系统在内核空间中操作文件,并将结果复制到用户空间。显而易见,这样做的效率会降低。于是Java在1.4以后添加了NIO类库,其java.nio.ByteBuffer.allocateDirect()方法通过os::malloc()函数向操作系统索要内存。于是对数据交互的过程就直接在内核空间中完成,省去了向JVM堆中复制的过程,也就提高了效率。
如果我们使用JNI技术完成一些操作,很显然,我们使用new
、malloc
,delete
和free
之类的操作同样不需要经过JVM的堆。这样做的话需要注意,这些内存既然不在JVM中分配,同样也就不受JVM规范的保护,切记释放内存。
JVM是按照运行时数据的存储结构来划分内存结构的,一般来说JVM将运行时数据划分为6种:
- PC寄存器数据
- Java栈
- 堆
- 方法区
- 本地方法栈
- 运行时常量池
PC寄存器是一种数据结构,用来存储当前线程执行到的位置,以便于线程切换后能够回到切换前的位置继续运行。
由于PC寄存器是用来保存线程的执行位置的,所以其被设计为与线程关联。每当线程启动时,都会随之创建一个PC寄存器[4]
JVM执行字节码指令是基于栈的架构,也就是说所有的操作数都必须先入栈,然后根据操作码进行相应的弹栈、计算、入栈的过程。当然,采用基于栈的设计方式带来的一个很明显的问题就是效率低下,那么JVM为什么还要采用栈模式呢?最主要的原因就是JVM的设计目标就是平台无关,所以为了保证class文件能够在不存在寄存器的平台上正常运行,就只能抛弃寄存器实现的方式。
Java中的栈是与线程相关联的,每当创建一个线程是,JVM就会为这个线程创建一个对应的Java栈。每当线程中执行一个方法的时候,栈就会创建一个栈帧来存放方法相关的信息(变量、操作数、返回值等)。
Java栈中存在着PC寄存器和栈帧,其栈帧中存在着多个不同的数据结构:1,用于存放指令的指令栈。2,用于存放局部变量的数据栈。3,用于存放操作数的操作栈。(具体流程以后在介绍)
Java中的的栈随着线程的创建而创建,随着线程的销毁而销毁。其中的栈帧随着方法的调用而创建,随着方法的结束而销毁。
Java堆用于存储对象,当我们使用new去创建一个对象的时候:
- JVM就会从方法区中取出这个类的信息。
- 根据类信息计算所需空间大小并申请内存。
- 从方法区中取出类的副本并存储到申请的空间。
- 返回对象的引用。
方法区是JVM用于存储类结构信息的区域,类中的常量池、属性、方法定义和方法体以及构造方法都存储在这片区域。
方法区是堆的一部分,也就是Java堆中的永久区。方法区大小一般在在JVM启动后就固定了,因为此时所有需要用到的类都已经被加载进JVM中了。
永久是指其中存放数据的生命周期,这个区域由于存储的都是一些程序运行过程中始终会用到的信息(比如类信息),所以该区域不像堆中的其他区域一样会被GC频繁的扫描。
在Java对象中有一些特殊的元素。如有些元素是比较特别的(如利用关键字Static定义的变量)。这些变量对于其他对象来说,可能就是静态的。为了更好的管理这些变量,Java在内存中专门划分了一个静态存储区域来管理这些元素。这里的静态存储区域就是指在固定的位置存放应用程序运行时一直存在的数据。这里需要明确的一点就是,Java对象是不保存在这个地方的,而只是把对象中的一些特殊元素放置这里。[5]
在Java对象中还有一类特殊的元素,我们叫做常量。由于常量的值是稳定不变的,如圆周率。为此把他们放在代码的内部是可行的。不过有些时候,在进行一些嵌入式系统开发的时候,我们往往不这么做。而是会把常量元素跟代码分开来保存。如我们会根据情况把常量的值存放在一些只读存储器中。这主要是为了一些特殊的功能考虑的。
通过上述5.5.1和5.5.2两个问题我们也能看出,常量池是方法区的一部分,它的存储同样收到方法区规范的限制。
本地方法栈是为了运行Native方法准备的空间,JVM对这个区域并没有规范,所以其实现原理与具体平台相关。