Java虚拟机(一)

Java虚拟机

java虚拟机(java virtual machine,JVM),一种能够运行java字节码的虚拟机。作为一种编程语言的虚拟机,实际上
不只是专用于Java语言,只要生成的编译文件匹配JVM对加载编译文件格式要求,任何语言都可以由JVM编译运行。
比如kotlin、scala等。

JVM的基本结构

JVM由三个主要的子系统构成

1.类加载子系统
2.运行时数据区(内存结构)
3.执行引擎
在这里插入图片描述

运行时数据区(内存结构)

在这里插入图片描述

类加载机制

类的生命周期(其中加载,连接,初始化是类加载过程)

在这里插入图片描述

1.加载

将.class文件从磁盘读到内存(通过类的全限命名(包名+类名)来读的)

2.连接
2.1 验证

验证字节码文件的正确性(有没符合.class规范,格式验证,符号验证等等)

2.2 准备

给类的静态变量分配内存,并赋予默认值(比如int的默认值为0)

2.3 解析

类装载器装入类所引用的其它所有类(静态链接)

3.初始化

为类的静态变量赋予正确的初始值,上述的准备阶段为静态变量赋予的是虚拟机默认的初始值,此处赋予的才是程序
编写者为变量分配的真正的初始值,执行静态代码块

4.使用
5.卸载
类加载器的种类
启动类加载器(Bootstrap ClassLoader)(由C语言来写的 )

负责加载JRE的核心类库,如JRE目标下的rt.jar,charsets.jar等

扩展类加载器(Extension ClassLoader)

负责加载JRE扩展目录ext中jar类包

系统类加载器(Application ClassLoader)

负责加载ClassPath路径下的类包

用户自定义加载器(User ClassLoader)

负责加载用户自定义路径下的类包
在这里插入图片描述

类加载机制
全盘负责委托机制

当一个ClassLoader加载一个类的时候,除非显示的使用另一个ClassLoader,该类所依赖和引用的类也由这个
ClassLoader载入

双亲委派机制

指先委托父类加载器寻找目标类,在找不到的情况下载自己的路径中查找并载入目标类
**打破双亲委派机制例子:**tomcat没有用双亲委派机制,因为tamcat有自己的类加载机制
MySQL连接也是没有用双亲委派机制

双亲委派模式的优势

**沙箱安全机制:**比如自己写的String.class类不会被加载,这样可以防止核心库被随意篡改
**避免类的重复加载:**当父ClassLoader已经加载了该类的时候,就不需要子ClassLoader再加载一次

运行时数据区(内存结构)
1.方法区(Method Area)

类的所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在这里定义。简单来说,所有定义的方法的
信息都保存在该区域,静态变量+常量+类信息(构造方法/接口定义)+运行时常量池都存在方法区中,虽然Java虚拟
机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是为了和Java的
堆区分开(jdk1.8以前hotspot虚拟机叫永久代、持久代,jdk1.8时叫元空间)

2.堆(Heap):Java储存单位,虚拟机管理最大内存区域

虚拟机启动时自动分配创建,用于存放对象的实例,几乎所有对象都在堆上分配内存,当对象无法在该空间申请到内
存是将抛出OutOfMemoryError异常。同时也是垃圾收集器管理的主要区域。
在这里插入图片描述

2.1 新生代(Young Generation)

类出生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。
新生代分为两部分:伊甸区(Eden space)和幸存者区(Survivor space),所有的类都是在伊甸区被new出来的。
幸存区又分为From和To区。当Eden区的空间用完是,程序又需要创建对象,JVM的垃圾回收器将Eden区进行垃圾回
收(Minor GC),将Eden区中的不再被其它对象应用的对象进行销毁。然后将Eden区中剩余的对象移到From
Survivor区。若From Survivor区也满了,再对该区进行垃圾回收,然后移动到To Survivor区(都是先GC再转移)。
在新生代GC叫做Minor GC或YGC,执行时间特别短,不会停止线程

2.2 老年代(Old Generation)

新生代经过多次GC仍然存活(15次)的对象移动到老年区。若老年代也满了,这时候将发生Major GC(也可以叫Full GC),
进行老年区的内存清理。若老年区执行了Full GC之后发现依然无法进行对象的保存,就会抛出
OOM(OutOfMemoryError)异常
当执行Full GC时,所有内存都会清理,会进行STW(Stop the world),所有用互线程都停掉,留下一个垃圾回收线程,导致用户卡在那一会儿,再启动线程,所以
**调优目的/指标:**减少Full GC次数以及执行Full GC时间

2.3 元空间(Meta Space)

在JDK1.8之后,元空间替代了永久代,它是对JVM规范中方法区的实现,区别在于元数据区不在虚拟机当中,而是用
的本地内存,永久代在虚拟机当中,永久代逻辑结构上也属于堆,但是物理上不属于。
为什么移除了永久代?
参考官方解释
大概意思是移除永久代是为融合HotSpot与 JRockit而做出的努力,因为JRockit没有永久代,不需要配置永久代。
在这里插入图片描述

3.栈(Stack):Java运行单位

Java线程执行方法的内存模型,一个线程对应一个栈,每个方法在执行的同时都会创建一个栈帧(用于存储局部变量
表,操作数栈,动态链接,方法出口等信息)不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一

4.本地方法栈(Native Method Stack)

和栈作用很相似,区别不过是Java栈为JVM执行Java方法服务,而本地方法栈为JVM执行native方法服务。登记native
方法,在Execution Engine执行时加载本地方法库

5.程序计数器(Program Counter Register):内存较小

就是一个指针,指向方法区中的方法字节码(用来存储指向吓一跳指令的地址,也即将要执行的指令代码),由执行
引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计

举个例子

代码

package Day5;

public class StackTest {
	public int math(){
		int a=1;
		int b=2;
		int c=(a+b)*10;
		return c;
	}
public static void main(String[] args) {
	StackTest st=new StackTest();
	st.math();
}
}

使用javac命令将.java文件编译成.class文件
![(https://img-blog.csdnimg.cn/20191220151323720.png)
打开.class文件是这样的
在这里插入图片描述
啥都看不懂,没关系,我们用命令javap -c反汇编
在这里插入图片描述
打印信息转成.txt文件分析
在这里插入图片描述
对照JVM指令集,查看代码在内存里的运行过程(下面例子已标注在指令集后面)
在这里插入图片描述
这里只演示math()方法执行过程
11行:将int型1推送至栈顶
在这里插入图片描述
12行:将栈顶int型数值存入第1个本地变量
在这里插入图片描述
13行:将int型2推送至栈顶
在这里插入图片描述
14行:将栈顶int型数值存入第2个本地变量
在这里插入图片描述
15行:将第1个int型本地变量推送至栈顶**(注意:这里是复制一份,不是推送,因为jvm是自动化管理的,在操作数栈运行完可能就被回收,就没有了)**
在这里插入图片描述
16行:将第2个int型本地变量推送至栈顶**(栈的特点:先进后出)**
在这里插入图片描述
17行:将栈顶两int型数值相加并将结果压入栈顶**(1+2过程交给CPU执行,执行完就被回收了)**
在这里插入图片描述
18行: bipush 10 将单字节的常量值(-128~127)推送至栈顶(这里的10是从常量池中拿的)
在这里插入图片描述
19行:将栈顶两int型数值相乘并将结果压入栈顶
在这里插入图片描述
20行:将栈顶int型数值存入第3个本地变量
在这里插入图片描述
21行:将第3个int型本地变量推送至栈顶
在这里插入图片描述
22行:从当前方法返回int(即将操作数栈中30返回给方法调用者

方法出口 (也叫 返回地址)

方法出口:是一个指针,指向方法调用者

程序计数器:存放下面的那些数字,表示程序接下来将要执行的代码

在这里插入图片描述
其中有个7直接跳到9:程序计数器记录7位置执行完后执行9位置,在7位置执行完,它去常量池拿数字10,这个10也是占了一个地址位,所以从7直接跳到9

本地方法栈

先看个栗子
这里创建一个线程,然后调用start()方法;

new Thread().start();

看看start()最底层实现

private native void start0();

可以看到java最终是用jnative申明,没有具体实现start0()的方法,因为Java最初使用C++和部分C语言写的,所以Java很多东西不需要自己实现,比如启动一个线程,java比没有真正启动一个线程,而是创建一个线程对象,真正启动线程是调用C++编写的本地方法,C++编写的本地方法也是创建一个对象,调用pthred,pthred再调用Window或Linux操作系统内核的内核函数来启动一个线程,而这些本地方法就放在本地磁盘dll编写的动态链接库,所以本地方法没有具体实现的方法

动态链接

先看下常识:Java是按需加载,Java有个JIT加载器(Just-In-Time Compiler)(也叫:即时编译器/运行时编译器):经过编译后的程式,被优化成相当精简的原生型指令码,在程序运行时load需要的类到内存
**静态链接:**在程序启动时加载类后的连接解析阶段,把符号引用转化会直接引用,这个过程叫做静态链接
**动态链接:**在程序运行时,把符号引用转化为直接引用的过程
**区别:**一个在类加载的连接解析阶段,另一个在程序运行阶段

举个栗子

通过javap -v命令,查看附加信息

Classfile /E:/newjava.sts/face/src/main/java/Day5/StackTest.class
  Last modified 2019-12-20; size 380 bytes
  MD5 checksum bbad300892b5ec97591d5183ce49c65e
  Compiled from "StackTest.java"
public class Day5.StackTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#16         // java/lang/Object."<init>":()V
   #2 = Class              #17            // Day5/StackTest
   #3 = Methodref          #2.#16         // Day5/StackTest."<init>":()V
   #4 = Methodref          #2.#18         // Day5/StackTest.math:()I
   #5 = Class              #19            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               math
  #11 = Utf8               ()I
  #12 = Utf8               main
  #13 = Utf8               ([Ljava/lang/String;)V
  #14 = Utf8               SourceFile
  #15 = Utf8               StackTest.java
  #16 = NameAndType        #6:#7          // "<init>":()V
  #17 = Utf8               Day5/StackTest
  #18 = NameAndType        #10:#11        // math:()I
  #19 = Utf8               java/lang/Object
{
  public Day5.StackTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public int math();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: bipush        10
         9: imul
        10: istore_3
        11: iload_3
        12: ireturn
      LineNumberTable:
        line 5: 0
        line 6: 2
        line 7: 4
        line 8: 11

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class Day5/StackTest
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method math:()I
        12: pop
        13: return
      LineNumberTable:
        line 11: 0
        line 12: 8
        line 13: 13
}
SourceFile: "StackTest.java"

其中调用math()函数时,实际上调用的是#4

#4 = Methodref          #2.#18         // Day5/StackTest.math:()I

#4作了解释,它是一个函数,指向#2.#18

  #2 = Class              #17            // Day5/StackTest
   #18 = NameAndType        #10:#11        // math:()I

#2是一个类,指向#17
#18是一个方法名和类型,指向 #10:#11

  #17 = Utf8               Day5/StackTest
  #10 = Utf8               math
  #11 = Utf8               ()I

#17是字符串,就是类所在地方(包名+类名)—— Day5/StackTest
#10是字符串,方法名math
#11是字符串,()I,其中I是返回类型
组合起来就是Day5/StackTest.math.()I

所以常量池Constant pool本质上放的就是一些字符串,这一堆字符串就是符号引用,
所以动态链接就是:(在这个例子中)当执行引擎运行java程序main()主方法时,发现需要调用math()方法,而math()属于Day5/StackTest这个类,所以需要找到StackTest.java这个类,而这些类的符号引用就位于常量池,常量池就位于方法区,而常量池放的就是一些字符串,通过这些字符串找到某些实例在堆里的引用地址,找到后就可以执行相应的方法,比如:在方法里new Object(), Object o会指向在堆里真正的new Object()实例内存地址,找到后就可以执行实例对应的方法,这就是直接引用,所谓符号引用转化为直接引用就是这么一个过程
在这里插入图片描述

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值