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

【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>]

比如下面的代码

void spin(){
    int i;
    for(i = 0; i < 100; i++){
        // 循环体
    }
}

反汇编后的结果如下:

0  iconst_0     // 将整型常量0入栈
1  istore_1     // 出栈赋值给局部变量1 (i=0)
2  goto 8       // 首次不增加,跳转第8行
5  iinc 1 1     // 给本地变量1加1 (i++)
8  iload_1      // 将局部变量1入栈
9  bipush 100   // 将整型常量100入栈
11 if_icmplt 5  // 比较如果小了则跳转到5行(i<100)
14 return       // 结束

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时则移动到了堆。

局部变量表

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

public static int runStatic(int i, long l,float f,Object o, byte b){
    return 0;
}

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

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

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

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

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

public int runInstance(char c,short s,boolean b){
    return 0;
}
0 reference this
1 int char c
2 int short s
3 int boolean b

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

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

比如下面这个方法

public static int add(int a, int b){
    int c = 0;
    c = a + b;
    return c;
}

它的反编译代码如下

0 iconst_0  // 0压栈
1 istore_2  // 弹出int,存放于局部变量2(c = 0)
2 iload_0   // 局部变量0压栈
3 iload_1   // 局部变量1压栈
4 iadd      // 弹出两个变量,求和,结果压栈 (a+b)
5 istore_2  // 弹出结果,存放在局部变量2(c=a+b)
6 iload_2   // 局部变量2压栈
7 ireturn   // 返回

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

img

栈上分配

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

class BcmBasicString{...};
class TestClass{
  public:
    void method(){
        BcmBasicString* str = new BcmBasicString;
        ...
        delete str;
    }
};

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

class BcmBasicString{...};
class TestClass{
  public:
    void method(){
        BcmBasicString str;
        ...
    }
};

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

public class OnStackTest{
    public static void alloc(){
        byte[] b = new byte[2];
        b[0] = 1;
    }

    public static void main(String[] args){
        long b = System.currentTimeMillis();
        for(int i = 0; i < 1000000000; i++){
            alloc();
        }
        long e = System.currentTimeMillis();
        System.out.println(e-b);
    }
}

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

-server -Xmx10m Xms10m
-XX:+DoEscapeAnalysis -XX:+PrintGC

输出结果:5

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

-server -Xmx10m Xms10m
-XX:-DoEscapeAnalysis -XX:+PrintGC

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

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

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

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

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

img

public class AppMain{
    // 运行时,JVM把AppMain的信息都放入方法区
    public static void main(String[] args){
        // main方法本身放入方法区
        Sample test1 = new Sample("测试1");
        // test1是引用。所以放到栈区中。Sample是自定义对象应该放到堆中
        Sample test2 = new Sample("测试2");

        test1.printName();
        test2.printName();
    }
}
public class Sample{
    private name;
    // new Sample实例后,name引用栈区里,name对象放入堆里

    public Sample(String name){
        this.name = name;
    }

    // printName方法本身放入方法区内
    public void printName(){
        System.out.println(name);
    }
}

Java内存模型

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

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

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

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

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

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

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

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

有序性:

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

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

发表评论

电子邮件地址不会被公开。 必填项已用*标注

%d 博主赞过: