JVM学习笔记之 基础概念

  • 2019-07-30
  • 61
  • 1

本章节主要讲一下你写了那么多年的java究竟是个什么东西,它又是如何运行的

什么是JVM?
jvm用什么用?
怎么实现跨平台?

C:\Users\coder>java -version
java version "1.8.0_66"
Java(TM) SE Runtime Environment (build 1.8.0_66-b17)
Java HotSpot(TM) 64-Bit Server VM (build 25.66-b17, mixed mode)

回忆一下,当我们使用JAVA开发程序的时候,一般会下载一个Eclipse或者IDEA,一个集成的开发环境可以帮我们节省很多繁琐的工作,提高开发效率。有时候条件比较简陋或者初学的话,可能会先下载安装JDK,配置环境变量,然后手写一个Hello World!

比如写下以下代码,保存扩展名为java的文本文件。

class application{
	public static void main(String[] args){
	 System.out.println("Hello World");
	}
}

命令行下使用javac命令编译这个java类。

E:\>javac application.java
E:\>java application
Hello World

E:\>

javac命令使得java文件变成出一个application.class,而java命令则运行了这个class,main方法为这个类默认的入口点,执行后输出了“hello world”。我们可以看到虽然我们写的是java代码,但真正被执行的是编译后的class文件。java虚拟机能识别的只有java字节码,也就是扩展名为class的文件。我们所写的java都会转化成对应的字节码指令。

具体的我们可以使用javap命令查看对应的字节码


E:\>javap -v application.class
Classfile /E:/application.class
  Last modified 2019-8-1; size 427 bytes
  MD5 checksum fa8707d525079f724e0e3c46c39c23f0
  Compiled from "application.java"
class application
  minor version: 0
  major version: 52 # class文件的主版本号是52 对应的jdk版本是1.8
  flags: ACC_SUPER # 这个标记用于纠正invokespecial 在调用父类方法存在的版本问题
Constant pool: # 常量池 初始化的时候这些数据会存放到方法区的常量池

   索引 类型       值(具体的值或者对索引引用)           解释      #额外注释                       
   #1 = Methodref          #6.#15         // java/lang/Object."":()V  # 很明显是这个类的空参构造函数 任何类的构造函数编译后方法名都会变成init
   #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream; #System.out.println 这个方法调用中out是System中的PrintStream类型的静态成员变量,
   #3 = String             #18            // Hello World #字符串常量 这个字符串应用内唯一
   #4 = Methodref          #19.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V #这个是方法原型,PrintStream类中的println方法,传入一个String 返回void
   #5 = Class              #21            // application # 名为application的class
   #6 = Class              #22            // java/lang/Object 
   #7 = Utf8                # utf8类型的字符串常量
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               application.java
  #15 = NameAndType        #7:#8          // "":()V   #NameAndType 其本身不存储值 只包含了#7#8的引用
  #16 = Class              #23            // java/lang/System
  #17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;
  #18 = Utf8               Hello World
  #19 = Class              #26            // java/io/PrintStream
  #20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V #这些全全限定名 字段的名称 方法的名称 描述符 都属于符号引用
  #21 = Utf8               application
  #22 = Utf8               java/lang/Object
  #23 = Utf8               java/lang/System
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (Ljava/lang/String;)V
{
  application();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0 #字节码指定 表示对this的操作 
         1: invokespecial #1                  // Method java/lang/Object."":()V #invokespecial字节码指令 可用于调用构造方法 私有方法和父类的方法
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]); #完整的方法原型
    descriptor: ([Ljava/lang/String;)V #方法的入参和输出 传入string return void
    flags: ACC_PUBLIC, ACC_STATIC #修饰符 public 和static
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World #执行了ldc操作 从常量池取值 #3指的是常量池里的#3 String类型的值 引用#18 utf8类型的值 hello world 
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V #println属于PrintStream实例的方法  使用invokevirtual指令调用
         8: return
      LineNumberTable:
        line 3: 0
        line 4: 8
}
SourceFile: "application.java"

E:\>



0x0 基本概念

接下来我们看一张图,途中涉及了几个概念:JVM规范,JVM具体的实现(各种JVM虚拟机),class文件的结构定义,JAVA语言规范,JAVA语言规范的具体实现,以及非JAVA语言但兼容JVM能再JVM里跑的语言。


(开篇一张图,内容全靠编)

1.其中我们比较熟悉的就是JAVA语言规范,就就是我们常说的java 1.8,java 10这些java版本。具体的我们一般都是使用了 oracle JDK去开发程序,但是现在oracle的JDK已经付费了,所以我们可能会选择使用开源免费的Open JDK。oracle jdk 和open jdk就是针对 java 1.8 java 10之类的版本的具体实现。现有规范,然后这些jdk升级版本支持最新的规范。

2.程序开发完毕后,需要再服务器上运行,那服务器需要安装什么吗? 一般服务器安装对应版本的JRE。JRE指的是java运行环境,JDK相比多了很多开发环境需要的工具,如果只是跑程序的话,安装JRE就可以了。当然JDK版本就包含了JRE的完整程序。

3.JVM规范指的是Java虚拟机的规范,它是独立于JAVA语言的一套体系。它定义了java程序如何在计算机中运行,如何管理java内存,怎么实现多线程等等。这么去解析运行java代码,全靠这个规范去定义。具体的参考这里

4.JVM虚拟机实现。针对JVM规范开发出的具体的实现,从第一代商用虚拟机 sun classic VM开始陆陆续续出现很多各有特色的虚拟机,目前我们一般使用jdk默认的hotspot VM。 当然亚马逊,阿里巴巴都基于open jdk开发出了定制的jdk版本。

5.class文件结构,之前也提到了java代码必须编译成class文件才能运行,因为java运行能运行的其实是class文件。虚拟机和java其实是解耦的,class文件你可以简单的看成经过优化整理后的java代码。class文件还是可以被完整的还原成java文件的。
class的版本与jdk版本对应,参考这里

6.既然java代码和java虚拟机不是耦合的,可就是说java虚拟机并不只是为了跑java代码,要不然也不需要定义两套规范对吧?那么,假设我设计了编程语言A,又设计了一个能把A编译成符合class文件规范的编译器,那么是不是A语言能在java虚拟机中运行呢?
答案是肯定的!目前有很多语言并没有自己的运行时环境,直接借用了原本是java专属的jvm。这样做可以跟着java实现了跨平台,又节省了很大的开发成本。对于java而言,实现了混合语言的开发,用用于应对现在越来越复炸的开发需求。

7.一般同一份java源码可以使用不同版本的jdk编译,前提是你的代码符合当前版本的规范,比如你使用了java11的新规范然后用jdk8去编译肯定不行,新语法不识别。
反之可能可以编译,因为java8的部分语法可能已经在java11中修改或者废弃。

8.java版本 class版本 jvm 版本一般是一一对应的,你在IDE中设置了使用java8的语法等级,设置了jdk8的安装路径,服务器环境也是安装了jre8.
但是jvm只识别class文件,和java源码没关系,且默认是向下兼容的,你用jdk8编译出来的class是可以在jdk11中运行,反之jvm会报错,提示不兼容的class版本

9.主流的java虚拟机有 hotspot(jdk默认) zing vm (付费) j9 vm(IBM的 已废弃) 其他的都黄的不能再黄了,都进棺材板了

E:\>java application
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.UnsupportedClassVersionError: application has been compiled by a more recent version of the Java Runtime (class file version 55.0), this version of the Java Runtime only recognizes class file versions up to 52.0
        at java.lang.ClassLoader.defineClass1(Native Method)
        at java.lang.ClassLoader.defineClass(Unknown Source)
        at java.security.SecureClassLoader.defineClass(Unknown Source)
        at java.net.URLClassLoader.defineClass(Unknown Source)
        at java.net.URLClassLoader.access$100(Unknown Source)
        at java.net.URLClassLoader$1.run(Unknown Source)
        at java.net.URLClassLoader$1.run(Unknown Source)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(Unknown Source)
        at java.lang.ClassLoader.loadClass(Unknown Source)
        at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
        at java.lang.ClassLoader.loadClass(Unknown Source)
        at sun.launcher.LauncherHelper.checkAndLoadMain(Unknown Source)


扩展内容


class文件如何在JVM中执行,如何转化成计算机能识别的机器码。
JIT机制是什么? 编译优化又是什么
class文件的大致结构

演示kotlin语言的hello world,最终编译成class文件并执行,class文件也能反编译


不同类型的方法 调用指令
invokestatic 静态方法调用 (编译时就可以确定调用的版本)
invokespecial 调用构造方法 私有方法 父类 (编译时就可以确定调用的版本)
invokevirtual 调用虚方法 (不属于上述两类的方法) 调用实例方法,基于类进行分派
invokeinterface 调用接口方法 (会在运行时确定实现此接口的对象)
invokedynamic 运行时动态解析出所调用的方法

相关PPT下载
链接: https://pan.baidu.com/s/13l5sT9pbJFAFEFX7CXZx1w 提取码: kbtg

感谢打赏!
微信
支付宝

评论

  • 社会主义廖回复

    老哥,用了您创作的flyme主题助手这么久,也打赏过,无意点进来这个,虽然不懂这些,但是依然一本正经的从上到下都看完了,然后有感而发:卧槽!牛逼牛逼!最后一脸懵逼的出去了!开个玩笑啦!您辛苦了大神