了解过C++和Java的同学应该都知道,C++的内存管理和Java的内存管理是不同的,C++的内存管理,开发人员即拥有最高权利,又需要从事最基础的工作,既可以管理每个对象的创建也要复旦每个对象从出生到终结的维护责任。
但是对于Java程序员来说,有虚拟机内存自动管理的机制,就方便很多,但是弊端也是很明显的,如果出错了,需要找到错误的地方,在这种情况下,如果不了解虚拟机的运行方法和原理,就很难完成。
所以,这篇文章带大家一起,了解一下虚拟机内存组成及各模块的功能。
数据区
在虚拟机运行Java程序时,会将内存划分成若干的数据区。
从总的来说,在程序进入JVM后,内存分为五个部分。
其中三个部分是线程私有的内存,包含程序计数器,本地方法栈,虚拟机栈,这些内存都是随着线程创建而分配,线程结束而回收的,所以不属于垃圾回收的范围。
另外两个部分是所有线程共享的,方法区和Java堆,因为是共享的,所以不会随线程结束而回收,需要垃圾回收机制来处理可以被回收的对象。
- 程序计数器
程序计数器是一块较小的内存空间,可以看作当前线程所执行的字节码的行号指示器。
由于Java虚拟机多线程是通过线程轮流切换并分配执行时间来执行的,在任何时刻一个处理器(如果是多核处理器,那就是其中一核)都会执行一条线程中的指令。所以在执行程序的过程中,会不断切换线程,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各条线程之间计数器互不影响,独立储存,所以程序计数器所在的内存也算“线程私有”。
如果线程正在执行的是Java方法,那么程序计数器就指向正在执行的虚拟机字节码指令的地址,如果正在执行的是Native方法,这个计数器值为空。 - Java虚拟机栈
与程序计数器一样,Java虚拟机栈也是线程私有的,他的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,用于储存局部变量表,操作栈数,动态连接,方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
一个线程由一个方法启动,但是一个线程里可以有多个方法,所以在一个线程中才需要私有的虚拟机栈,来存放多个对应的方法的栈帧。
在Java内存中大家喜欢说内存中的 “堆内存” 和 “栈内存” ,其中的 “栈内存” 说的就是虚拟机栈,而 “堆内存” 是用来存放Java对象的地方,后面会提到。 - 本地方法栈
本地方法栈与虚拟机栈基本一样,只是虚拟机栈是在线程中执行Java方法时存方法信息用的,本地方法栈是线程中指向Native方法的时候用来存方法信息的栈内存。(但有些虚拟机比如HotSpot虚拟机直接把本地方法栈和虚拟机栈合二为一)。
在本地方法栈和程序计数器中都提到了一个Native方法。
Native方法就是一个java调用非java代码的接口,其实也就是在Java内部除了Java语言,我们还可能要使用一些其他语言实现的方法,比如jre大部分是用java实现的,它也通过一些本地方法与外界交互。例如:类java.lang.Thread 的 setPriority()方法是用java实现的,但是它实现调用的是该类里的本地方法setPriority0()。这个本地方法是用C实现的,并被植入JVM内部,这个方法就被称作Native Method。 - Java堆
对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块(毕竟Java是OOP语言),这块内存是被所有线程共享的内存区域。在虚拟机启动时,这块区域就被创建了,并且唯一的目的就是存放对象实例,几乎所有对象实例以及数组都要在Java堆中分配内存。
并且Java堆也是垃圾收集器的主要区域,因此很多时候也被称为"GC堆"。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以分为:新生代和老年代,再细致一点就是:Eden空间,FromSurvivor空间,ToSurvivor空间等。这个可以在垃圾回收的地方着重理解。
在Java虚拟机规范的规定中,Java堆可以处在物理上不连续的内存空间中,只要逻辑上连续就可,跟磁盘空间一样。现在拓展堆空间基本都是-Xmx和-Xms来控制。因为Java中对象的空间占大部分,所以一般内存不够抛出错误java.lang.OutOfMemoryError: Java heap space都是Java堆内存不够,为了装入新对象98%的时间是用于GC且可用的 Heap size 不足2%的时候将抛出此异常信息。 - 方法区
方法区在JDK1.8之前为永久代实现,之后为元空间实现。
方法区与Java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。
因为在最常见的虚拟机HotSpot中,开发团队把GC分代收集拓展至方法区,这样HotSpot的垃圾收集器就可以像管理Java堆一样管理这部分内存,能够省去一些工作,而方法区又是用永久代来实现,所以很多人喜欢称方法区为"永久代"。
但是用永久代实现方法区现在看来并不是一个好主意,因为这样很容易遇到内存溢出问题,所以逐渐用Native Method来实现方法区的规划,在1.7的时候字符串常量池已经被移到堆中。
在1.8之后用 “元空间” 来实现其实也是同样的说法。但是本质上永久代和元空间只是方法区一种实现方法,在其他虚拟机中实现方式和HotSpot就有所不同。 - 运行时常量池
这个内存部分属于方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用。这部分内容在类加载之后进入方法区的运行时常量池存放。
Class文件中的常量池可以算是编译时常量池,运行时常量池与Class中的常量池的一个重要不同是运行时常量池具备动态性,意思就是进入方法区的运行时常量池不一定要在编译器,运行期间也可以把新的常量放入。一般用的比较多的额是String类中的intern()方法。