#1. 进程与线程
进程:进程是程序的一次执行过程。系统运行一个程序就是进程从创建到运行再到消亡的过程。
线程:一个进程中包含多个线程,线程共享进程的堆和方法区的资源以及直接内存,同时线程私有资源的包括程序计数器、虚拟机栈和本地方法栈。
#2. JVM运行时区域
#2.1 程序计数器:
- 字节码解释器通过改变程序计数器的值来一次读取指令,实现代码的流程控制:如顺序执行、选择、循环、异常处理等;
- 多线程情况下,程序计数器用于记录当前线程执行的位置,保证当线程切换回来时可以继续执行;
- 程序计数器私有化保证了线程切换回来后可以从正确的位置继续执行。
- 唯一一个不会出现 OutOfMemoryError 的内存区域,生命周期随线程的创建而创建,随线程的结束而死亡。
#2.2 Java虚拟机栈
虚拟机栈描述的是 Java 方法执行的内存模型,生命周期与线程相同。每个 Java 方法在执行时会创建一个栈帧用于存放局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
虚拟机栈中会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。
#2.3 本地方法栈
和虚拟机栈类似,区别在于虚拟机栈为执行 Java 方法服务,本地方法栈则为执行本地方法服务。本地方法栈中也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。
#2.4 堆
堆是 JVM 所管理的内存中最大的一块,是线程共享的一块内存区域,在 JVM 启动时创建,用于存放对象实例,几乎所有的对象实例以及数组都在堆上分配内存。
堆是垃圾收集管理的主要区域,也被称为 GC 堆。从 GC 的角度,当前的垃圾收集器都基本采用分代垃圾收集算法,因此堆可以细分为:新生代、老年代、永久代(JDK1.8 被移除,被直接内存中的元空间代替)。
#2.4.1 新生代
新生代用于存放新生的对象,一般占据堆空间的 1/3 。新生代又细分为 Eden 、 From Survivor 以及 To Survivor ,三者占比为 8:1:1 。新生代中会频繁地进行 Minor GC 操作实现垃圾回收。
- Eden:新创建的对象绝大部分会先分配在 Eden 区,如果对象太大会直接分配到老年代。当 Eden 区域内存不够时,就会触发 Minor GC(新生代采用复制算法),对新生代进行一次垃圾回收。
- From Survivor 和 To Survivor :在 GC 开始时,对象只会存在于 Eden 和 From Survivor 区,To Survivor 为空。在一次 Minor GC 发生后,仍然存活的对象会移动到 To Survivor ,其年龄加 1 ,并清空 Eden 和 rom Survivor 区。当对象的年龄达到一定程度(默认为 15 ),就会进入到老年代。Minor GC 完成后,From Survivor 和 To Survivor 功能互换,下一次 Minor GC 时,会把 To Survivor 和 Eden 区存活对象放入 From Survivor 区中,并计算对象存活的年龄。
#2.4.2 老年代
老年代主要存放应用中生命周期长的内存对象。老年代比较稳定,不会频繁地进行 Full GC 。在进行 Full GC 前会进行一次 Minor GC ,当新生对象进入老年代导致内存不够时 Full GC 才会触发。当无法找到足够大的连续空间分配给新创建的较大对象也会提前触发一次Full GC进行垃圾回收腾出空间。
老年代中 Full GC 采用标记-整理算法,Full GC 的耗时比较长,因为要扫描再回收。Full GC 会产生内存碎片,当老年代也没有内存分配给新来的对象的时候,就会抛出 OutOfMemoryError 异常。
#2.4.3 永久代
永久代指的是永久保存区域,主要存放 Class 和 Meta(元数据)的信息。在 JDK1.8 中,永久代被元空间代替,元空间不在虚拟机中,而是使用直接内存。
采用元空间不使用永久代的原因:
- 为了解决永久代的 OOM 问题,元数据和 class 对象存放在永久代中,容易出现性能问题和内存溢出。
- 类及方法的信息等比较难确定其大小,因此对于永久代大小指定比较困难,大小容易出现永久代溢出,太大容易导致老年代溢出(堆内存不变,此消彼长)。
- 永久代会为GC带来不必要的复杂度,并且回收效率偏低。
#2.5 方法区
方法区与对一样都是各个线程共享的内存区域,用于存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
JDK1.8 中将方法区移除,取而代之的是元空间,位于直接内存中。
方法区是 Java 虚拟机规范中的定义,是一种规范。永久代是对方法区的一种实现,是 HotSpot 的概念。
#2.6 运行时常量池
运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,收到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。
JDK1.7 及之后版本将运行时常量池从方法区中一处,在堆中开辟一块区域存放运行时常量池。
#2.7 直接内存
直接内存不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁使用,也可能导致抛出 OutOfMemoryError 异常。
JDK1.4 中新加入的 NIO(同步非阻塞IO),引入了一种基于通道(Channel)和缓存区(Buffer)的 I/O 方式,可以直接使用本地函数库分配对外内存,然后通过存储在堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,避免了在Java堆和本地堆之间来回复制数据。