Java-JVM
- 是可运行 Java 代码的假想计算机 ,包括字节码指令集、寄存器、栈、 垃圾回收堆和存储方法域。
- 运行在操作系统之上的,它与硬件没有直接的交互。
内存区域
- 分为线程私有区域(程序计数器、Java虚拟机栈、本地方法栈)、线程共享区域(堆、方法区)、直接内存。
线程私有区域
- 随用户线程的启动/结束而创建/销毁。
程序计数器
- 一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每个线程都有独立的程序计数器。
- 是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的内存区域。
Java 虚拟机栈
用于描述方法执行,一个线程的方法的调用和返回对应栈的压栈和出栈。
主要存放一些基本的数据类型和对象句柄(引用);存取速度比堆快,仅次于寄存器,栈数据可以共享;静态分配内存,编译时确定生存期大小。
每个方法在执行的同时都会创建一个栈帧 (Stack Frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
栈帧是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接 (Dynamic Linking)、 方法返回值和异常分派(Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁。
本地方法栈
- 与 Java 虚拟机栈作用类似,区别是后者为 Native 方法服务,前者为 Java 方法服务。
线程共享区域
- 随虚拟机的启动/关闭而创建/销毁。
堆
保存创建的对象和数组,是垃圾收集器进行垃圾收集的最重要的内存区域。
从垃圾收集的角度细分为: 新生代(Eden 区、From Survivor 区、To Survivor 区)与老年代。
方法区/永久代
- 永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据.
- HotSpot VM 把 GC 分代收集扩展至方法区, 即使用 Java 堆的永久代来实现方法区, 这样 HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理这部分内存, 而不必为方法区开发专门的内存管理器。
直接内容
- 不是 JVM 运行时数据区的一部分, 但也会被频繁的使用。
- 在 JDK 1.4 引入的 NIO 提供了基于 Channel 与 Buffer 的 IO 方式, 它可以使用 Native 函数库直接分配堆外内存, 然后使用 DirectByteBuffer 对象作为这块内存的引用进行操作,这样就避免了在 Java 堆和 Native 堆中来回复制数据, 因此在一些场景中可以显著提高性能。
垃圾回收
运行内存
新生代(Young Generation)
- 用来存放新生的对象。一般占据堆的 1/3 空间。
- 由于频繁创建对象,所以新生代会频繁触发 MinorGC 进行垃圾回收。
Eden
- Java 新对象的出生地。
- 如果新创建的对象占用内存很大,则直接分配到老年代。
- 当区内存不够的时候就会触发 MinorGC,对新生代进行一次垃圾回收。
From Survivor
- 上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
To Survivor
- 保留了一次 MinorGC 过程中的幸存者。
老年代(Tenured/Old Generation)
- 主要存放生命周期长的内存对象。
- 对象比较稳定,所以 MajorGC 不会频繁执行。
判断回收
引用计数器法
- 为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。
- 缺点是不能解决循环引用的问题。
可达性分析算法
- 从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则称该对象是不可达达。
- 不可达对象不等价于可回收对象,不可达对象经历两次标记后仍然是可回收对象,则将面临回收。
回收算法
标记-清除算法(Mark-Sweep)
- 最基础的垃圾回收算法,分为两个阶段,标记和清除。
- 标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。
- 缺点:内存碎片化严重。
复制算法(Copying)
为了解决标记-清除算法算法内存碎片化的缺陷而被提出的算法。
按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清理掉。
缺点:内存使用率不高,且存活对象过多,效率会大大降低。
标记-整理算法(Mark-Compact)
- 结合了以上两个算法,为了避免缺陷而提出。
- 标记阶段和 Mark-Sweep 算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。
分代收集算法
- 目前大部分 JVM 所采用的方法,核心思想是根据对象存活的不同生命周期将内存划分为不同区域,根据不同区域选择不同算法。
- 一般情况下将堆划分为新生代和老生代,新生代一般采用复制算法,老年代一般采用标记-整理算法。
新生代与复制算法
- 每次只使用 Eden 和 From Survivor 区域存储,当内存不够时触发 MinorGC 进行垃圾回收。
- Eden 和 From Survivor 区域中存活的对象复制到 To Survivor 区域,同时把这些对象的年龄+1。
- 如果有对象的年龄达到了老年的标准(默认为15岁),或者 To Survivor 区域位置不够,就复制到老年代。
- 清空 Eden 和 From Survivor 区域。
- From Survivor 和 To Survivor 互换,原 To Survivor 成为下一次 GC 时的 From Survivor 区域。
老年代与标记-整理算法
- 在进行 MajorGC 前一般都先进行 了一次 MinorGC,使得有新生代的对象晋身入老年代。
- 当无法找到足够大的连续空间分配给新创建的较大对象时会抛出OOM(Out of Memory)异常,并提前触发一次 MajorGC 以腾出空间。
分区收集算法
- 将整个堆空间划分为连续的不同小区间, 每个小区间独立使用,独立回收。
- 可以控制一次回收多少个小区间,根据目标停顿时间,每次合理地回收若干个小区间(而非整个堆),从而减少一次 GC 所产生的停顿。
垃圾收集器
Serial
单线程、复制算法
最基本的垃圾收集器, JDK1.3.1 之前新生代唯一的垃圾收集器。
在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。
简单高效,对于限定单 CPU 环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率。
JVM 运行在 Client 模式下默认的新生代垃圾收集器。
ParNew
多线程、复制算法
Serial 收集器的多线程版本,其余的行为和 Serial 收集器完全一样。
默认开启和 CPU 数目相同的线程数。
JVM 运行在 Server 模式下默认的新生代垃圾收集器。
Parallel Scavenge
- 多线程、复制算法、吞吐量
- 重点关注的是程序达到一个可控制的吞吐量(Thoughput)。
- 吞吐量:CPU 用于运行用户代码的时间/CPU 总消耗时间,即运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
- 采用自适应调节策略。
- 主要适用于在后台运算而不需要太多交互的任务。
Serial Old
单线程、标记-整理算法
Serial 的老年代版本。
JVM 运行在 Server 模式下默认的老年的代垃圾收集器。
在 Server 模式下,主要有两个用途:
- 在 JDK1.5 之前版本中与 Parallel Scavenge 搭配使用。
- 作为年老代中使用 CMS 收集器的后备垃圾收集方案。
Parallel Old
- 多线程、标记-整理算法、吞吐量
- Parallel Scavenge 的老年代版本。
- JDK1.6 开始提供。
CMS 收集器
- 多线程、标记-清除算法
- 年老代垃圾收集器,最主要目标是获取最短垃圾回收停顿时间,从而为交互比较高的程序提高用户体验。
- 初始标记:标记 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
- 并发标记:进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
- 重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
- 并发清除:清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。
由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,因此总体上来看 CMS 和用户线程是一起并发地执行的。
G1(Garbage first)收集器
- 标记-整理算法、分区收集算法、吞吐量
可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
避免全区域垃圾收集,把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。
引用类型
强引用
- 最常见的引用类型。
- 把一个对象赋给一个引用变量,这个引用变量就是一个强引用。
- 当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。
- 是造成 Java 内存泄漏的主要原因之 一。
软引用
- 用 SoftReference 类来实现。
- 对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。
- 软引用通常用在对内存敏感的程序中。
弱引用
- 用 WeakReference 类来实现。
- 它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
虚引用
- 用 PhantomReference 类来实现。
- 不能单独使用,必须和引用队列联合使用。
- 主要作用是跟踪对象被垃圾回收的状态。
类加载机制
将类的 .class
文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class
对象,用来封装类在方法区内的数据结构。
最终产品是位于堆区中的 Class 对象,Class 对象封装了类在方法区内的数据结构,并提供了访问方法区内的数据结构的接口。
加载
在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的入口。
一般的加载来源包括从本地路径下编译生成的、jar包中的、远程网络以及动态代理实时编译的.class文件。
验证
主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。
对于文件格式的验证,比如常量中是否有不被支持的常量,文件中是否有不规范的或者附加的其他信息。
对于元数据的验证,比如该类是否继承了被 final 修饰的类,类中的字段、方法是否与父类冲突,是否出现了不合理的重载。
对于字节码的验证,保证程序语义的合理性,比如要保证类型转换的合理性。
对于符号引用的验证,比如校验符号引用中通过全限定名是否能够找到对应的类,校验符号引用中的访问性(private,public等)是否可被当前类访问。
准备
主要是为类变量分配内存,并且赋予初值,即在方法区中分配这些变量所使用的内存空间。
初值不是代码中具体写的初始化的值,而是Java虚拟机根据不同变量类型的默认初始值:
- 8 种基本类型的初值,默认为 0;
- 引用类型的初值则为 null;
- 常量的初值即为代码中设置的值。
解析
- 将常量池内的符号引用(类名、方法名、字段名等)替换为直接引用(具体的内存地址或偏移量)。
符号引用
- 一个字符串,这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
- 与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。
- 各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中。
直接引用
- 指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。
初始化
执行类构造器 clinit()
方法
类构造器
- clinit 方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。
- 虚拟机会保证子 clinit 方法执行之前,父类的 clinit 方法已经执行完毕。
- 如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成 clinit 方法。
- 以下几种情况不会执行类初始化:
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
- 定义对象数组,不会触发该类的初始化。
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类的初始化。
- 通过类名获取 Class 对象,不会触发类的初始化。
- 通过 Class.forName 方法加载指定类,如果指定参数 initialize (是否要对类进行初始化)为 false 时,也不会触发类初始化。
- 通过 ClassLoader 默认的 loadClass 方法,不会触发该类的初始化。
类加载器
- 加载动作在 JVM 外部实现,以便让应用程序决定如何获取所需的类。
启动类加载器(Bootstrap ClassLoader)
- 负责加载
JAVA_HOME\lib
目录中的,或通过-Xbootclasspath
参数指定路径中的,且 JVM 认可的类。
扩展类加载器(Extension ClassLoader)
- 负责加载
JAVA_HOME\lib\ext
目录中的,或通过java.ext.dirs
系统变量指定路径中的类库。
应用程序类加载器(Application ClassLoader)
- 负责加载用户路径 (classpath) 上的类库。
自定义加载器(User ClassLoader)
- 通过继承
java.lang.ClassLoader
实现。
双亲委派机制
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载器。
只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的 Class),子类加载器才会尝试自己去加载。
不管是哪个加载器加载同一个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同一个 Object 对象。