查看原文
其他

初探:Java虚拟机那点破事

杨杰 终端研发部 2022-08-26

点击上方的终端研发部,右上角选择“设为星标

每日早10点半,技术文章准时送上

公众号后台回复“学习”,获取作者独家秘制精品资料


往期文章

如何让你的bug无处可藏?

记五月的一个Android面试经

接口测试时, 数据Mock为何如此重要?

什么样的简历,面试官一看就知道多少水份

方舟编译器源代码疑似曝光,华为李小龙:来自服务器部门?

本文由作者杨杰投稿,原创发布,

链接:blog.csdn.net/sinat_35938012/article/details/97790319

前言

从学习Java的第一天开始,到如今工作当中,想必大家都耳闻目染了各种Java的优点。其中肯定少不了:Java有虚拟机,java是跨平台的,一次编译到处运行。在相当长的一段时间里对此观点都只是一个很模糊的概念,对自己写的代码也有一种吃不透的感觉。犹如一只拦路的大老虎,望而生畏,止步不前。一番思量,一日不解决掉,对技术难以有更深层次的理解,只好硬着头皮上。

不能跨平台的原因是怎样造成的?

2.1 机器语言和汇编
计算机只认识0和1 这句话大家都听说过。的确,正所谓大道至简,0 和 1 足以撑起整个互联网世界。在早期编程中,都是编写一条条0和1组成的指令来开发,要自己处理每一块数据的存储分配和输入输出。可想而知,满屏的0和1,程序容易出错且可读性很差。
使用0和1组成的机器指令来编程,太过于繁琐,单单只是记住0和1组成的指令就令人头大。完全可以用一种简易的方式代替记忆,例如做加法运算,而这个 加操作在机器码中可能是一个 010010 固定的指令,完全可以用 add 这个单词来代替记忆,简化了编程过程,这就是汇编语言。汇编语言的特点是用符号代替了机器指令代码,而且符号与指令代码一一对应,基本保留了机器语言的灵活性。而再将add指令转为010010机器码的程序便是汇编语言编译器。
2.2 硬件关系
组装过电脑的朋友都知道,组装一台电脑需要购买:CPU、内存条、硬盘,主板等以及各种外设。对程序而言,一开始存储在硬盘当中,即便计算机断电,下次重启程序依旧存在。CPU 是一个复杂的计算机部件,它内部又包含很多小零件,如下图所示:
内存对于 CPU 来仅仅是一个存放指令和数据的地方,并不能在内存中完成计算功能。例如要计算 a = b + c,必须将 a、b、c 都读取到 CPU 内部才能进行加法运算,寄存器是存储 CPU 执行所需数据的区域,是 CPU 不可或缺的一部分,所有程序都只能通过操作寄存器,达到控制 CPU 目的,完成计算任务。
2.2 芯片架构
arm、X86两种芯片架构广泛应用在 PC 机和移动端嵌入式设备中。前者由arm公司设计,后者由Intel、amd共同设计,双方交叉授权使用。arm 是精简指令集架构(RSIC),功耗较低,性能随之也降了下来。x86 是复杂指令集架构(CISC),功耗较高,性能强。arm架构的寄存器 比 x86架构 的多不少。寄存器和指令集加架构本身的差异性,也是造成不能跨平台的原因。近几十年来,硬件的性能一直都在飞速发展,CPU架构 也经历了几次较大的改变。x86架构从最早的 16 位到 32 位再到现在的 64 位架构。arm架构 也从 v1 发展到了如今的 v8的64位架构。一般新的架构都会向前兼容几个版本,保证旧架构上的老代码,能够在新架构上运行。但这样做,却无法发挥出新架构硬件的性能,无疑是对资源的浪费。在开发中如果涉及到底层库的使用,则需要考虑兼容不同架构的CPU。例如在使用百度地图SDK时,会下载不同CPU架构的so文件,还有 X86 架构的,就是为了兼容不同CPU架构的手机。
Android可以通过adb命令来查看cpu信息1、adb shell 2、cat /proc/cpuinfo
2.3 C语言为什么不能夸平台?
通常认为 C 语言是编译型语言。在编译阶段,编译器直接将源码编译为 对应CPU架构和操作系统上的可执行文件。如下图所示 c 语言代码编译为的汇编代码:
Windows 部分汇编指令:
ubuntu 部分汇编指令:
虽然读不太懂汇编指令,比较了一下差异还是不小的。C 语言更多的是偏向底层开发,只要编译器足够强大,支持对应平台的编译,或者对应平台提供有C 编译器(C 语言的编译器也是众多语言中最多的)。程序就能在对应平台执行,也许 C 语言从来就没有想过要跨平台。
代码与平台有关性,是不能跨平台的原因。
3. JVM是如何做到跨平台的
讲了这么多不能夸平台的原因,再来理解Java是如何做到跨平台就容易得多了。JVM 在编译阶段,只将 .java的源码,编译为和平台无关的 .class 字节码文件。不同 CPU 架构和操作系统上都会编译为相同的 calss 文件(最多只是 JDK 版本不同,有些许差异,jdk 都会向前兼容几个版本)。再由不同平台上的自行实现JVM。我们只需要搭建相应平台的运行环境即可,便可做到任意平台开发编译,到处运行。
JVM 在真机基础之上模拟了一套自己的架构,有自己的指令集、内存管理等。在使用 Eclipse 追溯源码时,常常会遇到只有 class 文件,而没有源码出现下面的页面:
图中红色框内的便是字节码指令,运行时通过逐条解释执行,这也是以前 Java 被指性能底下的诟点。的确,解释执行的性能确实是和 C 编译目标代码比不了,但是在 JDK1.2 时就支持 JIT 及时编译器。程序运行期间,分析热点(经常调用)函数,编译为本地代码缓存起来,以后直接执行本地代码。虽然性能还是和编译型的语言有一定的差异,但 Java 凭借其语言特性以及各种成熟的 Web 解决方案,这点性能差显得不那么重要,完全能够接受。JIT 编译代码如下:
有些JVM是采用纯JIT编译方式实现的,内部没解释器,例如JRockit、Maxine VM和Jikes RVM —RednaxelaFX

4.JVM内存结构

内存作为程序运行中的临时存储介质,本质上不进行任何的区域划分,为了能够合理有效的使用回收内存,才将内存划分出更多的区域。平时听得较多的就是堆栈内存,堆栈是一种数据结构,也是一种概念模型。不同的语言有自己的实现方式,通常在 Oop编程中,栈存放函数执行时所需的局部变量,函数执行完即释放,堆内存存储对象。
操作系统内存布局
Windows 上栈内存由系统回收,堆内存由程序员自行回收。因为栈上内存不可控,JVM 只能在操作系统的堆内存上开辟自己的空间。
JVM运行时内存结构

JVM堆

所有类实例和数组都从堆中分配,官方JVMS8规范文档 的确是这样描述的 The heap is the run-time data area from which memory for all class instances and arrays is allocated 。有一个很常见情况下,函数执行中产生的对象在堆中分配,函数执行结束,不再引用的对象,已经没有存在的必要了。这些对象在堆中等待下一次GC,而大多对象朝生即死,生命周期极短,等待GC这段时间,也是对资源的浪费。在JDK1.5时JVM提供支持逃逸分析技术,通过分析对象作用域,实现了栈上分配、标量替换、同步消除优化等技术。通过函数传递对象,称之为方法逃逸。将对象赋值给其他线程变量,称之为线程逃逸:

标量替换

 不可再分解的基础数据类型称之为标量,例如Java中的八大基础类型和引用类型。反之、如果某个对象还可继续分解,则该对象属于聚合量,Java类就是典型的聚合量。标量替换则是将对象的成员变量分解成原始数据类型,代替对象在栈中分配。

栈上分配

JDK1.8默认开启逃逸分析,确定对象不会再被外部引用,通过标量替换将对象分解在栈中分配,栈中的对象随着栈帧的出栈而销毁,大大的减少了堆内存的占用和GC的压力。
开启逃逸分析(1.8默认开启)
关闭逃逸分析:
可以看到,关闭逃逸分析总共使用堆内存 22M ,开启逃逸分析只使用了 5M 左右。节约了不少堆内存空间,减少了 GC 压力。
开启逃逸-XX:+DoEscapeAnalysis -XX:+PrintGC
关闭逃逸-XX:-DoEscapeAnalysis -XX:+PrintGC
同步消除
如果逃逸分析确认对象的作用范围不会超过当前线程,则消除对变量的同步措施。

JVM栈

JVM栈 是方法执行所需的数据结构,每个线程都拥有一个JVM栈,随着线程的创建而创建,随着线程的销毁而销毁。JVM栈 以栈帧的单元,存放局部变量、操作数栈、动态链接、方法返回信息。具体可以参考
方法区/元数据区
方法区中存放已被虚拟机加载的类信息,并且每个类只会存在一份,作为使用该类的入口。我们所编写的代码类,经过javac编译器,编译存储为 class 文件,在使用该类时(创建类的实例,调用了类静态方法类等),如果该类还未加载,会先将该 class 字节流从磁盘或者其他途径方式,加载存储到方法区当中,并且创建该类的 class对象 供以后访问使用。
运行时常量池
运行时常量池作为方法区的一部分,为每一个类都维护一个常量池,存放着编译时已知的字面量和各种符号引用。可参考粗谈Java虚拟机2_Class文件分析
PC寄存器
每个JVM线程都有自己的PC(程序计数器)寄存器。在任何时候,每个JVM线程都在执行单个方法的代码,如果执行的不是native方法,则pc寄存器包含当前正在执行的Java字节码指令的地址。如果当前执行的native方法,则PC寄存器的值undefined。
本地方法栈

支持 native 方法调用,随着线程的创建来分配本地方法栈。

参考:深入理解Java虚拟机一书

阅读更多

如何让你的bug无处可藏?
热修复的几种方案,你可知否?
昨天,我又“装”上了Windows 1.0
老板:请将你的代码进行Code Review!为什么我把 Run 出来的 Apk 发给老板,却装不上!

相信自己,没有做不到的,只有想不到的

在这里获得的不仅仅是技术!

喜欢就给个“在看” 

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存