JAVA虚拟机篇---JVM

java程序的生命周期

java code从出生到game over大体分这么几步:编译,类加载,运行,GC

编译

Java语言的编译期其实是一段“不确定 ”的过程,因为可能是一个前端编译器把.java文件转变为.class文件的过程;也可能是指JVM的后端运行期编译器(JIT编译器)把字节码转变为机器码的过程;还可能是指使用静态提前编译器(AOT编译器)直接把.java文件编译成本地机器码的过程。但是在这里我们说的是第一类。也是符合我们大众对编译认知的。编译在这个时间段经历了哪些过程呢?

词法、语法分析

词法分析是将源代码的字符流转变为Token集合,而语法分析则是根据Token序列抽象构造语法树(ATS)的过程,ATS是一种用来描述程序代码语法结构的树形表示形式,语法树的每个节点都代表着程序代码中的一个语法结构,例如包、类型、修饰符、运算符、接口、返回值甚至代码注释都可以是一个语法结构。

填充符号表

完成了语法和词法分析之后,下一步就是填充符号表的过程,符号表中所登记的信息在编译的不同阶段都要用到。在这里延伸一下符号表的概念。符号表是什么呢?它是由一组符号地址和符号信息构成的表格,最简单的可以理解为哈希表的K-V值对的形式为什么会用到符号表呢?符号表最早期的应用之一就是组织程序代码的信息。最初,计算机程序只是一串简单的数字,但程序猿们很快发现使用符号来表示操作和内存地址(变量名)要方便得多。将名称和数字关联起来就需要一张符号表。随着程序的增长,符号表操作的性能逐渐变成了程序开发效率的瓶颈,为此从而诞生了许多提升序号表效率的数据结构和算法。至于所谓的数据结构和算法有哪些呢?大体说下:无序链表中的顺序查找、有序数组中的二分查找、二叉查找树、平衡查找树(在这我们主要接触到的是红黑树)、散列表(基于拉链法的散列表,基于线性探测法的散列表)。像Java中的java.util.TreeMap和java.util.HashMap分别是基于红黑树和拉链法的散列表的符号表实现的。

如果没有构造方法,它就会按照相应的类型创建一个无参构造。
变量名是给编译器看的,编译器根据变量是局部还是全局分配内存地址或栈空间,所谓的变量名在内存中不存在,操作时转换成地址数存放在寄存器中了。其实可以理解为是符号表起到了连接作用。

语义分析

经过上两步之后,我们获得了程序代码的抽象语法树表示,语法树能表示一个正确的源代码抽象,但无法保证源程序是符合逻辑的,这时候语义分析登场了,它的主要任务就是对结构上正确的源程序进行上下文有关性质的审查。标注检查、数据及控制流分析、解语法糖是语义分析阶段的几个步骤,在这具体说下语法糖的概念。语法糖是指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但更方便程序猿使用。Java中最常用的语法糖主要是泛型、变长参数、自从装箱/拆箱、遍历循环,JVM在运行时不支持这些语法,它们在编译阶段还原回简单的基础语法结构,这个过程也就是解语法糖。举个泛型擦除的例子,List和List在编译之后会进行泛型擦除,变成一样的原生类型List。

字节码生成

字节码生成是Javac编译过程的最后一个阶段,在这个阶段会把前面各步骤生成的信息转化成字节码写到磁盘中,还会进行了少量代码添加和转换的工作实例构造器()方法和类构造器()方法(这里的实例构造器并不是指默认构造函数,如果用户代码没有提供任何构造函数,那编译器将会添加一个没有参数的、访问性与当前类一致的默认构造函数,这个工作在填充符号表阶段已经完成,而类构造器()方法指的是编译器自动收集类中的所有类变量赋值动作和静态语句块中的语句合并产生的就是在这个阶段添加到语法树中的。到此为止整个编译过程结束。

类加载过程

类加载就是将.class文件加载到jvm虚拟机中。
在这里插入图片描述

往细了看大致分为5个阶段:
(1)加载:类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例(Java虚拟机规范并没有明确要求一定要存储在堆区中,只是hotspot选择将Class对应哪个存储在方法区中),这个Class对象在日后就会作为方法区中该类的各种数据的访问入口。
注意:
加载阶段即可以使用系统提供的类加载器在完成,也可以由用户自定义的类加载器来完成。加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。

(2)验证:是为了确保Class文件字节流中包含信息符合JVM的要求,因为Class文件的来源途径不一定中规中矩的从编译器产生,也有可能用十六进制编辑器直接编写Class文件。校验流程为文件格式校验、元数据验证、字节码验证。
(3)准备:准备阶段正式为类变量分配内存并设置初始值的阶段,这些变量所使用的内存都在方法区进行分配。
(4)解析:解析阶段是JVM将常量池内的符号引用替换为直接引用(指向目标的指针、相对偏移量或句柄)的过程,前面我们谈到的编译填充符号表的价值在这地方体现出来了。解析过程无非就是对类或接口、字段、接口方法进行解析。
(5)链接:这个过程就是把class文件加载到java虚拟机。
(6)初始化:初始化阶段是执行类构造器()方法的过程。

类初始化(类加载)时机:
1、创建类的实例
2、访问类的静态变量(除常量【被final修辞的静态变量】原因:常量一种特殊的变量,因为编译器把他们当作值(value)而不是域(field)来对待。
3、访问类的静态方法
4、反射如(Class.forName(“my.xyz.Test”))
5、当初始化一个类时,发现其父类还未初始化,则先出发父类的初始化
6、虚拟机启动时,定义了main()方法的那个类先初始化

(7)使用:这个过程大家都明白。
(8)卸载:使用完了,java虚拟机进行清理。

类加载器

启动类加载器

最顶层的类加载器,负责加载 JAVA_HOME\lib 目录中的所有类,或通过-Xbootclasspath参数指定路径中的类,且被虚拟机认可(按文件名识别,如rt.jar)的类。

扩展类加载器

负责加载 JAVA_HOME\lib\ext 目录中的所有类,或通过java.ext.dirs系统变量指定路径中的类库。

应用程序类加载器

也叫做系统类加载器,可以通过getSystemClassLoader()获取,负责加载用户路径(classpath)上的类库。如果没有自定义类加载器,一般这个就是默认的类加载器。

自定义类加载器

Java虚拟机规范将所有继承抽象类java.lang.ClassLoader的类加载器,定义为自定义类加载器;

双亲委派模型

在这里插入图片描述

类加载器之间的这种层次关系叫做双亲委派模型。
双亲委派模型要求除了顶层的启动类加载器(Bootstrap ClassLoader)外,其余的类加载器都应当有自己的父类加载器。这里的类加载器之间的父子关系一般不是以继承关系实现的,而是用组合实现的。

双亲委派模型的工作过程

如果一个类接收到类加载的请求,他自己不会去加载这个请求,而是将这个加载请求委派给父类加载器,这样一层一层传送,直到到达启动类加载器。

只有当父类加载器无法加载这个请求时,子类加载器才会去尝试自己加载。

双亲委派模型的代码实现

双亲委派模型的代码实现集中在java.lang.ClassLoader的loadClass()方法当中。

(1)首先检查类是否被加载,没有则调用父类加载器的loadClass()方法;
(2)若父类加载器为空,则默认使用启动类加载器作为父加载器;
(3)若父类加载失败,抛出ClassNotFoundException 异常后,再调用自己的findClass() 方法。

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    //1 首先检查类是否被加载
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
             //2 没有则调用父类加载器的loadClass()方法;
                c = parent.loadClass(name, false);
            } else {
            //3 若父类加载器为空,则默认使用启动类加载器作为父加载器;
                c = findBootstrapClass0(name);
            }
        } catch (ClassNotFoundException e) {
           //4 若父类加载失败,抛出ClassNotFoundException 异常后
            c = findClass(name);
        }
    }
    if (resolve) {
        //5 再调用自己的findClass() 方法。
        resolveClass(c);
    }
    return c;
}
双亲委派模型的好处

可以避免类的重复加载,另外也避免了java的核心API被篡改。也解决了java 基础类统一加载的问题,即越基础的类由越上层的加载器进行加载。

Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子类ClassLoader再加载一次。java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

破坏双亲委派模型

若加载的基础类中需要回调用户代码,而这时顶层的类加载器无法识别这些用户代码,怎么办呢?这时就需要破坏双亲委派模型了。(例如JDBC,JNDI)

破坏双亲委派模型的情况:

java.sql.DriverManager是Java的标准服务,该类放在rt.jar中,因此是由启动类加载器加载的,但是在应用启动的时候,该驱动类管理是需要加载由不同数据库厂商实现的驱动,但是启动类加载器找不到这些具体的实现类,为了解决这个问题,Java设计团队提供了一个不太优雅的设计:线程上下文加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时候它还没有被设置,就会从父线程中继承一个,如果再应用程序的全局范围都没有设置过的话,那这个类加载器就是应用程序类加载器。

有了线程上下文加载器,就可以解决上面的问题——父类加载器需要请求子类加载器完成类加载的动作,这种行为实际上就是打破了双亲委派的加载规则。

JDBC与JNDI

1. JDBC(Java Database Connectivity)是由数据库中间服务商提供的,用于连接数据库的Java API。一组类和接口(对接数据库)。
JDBC可以通过载入不同的数据库的“驱动程序”而与不同的数据库进行连接。

2. JNDI(Java Name Directory Interface)是Java 命名与目录接口为应用服务器(Tomcat)管理资源所设置的目录样式的唯一标识。(数据库、网页、文档等)为资源命名,再根据名字去寻找资源。

区别:使用起来的区别是使用jdbc的话如果你要修改数据库的相关信息、配置,你必须修改源代码,重新部署程序,而若是使用jndi你只需在服务器中修改相应的xml文件,对代码的影响降到最低。

(总结)J2EE 规范要求所有 J2EE 容器都要提供 JNDI 规范的实现。JNDI 在 J2EE 中的角色就是“交换机”。

SPI

定义:SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI的作用就是为这些被扩展的API寻找服务实现。

SPI和API的使用场景:

API (Application Programming Interface)在大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。

SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。

JDBC自动加载驱动的SPI机制实例

jvm内存结构分为

在这里插入图片描述

什么是线程私有?

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

虚拟机栈(VM Stack)

这一区域主要存放了大量的线程相关信息,它是随着虚拟机线程的诞生而诞生的,所以它是独属于线程私有的一块区域,其中存放了Java八大基本类型的变量,还有一些局部变量,也就是方法内部的变量。
栈溢出:1、如果线程请求的栈深度大于虚拟机所允许的深度(-Xss设置栈容量),将会排除StackOverFlowError异常。
内存溢出:2、虚拟机在动态扩展时无法申请到足够的内存,会抛出OOM异常。

本地方法栈

这里存放了Java代码中调用的本地方法的信息,主要用于native方法。
Native: 一个Native Method就是一个Java调用非Java代码的接口。方法的实现由非Java语言实现,比如C或C++。

方法区(Method Area)

方法区中主要存储了Java代码中的类加载的相关信息,比如类的名称、修饰符、构造器、静态方法、定义为final的常量、勒种的方法信息。这是一个面向全局线程共享的区域。

程序计数器

程序计数器是一块比较小的内存空间,可以看作是当前线程锁执行的字节码的行号指示器。
 如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个Native方法,这个计数器值为空。
 程序计数器内存区域是唯一一个在JVM规范中没有规定任何 OOM情况的区域。
 这个我们也十分容易理解,我们的程序计数器只是存储正在执行的虚拟机字节码的指令的地址,是一个定长的数据,所以它不会存在内存溢出的情况。所以我们不需要为他规定任何OOM情况。

堆(Heap)

堆是Jvm立马很重要的一个区域,也是跟方法区一样属于全部线程都可以共享访问的一个区域,在堆中存储了大量的Java对象信息,但凡是new出来的对象都存放在这里,并且可以通过jvm提供的-Xmx和-Xms来调节堆的大小。其中-Xmx是堆的最大值是多少,-Xms是堆的最小内存,如果两者一致,则这个Jvm的堆大小就不能弹性伸缩。
在这里插入图片描述

事实上,堆内部也划分除了多个区域,分别是新生代、老年代以及持久代。这其中新生代又分为Eden和Survivor01、Survivor02等等。为什么要这么划分,这就涉及到了Jvm的GC回收策略了。
新生代中存储了所有新创建的对象,一个新对象创建OK首先是存放在Eden区域的,当Eden区域快要被填满时,就会自动触发Jvm的GC机制,GC会回收一些空闲的对象,再将幸存下来的其他对象转移到Survivor01中去,同样,如果01也满了 再次对01这一区域进行GC回收,将幸存者放入Survivor02区域中去。一旦在Survivor02中历经磨难多次存活超过15次,就会将其转移至老年代中去。至于持久代中,则主要存储一些常量池、方法区等数据

2、Jvm GC 回收策略

JvmGC经过这么长时间的发展,逐渐划分了以下几个垃圾回收策略,他们分别是:

2.1、复制回收算法

此种方法通过依次扫描区域所有的可达对象,然后将其复制到另外一片区域保存起来,再将其现在正在使用的区域内存全部清空,此方法的优点在于方便快捷,只需要便利出所有的可达对象即可,而且不会出现碎片化内存。但是缺点也很明显,复制对象需要计算成本,此外需要准备一个额外相同Eden区域大小的内存空间,也是一笔巨大的开销。

2.2、标记清除法

这种方法首先遍历整个区域中的对象,然后标记所有的可达对象,再将所有内存中未被标记的对象全部清除。主要缺点在于会产生大量的碎片内存。

2.3、标记整理法

这种方法集上面两种算法的优点于一身,首先遍历整个空间对可达对象进行标记,然后再讲所有可达对象整理到一起去,最后清除掉不可达的对象,达到GC回收清理内存的目的。

3、Jvm如何检测对象是否有用?

上面一个章节我们简要的叙述了Jvm GC的回收策略,但是大家一定很好奇,Jvm是如何判断一个对象是否已经无人再使用的,这里大致说一下Jvm的策略。

3.1、引用计数法

顾名思义,这种方法其实很简单,就是你new出来一个对象之后,之后每次对该对象做了引用,那么就将该对象+1,在GC时,只需要判断该对象对应的count是否为0就可以轻松判断出是否还存在引用关系。但是这种方法很不严谨,循环引用会使得其产生内存泄漏,永远无法释放这些资源。

3.2、根搜索算法

这个算法其实也很简单,如下图所示,Jvm会起一个后台守护进程来维护一个树结构,如果发生引用就在树上维护一条边,那么同样的,如果这个引用被释放了,那么这个类也就和这个树失去了链接,从根节点GC Root对其进行搜索也搜索不到,便可以判断其对象已经无人再引用,可以释放

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值