Jvm 虚拟机概念
- Java 虚拟机(Jvm)是可运行 Java 代码的假想计算机,Java 虚拟机包括了一套字节码指令集、一组寄存器(用于存储每个线程下一条执行的 Jvm 指令)、一个栈、一个垃圾收集器和一个存储方法域
- 每一个平台(操作系统)的代码解释器是不同的,但是实现的虚拟机(Java虚拟机接口)是相同的,这也就是为什么 Java 能够跨平台
Java 代码编译和执行过程
- Java 源文件通过 Java 源码编译器生存相应的 .class 文件(字节码文件)
- 而字节码通过 Jvm 中的解释器(字节码指令集)编译成特定的机器码

Jvm 生命周期
- Jvm 实例对应了一个独立运行的 Java 程序
- 启动一个 Java 程序时,一个 Jvm 实例就产生了,main() 函数作为 Jvm 实例运行的起点
- Jvm 内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由 Jvm 自己使用,Java 程序也可以声明守护线程
- 当程序中的所有非守护线程都终止时,Jvm 才退出,若安全管理器允许,程序也可以使用 Runtime 类或者 System.exit() 退出
Jvm 内存组成
堆
- 所有通过 new 创建的对象的内存都在堆中分配,堆的大小可以通过 -Xmx 和 -Xms 来控制
- 堆内存分为 3 部分:
- 新生代(新生代进一步划分为 Eden、Survivor0、Survivor1 三个区):new 出来的对象一般情况下都会在新生代里的 Eden 区里面分配空间,如果存活时间足够长则会进入 Survivor 区
- 老年代:若新生代的对象存活更长则会被分配到老年代里
- 永久代:存放的是 Class 类元数据、方法等
栈
- 每个线程执行每个方法的时候都会在栈中申请一个栈空间
- 每个栈空间包括局部变量区和操作数栈,用于存放此次方法调用过程中的临时变量、参数和中间结果
本地方法栈
- 用于支持 native 方法的执行,存储了每个 native 方法调用的状态
方法区
- 方法区域存放了所有加载的类信息(名称、修饰符等)、类中的静态变量、常量、类的方法信息等
- 当开发人员通过 Class 对象中的 getName 等方法获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的

GC 垃圾回收
- Java 之所以不用像 C++ 一样手动处理内存的回收,是因为 JVM 虚拟机上增加了垃圾回收(GC)机制,GC 会在合适的时间触发垃圾回收,将不需要的内存空间回收释放,避免内存增长导致 OOM(内存泄露)
-
GC 会判断对象是否存活确定是否回收,判断对象是否存活一般有两种方式:
- 引用计数:每一个对象都有一个引用计数属性,新增一个引用时计数加 1,引用释放时减 1,计数为 0 时可以回收。此方法简单,但无法解决对象相互循环引用问题
-
可达性分析:
- 从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链时,则证明此对象时不可用的,不可达对象
- 在现实情况我们往往需要通过把指向某个对象的 Reference 置空来保证这个对象在下次 GB 运行的时候被回收
Object c = new Car(); c=null;
- 可手动置空是一个很繁琐的事情,对于简单状况,手动置空是不需要程序员来做的,因为对于简单对象,当调用它的方法执行完毕之后,所指向它的引用会被 stack(栈)中 popup(出栈),所以它就能在下一次 GB 执行时被回收了
垃圾收集算法
“标记-清除”
- “标记-清除”算法,算法分为“标记”和“清除”两个阶段:
- 首先标记出所有需要回收的对象
- 在标记完成后统一回收掉所有被标记的对象
- 之所以说它是最基础的收集算法,是因为后续算法都是基于这种思路对其改进
- 它的缺点主要是:
- 效率问题:通过标记和清除过程效率都不高
- 空间问题:标记清除后会产生大量不连续的内存碎片,导致程序在以后的运行过程中需要分配较大内存对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

“复制”
- “复制”算法将可用内存按容量划分为大小相等的两块,每次只使用其中一块,当这一块的内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次性清理掉
- 这样使得每次都是对其中一块内存进行回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。但这种算法代价是将内存缩小为原来的一半

“标记-压缩”
- “复制”收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低,更关键的是,如果不想浪费 50% 的空间,就需要额外的空间提供分配担保,以应对被使用的内存中所有对象都 100% 存活的极端情况,所以一般不能直接用这种算法
- “标记-压缩”标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存

分代回收算法
- “分代收集”算法把 Java 堆分成新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法
- 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量的存活,主要采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集
- 而老年代中因为对象存活率较高、没有额外空间对它进行分配担保,通常采用“标记-清除”或者“标记-压缩”算法
GC 收集器
Serial 串行收集器
- 串行收集器是最古老,最稳定以及效率最高的收集器,可能会产生较长的停顿
- 只使用一个线程去回收,采用分代回收算法
- 垃圾收集过程中会 Stop The World(服务暂停)
ParNew 并行收集器
- ParNew 收集器其实就是 Serial 收集器的多线程版本
Parallel 收集器
- Parallel 收集器类似 ParNew 收集器,Parallel 更关注系统的吞吐量,可以通过参数来打开自适应调节策略
- 虚拟机会根据当前系统运行情况收集性能监控信息,动态调整这些参数以提供最适合停顿时间或最大的吞吐量
- 也可以通过参数控制 GC 时间大于多少毫秒或者比例
CMS 收集器
- CMS 收集器是一种获取最短回收停顿时间为目标的收集器
- 目前很大一部分 Java 应用是B/S 系统的服务端上,这类应用尤其重视服务器的响应速度
- CMS 基于 “标记-清除” 算法实现,它的运作过程相对复杂,分为:
- 初始标记
- 并发标记
- 重新标记
- 并发清除
- 其中初始标记、重新标记这两步骤仍需要 “Stop The World”
- 优点:并发收集、低停顿
- 缺点:产生大量的空间碎片,并发阶段会降低吞吐量
G1 收集器
- G1 收集器是目前技术发展最前沿的成功之一,与CMS相比:
- 空间整合:G1 收集器采用”标记-压缩“算法,不会产生内存碎片,分配大对象时不会因为找不到连续空间而提前触发下一次 GC
- 可预测停顿:G1 追求低停顿时间外,还能建立可预测停顿时间模型,能让使用者指定在一个长度为 N 的毫秒时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒
- G1 收集器时,它将整个 Java 堆划分为多个大小相等的区域,虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔阂,他们都是一部分(可以不连续)Region 的集合。当新生代占用达到一定比例时,开始出发收集
网友评论