粗谈Java虚拟机1_开山篇

1. 前言

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


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

2.1 机器语言和汇编

计算机只认识0和1 这句话大家都听说过。的确,正所谓大道至简,0和1足以撑起整个互联网世界。在早期编程中,都是编写一条条0和1组成的指令来开发,要自己处理每一块数据的存储分配和输入输出。可想而知,满屏的0和1,程序容易出错且可读性很差。

case1.jpg

 使用 0和1 组成的机器指令来编程,太过于繁琐,单单只是记住0和1组成的指令就令人头大。完全可以用一种简易的方式代替记忆,例如做加法运算,而这个的操作在机器码中可能是一个 010010 固定的指令,完全可以用 add 这个单词来代替记忆,简化了编程过程,这就是汇编语言。汇编语言的特点是用符号代替了机器指令代码,而且符号与指令代码一一对应,基本保留了机器语言的灵活性。而再将add指令转为010010机器码的程序便是汇编语言编译器。

2.2 硬件关系

 组装过电脑的朋友都知道,组装一台电脑需要购买:CPU、内存条、硬盘,主板等以及各种外设。对程序而言,一开始存储在硬盘当中,即便计算机断电,下次重启程序依旧存在。CPU 是一个复杂的计算机部件,它内部又包含很多小零件,如下图所示:


118274690_1_20171206084852264.jpg

     图片摘自C语言中文网
 内存对于 CPU 来仅仅是一个存放指令和数据的地方,并不能在内存中完成计算功能。例如要计算 a = b + c,必须将 a、b、c 都读取到 CPU 内部才能进行加法运算,寄存器是存储 CPU 执行所需数据的区域,是 CPU 不可或缺的一部分,所有程序都只能通过操作寄存器,达到控制 CPU 目的,完成计算任务。

2.2 芯片架构

armX86两种芯片架构广泛应用在 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架构的手机。

cpu_so.png

Android可以通过adb命令来查看cpu信息1、adb shell 2、cat /proc/cpuinfo

2.3 C语言为什么不能夸平台?

 通常认为 C 语言是编译型语言。在编译阶段,编译器直接将源码编译为 对应CPU架构和操作系统上的可执行文件
如下图所示 c 语言代码编译为的汇编代码:

#include <stdio.h>
int main() {

    printf("Hello World");
    return 0;
}

Windows 部分汇编指令:


微信截图_20190723174501.png

ubuntu 部分汇编指令:


微信截图_20190724170811.png

虽然读不太懂汇编指令,比较了一下差异还是不小的。C 语言更多的是偏向底层开发,只要编译器足够强大,支持对应平台的编译,或者对应平台提供有C 编译器(C 语言的编译器也是众多语言中最多的)。程序就能在对应平台执行,也许 C 语言从来就没有想过要跨平台。

代码与平台有关性,是不能跨平台的原因。

3. JVM是如何做到跨平台的

 讲了这么多不能夸平台的原因,再来理解Java是如何做到跨平台就容易得多了。JVM 在编译阶段,只将 .java的源码,编译为和平台无关的 .class 字节码文件。不同 CPU 架构和操作系统上都会编译为相同的 calss 文件(最多只是 JDK 版本不同,有些许差异,jdk 都会向前兼容几个版本)。再由不同平台上的自行实现JVM。我们只需要搭建相应平台的运行环境即可,便可做到任意平台开发编译,到处运行。

未命名文件.png

 JVM 在真机基础之上模拟了一套自己的架构,有自己的指令集、内存管理等。在使用 Eclipse 追溯源码时,常常会遇到只有 class 文件,而没有源码出现下面的页面:
微信截图_20181120131556.png

 图中红色框内的便是字节码指令,运行时通过逐条解释执行,这也是以前 Java 被指性能底下的诟点。的确,解释执行的性能确实是和 C 编译目标代码比不了,但是在 JDK1.2 时就支持 JIT 及时编译器。程序运行期间,分析热点(经常调用)函数,编译为本地代码缓存起来,以后直接执行本地代码。虽然性能还是和编译型的语言有一定的差异,但 Java 凭借其语言特性以及各种成熟的 Web 解决方案,这点性能差显得不那么重要,完全能够接受。JIT 编译代码如下:
微信截图_20181202221710.png

有些JVM是采用纯JIT编译方式实现的,内部没解释器,例如JRockit、Maxine VM和Jikes RVM ---RednaxelaFX

4.JVM内存结构

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

操作系统内存布局
微信图片_20190730142730.png

 Windows 上栈内存由系统回收,堆内存由程序员自行回收。因为栈上内存不可控,JVM 只能在操作系统的堆内存上开辟自己的空间。

JVM运行时内存结构
微信图片_20190730145126.png
JVM堆

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

标量替换

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

栈上分配

 JDK1.8默认开启逃逸分析,确定对象不会再被外部引用,通过标量替换将对象分解在栈中分配,栈中的对象随着栈帧的出栈而销毁,大大的减少了堆内存的占用和GC的压力。

public class Main {

    public static void main(String[] args) throws Exception {
        for(int i = 0 ; i < 1000000;i++){
            Child child = new Child();
            child.setAge(1);
        }
        System.out.println("阻塞...");
        System.in.read();

    }
    public static class Child{
        
        private int age;
        
        private String name;
        //省略get/set方法
    }
}

开启逃逸分析(1.8默认开启)

C:\Program Files\Java\jdk1.8.0_91\bin>jps -l
17456 sun.tools.jps.Jps
19680 linked.Main
7608

C:\Program Files\Java\jdk1.8.0_91\bin>jmap -histo 19680

 num     #instances         #bytes  class name
----------------------------------------------
   1:        220734        5297616  linked.Main$Child
   2:           437        1763680  [I
   3:          3099         449536  [C
   4:          2392          57408  java.lang.String
   5:           488          55696  java.lang.Class
   6:            97          41776  [B
   7:           835          33400  java.util.TreeMap$Entry

关闭逃逸分析:

C:\Program Files\Java\jdk1.8.0_91\bin>jps -l
2436 sun.tools.jps.Jps
16536 linked.Main
7608

C:\Program Files\Java\jdk1.8.0_91\bin>jmap -histo 16536

 num     #instances         #bytes  class name
----------------------------------------------
   1:       1000000       24000000  linked.Main$Child
   2:           451        1873120  [I
   3:          3099         449536  [C
   4:          2392          57408  java.lang.String
   5:           488          55696  java.lang.Class
   6:            97          41776  [B
   7:           835          33400  java.util.TreeMap$Entry
   

可以看到,关闭逃逸分析总共使用堆内存 22M ,开启逃逸分析只使用了 5M 左右。节约了不少堆内存空间,减少了 GC 压力。

开启逃逸-XX:+DoEscapeAnalysis -XX:+PrintGC

关闭逃逸-XX:-DoEscapeAnalysis -XX:+PrintGC

同步消除

如果逃逸分析确认对象的作用范围不会超过当前线程,则消除对变量的同步措施。

JVM栈

 JVM栈 是方法执行所需的数据结构,每个线程都拥有一个JVM栈,随着线程的创建而创建,随着线程的销毁而销毁。JVM栈 以栈帧的单元,存放局部变量、操作数栈、动态链接、方法返回信息。具体可以参考

方法区/元数据区

 方法区中存放已被虚拟机加载的类信息,并且每个类只会存在一份,作为使用该类的入口。我们所编写的代码类,经过javac编译器,编译存储为 class 文件,在使用该类时(创建类的实例,调用了类静态方法类等),如果该类还未加载,会先将该 class 字节流从磁盘或者其他途径方式,加载存储到方法区当中,并且创建该类的 class对象 供以后访问使用。

微信图片_20190730152335.png

运行时常量池

 运行时常量池作为方法区的一部分,为每一个类都维护一个常量池,存放着编译时已知的字面量和各种符号引用。具体可见参考第二章

PC寄存器

 每个JVM线程都有自己的PC(程序计数器)寄存器。在任何时候,每个JVM线程都在执行单个方法的代码,如果执行的不是native方法,则pc寄存器包含当前正在执行的Java字节码指令的地址。如果当前执行的native方法,则PC寄存器的值undefined。

本地方法栈

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


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

RednaxelaFX

keycoding

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,427评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,551评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,747评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,939评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,955评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,737评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,448评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,352评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,834评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,992评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,133评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,815评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,477评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,022评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,147评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,398评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,077评论 2 355

推荐阅读更多精彩内容