【Java基础】JVM的学习与理解(1)


JVM是Java Virtual Machine的简称,也就是Java虚拟机。

  • 什么是虚拟机?
    • 虚拟机指通过软件模拟的具有硬件系统功能的,运行在隔离环境中的完整计算机系统。

JVM与其他虚拟机的区别

  • VMWare、Virtual Box都是通过软件模拟物理CPU的指令集
  • JVM是通过软件模拟Java字节码指令集,是一个被定制过的,现实中不存在的计算机

各种类型的JVM

  • KVM
    • SUN发布
    • IOS Android之前,广泛用于手机系统
  • CDC / CLDC HotSpot
    • 手机、电子书、PDA等设备上建立统一的Java编程接口
    • J2ME的重要组成部分
  • JRockit
    • BEA
  • IBM J9 VM
    • IBM内部使用
  • Apache Harmony
    • 开源
    • 兼容JDK1.5和JDK1.6的Java程序运行平台
    • 与Oracle关系恶劣,退出JCP,Java社区分裂
    • OpenJDK推出后,受到挑战,2011年退役
    • 没有大规模的商用的经历
    • 对Android发展有积极作用

规范

Java语言规范与JVM规范

Java语言规范定义了什么是Java语言,JVM规范定义了什么是虚拟机。Java语言是与JVM相对独立的。也就是说任何一门语言,只要满足了JVM的规范,都可以在JVM虚拟机上运行(如 Groovy 等)。

  • Java语言规范
    • 语法
    • 变量
    • 类型
    • 文法
  • JVM规范
    • Class文件类型
    • 运行时数据
    • 帧栈
    • 虚拟机的启动
    • 虚拟机的指令集

JVM规范定义的主要内容

  • Class文件格式
  • 数字的内存表示和存储
    • 比如Byte -128 - 127
  • returnAddress 数据类型定义
    • 指向操作码的指针。不对应Java数据类型,不能在运行时修改,Finally实现需要
  • 定义PC(指令寄存器)
  • 方法区

VM指令集

  • 类型转换
    • l2i (long to int) ...
  • 出栈入栈操作
    • aload astore
  • 运算
    • iadd isub
  • 流程控制
    • ifeq ifne
  • 函数调用
    • invokevirtual invokeinterface invokespecial invokestatic

JVM需要对Java Library提供的支持(无法通过语言实现的)

  • 反射 java.lang.reflect
  • ClassLoader
  • 初始化class和interface
  • 安全相关 java.security
  • 多线程
  • 弱引用

JVM的编译

  • 源码到JVM指令的对应格式
  • Javap(编译器)
  • JVM反汇编的格式
    • <index> <opcode>[ <oprand1> [ <oprand2>... ]] [<comment>]

比如下面的代码

反汇编后的结果如下:

JVM运行机制

JVM启动流程

启动命令

Jvm虚拟机启动时是由Java或Javaw命令启动的 (Java XXX)

XXX是启动类,启动类中有我们的main方法。

装载配置

之后系统会进行装载配置。它会根据当前路径和系统版本寻找jvm.cfg(配置文件)。

根据配置文件寻找JVM.dll

找到配置文件后,它会根据配置文件寻找对应的JVM.dll。JVM.dll是JVM的主要实现。

初始化JVM,获得JNIEnv接口

然后使用这个dll初始化JVM,获得相关供native调用的接口。

JNIEnv是JVM接口。findClass等等操作通过它来实现。

找到main方法并运行

JVM基本结构

JVM的基本结构如图:

img

首先通过类加载器Class Loader将Class文件加载到内存中去。

内存空间是被分为几个区域的,如方法区、堆、栈、本地方法栈(native方法调用)等等。

除此之外,在虚拟机运行时,与一般CPU一样,需要一个指针指向下一个指令的地址。也就是我们的PC寄存器。

这里还有一个执行引擎用来执行虚拟机的代码(类的字节码等等)

Java在运行时还有个十分重要的模块,就是垃圾收集器,也就是我们经常说到的GC。

PC寄存器

  • 每一个线程都拥有一个PC寄存器
  • 在线程创建的时创建
  • 指向下一条指令的地址
  • 执行本地方法时,PC寄存器的值为undefined

方法区

  • 保存装载的类的信息
    • 类型的常量值
    • 字段、方法信息
    • 方法字节码
  • 通常与永久区(Perm)关联在一起
    • 保存一些相对稳定的数据

Java堆

  • 和程序开发密切相关
  • 应用系统对象都保存在Java堆中
  • 所有的线程共享Java堆
  • 对于分代的GC,Java堆也是分代的
  • GC主要工作空间

Java栈

  • 线程私有
  • 栈由一系列帧组成(因此Java栈也叫作帧栈)
  • 帧保存一个方法的局部变量、操作数栈、常量池指针
  • 每一次方法调用都创建一个帧,并且压栈

注:JDK6时,String等常量信息保存在方法区,JDK7时则移动到了堆。

局部变量表

局部变量表包含了函数的参数以及局部变量。

看到上面这个static方法,它对应的局部变量表如下:

0 int int i
1 long long l
2float float f
3 reference Object o
4 int byte b

局部变量表它有很多槽位,一个槽位最大能够容纳32位的数据类型。

由于这个方法是static的,因此它没有this引用

下面的方法的局部变量表中除了参数外还有一个this引用

0 reference this
1 int char c
2 int short s
3 int boolean b

每一个方法调用时都会创建一个帧,当方法调用结束则从帧栈中将这个帧移除。帧中保存的信息除了局部变量表之外还有如操作数栈、返回地址等等。

操作数栈
  • Java没有寄存器,因此所有参数传递通过操作数栈。

比如下面这个方法

它的反编译代码如下

通过下面的图,可以看到,下面的就是操作数栈,而上面则是局部变量,我们在上面反汇编的代码里对它压栈,做加法,出栈等,最后得到了结果并出栈。

img

栈上分配

先看下面这段C++的代码,我们用new分配一个对象,使用完毕后用delete方法将其释放。大家都知道,C++中new的内存是在堆上分配的,每一次都需要手动的将这个对象的内存释放掉。难免会有分配了内存忘记释放的情况,导致了内存泄漏。

在C++中,我们还可以通过直接声明一个对象的方式为其在栈上分配空间,这样这份内存就会在它的生命周期结束时被自动回收。

然后我们看一下这段代码:

这里,当我们用下面的参数运行时

输出结果:5

而当我们用下面这些参数运行时

会发现输出中会有大量的GC日志。

这说明了第一种分配是在栈上分配的。

当我们需要的数据不是很大时,虚拟机会做一些优化,改为在栈上分配内存。使得GC的压力减小,运行性能加快

  • 小对象(一般几十个bytes),在没有逃逸的情况下,可以直接分配在栈上
  • 直接分配在栈上,可以自动回收,减少GC的压力
  • 大对象或者逃逸对象无法栈上分配

栈、堆、方法区之间的交互

img

Java内存模型

  • 每一个线程有一个工作内存与主存独立。
  • 工作内存中存放主存中变量的值的拷贝。

当数据从主内存复制到工作内存时,必须出现两个动作:

  1. 由主内存执行的读(read)操作
  2. 由工作内存执行的相应load操作。

当数据从工作内存拷贝到主内存时,也会出现两个动作:

  1. 由工作内存执行的存储(store)操作
  2. 由主内存执行的相应写(write)操作

原子性:每一个操作都是原子的,即执行期间不会被中断

可见性:对于普通变量,一个线程中更新的值,不能马上反应在其他变量中

如果需要在其他线程中立即可见,可以使用volatile

有序性:

  • 在本线程内,操作都是有序的
  • 在本线程外观察,操作都是无序的(指令重排或主内存同步延时)

指令重排-破坏线程的有序性。具体可以看多线程笔记: 【Java】多线程与并发学习笔记


Android Developer in GDUT