JVM基础知识

1.JVM启动流程

在这里插入图片描述
说明:

①.使用Java命令启动JVM时,首先要装载配置(即根据当前路径和系统版本寻找JVM的配置文件)

②.找到JVM配置文件之后就会去定位(JVM初始化)所需要的dll文件(jvm.dll为JVM主要实现),在找到匹配当前系统版本的dll文件之后,就会使用这个dll文件初始化JVM,获得相关的一些native调用的接口(例如JNIEnv接口,他是JVM的接口,他提供了大量的跟JVM交互的操作,如findClass)

③.通过获取到的native接口(JNIEnv接口)找到main()方法,运行

2.JVM内存结构

如图:
在这里插入图片描述

2.1.类加载器Class Loader

负责加载Class文件,Class文件在文件开头有特定的文件标识,将Class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构,并且ClassLoader只负责 Class 文件的加载,至于Class文件是否能够运行则由Execution Engine决定

2.1.1.类加载器的分类

在这里插入图片描述
注意:如果加载的是jdk自带的类,那么使用的是Bootstrap启动类加载器;如果加载的是用户自定义的类,则使用的是APP应用程序类加载器

2.1.2.类机制机制

双亲委派机制

2.1.3.双亲委派机制沙箱安全机制

①.当一个类加载器收到了类加载的请求时,他首先不会尝试自己去加载这个类,而是把这个类加载的请求委派给自己的父类去完成,每一个层次的类加载器都是如此,因此所有的类加载请求都应该传送到Bootstrap启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求的时候(即在他对应的加载路径下没有找到所要加载的Class),子类加载器才会尝试自己去加载

②.采用双亲委派机制的一个好处就是比如加载位于"rt.jar"包中的Object类(java.lang.Object),无论是哪个类加载器加载这个类,最终都是委托给顶层的Bootstrap启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象

②.为了防止用户自定义代码对jvm源代码(jvm内置的类)进行恶意篡改和污染,jvm在类加载的过程中使用了一种沙箱安全机制,基于双亲委派机制优先使用父类加载器(依次递归,优先使用bootstarp启动类加载器)去加载jvm内置的类

jvm内置的类(jdk安装目录\jre\lib\rt.jar文件中)

例如:String类,如果我们在程序中再次定义一个String类(类中包含了一些自定义的方法),包名和类名都和内置的String类保持一致,那么在运行的时候会报错;因为当类加载器在加载String类时基于双亲委派机制,会使用Bootstrap启动类加载器去加载String类,由于JDK已经内置了String类,而这个内置的String类中并没有我们自己定义的那些方法,所以在执行时候会提示找不到对应的方法,就报错了

2.1.4.类缓存

标准的JAVASE类加载器可以按照要求查找类,但是一旦某个类被加载到类加载器中,他将维持加载(/缓存)一段时间.不过,JVM垃圾收集器可以回收这些类

2.2.运行时数据区Runtime Data Area

JVM的运行时数据区是整个JVM的重点.我们所有写的程序都被加载到这里,之后才开始运行,Java生态系统如此的繁荣,得益于该区域的优良自治.运行时数据区的组成部分:程序计数器、java栈、本地方法栈、堆、方法区.

2.3.执行引擎Execution Engine

Execution Engine 执行引擎负责解释命令,提交操作系统执行.执行引擎是JVM的核心组成部分之一,任何JVM实现的核心都是Execution Engine.执行引擎将java字节码转为机器能够识别的机器码,并调用机器的指令集进行计算等.不同JVM的执行效率很大程度上取决于它们各自实现的Execution Engine的好坏

##扩展:解释运行和编译运行
在这里插入图片描述

2.4.本地接口Native Interface

Java本地接口是一个标准的JAVA API,它支持将Java代码与其他语言编写的代码相集成.Java语言本身并不能对操作系统底层(函数库接口)进行访问和操作,但是可以通过JNI接口调用其他语言来实现对底层的访问,JNI接口的作用就是融合不同的编程语言为java所用(例如c/c++)

3.运行时数据区Runtime Data Area

如图:
在这里插入图片描述

3.1.方法区Method Area

保存类(class文件)的元数据信息,或者说他存储了每一个类的结构信息

①.运行时常量池(JDK6时,String等常量池信息放在方法区中;JDK7时,已经移动到堆了)

②.字段,方法信息

③.方法字节码

注意:
A.虽然JVM规范将方法区和描述为堆的一个逻辑部分,但是他却还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开
B.方法区通常和永久区(Perm)关联在一起,保存是一些相对静止,相对稳定的数据

3.2.堆Heap

一个JVM实例只存在一个堆内存,堆内存大小是可以调节的.类加载器读取了类文件之后,需要把类,方法,常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行;

3.2.1.堆内存逻辑上分为三部分,物理上分为两部分

①.Young Generation Space    新生区(Young/New)

②,Tenure Generation Space    老年区(Tenure/Old)

③.Permanent Space    永久区(Perm)

注意:java8开始,永久区变成了元空间(Meta Space),而且直接使用物理内存!

如图:堆内存在逻辑上分为三部分
在这里插入图片描述
如图:堆内存在物理上分为两部分
在这里插入图片描述

3.2.2.堆内存中new对象的过程

①.新生区是类的诞生,成长,消亡的区域,一个类在这里产生,应用,最后被垃圾收集器回收,结束生命;新生区又分为两部分:伊甸区(Eden Space)和幸存区(Survivor Space),幸存区有两个:0区(Survivor 0 Space/From)和1区(Survivor1 Space/To);

②.所有的类都是在伊甸区被New出来,当伊甸区的空间用完时,而程序又需要创建新的对象,那么JVM的垃圾回收器将对伊甸区进行垃圾回收(Minor GC),将伊甸区中那些不再被其他对象所引用的对象进行销毁/回收,然后对伊甸区中剩余的存活的对象拷贝到幸存0区中,GC之后Eden区的对象被清空.若幸存0区的空间也满了,再对幸存0区进行垃圾回收,将幸存0区中剩余的存活的对象拷贝到幸存1区,再将幸存0区和幸存1区进行交换,谁空谁变成幸存1区,剩下的就变成幸存0区.对象从幸存0区拷贝到幸存1区之后年龄(+1),当某个对象存活次数(/年龄)达到了15次,直接将这个对象移动都老年区;若老年区的空间也满了,则进行MajorGC(/FullGC),进行老年区内存空间的清理,若老年区进行了FullGC之后发现依然无法腾出空间来保存进入老年区的对象,此时就会产生OOM异常

3.2.3.MinorGC的过程

①.Eden,SurvivorFrom复制到SurvivorTo,年龄+1
首先,当Eden区满的时候就会触发第一次GC,把还活着的对象拷贝到SurvivorFrom区,当Eden区再次触发GC的时候会扫描Eden区和SurvivorFrom区,对这两个区域进行垃圾回收,经过这次(第二次)回收之后还存活的对象(Eden区存活的对象+SurvivorFrom区存活的对象),直接将他们复制到SurvivorTo区(如果有对象的年龄已经达到了老年的标准,则复制到老年区),同时把这些对象的年龄+1

②.清空Eden区,SurvivorFrom区
然后,清Eden区和SurvivorFrom区中的对象,也就是复制之后有交换,谁空谁就变成SurvivorTo区

③.SurvivorTo和SurvivorFrom互换
最后,SurvivorTo和SurvivorFrom进行互换,原来的SurvivorTo区就变成了下一次GC时的SurvivorFrom区,部分对象会在From和To区域之间不停的复制,如此交换15次(由JVM参数MaxTenuringThreshold觉得,默认是15)还存活的对象则进入老年区

3.2.4.永久代

①.对于HotSpot虚拟机,很多开发者习惯将方法区称之为"永久代(Permanent Gen)",但是严格本质上说两者不同,或者说使用永久代实现方法区而已,永久代是方法区(相当于是一个接口interface)的一个实现

②.永久代是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说他存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放次区域所占用的内存

如图:
在这里插入图片描述

3.3.虚拟机栈 Virtual Machine Stack

1>.栈也叫栈内存,主管java程序的运行,是在线程创建时被创建的,他的生命周期是跟随线程的生命周期,线程结束栈内存也随之释放.

2>.对于栈来说,不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的,8种基本数据类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配的

3>.Java栈是线程私有的,栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集;

例如:

当一个方法A被调用时就产生了一个栈帧F1,并被压入栈中;若方法A中又调用了方法B,则又会产生一个栈帧F2,也被压入栈中;方法B又调用了方法C,于是又产生了栈帧F3,也被压入栈中…由于栈是一个先进后出的数据结构,因此后调用的方法会先执行(C->B->A),每执行完一个方法,就会从栈中弹出对应的栈帧(F3->F2->F1)

4>.如图所示:一个栈中有两个栈帧
在这里插入图片描述
5>.每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息,每个方法从调用开始直到执行完毕,这个过程就对应着一个栈帧在虚拟机中入栈到出栈的过程

如图:
在这里插入图片描述
6>.栈帧中主要保存着3类数据:

①.本地变量(local variables): 输入参数和输出参数以及方法内部的变量;

②.栈操作(Operand Stack): 记录出栈,入栈的操作;

③.栈帧数据(Frame Data): 包括类文件,方法等等;

注意:当一个方法递归调用自己时,就会出现"stackOverflowError"错误

7>.栈上分配
小对象(一般几十个bytes),在没有逃逸的情况下,可以直接分配在栈上,那么在函数调用结束之后会自动回收,减轻GC压力;大对象或者逃逸对象无法栈上分配

3.4.本地方法栈Native Method Stack

本地方法栈是线程私有的;本地方法栈和Java栈所发挥的作用是非常相似的,他们之间的区别就在于Java栈是为虚拟机执行Java方法(也就是字节码)服务的,而本地方法栈则是为虚拟机使用到的native原生方法服务的;具体做法是在Native Method Stack中登记Native方法,在Execution Engine执行引擎执行时加载本地方法库

3.5.程序计数器/PC寄存器(Program Counter Register)

程序计数器是线程私有的;是一块较小的内存空间,在创建线程时候被创建,他可以看作是当前线程所执行到的字节码的行号指示器(或者说他是用来存储指向下一条指令的地址,也即将要执行的指令代码).在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成

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

如果线程正在执行一个Java方法,那么这个计数器记录的就是正在执行的虚拟机字节码指令的地址;如果正在执行一个Navicat方法,这个计数器的值是空(Undefined),而且这块内存区域是唯一一个在Java虚拟机规范中没有规定任何"OutOfMemoryError"情况的区域

3.6.扩展.JVM中栈,堆,方法区交互关系

在这里插入图片描述
①.HotSpot(Java虚拟机)使用指针的方式来访问对象;
②.Java堆中会存放着访问存储在方法区中的类元数据(类结构信息)的地址;
③.reference存储的就是对象的(内存)地址;

4.JVM中类加载过程

Java类加载全过程: 加载-->>链接-->>初始化

注意:整个过程只执行一次,不会反复执行

4.1.加载

类加载器把编译好的class文件中的内容加载到内存中,并将这些静态数据(类的描述信息)转换成方法区中运行时数据结构,同时在堆中生成一个代表这个类的Java.lang.Class对象(/反射对象),作为方法区中类数据的访问入口

如图:

在这里插入图片描述
car1,car2,car3三个不同的实例对象均来自于同一个模板Car Class

4.2.链接

将Java类的二进制代码合并到JVM的运行状态之中的过程

  1. 验证:确保加载的类信息符合JVM规范,没有安全方面的问题
  2. 准备:正式为类变量(static变量)分配内存并设置类变量初始值的阶段.这些内存都将在方法区中进行分配
  3. 解析:虚拟机常量池内的符号引用替换为直接引用的过程
4.3.初始化

初始化阶段是执行类构造器(()方法)的过程

  1. 当初始化一个类的时候,如果发现他的父类还没有进行初始化,则需要先触发其父类的初始化

  2. 虚拟机会保证一个类的<clinit()方法>(即初始化方法)在多线程环境中被正确加锁和同步

  3. 当访问一个Java类的静态域时,只有真正声明这个域的类才会被初始化
    //通过子类引用父类的静态变量,不会导致子类初始化

4.4.例子
/**
* java类加载的全过程
*/
public class MeetTest1 {
    public static void main(String[] args) {
        A a = new A();//New一个对象会调用类的构造器(即调用public A()方法)
        System.out.println(a.width);
    }
}

class A {
    public static int width = 100;  //静态变量,静态域,field

    static {
        System.out.println("初始化类A: " + width);
        width = 300;//这个值会覆盖前面声明的值
        System.out.println("初始化类A:width值被覆盖" + width);
    }

    //类加载会执行类的初始化方法(即将静态变量width和static静态语句块合并成为一个类的初始化方法)
    public A() {
        System.out.println("创建A类的对象");
    }
}

结果:

在这里插入图片描述

5.扩展:主动引用和被动引用

5.1.主动引用:一定会发生类的初始化
  1. new一个类的对象
  2. 调用类的静态成员(除了final常量)和静态方法
  3. 使用反射包对类进行反射调用
  4. 当初始化一个类,如果他的父类没有被初始化,那么就会先初始化他的父类
5.2.被动引用:不会发生类的初始化
  1. 当访问一个静态域时,只有真正声明这个域的类才会被初始化
    //通过子类引用父类的静态变量,不会导致子类初始化
  2. 通过数组定义类引用,不会触发此类的初始化
    //A[] as = new A[10] //不会导致类A的初始化
  3. 引用常量不会触发此类的初始化(常量在编译阶段就存入调用类的常量池中了)

6.扩展:Java中static静态变量的初始化完全解析

  1. 加上static限定的字段,是所谓的类字段,也就是说这个字段的拥有者不是对象而是类,无论创建多少对象,static数据都只有一份;静态变量会按照声明的顺序先依次声明并设置为该类型的默认值,但不赋值为初始化的值;声明完毕后,再按声明的顺序依次设置为初始化的值,如果没有初始化的值就跳过

  2. 类中总是先初始化static字段,再初始化一般字段.接着初始化构造器.但是如果不创建这个类的对象,那这个类是不会进行初始化的,并且只执行一次

  3. 把多个初始化语句包在一个static花括号里,叫做静态代码块,其实就是把多个static合在一起写了,本质是一样的,只有首次创建对象实例或者首次访问类的字段时才会执行,而且仅仅一次

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值