一图胜千言:JVM整体架构长什么样
用一张图看全貌,是理解JVM的最快方法。这张图可以分成几个大块:最底下的部分是我们写的Java代码,也就是源码(来源:技术社区分享的JVM架构图)。源码经过编译器变成一种叫‘字节码’的东西,字节码就是.class文件里的内容,可以把它想象成一套JVM能看懂的通用指令集。字节码被一个叫‘类加载器’的模块加载到JVM里面(来源:Oracle官方JVM规范概述)。加载进来的东西放在一个叫‘运行时数据区’的地方,这是JVM干活的‘内存舞台’。紧接着,一个叫‘执行引擎’的核心部件开始解释或者编译这些字节码指令,让它们在真正的计算机CPU上跑起来。执行引擎干活的时候,还需要调用一些本地方法的接口,去用到底层操作系统或其他语言写的库。整张图把‘代码怎么变成指令’、‘指令和数据放在哪’、‘谁来执行指令’这几个核心流程串了起来,一目了然。(来源:综合多家技术博客对JVM架构的图解分析)
内存舞台:运行时数据区里发生了什么
JVM的内存舞台——运行时数据区,是它最核心、也最容易出问题的地方。这块内存主要分成几个区域,各自有明确的职责(来源:《深入理解Java虚拟机》核心概念)。第一个是‘堆’,这是最大的一块,几乎所有我们通过‘new’关键字创建出来的对象都放在这里,它是被所有线程共享的。因为对象在这里生生死死,会产生很多垃圾,所以这里也是垃圾回收机制主要工作的战场。第二个是‘方法区’,它用来存放加载进来的类信息、常量、静态变量这些数据,它也是共享的。第三个是‘虚拟机栈’,每个线程在运行时都会有一个自己的栈。栈里面是一个个‘栈帧’,每个方法被调用时,就会压入一个栈帧,里面存放这个方法的局部变量、操作数栈等信息,方法执行完,这个栈帧就被弹出。还有一个‘程序计数器’,它非常小,但很关键,用来记录当前线程执行到哪一条字节码指令了。本地方法栈和虚拟机栈类似,不过是给本地方法(用其他语言写的方法)用的。理解这几块内存谁管什么、谁共享谁私有,是理解程序运行和排查内存问题的关键。(来源:技术社区常见的JVM内存模型讲解)
垃圾回收:自动打扫内存的清洁工
在堆里创建对象很方便,但对象如果没人用了还占着地方,内存很快就会不够。JVM的垃圾回收机制就是自动打扫内存的清洁工。它怎么知道哪个对象是垃圾呢?主要靠‘引用计数’和‘可达性分析’两种算法(来源:垃圾回收算法经典论文概述)。简单说,可达性分析就是从一些肯定不能被回收的‘根’对象(比如正在运行的线程、静态变量等)出发,看看哪些对象能被直接或间接引用到,那些从任何根都走不到的对象,就被标记为‘垃圾’。确定了垃圾,接下来就是回收。回收不是简单的一删了事,因为会产生内存碎片。所以清洁工有不同的打扫策略,形成了不同的垃圾收集器,比如Serial、Parallel、CMS、G1等(来源:HotSpot虚拟机垃圾收集器文档)。它们有的追求停顿时间短(让程序卡住的时间少),有的追求总体吞吐量大(单位时间内干的活多)。调优时经常要做的就是根据应用特点,选择合适的清洁工和它的工作参数。这个机制让Java程序员不用太操心内存释放,但也因为它的自动和复杂,一旦出问题,排查起来就需要对这些原理有深入理解。(来源:众多线上故障排查经验总结)
执行引擎:让字节码跑起来
类加载好了,内存也分配了,最后一步就是执行字节码,这是执行引擎的工作。执行引擎并不是直接执行字节码,它有两种主要工作模式(来源:JVM即时编译器技术介绍)。一种是‘解释执行’,就是读一条字节码指令,就翻译一条对应的本地机器指令去执行,好处是启动快。另一种是‘编译执行’,也叫即时编译(JIT)。HotSpot虚拟机会监控那些被频繁执行的代码(热点代码),把整段字节码一次性编译优化成本地机器码,下次再执行这段代码就直接用更快的本地代码,大大提升了效率。这种混合模式让Java程序在启动速度和长期运行效率之间取得了很好的平衡。执行引擎在运行过程中,还会通过‘本地方法接口’去调用一些用C/C++等语言写的函数库,来完成一些需要直接操作底层硬件的任务。所以,从字节码到机器码,这最后一步的转换和优化,是JVM性能的关键所在,也是各大厂商JVM实现竞争的技术高地。(来源:OpenJDK项目相关技术讨论)