🍎一.JVM
🍒1.1JVM简介
JVM 是 Java Virtual Machine 的简称,意为 Java虚拟机
虚拟机是指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统
常见的虚拟机:JVM、VMwave、Virtual Box
JVM 和其他两个虚拟机的区别:
- VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器
- JVM则是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进行了裁剪
JVM 是一台被定制过的现实当中不存在的计算机
🍒1.2JVM执行流程
程序在执行之前先要把java代码转换成字节码(class文件),JVM 首先需要把字节码通过一定的方式类加载器(ClassLoader) 把文件加载到内存中 运行时数据区(Runtime Data Area) ,而字节码文件是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine)将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这4个主要组成部分的职责与功能
🍎二.JVM运行时数据区
🍒2.1 程序计数器(线程私有)
程序计数器的作用:用来记录当前线程执行的行号的,用来存储下一条指令的地址
程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个Native方法,这个计数器值为空
总结:
程序计数器:内存最小的一块区域,保存了下一条要执行的指令地址在哪里,与书签类似
什么是线程私有?
由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,因此在任何一个确定的时刻,一个处理器(多核处理器则指的是一个内核)都只会执行一条线程中的指令。因此为了切换线程后能恢复到正确的执行位置,每条线程都需要独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们就把类似这类区域称之为"线程私有"的内存
🍒2.2 栈(线程私有)
● Java虚拟机栈(线程私有)
Java 虚拟机栈的作用:Java 虚拟机栈的生命周期和线程相同,Java 虚拟机栈描述的是 Java 方法执行的
内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,咱们常说的堆内存、栈内存中,栈内存指的就是虚拟机栈
Java 虚拟机栈中包含了以下 4 部分:
局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小。简单来说就是存放方法参数和局部变量
操作栈:每个方法会生成一个先进后出的操作栈。
动态链接:指向运行时常量池的方法引用。
方法返回地址:PC 寄存器的地址
关于虚拟机栈会产生的两种异常:
● 如果线程请求的栈深度大于虚拟机所允许的最大深度,会抛出StackOverFlow异常
● 如果虚拟机在拓展栈时无法申请到足够的内存空间,则会抛出OOM异常
出现StackOverflowError异常时有错误堆栈可以阅读,比较好找到问题所在。如果使用虚拟机默认参数,栈深度在多多数情况下达到1000-2000完全没问题,对于正常的方法调用(包括递归),完全够用
如果是因为多线程导致的内存溢出问题,在不能减少线程数的情况下,只能减少最大堆和减少栈容量的方式来换取更多线程
●本地方法栈
本地方法栈和虚拟机栈类似,只不过 Java 虚拟机栈是给 JVM 使用的,而本地方法栈是给本地方法使用的
🍒2.3 堆(线程共享)
堆:储存对象以及对象的成员变量,一个进程只有一个,多个线程共用一个堆,内存中空间最大的区域,我们看到下图对堆做了细分,Java堆是垃圾收集器管理的内存区域,所以后介绍GC的时候我们细说
🍒2.4 方法区(线程共享)
方法区:存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据,即就是储存“类对象”,被static修饰的变量或方法就成了类属性,.java文件会被编译成.class文件,.class会被加载到内存中,也就被JVM构造成类对象了,这个加载的过程叫做类加载,类对象描述了类的信息,如类名,类有哪些成员,每个成员叫什么名字,权限是什么,方法名等
所以可以得到结论,静态的代码块,普通代码块,构造方法执行顺序为:静态的代码块->普通代码块->构造方法
🍎三.JVM类加载
🍒3.1类加载过程
对于一个类来说,它的生命周期是这样的:
其中前 5 步是固定的顺序并且也是类加载的过程,其中中间的 3 步我们都属于连接,所以对于类加载来
说总共分为以下几个步骤:
1. 加载(Loading) 2. 连接(Linking) .验证 .准备 .解析 3. 初始化(Initialization)
下面我们分别来看每个步骤的具体执行内容
(1) 加载(Loading)
“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,它和类加载 ClassLoading 是不同的,一个是加载 Loading 另一个是类加载 Class Loading,所以不要把二者搞混了。在加载 Loading 阶段,Java虚拟机需要完成以下三件事情:
● 通过一个类的全限定名来获取定义此类的二进制字节流。
● 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
● 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
(2) 验证
主要就是验证读取到的内容是不是和规范中规定的格式完全匹配,如果不匹配,就会类加载失败,并且会抛出异常 验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节 流中包含的信息符合《Java虚拟机规范》的全部约束要求, 保证这些信 息被当作代码运行后不会危害虚拟机自身的安全 验证选项: 文件格式验证 字节码验证 符号引用验证...
(3) 准备
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。 比如此时有这样一行代码: public static int value = 123; 它是初始化 value 的 int 值为 0,而非 123。
(4) 解析
.class文件中,常量是集中放置的(常量池),并且每一个常量都有一个编号,.class文件中的结构体初始情况 下它只记录了常量的编号,解析过程简单来说就是根据编号将对应的常量填充到类对象中
(5) 初始化(Initialization)
这里是真正地对类对象进行初始化,特别是静态成员 类加载过程是在执行某方法(如main方法)之前执行的,类加载的时候会进行静态代码块的执行,想要创建实例, 必然先得类加载,静态代码块只会执行一次,构造方法与普通代码块每次实例对象都会执行,并且普通代码块比静态代码块先执行
常见笔试题
所以可以得到结论,静态的代码块,普通代码块,构造方法执行顺序为:静态的代码块->普通代码块->构造方法
class A{ public A(){ System.out.println("这是A的构造方法"); } { System.out.println("这是A的代码块"); } static { System.out.println("这是A的静态代码块"); } public void fun(){ System.out.println("方法A"); } } class B extends A { public B() { System.out.println("这是B的构造方法"); } { System.out.println("这是B的代码块"); } static { System.out.println("这是B的静态代码块"); } } public class Test extends B{ public static void main(String[] args) { new Test(); new Test(); int s = 10; System.out.println(s); } }