初探:Java虚拟机那点破事

前言

从学习Java的第一天开始,到现在工做当中,想必你们都耳闻目染了各类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 是一个复杂的计算机部件,它内部又包含不少小零件,以下图所示:shell

内存对于 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架构的手机。ubuntu

Android能够经过adb命令来查看cpu信息一、adb shell 二、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 编译代码以下:image

有些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对象 供之后访问使用。image

运行时常量池运行时常量池做为方法区的一部分,为每个类都维护一个常量池,存放着编译时已知的字面量和各类符号引用。可参考粗谈Java虚拟机2_Class文件分析PC寄存器每一个JVM线程都有本身的PC(程序计数器)寄存器。在任什么时候候,每一个JVM线程都在执行单个方法的代码,若是执行的不是native方法,则pc寄存器包含当前正在执行的Java字节码指令的地址。若是当前执行的native方法,则PC寄存器的值undefined。本地方法栈

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

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

Android开发资料+面试架构资料 免费分享 点击连接 便可领取

《Android架构师必备学习资源免费领取(架构视频+面试专题文档+学习笔记)》

相关文章
相关标签/搜索