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文件
打开.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()实例内存地址,找到后就可以执行实例对应的方法,这就是直接引用,所谓符号引用转化为直接引用就是这么一个过程