A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

© 不二晨 金牌黑马   /  2019-2-15 10:11  /  658 人查看  /  2 人回复  /   1 人收藏 转载请遵从CC协议 禁止商业使用本文

1. 什么是JVM?

维基百科是这么描述的:

A Java virtual machine (JVM) is a virtual machine that enables a computer to run Java programs as well as programs written in other languages that are also compiled to Java bytecode. The JVM is detailed by a specification that formally describes what is required of a JVM implementation.

翻译来就是:JVM是虚拟机,运行Java字节码文件。

那么什么是虚拟机:

虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。各个平台只要遵循JVM规范实现Java虚拟机,就能运行Java字节码。

现在能理解什么是Java的优势了嚒:跨平台,即一次编译,到处运行。



2. 什么是Java字节码?

刚刚有提到JVM是用来运行Java字节码的,即class文件。从Java源文件-编译成Java字节码,到JVM解释执行。

那么Java字节码是什么样的呢?

先举个例子:

public class TestJvm {

    public static final String STRING = "hello";
    public static final int CONSTANT = 1;

    public String name;
    public int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
看看编译后的TestJvm.class文件。我这边用的是Mac os系统,用的Intellij IDEA编译的字节码文件,使用Sublime Text打开字节码文件:

cafe babe 0000 0034 0026 0a00 0500 2009
0004 0021 0900 0400 2207 0023 0700 2401
0006 5354 5249 4e47 0100 124c 6a61 7661
2f6c 616e 672f 5374 7269 6e67 3b01 000d
436f 6e73 7461 6e74 5661 6c75 6508 0025
0100 0843 4f4e 5354 414e 5401 0001 4903
0000 0001 0100 046e 616d 6501 0003 6167
6501 0006 3c69 6e69 743e 0100 0328 2956
0100 0443 6f64 6501 000f 4c69 6e65 4e75
6d62 6572 5461 626c 6501 0012 4c6f 6361
6c56 6172 6961 626c 6554 6162 6c65 0100
0474 6869 7301 0022 4c69 6f2f 6b7a 772f
6164 7661 6e63 652f 6373 646e 5f62 6c6f
672f 5465 7374 4a76 6d3b 0100 0767 6574
4e61 6d65 0100 1428 294c 6a61 7661 2f6c
616e 672f 5374 7269 6e67 3b01 0007 7365
744e 616d 6501 0015 284c 6a61 7661 2f6c
616e 672f 5374 7269 6e67 3b29 5601 0006
6765 7441 6765 0100 0328 2949 0100 0673
6574 4167 6501 0004 2849 2956 0100 0a53
6f75 7263 6546 696c 6501 000c 5465 7374
4a76 6d2e 6a61 7661 0c00 0f00 100c 000d
0007 0c00 0e00 0b01 0020 696f 2f6b 7a77
2f61 6476 616e 6365 2f63 7364 6e5f 626c
6f67 2f54 6573 744a 766d 0100 106a 6176
612f 6c61 6e67 2f4f 626a 6563 7401 0005
6865 6c6c 6f00 2100 0400 0500 0000 0400
1900 0600 0700 0100 0800 0000 0200 0900
1900 0a00 0b00 0100 0800 0000 0200 0c00
0100 0d00 0700 0000 0100 0e00 0b00 0000
0500 0100 0f00 1000 0100 1100 0000 2f00
0100 0100 0000 052a b700 01b1 0000 0002
0012 0000 0006 0001 0000 0003 0013 0000
000c 0001 0000 0005 0014 0015 0000 0001
0016 0017 0001 0011 0000 002f 0001 0001
0000 0005 2ab4 0002 b000 0000 0200 1200
0000 0600 0100 0000 0c00 1300 0000 0c00
0100 0000 0500 1400 1500 0000 0100 1800
1900 0100 1100 0000 3e00 0200 0200 0000
062a 2bb5 0002 b100 0000 0200 1200 0000
0a00 0200 0000 1000 0500 1100 1300 0000
1600 0200 0000 0600 1400 1500 0000 0000
0600 0d00 0700 0100 0100 1a00 1b00 0100
1100 0000 2f00 0100 0100 0000 052a b400
03ac 0000 0002 0012 0000 0006 0001 0000
0014 0013 0000 000c 0001 0000 0005 0014
0015 0000 0001 001c 001d 0001 0011 0000
003e 0002 0002 0000 0006 2a1b b500 03b1
0000 0002 0012 0000 000a 0002 0000 0018
0005 0019 0013 0000 0016 0002 0000 0006
0014 0015 0000 0000 0006 000e 000b 0001
0001 001e 0000 0002 001f
这是什么鬼,一堆16进制(当然,如果你用IDE打开,IDE会自动帮你转换成可读的内容)。

看看这张图:



大概分析一下:头4个字节称为魔数(Magic Number) 16进制表中为0xCAFEBABE,它的唯一作用是确定这个文件是否是一个能被虚拟机接收的Class文件,是用来标识Class文件的。第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version),Java的版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上加1,高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件。次版本号值为0x0000,主版本号值为0x0034,也就是十进制的52,代表JDK是1.8版本。

紧接着主版本号之后两个字节的是常量池计数值(constant_pool_count),表示常量池中的项目数量,这个容量计数是从1开始的。

接着是常量池,访问标记...



3. JVM的执行模式。

上面我们有提到: JVM解释执行Java字节码文件。其实这个说法不够严谨。

目前用的比较主流的是hotspot虚拟机的JIT即时编译器。啥意思???

那得先搞懂解释器和编译器的概念:

解释器是一条条的解释执行源语言,比如PHP, JS。

编译器是将源代码先编译成目标代码,执行时直接在支持支持目标代码的平台上运行,这样效率快很多,比如C, C++。

对于解释执行,不经过JIT直接由解释器解释执行所有字节码,执行效率不高。 而编译执行不加筛选的将全部代码进行编译机器码不论其执行频率是否有编译价值,在程序响应时间的限制下,编译器没法采用编译耗时较高的优化技术(因为JIT的编译是首次运行或启动的时候进行的!),所以,在纯编译执行模式下的java程序执行效率跟C/C++也是具有较大差距的。

因此,新版本的jvm默认都是采用混合执行模式。

看下面步骤就能明白:

1、源代码经javac编译成字节码,class文件

2、程序字节码经过JIT环境变量进行判断,是否属于“热点代码”(多次调用的方法,或循环等)

3、如是,走JIT编译为具体硬件处理器(如sparc、intel)机器码

4、如否,则直接由解释器解释执行

5、操作系统及类库调用

6、硬件
补充一下,其实Android的Dalvik是依靠一个JIT编译器去解释字节码,而新版Android的虚拟机ART则完全改变了这套做法,在应用安装时就预编译字节码到机器语言,在移除解释代码这一过程后,应用程序执行将更有效率,启动更快,但是安装过程更长,更占本地存储空间。



4. 简单聊聊字节码的加载流程。

Java字节码的执行需要经过以下3个步骤:

(1)由类装载器(class loader)负责把类文件(.class文件)加载到Java虚拟机中。在此过程需要检验该类文件是否符合类文件规范。
(2)字节码校验器(bytecode verifier)检查该类文件的代码中是否存在着某些非法操作,例如Applet程序中写本地计算机文件系统的操作。 这一步最耗时,JVM会执行大量测试用例去校验。
(3)如果字节码校验器检验通过,由Java解释器负责把该类文件解释成为机器码进行执行。


而类加载器可以分为:

1. 启动类加载器(Bootstrap ClassLoader):

这个类加载器负责将<JAVA_HOME>\lib目录下的类库加载到虚拟机内存中,用来加载java的核心库,此类加载器并不继承于java.lang.ClassLoader,不能被java程序直接调用,代码是使用C++编写的.是虚拟机自身的一部分.

2. 扩展类加载器(Extendsion ClassLoader):
这个类加载器负责加载<JAVA_HOME>\lib\ext目录下的类库,用来加载java的扩展库,开发者可以直接使用这个类加载器.

3. 应用程序类加载器(Application ClassLoader):

这个类加载器负责加载用户类路径(CLASSPATH)下的类库,一般我们编写的java类都是由这个类加载器加载,这个类加载器是CLassLoader中的getSystemClassLoader()方法的返回值,所以也称为系统类加载器.一般情况下这就是系统默认的类加载器.

除此之外,我们还可以加入自己定义的类加载器,以满足特殊的需求,需要继承java.lang.ClassLoader类.
类加载器就是根据指定全限定名称将class文件加载到JVM内存,转为Class对象。而使用的机制是:双亲委派模型。

双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时,子加载器才会尝试自己去加载。

为什么要这么做???

黑客自定义一个java.lang.String类,该String类具有系统的String类一样的功能,只是在某个函数稍作修改。比如equals函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM中。此时,如果没有双亲委派模型,那么JVM就可能误以为黑客自定义的java.lang.String类是系统的String类,导致“病毒代码”被执行。而有了双亲委派模型,黑客自定义的java.lang.String类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的java.lang.String类,最终自定义的类加载器无法加载java.lang.String类。



5. JVM的运行期数据区。



能看到:分为方法区,Java堆,Java栈,程序计数器,本地方法栈。



(1)程序计数器:(每个线程有一个,不是线程共享的区域)

程序计数器是一块比较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。


(2)Java虚拟机栈:(每个线程有一个,不是线程共享的区域)

每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。在Java 虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常;如果虚拟机栈可以动态扩展(当前大部分的Java 虚拟机都可动态扩展,只不过Java 虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。


(3)本地方法栈:(每个线程有一个,不是线程共享的区域)

和Java虚拟机栈差不多,只不过它是执行本地方法的(C, C++)。


(4)Java 堆:(线程共享区域,总共1个,GC管理的区域)

对于大多数应用来说,Java 堆(Java Heap)是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

(5)方法区:(线程共享区域,总共1个,GC管理的区域)

方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java 堆区分开来。

很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区。(下一篇文章将介绍GC的内容)


方法区中包含 - 运行时常量池:::

前面讲了Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

运行时常量池相对于Class 文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只能在编译期产生,也就是并非预置入Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern() 方法。
---------------------
【转载,仅作分享,侵删】
作者:况众文
原文:https://blog.csdn.net/u014294681/article/details/85104210
版权声明:本文为博主原创文章,转载请附上博文链接!

2 个回复

倒序浏览
一个人一座城0.0 来自手机 中级黑马 2019-2-16 09:51:11
沙发
看一看。
回复 使用道具 举报
今天也要加油鸭
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 加入黑马