008. Spring Boot(1.5.10版本)热部署

java热部署与热加载的联系

效果:

  • 不重启服务器 就可以部署项目
  • 减少时间
  • 都基于java类加载器

区别

部署方式

  • 热部署 在服务器运行时重新部署项目

  • 热加载 在运行时重新加载class

场景

  • 热部署 在生产环境使用

  • 热加载 开发环境使用
    这种方式难以监控,生产环境使用很危险

使用

SprigBoot 的热部署方式分为两种
 SpringLoader 插件
 DevTools 工具

SpringLoader 插件的使用

SpringLoader 缺陷: 就是 Java 代码做部署处理。 但是对页面无能为力。

先部署一个项目 前后台很显示内容即可

方式一: 以 maven 插件方式使用 SpringLoader

pom.xml

<!-- springloader 插件 -->
<build>
<plugins><plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>springloaded</artifactId>
<version>1.2.5.RELEASE</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>

使用maven命令启动
Edit Configuration 窗口
Goals: 空里写

spring-boot:run

注意: 这种方式的缺点是 Springloader 热部署程序是在
系统后台以进程的形式来运行。 需要手动关闭该进程 java.exe

方式二: 在项目中直接使用 jar 包的方式

lib目录下 添加 springloader 的 jar 包

Run As—>Run Configurations
在这里插入图片描述

启动命令:
-javaagent:.\lib\springloaded-1.2.5.RELEASE.jar -noverify

DevTools 工具

SpringLoader 与 DevTools 的区别:

SpringLoader: SpringLoader 在部署项目时使用的是热部署的方式。
DevTools: DevTools 在部署项目时使用的是重新部署的方式

<!-- DevTools 的坐标 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>

原理

原文出处:
https://mp.weixin.qq.com/s?src=11&timestamp=1576545223&ver=2039&signature=VJZ9X7lkE1rwPPM0VMD02YjmsRSw5S-1jYb9mqPfLNpo0l9S0OMuuZt-m1EKPg9i0iXbk6Rg7pGtDbzfvf5BHvPMTmsFn5HXvhlLB1gVB0t5KT1BStcC*mOF6fQxjqUS&new=1

在这里插入图片描述
对类进行初始化工作

什么时候要对类进行初始化工作(加载+链接在此之前已经完成了),jvm有严格的规定(四种情况):

1.遇到new,getstatic,putstatic,invokestatic这4条字节码指令时,加入类还没进行初始化,则马上对其进行初始化工作。其实就是3种情况:用new实例化一个类时、读取或者设置类的静态字段时(不包括被final修饰的静态字段,因为他们已经被塞进常量池了)、以及执行静态方法的时候。

2.使用*java.lang.reflect.的方法对类进行反射调用的时候,如果类还没有进行过初始化,马上对其进行。

3.初始化一个类的时候,如果他的父亲还没有被初始化,则先去初始化其父亲。

4.当jvm启动时,用户需要指定一个要执行的主类(包含static void main(String[] args)的那个类),则jvm会先去初始化这个类

以上4种预处理称为对一个类进行主动的引用,其余的其他情况,称为被动引用,都不会触发类的初始化。

下面也举了些被动引用的例子(类的初始化):

1.通过子类引用父类的静态字段,不会导致子类的初始化

class SuperClass{ 
  static{ 
    System.out.println("super class init."); 
  } 
  public static int  value=123; 
} 

class SubClass extends SuperClass{ 
  static{ 
    System.out.println("sub class init."); //没有被执行
  } 
} 

public class test{ 
  public static void main(String[]args){ 
    System.out.println(SubClass.value); 
  } 

} 

输出结果是:

super class init.
123

2.通过数组引用来引用类,不会触发此类的初始化

public class test{ 
  public static void main(String[] args){ 
    SuperClass s_list=new SuperClass[10]; 
  } 
} 

输出结果:没输出

3.常量在编译阶段会被存入调用类的常量池中,本质上并没有引用到定义常量类类,所以自然不会触发定义常量的类的初始化

class ConstClass{ 
  static{ 
    System.out.println("ConstClass init."); 
  } 
  public final static String value="hello"; 
} 

public class test{ 
  public static void main(String[] args){ 
    System.out.println(ConstClass.value); 
  } 
} 

针对接口也要初始化

接口的初始化跟类的初始化有点不同:

上面的代码都是用static{}来输出初始化信息的,接口没法做到,但接口初始化的时候编译器仍然会给接口生成一个()的类构造器,用来初始化接口中的成员变量,这点在类的初始化上也有做到。

真正不同的地方在于第三点,类的初始化执行之前要求父类全部都初始化完成了,但接口不同,也就是说,子接口初始化的时候并不要求其父接口也完成初始化,只有在真正使用到父接口的时候它才会被初始化(比如引用接口上的常量的时候啦)。

类加载的五个阶段

加载->链接(验证+准备+解析)->初始化(使用前的准备)->使用->卸载

加载(允许用户自定义类加载器 参与进来)

这一块虚拟机要完成3件事:

1.通过一个类的全限定名来获取定义此类的二进制字节流

2.将这个字节流所代表的静态存储结构 转化 为方法区的运行时数据结构

3.在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口

关于第一点,很灵活,很多技术都是在这里切入,因为它并没有限定二进制流从哪里来:

从class文件来->一般的文件加载

从zip包中来->加载jar中的类

从网络中来->Applet

……….

相比与加载过程的其他几个阶段,加载阶段可控性最强,因为类的加载器可以用系统的,也可以用自己写的,程序猿可以用自己的方式写加载器来控制字节流的获取。

获取二进制流获取完成后会按照jvm所需的方式保存在方法区中,同时会在java堆中实例化一个java.lang.Class对象与堆中的数据关联起来。

验证(确保内容都是符合规范的+安全的)

检验主要经历几个步骤:文件格式验证->元数据验证->字节码验证->符号引用验证

  • 文件格式验证:验证字节流是否符合Class文件格式的规范并 验证其版本是否能被当前的jvm版本所处理。ok没问题后,字节流就可以进入内存的方法区进行保存了。后面的3个校验都是在方法区进行的。

  • 元数据验证:对字节码描述的信息进行语义化分析,保证其描述的内容符合java语言的语法规范。

  • 字节码检验:最复杂,对方法体的内容进行检验,保证其在运行时不会作出什么出格的事来。

  • 符号引用验证:来验证一些引用的真实性与可行性,比如代码里面引了其他类,这里就要去检测一下那些来究竟是否存在;或者说代码中访问了其他类的一些属性,这里就对那些属性的可以访问行进行了检验。(这一步将为后面的解析工作打下基础)

验证阶段很重要,但也不是必要的,假如说一些代码被反复使用并验证过可靠性了,实施阶段就可以尝试用-Xverify:none参数来关闭大部分的类验证措施,以简短类加载时间。

准备

这阶段会为类变量(指那些静态变量)分配内存并设置类比那辆初始值的阶段,这些内存在方法区中进行分配。这里要说明一下,这一步只会给那些静态变量设置一个初始的值,而那些实例变量是在实例化对象时进行分配的。这里的给类变量设初始值跟类变量的赋值有点不同,比如下面:

public static int value=123;

在这一阶段,value的值将会是0,而不是123,因为这个时候还没开始执行任何java代码,123还是不可见的,而我们所看到的把123赋值给value的putstatic指令是程序被编译后存在于(),所以,给value赋值为123是在初始化的时候才会执行的。

这里也有个例外:

public static final int value=123;

这里在准备阶段value的值就会初始化为123了。这个是说,在编译期,javac会为这个特殊的value生成一个ConstantValue属性,并在准备阶段jm就会根据这个ConstantValue的值来为value赋值了。

解析

初始化

在前面的类加载过程中,除了在加载阶段用户可以通过自定义类加载器参与之外,其他的动作完全有jvm主导,到了初始化这块,才开始真正执行java里面的代码。

这一步将会执行一些预操作,注意区分在准备阶段,已经为类变量执行过一次系统赋值了。

其实说白了,这一步就是执行程序的();方法的过程。下面我们来研究一下()方法:

()方法叫做类构造器方法,有编译器自动搜集类中的所有类变量的赋值动作和静态语句块中的语句合并而成的,置于他们的顺序与在源文件中排列的一样。

();方法与类构造方法不一样,他不需要显示得调用父类的();方法,虚拟机会保证子类的();方法在执行前父类的这个方法已经执行完毕了,也就是说**,虚拟机中第一个被执行的();方法肯定是java.lang.Object类的。**

下面来个例子说明一下:

static class Parent{ 
  public static int A=1; 
  static{ 
    A=2; 
  } 
} 


static class Sub extends Parent{ 
  public static int B=A; 
} 
public static void main(String[] args){ 
  System.out.println(Sub.B); 
} 

首先Sub.B中对静态数据进行了引用,Sub类要进行初始化了。同时,其父类Parent要先进行初始化动作。Parent初始化后,A=2,所以B=2;上个过程相当于:

static class Parent{ 
  <clinit>(){ 
    public static int A=1; 
    static{ 
      A=2; 
    } 
  } 
} 
static class Sub extends Parent{ 
  <clinit>(){ //jvm会先让父类的该方法执行完在执行这里 
  public static int B=A; 
  } 
} 
public static void main(String[] args){ 
  System.out.println(Sub.B); 
}

();方法对类跟接口来说不是必须的,加入类或者接口中没有对类变量进行赋值且没有静态代码块,()方法就不会被编译器生成。
由于接口里面不能存在static{}这种静态代码块,但仍然可能存在变量初始化时的变量赋值操作,所以接口里面也会生成()构造器。但跟类的不同的是,执行子接口的();方法前并不需要执行父接口的();方法,当父接口中定义的变量被使用时,父接口才会被初始化。

另外,接口的实现类在初始化的时候也一样不会执行接口的();方法。

另外,jvm会保证一个类的();方法在多线程环境下能被正确地加锁同步。<因为初始化只会被执行一次>。

下面用个例子说明一下:

public class DeadLoopClass { 
  static{ 
    if(true){ 
    System.out.println("要被 ["+Thread.currentThread()+"] 初始化了,下面来一个无限循环"); 
    while(treu){}   
    } 
  } 
  public static void main(String[] args) { 
    System.out.println("toplaile"); 
    Runnable run=new Runnable(){ 
      @Override 
      public void run() { 
        // TODO Auto-generated method stub 
        System.out.println("["+Thread.currentThread()+"] 要去实例化那个类了"); 
        DeadLoopClass d=new DeadLoopClass(); 
        System.out.println("["+Thread.currentThread()+"] 完成了那个类的初始化工作"); 
      }}; 
      new Thread(run).start(); 
      new Thread(run).start(); 
  } 
}

里面,运行的时候将会看到阻塞现象。

发布了162 篇原创文章 · 获赞 13 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_39088066/article/details/103573812