当前位置:首页 > 问答 > 正文

Java虚拟机那些不为人知的秘密和运行机制大揭秘

综合自多位资深JVM工程师的技术博客、公开演讲以及《深入理解Java虚拟机》等经典著作中的非核心机密部分,以通俗化语言呈现)

Java虚拟机,也就是我们常说的JVM,对很多程序员来说就像一个黑盒子,我们知道Java代码编译成class文件后,扔给它就能跑,但里面到底发生了什么,却鲜为人知,今天就来揭秘一些不那么广为人知,但又非常有趣的秘密和运行机制。

第一个秘密:JVM并不是“一次编译,到处运行”的绝对执行者,它其实是个“翻译官”加“临时工”。

Java虚拟机那些不为人知的秘密和运行机制大揭秘

很多人以为JVM直接执行class文件里的指令,其实不然,Class文件里存的是一种高度优化的中间代码,叫做“字节码”,JVM的工作是读取这些字节码,然后在自己所在的特定操作系统和硬件平台上,把它“翻译”成当地CPU能直接理解的机器码来执行,这个翻译过程,就是JVM最核心的秘密之一,更妙的是,JVM并不是在程序启动时就把所有字节码都一次性翻译完,那样会非常慢,它采用的是“即时编译”技术,也就是JIT编译器在起作用,JVM会先让一个叫“解释器”的家伙一行行地边翻译边执行字节码,这样程序能马上跑起来,JVM会偷偷地在后台监视着,哪些方法被调用的特别频繁,哪些循环跑的特别多,它就会把这些“热点代码”标记出来,JIT编译器就会出手,把这些热点代码一次性编译成本地机器码,并优化保存起来,下次再执行到这段代码时,就直接运行优化后的机器码,速度就飞起来了,所以JVM是个非常聪明的动态优化系统,而不是个死板的执行机器。(来源:Oracle官方JVM规范及JIT编译器相关技术文档)

第二个秘密:Java号称的自动内存管理(垃圾回收),其实并不是真的“自动”,它只是在帮你做你不想做的脏活累活,而且方式很“势利眼”。

我们都知道JVM有垃圾回收器(GC)来回收不用的内存,避免内存泄漏,但GC的工作机制有个不为人知的特点:它存在“代际假设”,JVM把内存堆区大致分为年轻代和老年代,它假设绝大部分对象都是“朝生夕死”的,比如方法里的局部变量,用一下就没用了,所以新创建的对象基本都放在年轻代,年轻代的GC发生非常频繁,但每次回收速度都很快,因为里面大部分对象都是垃圾,直接清掉就行,这叫“Minor GC”,而经过多次年轻代GC还能“存活”下来的对象,就会被认为是重要的、会长期存在的对象,JVM就会把它们提拔到老年代,老年代里的对象都是“老油条”了,所以针对老年代的“Major GC”或“Full GC”不会频繁发生,但一旦发生,因为要检查的对象又多又复杂,会导致整个应用程序停顿比较长的时间,这就是所谓的“Stop-The-World”现象,所以你看,GC并不是平等地对待所有对象,它对年轻对象很“残忍”,频繁清理;对老年对象很“宽容”,但清理一次代价很大,理解这一点,对于写出对GC友好的高性能代码至关重要。(来源:《深入理解Java虚拟机》中关于分代收集理论的章节)

Java虚拟机那些不为人知的秘密和运行机制大揭秘

第三个秘密:那个看似简单的Class文件,内部结构复杂的像个精密的集装箱,藏着许多编译期的小心机。

Class文件不仅仅是字节码的集合,它是一个结构非常严谨的格式,里面不仅有执行代码,还有一个叫“常量池”的东西,像个仓库,存放了程序中使用到的各种常量值、类名、方法名、字段名甚至符号引用,这带来一个有趣的现象:JVM在运行时,对于字符串常量和基本类型的包装类(比如Integer),为了效率,会使用一种叫“缓存”或“驻留”的机制,最典型的就是字符串常量池,当你写 String s = "abc" 时,“abc”这个字符串字面量在编译期就被放进Class文件的常量池,当JVM加载这个类时,会在内存的元空间(Metaspace,可以理解为方法区的一种实现)里创建一块叫“字符串常量池”的区域来存放它,如果后面再有代码也写了 String s2 = "abc",JVM不会创建新的“abc”字符串对象,而是直接让s2指向常量池里已经存在的那个“abc”,这能节省内存,但反过来,如果你用 new String("abc"),就一定会创建一个新的对象,这种编译期和运行期联手做的优化,很多时候程序员是感知不到的,但理解了就能避免一些坑。(来源:Java语言规范及Class文件结构详解)

第四个秘密:JVM的类加载过程,远不止“找到class文件然后加载”那么简单,它其实是一场严肃的“入职审查”。

Java虚拟机那些不为人知的秘密和运行机制大揭秘

一个类从被加载到可以正常使用,要经历加载、链接、初始化三个大阶段,而链接又细分为验证、准备、解析三步,这其中,“验证”这一步尤其重要且不为人知,JVM对准备加载的class文件会进行非常严格且多方面的验证,比如文件格式验证(是不是以魔数0xCAFEBABE开头)、元数据验证(类是否有正确的父类、是否实现了该实现的方法)、字节码验证(确保代码不会对数据类型做危险操作,比如把整数当成对象引用来用)、符号引用验证(确保能找到所引用的其他类、方法或字段)等,这套严格的验证机制,是Java语言安全性的基石之一,它确保了被加载的类不会包含恶意代码去破坏JVM本身的运行(比如篡改内存),这也是为什么Java的Applet技术当年敢在浏览器沙箱里运行的原因,类加载不是一个简单的搬运工,而是一个高度警惕的安全检查员。(来源:Java虚拟机规范中关于类加载和验证机制的章节)

第五个秘密:我们常说的“JVM内存模型”,其实主要不是为了告诉你内存怎么分配,而是为了规定多线程环境下,数据该如何“安全地”被看见。

Java内存模型(JMM)是一个常被误解的概念,它并不是指前面提到的堆、栈那些内存区域划分,而是一套抽象的规则,定义了程序中各个变量(尤其是共享变量)的访问方式,以及如何在多线程环境下保证内存的可见性、有序性和原子性,一个核心的、不为人知的细节是“主内存”和“工作内存”的抽象,JMM规定,每个线程都有自己的“工作内存”,它并不是真实存在的一块内存,而是对CPU缓存和寄存器的抽象,线程不能直接操作主内存的变量,必须先把变量拷贝到自己的工作内存,操作完再同步回主内存,这就带来了著名的“可见性”问题:线程A修改了共享变量,但可能只是修改了自己工作内存里的副本,还没来得及刷回主内存,此时线程B去主内存读取,读到的就是旧值,JMM通过volatile、synchronized等关键字定义了一系列规则(如happens-before原则),来告诉编译器和JVM,在什么情况下必须保证一个线程的修改能及时被其他线程看到,什么情况下不能随意重排序指令,理解JMM,才是真正理解Java并发编程的关键。(来源:JSR-133(Java内存模型与线程规范)及相关解读文章)

就是Java虚拟机一些不那么广为人知但至关重要的运行机制和秘密,它远不是一个简单的代码执行器,而是一个融合了编译优化、内存管理、安全控制和并发支持的复杂而精妙的系统。