轻松学JVM(一)——基本原理

前言

JVM一直是java知识里面进阶阶段的重要部分,如果希望在java领域研究的更深入,则JVM则是如论如何也避开不了的话题,本系列试图通过简洁易读的方式,讲解JVM必要的知识点。

运行流程

我们都知道java一直宣传的口号是:一次编译,到处运行。那么它如何实现的呢?我们看下图:

java程序经过一次编译之后,将java代码编译为字节码也就是class文件,然后在不同的操作系统上依靠不同的java虚拟机进行解释,最后再转换为不同平台的机器码,最终得到执行。这样我们是不是可以推演,如果要在mac系统上运行,是不是只需要安装mac java虚拟机就行了。那么了解了这个基本原理后,我们尝试去做更深的研究,一个普通的java程序它的执行流程到底是怎样的呢?例如我们写了一段这样的代码:

public class HelloWorld {
public static void main(String[] args) {
System.out.print("Hello world");
}
}

这段程序从编译到运行,最终打印出“Hello world”中间经过了哪些步骤呢?我们直接上图:

java代码通过编译之后生成字节码文件(class文件),通过:java HelloWorld执行,此时java根据系统版本找到jvm.cfg,各位可以搜索一下自己电脑上的jvm.cfg文件在哪,它会根据你的系统版本放在不同的位置,比如我的这个文件就在:C:\Program Files\Java\jdk1.8.0_101\jre\lib\amd64\jvm.cfg,打开看一下:

这是我电脑上的文件,其中-server KNOWN就表示名称为server的jvm可用。如果这时你搜索一下你电脑上jvm.dll,你就会发现它一定在你的某个server目录下,比如我的:C:\Program Files\Java\jdk1.8.0_101\jre\bin\server\jvm.dll。简而言之就是通过jvm.cfg文件找到对应的jvm.dll,jvm.dll则是java虚拟机的主要实现。接下来会初始化JVM,并且获取JNI接口,什么是JNI接口,就是java本地接口,你想啊java被编译成了class文件,JVM怎么从硬盘上找到这个文件并装载到JVM里呢,就是通过JNI接口(它还常用于java与操作系统、硬件交互),找到class文件后并装载进JVM,然后找到main方法,最后执行。

JVM基本结构

可能通过上面的描述,大家对JVM运行流程有了一个粗略的认识,那么JVM内部到底是怎么执行一个class文件的呢,也就是上图中最后一步第6步的内部细节是怎样的呢?要了解这个问题,我们首先得看一下JVM的内部结构:

从这个结构不难看出,class文件被jvm装载以后,经过jvm的内存空间调配,最终是由执行引擎完成class文件的执行。当然这个过程还有其他角色模块的协助,这些模块协同配合才能让一个java程序成功的运行,下面就详细介绍这些模板,它们也是后面学习jvm最重要的部分。

内存空间:

JVM内存空间包含:方法区、java堆、java栈、本地方法栈。

方法区是各个线程共享的区域,存放类信息、常量、静态变量。

java堆也是线程共享的区域,我们的类的实例就放在这个区域,可以想象你的一个系统会产生很多实例,因此java堆的空间也是最大的。如果java堆空间不足了,程序会抛出OutOfMemoryError异常。

java栈是每个线程私有的区域,它的生命周期与线程相同,一个线程对应一个java栈,每执行一个方法就会往栈中压入一个元素,这个元素叫“栈帧”,而栈帧中包括了方法中的局部变量、用于存放中间状态值的操作栈,这里面有很多细节,我们以后再讲。如果java栈空间不足了,程序会抛出StackOverflowError异常,想一想什么情况下会容易产生这个错误,对,递归,递归如果深度很深,就会执行大量的方法,方法越多java栈的占用空间越大。

本地方法栈角色和java栈类似,只不过它是用来表示执行本地方法的,本地方法栈存放的方法调用本地方法接口,最终调用本地方法库,实现与操作系统、硬件交互的目的。

PC寄存器,说到这里我们的类已经加载了,实例对象、方法、静态变量都去了自己改去的地方,那么问题来了,程序该怎么执行,哪个方法先执行,哪个方法后执行,这些指令执行的顺序就是PC寄存器在管,它的作用就是控制程序指令的执行顺序。

执行引擎当然就是根据PC寄存器调配的指令顺序,依次执行程序指令。

结语

本文主要介绍了java虚拟机运行的基本流程,以及java虚拟机内部结构。下一篇我们将学习java内存模型以及探索java变量的可见性、有序性、指令重排等问题.

轻松学JVM(二)——内存模型、可见性、指令重排序

上一篇我们介绍了JVM的基本运行流程以及内存结构,对JVM有了初步的认识,这篇文章我们将根据JVM的内存模型探索java当中变量的可见性以及不同的java指令在并发时可能发生的指令重排序的情况。

内存模型

首先我们思考一下一个java线程要向另外一个线程进行通信,应该怎么做,我们再把需求明确一点,一个java线程对一个变量的更新怎么通知到另外一个线程呢?我们知道java当中的实例对象、数组元素都放在java堆中,java堆是线程共享的。(我们这里把java堆称为主内存),而每一个线程都是自己私有的内存空间(称为工作内存),如果线程1要向线程2通信,一定会经过类似的流程:

1、 线程1将自己工作内存中的X更新为1并刷新到主内存中;

2、 线程2从主内存读取变量X=1,更新到自己的工作内存中,从而线程2读取的X就是线程1更新后的值。

从上面的流程看出线程之间的通信都需要经过主内存,而主内存与工作内存的交互,则需要Java内存模型(JMM)来管理器。下图演示了JMM如何管理主内存和工作内存:

当线程1需要将一个更新后的变量值刷新到主内存中时,需要经过两个步骤:

1、 工作内存执行store操作;

2、 主内存执行write操作;

完成这两步即可将工作内存中的变量值刷新到主内存,即线程1工作内存和主内存的变量值保持一致;

当线程2需要从主内存中读取变量的最新值时,同样需要经过两个步骤:

1、主内存执行read操作,将变量值从主内存中读取出来;

2、工作内存执行load操作,将读取出来的变量值更新到本地内存的副本;

完成这两步,线程2的变量和主内存的变量值就保持一致了。

可见性

Java中有一个关键字volatile,它有什么用呢?这个答案其实就在上述java线程间通信机制中,我们想象一下,由于工作内存这个中间层的出现,线程1和线程2必然存在延迟的问题,例如线程1在工作内存中更新了变量,但还没刷新到主内存,而此时线程2获取到的变量值就是未更新的变量值,又或者线程1成功将变量更新到主内存,但线程2依然使用自己工作内存中的变量值,同样会出问题。不管出现哪种情况都可能导致线程间的通信不能达到预期的目的。例如以下例子:

//线程1 boolean stop = false; while(!stop){ doSomething(); } //线程2

stop 

= true;

这个经典的例子表示线程2通过修改stop的值,控制线程1中断,但在真实环境中可能会出现意想不到的结果,线程2在执行之后,线程1并没有立刻中断甚至一直不会中断。出现这种现象的原因就是线程2对线程1的变量更新无法第一时间获取到。

但这一切等到Volatile出现后,再也不是问题,Volatile保证两件事:

1、 线程1工作内存中的变量更新会强制立即写入到主内存;

2、 线程2工作内存中的变量会强制立即失效,这使得线程2必须去主内存中获取最新的变量值。

所以这就理解了Volatile保证了变量的可见性,因为线程1对变量的修改能第一时间让线程2可见。

指令重排序

关于指令排序我们先看一段代码:

int a = 0;
boolean flag = false;

//线程1

public void writer() {

a = 1;

flag = true;

}

//线程2

public void reader() {

if (flag) {

int i= a+1;

...... }

}

线程1依次执行a=1,flag=true;线程2判断到flag==true后,设置i=a+1,根据代码语义,我们可能会推断此时i的值等于2,因为线程2在判断flag==true时,线程1已经执行了a=1;所以i的值等于a+1=1+1=2;但真实情况却不一定如此,引起这个问题的原因是线程1内部的两条语句a=1;flag=true;可能被重新排序执行,如图:

这就是指令重排序的简单演示,两个赋值语句尽管他们的代码顺序是一前一后,但真正执行时却不一定按照代码顺序执行。你可能会说,有这个指令重排序那不是乱套了吗?我写的程序都不按我的代码流程走,这怎么玩?这个你可以放心,你的程序不会乱套,因为java和CPU、内存之间都有一套严格的指令重排序规则,哪些可以重排,哪些不能重排都有规矩的。下列流程演示了一个java程序从编译到执行会经历哪些重排序:

在这个流程中第一步属于编译器重排查,编译器重排序会按JMM的规范严格进行,换言之编译器重排序一般不会对程序的正确逻辑造成影响。第二、三步属于处理器重排序,处理器重排序JMM就不好管了,怎么办呢?它会要求java编译器在生成指令时加入内存屏障,内存屏障是什么?你可以理解为一个不透风的保护罩,把不能重排序的java指令保护起来,那么处理器在遇到内存屏障保护的指令时就不会对它进行重排序了。关于在哪些地方该加入内存屏障,内存屏障有哪些种类,各有什么作用,这些知识点这里就不再阐述了。可以参考JVM规范相关资料。

下面介绍一下在同一个线程中,不会被重排序的逻辑:

这三种情况中,任意改变一个代码的顺序,结果都会大不相同,对于这样的逻辑代码,是不会被重排序的。注意这是指单线程中不会被重排序,如果在多线程环境下,还是会产生逻辑问题,例如我们一开始举的例子。

结语

本文简单介绍了java在实现线程间通信时的简单原理,并介绍了volatile关键字的作用,最后介绍了java当中可能会出现指令重排序的情况。下一篇将介绍JVM中的参数设置对java程序的影响。

参考资料:

《实战Java虚拟机》 葛一鸣

《深入理解Java虚拟机(第2版)》 周志明

《深入理解Java内存模型》 程晓明

轻松学JVM的更多相关文章

  1. 轻松学JVM(一)——基本原理

    前言 JVM一直是java知识里面进阶阶段的重要部分,如果希望在java领域研究的更深入,则jvm则是如论如何也避开不了的话题,本系列试图通过简洁易读的方式,讲解jvm必要的知识点. 运行流程 我们都 ...

  2. 轻松学JVM(二)——内存模型、可见性、指令重排序

    上一篇我们介绍了JVM的基本运行流程以及内存结构,对JVM有了初步的认识,这篇文章我们将根据JVM的内存模型探索java当中变量的可见性以及不同的java指令在并发时可能发生的指令重排序的情况. 内存 ...

  3. 轻松学JVM(四)——垃圾回收算法

    我们都知道java语言与C语言最大的区别就是内存自动回收,那么JVM是怎么控制内存回收的,这篇文章将介绍JVM垃圾回收的几种算法,从而了解内存回收的基本原理. stop the world 在介绍垃圾 ...

  4. Asp.NETCore轻松学系列阅读指引目录

    前言 耗时两个多月,坚持写这个入门系列文章,就是想给后来者更好更快的上手体验,这个系列可以说是从入门到进阶,适合没有 .NETCore 编程经验到小白同学,也适合从 .NET Framework 迁移 ...

  5. Asp.NetCore轻松学-使用Supervisor进行托管部署

    前言 上一篇文章 Asp.NetCore轻松学-部署到 Linux 进行托管 介绍了如何在 Centos 上部署自托管的 .NET Core 应用程序,接下来的内容就是介绍如何使用第三方任务管理程序来 ...

  6. Asp.Net Core 轻松学-使用MariaDB/MySql/PostgreSQL和支持多个上下文对象

    前言 在上一篇文章中(Asp.Net Core 轻松学-10分钟使用EFCore连接MSSQL数据库)[https://www.cnblogs.com/viter/p/10243577.html],介 ...

  7. Asp.Net Core 轻松学-多线程之Task(补充)

    前言     在上一章 Asp.Net Core 轻松学-多线程之Task快速上手 文章中,介绍了使用Task的各种常用场景,但是感觉有部分内容还没有完善,在这里补充一下. 1. 任务的等待 在使用 ...

  8. Asp.Net Core 轻松学-利用文件监视进行快速测试开发

    前言     在进行 Asp.Net Core 应用程序开发过程中,通常的做法是先把业务代码开发完成,然后建立单元测试,最后进入本地系统集成测试:在这个过程中,程序员的大部分时间几乎都花费在开发.运行 ...

  9. (转)轻松学,Java 中的代理模式及动态代理

    背景:讲到反射机制,肯定会想到动态代理. 轻松学,Java 中的代理模式及动态代理 代理模式可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强.值得注意的是,代理类和被代理类应该 ...

随机推荐

  1. 求 1+2+3+ …… +n

    题目来源: 自我感觉难度/真实难度: 题意: 分析: 自己的代码: def Sum_Solution(n): # write code here if n==1: return 1 else: ans ...

  2. 蓝桥杯 历届试题 网络寻路(dfs搜索合法路径计数)

    X 国的一个网络使用若干条线路连接若干个节点.节点间的通信是双向的.某重要数据包,为了安全起见,必须恰好被转发两次到达目的地.该包可能在任意一个节点产生,我们需要知道该网络中一共有多少种不同的转发路径 ...

  3. qmlscene: could not find a Qt installation of ''

    qt └── helloqt.qml 在qt目录下运行helloqt.qml文件时出现这个提示: $ qmlscene helloqt.qml qmlscene: could not find a Q ...

  4. input:file onchange事件无法读取解决方法

    最近做项目,移动端的多文件上传,使用input:file读取文件 <input type='file' name='file' multiple accept='image/*' capture ...

  5. DDD China Conference 2017

    DDD China Conference 2017 时间:2017.12.9地点:国际会议中心

  6. IDEA创建Scala项目

    一.安装插件 见Scala入门篇 二.新建项目 选择new project,其中SBT相当于精简版的maven,其他的待补充.这里选择IDEA 填写信息,选择Scala SDK 在src目录下新建Sc ...

  7. PostgreSQL索引页

    磨砺技术珠矶,践行数据之道,追求卓越价值   [作者 高健@博客园  luckyjackgao@gmail.com] 本页目的,是起到索引其他所有本人所写文档的作用: 分类一:PostgreSQL基础 ...

  8. 原创zynq文章整理(MiZ702教程+例程)

    MiZ702教程+例程  网盘链接:  http://pan.baidu.com/s/1sj23yxv 不时会跟新版本,增加勘误之类的,请关注--

  9. mfc 进程的优先级

    知识点:  进程优先级  获取当前进程句柄  优先级设置  优先级变动  优先级获取 一.进程优先级(优先级等级) 简单的说就是进程(线程)的优先级越高,那么就可以分占相对多的CPU时间片. ...

  10. GIT命令基本使用

    记录摘选自廖雪峰的官方网站归纳总结 1.centos下安装git [root@cdw-lj ~]# yum install git 2.配置用户名以及邮箱 [root@cdw-lj opt]# git ...