运行时数据区域

Java 线程 & 进程 & JVM 关系

Java 编写的程序都运行在 Java 虚拟机 (JVM) 中

每用 Java 命令启动一个 Java 应用程序,就会启动一个 JVM 进程。在同一个 JVM 进程中,有且只有一个进程,那就是 JVM 进程

当启动一个 JVM 进程时,就会向操作系统申请一片内存区域,该内存区域由 JVM 管理,所有的程序代码都以线程运行在这个 JVM 环境中

JVM 找到程序的入口:main(),然后运行该方法,这样就产生了一个线程,这个线程称之为主线程。当main()方法执行结束后,主线程运行完成,JVM 进程也随即结束

所以一个应用程序只对应着一个进程,即:JVM 进程,但可以包含多个线程!!

JVM 组成部分

JVM 主要由四部分组成

1

通过上面的图,可以大致了解 Java 代码运行的过程,更具体的如下图所示:

2

首先 Java 源代码通过编译器转换成字节码,接着类加载系统把字节码加载到运行时数据区的方法区内,再使用执行引擎将字节码翻译成计算机可执行的底层指令,最后交由 CPU 去执行,而这个过程可能需要调用其他语言的本地库接口来实现整个程序的功能

字节码:一套 JVM 指令集规范,并不能直接交给底层操作系统去执行,需要通过执行引擎将字节码翻译成底层系统指令

跨平台特性:从这个过程也可以看出来 Java 的跨平台特性较好,同一个字节码文件可运行在不同的系统上。不同的操作系统有不同版本的 JVM,字节码实际上是由 JVM 翻译成操作系统可执行的指令

3

程序计数器

Java 虚拟机的多线程是通过给线程分配时间片,轮流切换线程来实现的,在任一确定的时刻,一个单核处理器只能执行一条指令

为了线程切换后可以恢复到正确的执行位置,每个线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储,为线程私有

如果线程正在执行的是一个 Java 方法,那么计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个本地 (Native) 方法,那么计数器记录的值为空

虚拟机栈

与程序计数器一样,虚拟机栈也是线程私有,它的生命周期与线程相同

每创建一个线程时,虚拟机会在栈空间中分配一小块内存给该线程,用来存放与该线程运行相关的局部变量

线程内的每个方法被执行时,Java 虚拟机都会同步创建一个栈帧用于存储局部变量表操作数栈动态连接方法出口等信息

一个栈帧需要分配的内存在编译 Java 源程序的时候就已经可以完全确定,并不会受到程序运行期间变量数据的影响

每一个方法被调用直至执行完毕的过程,就对应着一个栈帧从入栈到出栈的过程

关于虚拟机栈的细节可见 虚拟机栈

本地方法栈

本地方法栈和虚拟机栈一样都是线程私有的。不仅如此,它们俩所发挥的作用也是非常相似的,其区别:

本地方法栈允许被实现成固定或可动态扩展的内存大小 (在内存溢出方面也是和虚拟机栈相同的)

当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界,它和虚拟机拥有同样的权限

《Java 虚拟机规范》对本地方法栈中方法使用的语言、使用方式与数据结构并没有任何强制规定,因此具体的虚拟机可以根据需要自由实现它,甚至有的 Java 虚拟机 (如:HotSpot 虚拟机) 直接把本地方法栈和虚拟机合二为一

堆和上面介绍的三个区域都不太一样,它是被所有线程共享的一块内存区域,而且也是虚拟机所管理内存中最大的一块

《Java 虚拟机规范》中对堆的描述是:所有的对象实例以及数组都应当在堆上分配。但随着 Java 的发展,我们需要用辩证的态度去看待这句话,关于这个观点更多分析可见 逃逸分析

所以在《深入理解 Java 虚拟机》中,作者并没有说的那么绝对,而是说:Java 世界里几乎所有的对象实例都在堆中分配内存。关于在堆上创建对象的详细内容可见 对象的创建

回收内存角度

提到「堆」,就肯定离不开「垃圾收集」,这里先浅浅分析一下两者的关系,后面再专门详细介绍!

在运行时数据区的五个部分中,垃圾收集器只负责管理「堆」和后面要介绍的「方法区」,至于为什么只对这两个区域进行垃圾收集,详情可见 生存还是死亡??

从回收内存的角度看,由于现代垃圾收集器大部分都是基于分代收集理论设计的,所以 Java 堆经常出现「新生代、老年代、永久代、Eden 空间、From Survivor 空间、To Survivor 空间」等划分

上面提到的划分仅仅只是一部分垃圾收集器的共同特性或设计风格而已,而非某个 Java 虚拟机具体实现的固有内存布局,

在 10 年前,作为业界绝对主流的 HotSpot 虚拟机,它内部的垃圾收集器全部基于「经典分代」来设计,需要新生代和老年代收集器搭配才能工作,在这种背景下,上述说法还算是不会产生太大的歧义

但是到了今天,垃圾收集器技术与 10 年前已不可同日而语,HotSpot 里面也出现了不采用分代设计的新垃圾收集器

本人的总结主要还是围绕 HotSpot 基于「经典分代」的垃圾收集器,上面巴拉巴拉说一堆只是希望不要形成一种固有思想,认为所有垃圾收集器都是基于「经典分代」,也有不采用分代滴!!

分配内存角度

堆是用来给实例对象分配内存的区域,说完了「回收内存」,肯定还要聊一聊「分配内存」

Java 程序中创建对象的频率非常高,如果没有处理好内存分配,不仅可能导致效率非常低,更严重的,还会在多线程下出现同步问题

虽然 Java 堆是所有线程共享的,但是也会划分出多个线程私有的分配缓冲区 (TLAB),以提升对象分配时的效率

每个线程在 Java 堆中预先分配一小块内存 (TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有缓冲区用完了,分配新的缓冲区时才需要同步锁定

扯了这么多,无论从什么角度,无论如何划分,都要把握好一个原则:Java 堆就是用来存储对象的,无论是哪个区域,存储的只能是对象的实例。将 Java 堆细分的目的只是为了更好地回收内存,或是更快地分配内存

根据《Java 虚拟机规范》,Java 堆可以是处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的!!

Java 堆即可以实现成固定大小的,也可以是可扩展的,当前主流的 Java 虚拟机都是按照可扩展来实现的 (可通过参数-Xms-Xmx设定)

将堆的-Xms参数和-Xmx参数设置为一样即可避免堆自动扩展;当堆中没有内存完成实例分配,并且堆也无法再扩展时,Java 虚拟机将会抛出 OutOfMemoryError (OOM) 异常

方法区

「方法区」和「堆」一样,它们都是线程共享的,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据

虽然《Java 虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作「非堆」,目的是与 Java 堆区分开来

关于方法区的细节可见 方法区