JVM核心知识体系

JVM核心知识体系

1.问题

  • 一、如何理解类文件结构布局?
  • 二、如何应用类加载器的工做原理进行将应用展转腾挪?
  • 三、热部署与热替换有何区别,如何隔离类冲突?
  • 四、JVM如何管理内存,有何内存淘汰机制?
  • 五、JVM执行引擎的工做机制是什么?
  • 六、JVM调优应该遵循什么原则,使用什么工具?
  • 七、JPDA架构是什么,如何应用代码热替换?
  • 八、JVM字节码加强技术有哪些?

2.关键词

类结构,类加载器,加载,连接,初始化,双亲委派,热部署,隔离,堆,栈,方法区,计数器,内存回收,执行引擎,调优工具,JVMTI,JDWP,JDI,热替换,字节码,ASM,CGLIB,DCEVMhtml

3.全文概要

做为三大工业级别语言之一的JAVA如此受企业青睐有加,离不开她背后JVM的默默复出。只是因为JAVA过于成功以致于咱们经常忘了JVM平台上还运行着像Clojure/Groovy/Kotlin/Scala/JRuby/Jython这样的语言。咱们享受着JVM带来跨平台“一次编译处处执行”台的便利和自动内存回收的安逸。本文从JVM的最小元素类的结构出发,介绍类加载器的工做原理和应用场景,思考类加载器存在的意义。进而描述JVM逻辑内存的分布和管理方式,同时列举经常使用的JVM调优工具和使用方法,最后介绍高级特性JDPA框架和字节码加强技术,实现热替换。从微观到宏观,从静态到动态,从基础到高阶介绍JVM的知识体系。java

4.类的装载

4.1类的结构

咱们知道不仅JAVA文本文件,像Clojure/Groovy/Kotlin/Scala这些文本文件也一样会通过JDK的编译器编程成class文件。进入到JVM领域后,其实就跟JAVA没什么关系了,JVM只认得class文件,那么咱们须要先了解class这个黑箱里面包含的是什么东西。git

JVM规范严格定义了CLASS文件的格式,有严格的数据结构,下面咱们能够观察一个简单CLASS文件包含的字段和数据类型。github

ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; } 

详细的描述咱们能够从JVM规范说明书里面查阅类文件格式,类的总体布局以下图展现的。web

java-class-file-internal-structure

在个人理解,我想把每一个CLASS文件类别成一个一个的数据库,里面包含的常量池/类索引/属性表集合就像数据库的表,并且表之间也有关联,常量池则存放着其余表所须要的全部字面量。了解完类的数据结构后,咱们须要来观察JVM是如何使用这些从硬盘上或者网络传输过来的CLASS文件。算法

4.2加载机制

4.2.1类的入口

在咱们探究JVM如何使用CLASS文件以前,咱们快速回忆一下编写好的C语言文件是如何执行的?咱们从C的HelloWorld入手看看先。spring

#include <stdio.h> int main() { /* my first program in C */ printf("Hello, World! \n"); return 0; } 

编辑完保存为hello.c文本文件,而后安装gcc编译器(GNU C/C++)shell

$ gcc hello.c $ ./a.out Hello, World! 

这个过程就是gcc编译器将hello.c文本文件编译成机器指令集,而后读取到内存直接在计算机的CPU运行。从操做系统层面看的话,就是一个进程的启动到结束的生命周期。数据库

下面咱们看JAVA是怎么运行的。学习JAVA开发的第一件事就是先下载JDK安装包,安装完配置好环境变量,而后写一个名字为helloWorld的类,而后编译执行,咱们来观察一下发生了什么事情?apache

先看源码,有够简单了吧。

package com.zooncool.example.theory.jvm; /** * Created with IntelliJ IDEA. User: linzhenhua Date: 2019/1/3 Time: 11:56 PM * @author linzhenhua */ public class HelloWorld { public static void main(String[] args) { System.out.println("my classLoader is " + HelloWorld.class.getClassLoader()); } } 

编译执行

$ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld my classLoader is sun.misc.Launcher$AppClassLoader@2a139a55 

对比C语言在命令行直接运行编译后的a.out二进制文件,JAVA的则是在命令行执行java classFile,从命令的区别咱们知道操做系统启动的是java进程,而HelloWorld类只是命令行的入参,在操做系统来看java也就是一个普通的应用进程而已,而这个进程就是JVM的执行形态(JVM静态就是硬盘里JDK包下的二进制文件集合)。

学习过JAVA的都知道入口方法是public static void main(String[] args),缺一不可,那我猜执行java命令时JVM对该入口方法作了惟一验证,经过了才容许启动JVM进程,下面咱们来看这个入口方法有啥特色。

  • 去掉public限定

    $ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld 错误: 在类 com.zooncool.example.theory.jvm.HelloWorld 中找不到 main 方法, 请将 main 方法定义为: public static void main(String[] args) 不然 JavaFX 应用程序类必须扩展javafx.application.Application 

说名入口方法须要被public修饰,固然JVM调用main方法是底层的JNI方法调用不受修饰符影响。

  • 去掉static限定

    $ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld 错误: main 方法不是类 com.zooncool.example.theory.jvm.HelloWorld 中的static, 请将 main 方法定义为: public static void main(String[] args) 

咱们是从类对象调用而不是类建立的对象才调用,索引须要静态修饰

  • 返回类型改成int

    $ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld 错误: main 方法必须返回类 com.zooncool.example.theory.jvm.HelloWorld 中的空类型值, 请 将 main 方法定义为: public static void main(String[] args) 

void返回类型让JVM调用后无需关心调用者的使用状况,执行完就中止,简化JVM的设计。

  • 方法签名改成main1

    $ javac src/main/java/com/zooncool/example/theory/jvm/HelloWorld.java $ java -cp src/main/java/ com.zooncool.example.theory.jvm.HelloWorld 错误: 在类 com.zooncool.example.theory.jvm.HelloWorld 中找不到 main 方法, 请将 main 方法定义为: public static void main(String[] args) 不然 JavaFX 应用程序类必须扩展javafx.application.Application 

这个我也不清楚,多是约定俗成吧,毕竟C/C++也是用main方法的。

说了这么多main方法的规则,其实咱们关心的只有两点:

  • HelloWorld类是如何被JVM使用的
  • HelloWorld类里面的main方法是如何被执行的

关于JVM如何使用HelloWorld下文咱们会详细讲到。

咱们知道JVM是由C/C++语言实现的,那么JVM跟CLASS打交道则须要JNI(Java Native Interface)这座桥梁,当咱们在命令行执行java时,由C/C++实现的java应用经过JNI找到了HelloWorld里面符合规范的main方法,而后开始调用。咱们来看下java命令的源码就知道了

/* * Get the application's main class. */ if (jarfile != 0) { mainClassName = GetMainClassName(env, jarfile); ... ... mainClass = LoadClass(env, classname); if(mainClass == NULL) { /* exception occured */ ... ... /* Get the application's main method */ mainID = (*env)->GetStaticMethodID(env, mainClass, "main", "([Ljava/lang/String;)V"); ... ... {/* Make sure the main method is public */ jint mods; jmethodID mid; jobject obj = (*env)->ToReflectedMethod(env, mainClass, mainID, JNI_TRUE); ... ... /* Build argument array */ mainArgs = NewPlatformStringArray(env, argv, argc); if (mainArgs == NULL) { ReportExceptionDescription(env); goto leave; } /* Invoke main method. */ (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs); 

4.2.2类加载器

上一节咱们留了一个核心的环节,就是JVM在执行类的入口以前,首先得找到类再而后再把类装到JVM实例里面,也便是JVM进程维护的内存区域内。咱们固然知道是一个叫作类加载器的工具把类加载到JVM实例里面,抛开细节从操做系统层面观察,那么就是JVM实例在运行过程当中经过IO从硬盘或者网络读取CLASS二进制文件,而后在JVM管辖的内存区域存放对应的文件。咱们目前还不知道类加载器的实现,可是咱们从功能上判断无非就是读取文件到内存,这个是很普通也很简单的操做。

若是类加载器是C/C++实现的话,那么大概就是以下代码就能够实现

char *fgets( char *buf, int n, FILE *fp ); 

若是是JAVA实现,那么也很简单

InputStream f = new FileInputStream("theory/jvm/HelloWorld.class"); 

从操做系统层面看的话,若是只是加载,以上代码就足以把类文件加载到JVM内存里面了。可是结果就是乱糟糟的把一堆毫无秩序的类文件往内存里面扔,没有良好的管理也无法用,因此须要咱们须要设计一套规则来管理存放内存里面的CLASS文件,咱们称为类加载的设计模式或者类加载机制,这个下文会重点解释。

根据官网的定义A class loader is an object that is responsible for loading classes. 类加载器就是负责加载类的。咱们知道启动JVM的时候会把JRE默认的一些类加载到内存,这部分类使用的加载器是JVM默认内置的由C/C++实现的,好比咱们上文加载的HelloWorld.class。可是内置的类加载器有明确的范围限定,也就是只能加载指定路径下的jar包(类文件的集合)。若是只是加载JRE的类,那可玩的花样就少不少,JRE只是提供了底层所需的类,更多的业务须要咱们从外部加载类来支持,因此咱们须要指定新的规则,以方便咱们加载外部路径的类文件。

系统默认加载器
  • Bootstrap class loader

    做用:启动类加载器,加载JDK核心类

    类加载器:C/C++实现

    类加载路径: <java_home>/jre/lib

    URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs(); /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/resources.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/sunrsasig.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jsse.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jce.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/charsets.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jfr.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/classes 

    实现原理:本地方法由C++实现

  • Extensions class loader

    做用:扩展类加载器,加载JAVA扩展类库。

    类加载器:JAVA实现

    类加载路径:<java_home>/jre/lib/ext

    System.out.println(System.getProperty("java.ext.dirs")); /Users/linzhenhua/Library/Java/Extensions: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext: /Library/Java/Extensions: /Network/Library/Java/Extensions: /System/Library/Java/Extensions: /usr/lib/java 

    实现原理:扩展类加载器ExtClassLoader本质上也是URLClassLoader

    Launcher.java

    //构造方法返回扩展类加载器 public Launcher() { //定义扩展类加载器 Launcher.ExtClassLoader var1; try { //一、获取扩展类加载器 var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); } ... } //扩展类加载器 static class ExtClassLoader extends URLClassLoader { private static volatile Launcher.ExtClassLoader instance; //二、获取扩展类加载器实现 public static Launcher.ExtClassLoader getExtClassLoader() throws IOException { if (instance == null) { Class var0 = Launcher.ExtClassLoader.class; synchronized(Launcher.ExtClassLoader.class) { if (instance == null) { //三、构造扩展类加载器 instance = createExtClassLoader(); } } } return instance; } //四、构造扩展类加载器具体实现 private static Launcher.ExtClassLoader createExtClassLoader() throws IOException { try { return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() { public Launcher.ExtClassLoader run() throws IOException { //五、获取扩展类加载器加载目标类的目录 File[] var1 = Launcher.ExtClassLoader.getExtDirs(); int var2 = var1.length; for(int var3 = 0; var3 < var2; ++var3) { MetaIndex.registerDirectory(var1[var3]); } //七、构造扩展类加载器 return new Launcher.ExtClassLoader(var1); } }); } catch (PrivilegedActionException var1) { throw (IOException)var1.getException(); } } //六、扩展类加载器目录路径 private static File[] getExtDirs() { String var0 = System.getProperty("java.ext.dirs"); File[] var1; if (var0 != null) { StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator); int var3 = var2.countTokens(); var1 = new File[var3]; for(int var4 = 0; var4 < var3; ++var4) { var1[var4] = new File(var2.nextToken()); } } else { var1 = new File[0]; } return var1; } //八、扩展类加载器构造方法 public ExtClassLoader(File[] var1) throws IOException { super(getExtURLs(var1), (ClassLoader)null, Launcher.factory); SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this); } } 
  • System class loader

    做用:系统类加载器,加载应用指定环境变量路径下的类

    类加载器:sun.misc.Launcher$AppClassLoader

    类加载路径:-classpath下面的全部类

    实现原理:系统类加载器AppClassLoader本质上也是URLClassLoader

    Launcher.java

    //构造方法返回系统类加载器 public Launcher() { try { //获取系统类加载器 this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } } static class AppClassLoader extends URLClassLoader { final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this); //系统类加载器实现逻辑 public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException { //类比扩展类加载器,类似的逻辑 final String var1 = System.getProperty("java.class.path"); final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1); return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() { public Launcher.AppClassLoader run() { URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2); return new Launcher.AppClassLoader(var1x, var0); } }); } //系统类加载器构造方法 AppClassLoader(URL[] var1, ClassLoader var2) { super(var1, var2, Launcher.factory); this.ucp.initLookupCache(this); } } 

经过上文运行HelloWorld咱们知道JVM系统默认加载的类大改是1560个,以下图

image-20190105132642932

自定义类加载器

内置类加载器只加载了最少须要的核心JAVA基础类和环境变量下的类,可是咱们应用每每须要依赖第三方中间件来完成额外的业务,那么如何把它们的类加载进来就显得格外重要了。幸亏JVM提供了自定义类加载器,能够很方便的完成自定义操做,最终目的也是把外部的类文件加载到JVM内存。经过继承ClassLoader类而且复写findClass和loadClass方法就能够达到自定义获取CLASS文件的目的。

首先咱们看ClassLoader的核心方法loadClass

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded,看缓存有没有没有才去找 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { //先看是否是最顶层,若是不是则parent为空,而后获取父类 if (parent != null) { c = parent.loadClass(name, false); } else { //若是为空则说明应用启动类加载器,让它去加载 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. //若是仍是没有就调用本身的方法,确保调用本身方法前都使用了父类方法,如此递归三次到顶 long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } } protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); } 

经过复写loadClass方法,咱们甚至能够读取一份加了密的文件,而后在内存里面解密,这样别人反编译你的源码也没用,由于class是通过加密的,也就是理论上咱们经过自定义类加载器能够作到随心所欲,可是有个重要的原则下文介绍类加载器设计模式会提到。

一下给出一个自定义类加载器极简的案例,来讲明自定义类加载器的实现。

package com.zooncool.example.theory.jvm; import java.io.FileInputStream; import static java.lang.System.out; public class ClassIsolationPrinciple { public static void main(String[] args) { try { String className = "com.zooncool.example.theory.jvm.ClassIsolationPrinciple$Demo"; //定义要加载类的全限定名 Class<?> class1 = Demo.class; //第一个类又系统默认类加载器加载 //第二个类MyClassLoader为自定义类加载器,自定义的目的是覆盖加载类的逻辑 Class<?> class2 = new MyClassLoader("target/classes").loadClass(className); out.println("-----------------class name-----------------"); out.println(class1.getName()); out.println(class2.getName()); out.println("-----------------classLoader name-----------------"); out.println(class1.getClassLoader()); out.println(class2.getClassLoader()); Demo.example = 1;//这里修改的系统类加载器加载的那个类的对象,而自定义加载器加载进去的类的对象保持不变,也便是同时存在内存,但没有修改example的值。 out.println("-----------------field value-----------------"); out.println(class1.getDeclaredField("example").get(null)); out.println(class2.getDeclaredField("example").get(null)); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } public static class Demo { public static int example = 0; } public static class MyClassLoader extends ClassLoader{ private String classPath; public MyClassLoader(String classPath) { this.classPath = classPath; } //自定义类加载器继承了ClassLoader,称为一个能够加载类的加载器,同时覆盖了loadClass方法,实现本身的逻辑 @Override public Class<?> loadClass(String name) throws ClassNotFoundException { if(!name.contains("java.lang")){//排除掉加载系统默认须要加载的心里类,由于些类只能又默认类加载器去加载,第三方加载会抛异常,具体缘由下文解释 byte[] data = new byte[0]; try { data = loadByte(name); } catch (Exception e) { e.printStackTrace(); } return defineClass(name,data,0,data.length); }else{ return super.loadClass(name); } } //把影片的二进制类文件读入内存字节流 private byte[] loadByte(String name) throws Exception { name = name.replaceAll("\\.", "/"); String dir = classPath + "/" + name + ".class"; FileInputStream fis = new FileInputStream(dir); int len = fis.available(); byte[] data = new byte[len]; fis.read(data); fis.close(); return data; } } } 

执行结果以下,咱们能够看到加载到内存方法区的两个类的包名+名称是同样的,而对应的类加载器却不同,并且输出被加载类的值也是不同的。

-----------------class name-----------------
com.zooncool.example.theory.jvm.ClassIsolationPrinciple2$Demo
com.zooncool.example.theory.jvm.ClassIsolationPrinciple2$Demo
-----------------classLoader name-----------------
sun.misc.Launcher$AppClassLoader@18b4aac2
com.zooncool.example.theory.jvm.ClassIsolationPrinciple2$MyClassLoader@511d50c0
-----------------field value-----------------
1
0

4.2.3设计模式

现有的加载器分为内置类加载器和自定义加载器,无论它们是经过C或者JAVA实现的最终都是为了把外部的CLASS文件加载到JVM内存里面。那么咱们就须要设计一套规则来管理组织内存里面的CLASS文件,下面咱们就来介绍下经过这套规则如何来协调好内置类加载器和自定义类加载器之间的权责。

咱们知道经过自定义类加载器能够干出不少黑科技,可是有个基本的雷区就是,不能随便替代JAVA的核心基础类,或者说便是你写了一个跟核心类如出一辙的类,JVM也不会使用。你想一下,若是随心所欲的你能够把最基础本的java.lang.Object都换成你本身定义的同名类,而后搞个后门进去,并且JVM还使用的话,那谁还敢用JAVA了是吧,因此咱们会介绍一个重要的原则,在此以前咱们先介绍一下内置类加载器和自定义类加载器是如何协同的。

  • 双亲委派机制

    定义:某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,若是父类加载器能够完成类加载任务,就成功返回;只有父类加载器没法完成此加载任务时,才本身去加载。

    实现:参考上文loadClass方法的源码和注释,经过最多三次递归能够到启动类加载器,若是仍是找不到这调用自定义方法。

image-20190105230855889

双亲委派机制很好理解,目的就是为了避免重复加载已有的类,提升效率,还有就是强制从父类加载器开始逐级搜索类文件,确保核心基础类优先加载。下面介绍的是破坏双亲委派机制,了解为何要破坏这种看似稳固的双亲委派机制。

  • 破坏委派机制

    定义:打破类加载自上而上委托的约束。

    实现:一、继承ClassLoader而且重写loadClass方法体,覆盖依赖上层类加载器的逻辑;

    ​ 二、”启动类加载器”能够指定“线程上下文类加载器”为任意类加载器,便是“父类加载器”委托“子类加载器”去加载不属于它加载范围的类文件;

    说明:双亲委派机制的好处上面咱们已经提过了,可是因为一些历史缘由(JDK1.2加上双亲委派机制前的JDK1.1就已经存在,为了向前兼容不得不开这个后门让1.2版本的类加载器拥有1.1随意加载的功能)。还有就是JNDI的服务调用机制,例如调用JDBC须要从外部加载相关类到JVM实例的内存空间。

介绍完内置类加载器和自定义类加载器的协同关系后,咱们要重点强调上文提到的重要原则。

  • 惟一标识

    定义:JVM实例由类加载器+类的全限定包名和类名组成类的惟一标志。

    实现:加载类的时候,JVM 判断类是否来自相同的加载器,若是相同并且全限定名则直接返回内存已有的类。

    说明:上文咱们提到如何防止相同类的后门问题,有了这个黄金法则,即便相同的类路径和类,可是因为是由自定义类加载器加载的,即便编译经过能被加载到内存,也没法使用,由于JVM核心类是由内置类加载器加载标志和使用的,从而保证了JVM的安全加载。经过缓存类加载器和全限定包名和类名做为类惟一索引,加载重复类则抛异常提示"attempted duplicate class definition for name"。

    原理:双亲委派机制父类检查缓存,源码咱们介绍loadClass方法的时候已经讲过,破坏双亲委派的自定义类加载器在加载类二进制字节码后须要调用defineClass方法,而该方法一样会从JVM方法区检索缓存类,存在的话则提示重复定义。

    URLClassLoader.java 443

    /* * Defines a Class using the class bytes obtained from the specified * Resource. The resulting Class must be resolved before it can be * used. */ private Class<?> defineClass(String name, Resource res) throws IOException { //加载资源 long t0 = System.nanoTime(); int i = name.lastIndexOf('.'); URL url = res.getCodeSourceURL(); if (i != -1) { String pkgname = name.substring(0, i); // Check if package already loaded. Manifest man = res.getManifest(); definePackageInternal(pkgname, man, url); } // Now read the class bytes and define the class java.nio.ByteBuffer bb = res.getByteBuffer(); if (bb != null) { // Use (direct) ByteBuffer: CodeSigner[] signers = res.getCodeSigners(); CodeSource cs = new CodeSource(url, signers); sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0); //字节码转为内存类格式规范 return defineClass(name, bb, cs); } else { byte[] b = res.getBytes(); // must read certificates AFTER reading bytes. CodeSigner[] signers = res.getCodeSigners(); CodeSource cs = new CodeSource(url, signers); sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0); //字节码转为内存类格式规范 return defineClass(name, b, 0, b.length, cs); } } //调用类定义方法 protected final Class<?> defineClass(String name, byte[] b, int off, int len, CodeSource cs) { return defineClass(name, b, off, len, getProtectionDomain(cs)); } //调用类定义方法 protected final Class<?> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain) throws ClassFormatError{ protectionDomain = preDefineClass(name, protectionDomain); String source = defineClassSourceLocation(protectionDomain); //本地方法,定义类 Class<?> c = defineClass1(name, b, off, len, protectionDomain, source); postDefineClass(c, protectionDomain); return c; } //本地方法,将加载的字节码定义为规范的class对象 private native Class<?> defineClass1(String name, byte[] b, int off, int len, ProtectionDomain pd, String source); 

4.2.4加载过程

至此咱们已经深入认识到类加载器的工做原理及其存在的意义,下面咱们将介绍类从外部介质加载使用到卸载整个闭环的生命周期。

加载

上文花了很多的篇幅说明了类的结构和类是如何被加载到JVM内存里面的,那究竟何时JVM才会触发类加载器去加载外部的CLASS文件呢?一般有以下四种状况会触发到:

  • 显式字节码指令集(new/getstatic/putstatic/invokestatic):对应的场景就是建立对象或者调用到类文件的静态变量/静态方法/静态代码块
  • 反射:经过对象反射获取类对象时
  • 继承:建立子类触发父类加载
  • 入口:包含main方法的类首先被加载

JVM只定了类加载器的规范,但却不明确规定类加载器的目标文件,把加载的具体逻辑充分交给了用户,包括重硬盘加载的CLASS类到网络,中间文件等,只要加载进去内存的二进制数据流符合JVM规定的格式,都是合法的。

连接

类加载器加载完类到JVM实例的指定内存区域(方法区下文会提到)后,是使用前会通过验证,准备解析的阶段。

  • 验证:主要包含对类文件对应内存二进制数据的格式、语义关联、语法逻辑和符合引用的验证,若是验证不经过则跑出VerifyError的错误。可是该阶段并不是强制执行,能够经过-Xverify:none来关闭,提升性能。
  • 准备:但咱们验证经过时,内存的方法区存放的是被“紧密压缩”的数据段,这个时候会对static的变量进行内存分配,也就是扩展内存段的空间,为该变量匹配对应类型的内存空间,但还未初始化数据,也就是0或者null的值。
  • 解析:咱们知道类的数据结构相似一个数据库,里面多张不一样类型的“表”紧凑的挨在一块儿,最大的节省类占用的空间。多数表都会应用到常量池表里面的字面量,这个时候就是把引用的字面量转化为直接的变量空间。好比某一个复杂类变量字面量在类文件里只占2个字节,可是经过常量池引用的转换为实际的变量类型,须要占用32个字节。因此通过解析阶段后,类在方法区占用的空间就会膨胀,长得更像一个”类“了。
初始化

方法区通过解析后类已经为各个变量占好坑了,初始化就是把变量的初始值和构造方法的内容初始化到变量的空间里面。这时候咱们介质的类二进制文件所定义的内容,已经彻底被“翻译”方法区的某一段内存空间了。万事俱备只待使用了。

使用

使用呼应了咱们加载类的触发条件,也便是触发类加载的条件也是类应用的条件,该操做会在初始化完成后进行。

卸载

咱们知道JVM有垃圾回收机制(下文会详细介绍),不须要咱们操心,整体上有三个条件会触发垃圾回收期清理方法区的空间:

  • 类对应实例被回收
  • 类对应加载器被回收
  • 类无反射引用

本节结束咱们已经对整个类的生命周期烂熟于胸了,下面咱们来介绍类加载机制最核心的几种应用场景,来加深对类加载技术的认识。

4.3应用场景

经过前文的剖析咱们已经很是清楚类加载器的工做原理,那么咱们该如何利用类加载器的特色,最大限度的发挥它的做用呢?

4.3.1热部署

背景

热部署这个词汇咱们常常据说也常常提起,可是却不多可以准确的描述出它的定义。说到热部署咱们第一时间想到的多是生产上的机器更新代码后无需重启应用容器就能更新服务,这样的好处就是服务无需中断可持续运行,那么与之对应的冷部署固然就是要重启应用容器实例了。还有可能会想到的是使用IDE工具开发时不须要重启服务,修改代码后即时生效,这看起来可能都是服务无需重启,但背后的运行机制确大相径庭,首先咱们须要对热部署下一个准确的定义。

  • 热部署(Hot Deployment):热部署是应用容器自动更新应用的一种能力。

首先热部署应用容器拥有的一种能力,这种能力是容器自己设计出来的,跟具体的IDE开发工具无关。并且热部署无需重启服务器,应用能够保持用户态不受影响。上文提到咱们开发环境使用IDE工具一般也能够设置无需重启的功能,有别于热部署的是此时咱们应用的是JVM的自己附带的热替换能力(HotSwap)。热部署和热替换是两个彻底不一样概念,在开发过程当中也经常相互配合使用,致使咱们不少人常常混淆概念,因此接下来咱们来剖析热部署的实现原理,而热替换的高级特性咱们会在下文字节码加强的章节中介绍。

原理

从热部署的定义咱们知道它是应用容器蕴含的一项能力,要达到的目的就是在服务没有重启的状况下更新应用,也就是把新的代码编译后产生的新类文件替换掉内存里的旧类文件。结合前文咱们介绍的类加载器特性,这彷佛也不是很难,分两步应该能够完成。因为同一个类加载器只能加载一次类文件,那么新增一个类加载器把新的类文件加载进内存。此时内存里面同时存在新旧的两个类(类名路径同样,可是类加载器不同),要作的就是如何使用新的类,同时卸载旧的类及其对象,完成这两步其实也就是热部署的过程了。也便是经过使用新的类加载器,从新加载应用的类,从而达到新代码热部署。

实现

理解了热部署的工做原理,下面经过一系列极简的例子来一步步实现热部署,为了方便读者演示,如下例子我尽可能都在一个java文件里面完成全部功能,运行的时候复制下去就能够跑起来。

  • 实现自定义类加载器

参考4.2.2中自定义类加载器区别系统默认加载器的案例,从该案例实践中咱们能够将相同的类(包名+类名),不一样”版本“(类加载器不同)的类同时加载进JVM内存方法区。

  • 替换自定义类加载器

既然一个类经过不一样类加载器能够被屡次加载到JVM内存里面,那么类的通过修改编译后再加载进内存。有别于上一步给出的例子只是修改对象的值,此次咱们是直接修改类的内容,从应用的视角看其实就是应用更新,那如何作到在线程运行不中断的状况下更换新类呢?

下面给出的也是一个很简单的例子,ClassReloading启动main方法经过死循环不断建立类加载器,同时不断加载类并且执行类的方法。注意new MyClassLoader("target/classes")的路径更加编译的class路径来修改,其余直接复制过去就能够执行演示了。

package com.zooncool.example.theory.jvm; import java.io.FileInputStream; import java.lang.reflect.InvocationTargetException; public class ClassReloading { public static void main(String[] args) throws NoSuchMethodException, ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, InterruptedException { for (;;){//用死循环让线程持续运行未中断状态 //经过反射调用目标类的入口方法 String className = "com.zooncool.example.theory.jvm.ClassReloading$User"; Class<?> target = new MyClassLoader("target/classes").loadClass(className); //加载进来的类,经过反射调用execute方法 target.getDeclaredMethod("execute").invoke(targetClass.newInstance()); //HelloWorld.class.getDeclaredMethod("execute").invoke(HelloWorld.class.newInstance()); //若是换成系统默认类加载器的话,由于双亲委派原则,默认使用应用类加载器,并且能加载一次 //休眠是为了在删除旧类编译新类的这段时间内不执行加载动做 //否则会找不到类文件 Thread.sleep(10000); } } //自定义类加载器加载的目标类 public static class User { public void execute() throws InterruptedException { //say(); ask(); } public void ask(){ System.out.println("what is your name"); } public void say(){ System.out.println("my name is lucy"); } } //下面是自定义类加载器,跟第一个例子同样,可略过 public static class MyClassLoader extends ClassLoader{ private String classPath; public MyClassLoader(String classPath) { this.classPath = classPath; } @Override public Class<?> loadClass(String name) throws ClassNotFoundException { if(!name.contains("java")){ byte[] data = new byte[0]; try { data = loadByte(name); } catch (Exception e) { e.printStackTrace(); } return defineClass(name,data,0,data.length); }else{ return super.loadClass(name); } } private byte[] loadByte(String name) throws Exception { name = name.replaceAll("\\.", "/"); String dir = classPath + "/" + name + ".class"; FileInputStream fis = new FileInputStream(dir); int len = fis.available(); byte[] data = new byte[len]; fis.read(data); fis.close(); return data; } } } 

ClassReloading线程执行过程不断轮流注释say()和ask()代码,而后编译类,观察程序输出。

以下输出结果,咱们能够看出每一次循环调用都新建立一个自定义类加载器,而后经过反射建立对象调用方法,在修改代码编译后,新的类就会经过反射建立对象执行新的代码业务,而主线程则一直没有中断运行。读到这里,其实咱们已经基本触达了热部署的本质了,也就是实现了手动无中断部署。可是缺点就是须要咱们手动编译代码,并且内存不断新增类加载器和对象,若是速度过快并且频繁更新,还可能形成堆溢出,下一个例子咱们将增长一些机制来保证旧的类和对象能被垃圾收集器自动回收。

what is your name
what is your name
what is your name//修改代码,编译新类
my name is lucy
my name is lucy
what is your name//修改代码,编译新类
  • 回收自定义类加载器

一般状况下类加载器会持有该加载器加载过的全部类的引用,全部若是类是通过系统默认类加载器加载的话,那就很难被垃圾收集器回收,除非符合根节点不可达原则才会被回收。

下面继续给出一个很简单的例子,咱们知道ClassReloading只是不断建立新的类加载器来加载新类从而更新类的方法。下面的例子咱们模拟WEB应用,更新整个应用的上下文Context。下面代码本质上跟上个例子的功能是同样的,只不过咱们经过加载Model层、DAO层和Service层来模拟web应用,显得更加真实。

package com.zooncool.example.theory.jvm; import java.io.FileInputStream; import java.lang.reflect.InvocationTargetException; /** * 应用上下文热加载 * Created with IntelliJ IDEA. User: linzhenhua Date: 2019/2/24 Time: 10:25 PM * @author linzhenhua */ public class ContextReloading { public static void main(String[] args) throws NoSuchMethodException, ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, InterruptedException { for (;;){ Object context = newContext();//建立应用上下文 invokeContext(context);//经过上下文对象context调用业务方法 Thread.sleep(5000); } } //建立应用的上下文,context是整个应用的GC roots,建立完返回对象以前调用init()初始化对象 public static Object newContext() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException { String className = "com.zooncool.example.theory.jvm.ContextReloading$Context"; //经过自定义类加载器加载Context类 Class<?> contextClass = new MyClassLoader("target/classes").loadClass(className); Object context = contextClass.newInstance();//经过反射建立对象 contextClass.getDeclaredMethod("init").invoke(context);//经过反射调用初始化方法init() return context; } //业务方法,调用context的业务方法showUser() public static void invokeContext(Object context) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { context.getClass().getDeclaredMethod("showUser").invoke(context); } public static class Context{ private UserService userService = new UserService(); public String showUser(){ return userService.getUserMessage(); } //初始化对象 public void init(){ UserDao userDao = new UserDao(); userDao.setUser(new User()); userService.setUserDao(userDao); } } public static class UserService{ private UserDao userDao; public String getUserMessage(){ return userDao.getUserName(); } public void setUserDao(UserDao userDao) { this.userDao = userDao; } } public static class UserDao{ private User user; public String getUserName(){ //关键操做,运行main方法后切换下面方法,编译后下一次调用生效 return user.getName(); //return user.getFullName(); } public void setUser(User user) { this.user = user; } } public static class User{ private String name = "lucy"; private String fullName = "hank.lucy"; public String getName() { System.out.println("my name is " + name); return name; } public String getFullName() { System.out.println("my full name is " + fullName); return name; } } //跟以前的类加载器如出一辙,能够略过 public static class MyClassLoader extends ClassLoader{ private String classPath; public MyClassLoader(String classPath) { this.classPath = classPath; } @Override public Class<?> loadClass(String name) throws ClassNotFoundException { if(!name.contains("java")){ byte[] data = new byte[0]; try { data = loadByte(name); } catch (Exception e) { e.printStackTrace(); } return defineClass(name,data,0,data.length); }else{ return super.loadClass(name); } } private byte[] loadByte(String name) throws Exception { name = name.replaceAll("\\.", "/"); String dir = classPath + "/" + name + ".class"; FileInputStream fis = new FileInputStream(dir); int len = fis.available(); byte[] data = new byte[len]; fis.read(data); fis.close(); return data; } } } 

输出结果跟上一个例子类似,能够本身运行试试。咱们更新业务方法编译经过后,无需重启main方法,新的业务就能生效,并且也解决了旧类卸载的核心问题,由于context的应用对象的跟节点,context是由咱们自定义类加载器所加载,因为User/Dao/Service都是依赖context,因此其类也是又自定义类加载器所加载。根据GC roots原理,在建立新的自定义类加载器以后,旧的类加载器已经没有任何引用链可访达,符合GC回收规则,将会被GC收集器回收释放内存。至此已经完成应用热部署的流程,可是细心的朋友可能会发现,咱们热部署的策略是整个上下文context都替换成新的,那么用户的状态也将没法保留。而实际状况是咱们只须要动态更新某些模块的功能,而不是全局。这个其实也好办,就是咱们从业务上把须要热部署的由自定义类加载器加载,而持久化的类资源则由系统默认类加载器去完成。

  • 隔离自定义类加载器

咱们上个例子已经完美的演示了热部署的整个流程,本节咱们将更加接近实际的应用场景,也就是上文提到的保持用户态。咱们知道默认系统类加载器遵循双亲委派原则,也就是系统类加载器对特定类只加载一次,若是要再次加载该类,JVM会从缓存中检测到已存在而后从缓存中获取,而不会再次加载,咱们利用这个特性来保存用户态。至于以前写好的自定义类加载器,则是咱们热部署所须要的,下面咱们过一下这个例子,延续上一个例子,咱们增长了Cache类表示缓存用户态,重点关注Cache赋值的注释。

package com.zooncool.example.theory.jvm; import java.io.FileInputStream; import java.lang.reflect.InvocationTargetException; /** * 应用上下文热加载,增长缓存用户态 * Created with IntelliJ IDEA. User: linzhenhua Date: 2019/2/24 Time: 10:25 PM * @author linzhenhua */ public class ContextStaticReloading { public static void main(String[] args) throws NoSuchMethodException, ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, InterruptedException, NoSuchFieldException { Cache cache = new Cache();//根据GCRoots可达原则,Cache会被系统类加载器加载,并且不会被GC回收 cache.setName("Jack"); for (;;){ Object context = newContext(cache);//把cache赋给context,注意context是由自定义类加载器的,因此每次循环都会被新版本的context替换,而cache不会被GC因此一直保持用户态。 invokeContext(context); Thread.sleep(5000); } } public static Object newContext(Cache cache) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchFieldException { String className = "com.zooncool.example.theory.jvm.ContextStaticReloading$Context"; Class<?> contextClass = new MyClassLoader("target/classes").loadClass(className); Object context = contextClass.newInstance(); contextClass.getDeclaredField("cache").set(context,cache);//初始化以前显把context对象里面的cache进行赋值,这里要特别注意的是,咱们传入的cache对象是由系统类加载器加载的。而Context类里面的Cache类倒是由自定义类加载器MyClassLoader加载,这样赋值会形成类型和值不一致,致使抛出IllegalArgumentException异常。解决思路很简单,让他们保持一致就行,可是因为咱们Cache必须又系统默认类加载器加载,那么只能让Context里面的Cache显性的指定系统类加载器来加载(由于不指定的话,根据类加载器依赖传导原则,依赖类复用被依赖的类的加载器)。本例子很简单,就是在复写loadClass方法的时候,增长条件!name.contains("Cache"),让MyClassLoader加载Cache时抛给系统类加载器。 contextClass.getDeclaredMethod("init").invoke(context); return context; } public static void invokeContext(Object context) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { context.getClass().getDeclaredMethod("showUser").invoke(context); } public static class Context{ public Cache cache; private UserService userService = new UserService(); public void init(){ User user = new User(); user.setName("Rose"); UserDao userDao = new UserDao(); userDao.setUser(user); userService.setUserDao(userDao); } public void showUser(){ System.out.println("from context name is " + userService.getUserMessage()); System.out.println("from cache name is " + cache.getName()); System.out.println(); } } public static class UserService{ private UserDao userDao; public String getUserMessage(){ return userDao.getUserName(); } public void setUserDao(UserDao userDao) { this.userDao = userDao; } } public static class UserDao{ private User user; public String getUserName(){ return user.getName(); } public void setUser(User user) { this.user = user; } } public static class User{ private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } public static class Cache{ private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } public static class MyClassLoader extends ClassLoader{ private String classPath; public MyClassLoader(String classPath) { this.classPath = classPath; } @Override public Class<?> loadClass(String name) throws ClassNotFoundException { if(!name.contains("java") && !name.contains("Cache") ){//就是这里加条件过滤Cache byte[] data = new byte[0]; try { data = loadByte(name); } catch (Exception e) { e.printStackTrace(); } return defineClass(name,data,0,data.length); }else{ return super.loadClass(name); } } private byte[] loadByte(String name) throws Exception { name = name.replaceAll("\\.", "/"); String dir = classPath + "/" + name + ".class"; FileInputStream fis = new FileInputStream(dir); int len = fis.available(); byte[] data = new byte[len]; fis.read(data); fis.close(); return data; } } } 

以上代码重点关注注释部分,main运行后修改cache和user的name值,而后编译,咱们会发现user的name即时改变,而cache的却没有变化,从而实现了用户态的固化。

cache.setName("Jack");

user.setName("Rose");

from context name is Rose
from  cache  name is Jack

from context name is Lucy
from  cache  name is Jack

from context name is hanmei
from  cache  name is Jack

到了这里相信咱们对类加载器的展转腾挪已经烂熟于胸,虽然咱们已经实现了应用热部署,常态数据持久化,可是若是从设计上考虑这个流程无疑还不够优雅,由于咱们的实现热部署的逻辑彻底暴露给了用户,这在安全的维度看不可接受的。下一节咱们将进一步考虑如何优雅的实现热部署。

  • 自动加载类加载器

其实设计到代码设计优雅问题,基本上咱们拿出设计模式23章经对号入座基本能够解决问题,毕竟这是前人通过千万实践锤炼出来的软件构建内功心法。那么针对咱们热部署的场景,若是想把热部署细节封装出来,那代理模式无疑是最符合要求的,也就是我们弄出个代理对象来面向用户,把类加载器的更替,回收,隔离等细节都放在代理对象里面完成,而对于用户来讲是透明无感知的,那么终端用户体验起来就是纯粹的热部署了。至于如何实现自动热部署,方式也很简单,监听咱们部署的目录,若是文件时间和大小发生变化,则判断应用须要更新,这时候就触发类加载器的建立和旧对象的回收,这个时候也能够引入观察者模式来实现。因为篇幅限制,本例子就留给读者朋友自行设计,相信也是不难完成的。

案例

上一节咱们深刻浅出的从自定义类加载器的开始引入,到实现多个类加载器加载同个类文件,最后完成旧类加载器和对象的回收,整个流程阐述了热部署的实现细节。那么这一节咱们介绍现有实现热部署的通用解决方案,本质就是对上文原理的实现,加上性能和设计上的优化,注意本节咱们应用的只是类加载器的技术,后面章节还会介绍的字节码层面的底层操做技术。

  • OSGI

OSGI(Open Service Gateway Initiative)是一套开发和部署应用程序的java框架。咱们从官网能够看到OSGI实际上是一套规范,比如Servlet定义了服务端对于处理来自网络请求的一套规范,好比init,service,destroy的生命周期。而后咱们经过实行这套规范来实现与客户端的交互,在调用init初始化完Servlet对象后经过多线程模式使用service响应网络请求。若是从响应模式比较咱们还能够了解下Webflux的规范,以上两种都是处理网络请求的方式,固然你举例说CGI也是一种处理网络请求的规范,CGI采用的是多进程方式来处理网络请求,咱们暂时不对这两种规范进行优劣评价,只是说明在处理网络请求的场景下能够采用不一样的规范来实现。

好了如今回到OSGi,有了上面的铺垫,相信对咱们理解OSGI大有帮助。咱们说OSGI首先是一种规范,既然是规范咱们就要看看都规范了啥,好比Servlet也是一种规范,它规范了生命周期,规定应用容器中WEB-INF/classes目录或WEB-INF/lib目录下的jar包才会被Web容器处理。一样OSGI的实现框架对管辖的Bundle下面的目录组织和文本格式也有严格规范,更重要的是OSGI对模块化架构生命周期的管理。而模块化也不仅是把系统拆分红不一样的JAR包造成模块而已,真正的模块化必须将模块中类的引入/导出、隐藏、依赖、版本管理贯穿到生命周期管理中去。

定义:OSGI是脱胎于(OSGI Alliance)技术联盟由一组规范和对应子规范共同定义的JAVA动态模块化技术。实现该规范的OSGI框架(如Apache Felix)使应用程序的模块可以在本地或者网络中实现端到端的通讯,目前已经发布了第7版。OSGI有不少优势诸如热部署,类隔离,高内聚,低耦合的优点,但同时也带来了性能损耗,并且基于OSGI目前的规范繁多复杂,开发门槛较高。

组成:执行环境,安全层,模块层,生命周期层,服务层,框架API

核心服务:

事件服务(Event Admin Service),

包管理服务(Package Admin Service)

日志服务(Log Service)

配置管理服务(Configuration Admin Service)

HTTP服务(HTTP Service)

用户管理服务(User Admin Service)

设备访问服务(Device Access Service)

IO链接器服务(IO Connector Service)

声明式服务(Declarative Services)

其余OSGi标准服务

osgi

本节咱们讨论的核心是热部署,因此咱们不打算在这里讲解所有得OSGI技术,在上文实现热部署后咱们重点来剖析OSGI关于热部署的机制。至于OSGI模块化技术和java9的模块化的对比和关联,后面有时间会开个专题专门介绍模块化技术。

从类加载器技术应用的角度切入咱们知道OSGI规范也是打破双亲委派机制,除了框架层面须要依赖JVM默认类加载器以外,其余Bundle(OSGI定义的模块单元)都是由各自的类加载器来加载,而OSGI框架就负责模块生命周期,模块交互这些核心功能,同时建立各个Bundle的类加载器,用于直接加载Bundle定义的jar包。因为打破双亲委派模式,Bundle类加载器再也不是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构(由于各个Bundle之间有相互依赖关系),当收到类加载请求时,OSGi将按照下面的顺序进行类搜索:

1)将以java.*开头的类委派给父类加载器加载。

2)不然,将委派列表名单内(好比sun或者javax这类核心类的包加入白名单)的类委派给父类加载器加载。

3)不然,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。

4)不然,查找当前Bundle的ClassPath,使用本身的类加载器加载。

5)不然,查找类是否在本身的Fragment Bundle(OSGI框架缓存包)中,若是在,则委派给Fragment Bundle的类加载器加载。

6)不然,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。

7)不然,类查找失败。

这一系列的类加载操做,其实跟咱们上节实现的自定义类加载技术本质上是同样的,只不过实现OSGI规范的框架须要提供模块之间的注册通讯组件,还有模块的生命周期管理,版本管理。OSGI也只是JVM上面运行的一个普通应用实例,只不过经过模块内聚,版本管理,服务依赖一系列的管理,实现了模块的即时更新,实现了热部署。

其余热部署解决方案多数也是利用类加载器的特色作文章,固然不止是类加载器,还会应用字节码技术,下面咱们主要简单列举应用类加载器实现的热部署解决方案。

  • Groovy

​ Groovy兼顾动态脚本语言的功能,使用的时候无外乎也是经过GroovyClassLoader来加载脚本文件,转为JVM的类对象。那么每次更新groovy脚本就能够动态更新应用,也就达到了热部署的功能了。

Class groovyClass = classLoader.parseClass(new GroovyCodeSource(sourceFile)); GroovyObject instance = (GroovyObject)groovyClass.newInstance();//proxy 
  • Clojure

  • JSP

    JSP其实翻译为Servlet后也是由对应新的类加载器去加载,这跟咱们上节讲的流程如出一辙,因此这里就补展开讲解了。

介绍完热部署技术,可能不少同窗对热部署的需求已经没有那么强烈,毕竟热部署过程当中带来的弊端也不容忽视,好比替换旧的类加载器过程会产生大量的内存碎片,致使JVM进行高负荷的GC工做,反复进行热部署还会致使JVM内存不足而致使内存溢出,有时候甚至还不如直接重启应用来得更快一点,并且随着分布式架构的演进和微服务的流行,应用重启也早就实现服务编排化,配合丰富的部署策略,也能够一样保证系统稳定持续服务,咱们更多的是经过热部署技术来深入认识到JVM加载类的技术演进。

4.3.2类隔离

背景

先介绍一下类隔离的背景,咱们费了那么大的劲设计出类加载器,若是只是用于加载外部类字节流那就过于浪费了。一般咱们的应用依赖不一样的第三方类库常常会出现不一样版本的类库,若是只是使用系统内置的类加载器的话,那么一个类库只能加载惟一的一个版本,想加载其余版本的时候会从缓存里面发现已经存在而中止加载。可是咱们的不一样业务以来的每每是不一样版本的类库,这时候就会出现ClassNotFoundException。为何只有运行的是才会出现这个异常呢,由于编译的时候咱们一般会使用MAVEN等编译工具把冲突的版本排除掉。另一种状况是WEB容器的内核依赖的第三方类库须要跟应用依赖的第三方类库隔离开来,避免一些安全隐患,否则若是共用的话,应用升级依赖版本就会致使WEB容器不稳定。

基于以上的介绍咱们知道类隔离实在是刚需,那么接下来介绍一下如何实现这个刚需。

原理

首先咱们要了解一下原理,其实原理很简单,真的很简单,请容许我总结为“惟一标识原理”。咱们知道内存里面定位类实例的坐标<类加载器,类全限定名>。那么由这两个因子组合起来咱们能够得出一种广泛的应用,用不一样类加载器来加载类相同类(类全限定名一致,版本不一致)是能够实现的,也就是在JVM看来,有相同类全名的类是彻底不一样的两个实例,可是在业务视角咱们却能够视为相同的类。

public static void main(String[] args) { Class<?> userClass1 = User.class; Class<?> userClass2 = new DynamicClassLoader("target/classes") .load("qj.blog.classreloading.example1.StaticInt$User"); out.println("Seems to be the same class:"); out.println(userClass1.getName()); out.println(userClass2.getName()); out.println(); out.println("But why there are 2 different class loaders:"); out.println(userClass1.getClassLoader()); out.println(userClass2.getClassLoader()); out.println(); User.age = 11; out.println("And different age values:"); out.println((int) ReflectUtil.getStaticFieldValue("age", userClass1)); out.println((int) ReflectUtil.getStaticFieldValue("age", userClass2)); } public static class User { public static int age = 10; } 
实现

原理很简单,好比咱们知道Spring容器本质就是一个生产和管理bean的集合对象,可是却包含了大量的优秀设计模式和复杂的框架实现。同理隔离容器虽然原理很简单,可是要实现一个高性能可扩展的高可用隔离容器,却不是那么简单。咱们上文谈的场景是在内存运行的时候才发现问题,介绍内存隔离技术以前,咱们先普及更为通用的冲突解决方法。

  • 冲突排除

    冲突老是先发生在编译时期,那么基本Maven工具能够帮咱们完成大部分的工做,Maven的工做模式就是将咱们第三方类库的全部依赖都依次检索,最终排除掉产生冲突的jar包版本。

  • 冲突适配

    当咱们没法经过简单的排除来解决的时候,另一个方法就是从新装配第三方类库,这里咱们要介绍一个开源工具jarjar 。该工具包能够经过字节码技术将咱们依赖的第三方类库重命名,同时修改代码里面对第三方类库引用的路径。这样若是出现同名第三方类库的话,经过该“硬编码”的方式修改其中一个类库,从而消除了冲突。

  • 冲突隔离

    上面两种方式在小型系统比较适合,也比较敏捷高效。可是对于分布式大型系统的话,经过硬编码方式来解决冲突就难以完成了。办法就是经过隔离容器,从逻辑上区分类库的做用域,从而对内存的类进行隔离。

容器
  • Tomcat

类隔离容器很好理解,就是建立一个类容器,容器进行了分区,而后不一样的分区对应不用的类加载器,这样相同的类名分布在不一样的分区也能够独立的运行,说白了就是给不一样性质的类分配不一样的类加载器。首先咱们要讲的是咱们很是熟悉的Tomcat中间件,咱们对它已经很熟悉,以致于都忘了Tomcat底层在类隔离方面作出的优良设计,下面咱们就从源码开始来分析Tomcat是如何设计类加载器规则来作到类隔离的。

首先固然是寻找源码的入口main方法了,对于JVM来讲,全部的启动指令都是始于main的调用,main除了启动JVM之外最重要的做用是建立不一样用途的类加载器,而后把咱们写的一堆Tomcat源码编译后的CLASS文件加载进JVM内存。

//Bootstrap.java 457

public static void main(String args[]) { if (daemon == null) { // Don't set daemon until init() has completed Bootstrap bootstrap = new Bootstrap(); try { //一、初始化守护进程 bootstrap.init(); } catch (Throwable t) { handleThrowable(t); t.printStackTrace(); return; } daemon = bootstrap; } else { // When running as a service the call to stop will be on a new // thread so make sure the correct class loader is used to prevent // a range of class not found exceptions. Thread.currentThread().setContextClassLoader(daemon.catalinaLoader); } //下面的逻辑是执行startup.sh脚本里面执行的exec "$PRGDIR"/"$EXECUTABLE" start "$@" //篇幅限制,省略 } 

追踪初始化流程以前咱们要先观察下列的类加载器

//Bootstrap.java 136

ClassLoader commonLoader = null;//公共类加载器 ClassLoader catalinaLoader = null;// ClassLoader sharedLoader = null;// 

继续追踪初始化方法的逻辑

//Bootstrap.java 255

/** * Initialize daemon. * @throws Exception Fatal initialization error */ public void init() throws Exception { //二、初始化类加载器 initClassLoaders(); Thread.currentThread().setContextClassLoader(catalinaLoader); SecurityClassLoad.securityClassLoad(catalinaLoader); // Load our startup class and call its process() method if (log.isDebugEnabled()) log.debug("Loading startup class"); //六、经过类加载器catalinaLoader加载Catalina,并经过反射建立对象startupInstance Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina"); Object startupInstance = startupClass.getConstructor().newInstance(); // Set the shared extensions class loader if (log.isDebugEnabled()) log.debug("Setting startup class properties"); String methodName = "setParentClassLoader"; Class<?> paramTypes[] = new Class[1]; paramTypes[0] = Class.forName("java.lang.ClassLoader"); Object paramValues[] = new Object[1]; paramValues[0] = sharedLoader; //七、startupInstance实例调用setParentClassLoader方法设置父类加载器为sharedLoader Method method = startupInstance.getClass().getMethod(methodName, paramTypes); method.invoke(startupInstance, paramValues); catalinaDaemon = startupInstance; } 

//Bootstrap.java 144

private void initClassLoaders() { try { //3.一、建立通用类加载器 commonLoader = createClassLoader("common", null); if( commonLoader == null ) { // no config file, default to this loader - we might be in a 'single' env. commonLoader=this.getClass().getClassLoader(); } //3.二、建立系统类加载器 catalinaLoader = createClassLoader("server", commonLoader); //3.三、建立共享类加载器 sharedLoader = createClassLoader("shared", commonLoader); } catch (Throwable t) { handleThrowable(t); log.error("Class loader creation threw exception", t); System.exit(1); } } 

建立类加载器具体逻辑,返回ClassLoader。

//Bootstrap.java 161

//不一样功能类加载器经过配置文件加载不一样路径的类库,父加载器显式指定parent //经过parent传参方式,咱们知道commonLoader的父类加载器是系统类加载器 //而catalinaLoader和sharedLoader的父类加载器则是commonLoader private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception { //省略从catalina.properties读取common.loader,server.loader,shared.loader加载类库逻辑 //common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar"... //server.loader= //shared.loader= //四、工厂类ClassLoaderFactory建立类加载器 return ClassLoaderFactory.createClassLoader(repositories, parent); } 

//ClassLoaderFactory.java 151

//建立类加载器 public static ClassLoader createClassLoader(List<Repository> repositories, final ClassLoader parent) throws Exception { // Construct the "class path" for this class loader Set<URL> set = new LinkedHashSet<>(); //省略从repositories循环迭代出Repository而后装配URL的逻辑,最终就是指定类加载器所要加载类的路径 //五、建立URLClassLoader类加载器 return AccessController.doPrivileged( new PrivilegedAction<URLClassLoader>() { @Override public URLClassLoader run() { if (parent == null) return new URLClassLoader(array); else return new URLClassLoader(array, parent); } }); } 

通过源码的解析,咱们知道commonLoader为catalinaLoader和sharedLoader的父类加载器,接下来咱们继续分析隔离容器还包含其余什么样的类加载器。

具体流程放在《Tomcat源码解析》专题来展开分析,下面咱们给出结论,总结起来Tomcat的类加载器结构以下:

image-20190115175422906

  • Pandora

理解了Tomcat的类加载机制后,咱们来介绍另一个类加载容器。类隔离容器要完成的功能说白了就是把引发冲突的类隔离起来,而后把相同版本的类共享出来避免浪费内存空间。

具体介绍参考《Pandora容器设计的本质》

5.内存管理

5.1内存结构

5.1.1逻辑分区

JVM内存从应用逻辑上可分为以下区域。

  • 程序计数器:字节码行号指示器,每一个线程须要一个程序计数器
  • 虚拟机栈:方法执行时建立栈帧(存储局部变量,操做栈,动态连接,方法出口)编译时期就能肯定占用空间大小,线程请求的栈深度超过jvm运行深度时抛StackOverflowError,当jvm栈没法申请到空闲内存时抛OutOfMemoryError,经过-Xss,-Xsx来配置初始内存
  • 本地方法栈:执行本地方法,如操做系统native接口
  • 堆:存放对象的空间,经过-Xmx,-Xms配置堆大小,当堆没法申请到内存时抛OutOfMemoryError
  • 方法区:存储类数据,常量,常量池,静态变量,经过MaxPermSize参数配置
  • 对象访问:初始化一个对象,其引用存放于栈帧,对象存放于堆内存,对象包含属性信息和该对象父类、接口等类型数据(该类型数据存储在方法区空间,对象拥有类型数据的地址)

而实际上JVM内存分类实际上的物理分区还有更为详细,总体上分为堆内存和非堆内存,具体介绍以下。

5.1.2 内存模型

堆内存

堆内存是运行时的数据区,从中分配全部java类实例和数组的内存,能够理解为目标应用依赖的对象。堆在JVM启动时建立,而且在应用程序运行时可能会增大或减少。能够使用-Xms 选项指定堆的大小。堆能够是固定大小或可变大小,具体取决于垃圾收集策略。能够使用-Xmx选项设置最大堆大小。默认状况下,最大堆大小设置为64 MB。

JVM堆内存在物理上分为两部分:新生代和老年代。新生代是为分配新对象而保留堆空间。当新生代占用完时,Minor GC垃圾收集器会对新生代区域执行垃圾回收动做,其中在新生代中生活了足够长的全部对象被迁移到老年代,从而释放新生代空间以进行更多的对象分配。此垃圾收集称为 Minor GC。新生代分为三个子区域:伊甸园Eden区和两个幸存区S0和S1。

img

关于新生代内存空间:

  • 大多数新建立的对象都位于Eden区内存空间

  • 当Eden区填满对象时,执行Minor GC并将全部幸存对象移动到其中一个幸存区空间

  • Minor GC还会检查幸存区对象并将其移动到其余幸存者空间,也便是幸存区总有一个是空的

  • 在屡次GC后还存活的对象被移动到老年代内存空间。至于通过多少次GC晋升老年代则由参数配置,一般为15

当老年区填满时,老年区一样会执行垃圾回收,老年区还包含那些通过多Minor GC后还存活的长寿对象。垃圾收集器在老年代内存中执行的回收称为Major GC,一般须要更长的时间。

非堆内存

JVM的堆之外内存称为非堆内存。也便是JVM自身预留的内存区域,包含JVM缓存空间,类结构如常量池、字段和方法数据,方法,构造方法。类非堆内存的默认最大大小为64 MB。能够使用-XX:MaxPermSize VM选项更改此选项,非堆内存一般包含以下性质的区域空间:

  • 元空间(Metaspace)

在Java 8以上版本已经没有Perm Gen这块区域了,这也意味着不会再由关于“java.lang.OutOfMemoryError:PermGen”内存问题存在了。与驻留在Java堆中的Perm Gen不一样,Metaspace不是堆的一部分。类元数据多数状况下都是从本地内存中分配的。默认状况下,元空间会自动增长其大小(直接又底层操做系统提供),而Perm Gen始终具备固定的上限。能够使用两个新标志来设置Metaspace的大小,它们是:“ - XX:MetaspaceSize ”和“ -XX:MaxMetaspaceSize ”。Metaspace背后的含义是类的生命周期及其元数据与类加载器的生命周期相匹配。也就是说,只要类加载器处于活动状态,元数据就会在元数据空间中保持活动状态,而且没法释放。

  • 代码缓存

运行Java程序时,它以分层方式执行代码。在第一层,它使用客户端编译器(C1编译器)来编译代码。分析数据用于服务器编译的第二层(C2编译器),以优化的方式编译该代码。默认状况下,Java 7中未启用分层编译,但在Java 8中启用了分层编译。实时(JIT)编译器将编译的代码存储在称为代码缓存的区域中。它是一个保存已编译代码的特殊堆。若是该区域的大小超过阈值,则该区域将被刷新,而且GC不会从新定位这些对象。Java 8中已经解决了一些性能问题和编译器未从新启用的问题,而且在Java 7中避免这些问题的解决方案之一是将代码缓存的大小增长到一个永远不会达到的程度。

  • 方法区

方法区域是Perm Gen中空间的一部分,用于存储类结构(运行时常量和静态变量)以及方法和构造函数的代码。

  • 内存池

内存池由JVM内存管理器建立,用于建立不可变对象池。内存池能够属于Heap或Perm Gen,具体取决于JVM内存管理器实现。

  • 运行时常量池

常量包含类运行时常量和静态方法,常量池是方法区域的一部分。

  • Java堆栈内存

Java堆栈内存用于执行线程。它们包含特定于方法的特定值,以及对从该方法引用的堆中其余对象的引用。

  • Java堆内存配置项

Java提供了许多内存配置项,咱们能够使用它们来设置内存大小及其比例,经常使用的以下:

VM Switch 描述
- Xms 用于在JVM启动时设置初始堆大小
-Xmx 用于设置最大堆大小
-Xmn 设置新生区的大小,剩下的空间用于老年区
-XX:PermGen 用于设置永久区存初始大小
-XX:MaxPermGen 用于设置Perm Gen的最大尺寸
-XX:SurvivorRatio 提供Eden区域的比例
-XX:NewRatio 用于提供老年代/新生代大小的比例,默认值为2

5.2垃圾回收

5.2.1垃圾回收策略

流程

垃圾收集是释放堆中的空间以分配新对象的过程。垃圾收集器是JVM管理的进程,它能够查看内存中的全部对象,并找出程序任何部分未引用的对象,删除并回收空间以分配给其余对象。一般会通过以下步骤:

  • 标记:标记哪些对象被使用,哪些已是没法触达的无用对象
  • 删除:删除无用对象并回收要分配给其余对象
  • 压缩:性能考虑,在删除无用的对象后,会将全部幸存对象集中移动到一块儿,腾出整段空间
策略

虚拟机栈、本地栈和程序计数器在编译完毕后已经能够肯定所需内存空间,程序执行完毕后也会自动释放全部内存空间,因此不须要进行动态回收优化。JVM内存调优主要针对堆和方法区两大区域的内存。一般对象分为Strong、sfot、weak和phantom四种类型,强引用不会被回收,软引用在内存达到溢出边界时回收,弱引用在每次回收周期时回收,虚引用专门被标记为回收对象,具体回收策略以下:

  • 对象优先在Eden区分配:
  • 新生对象回收策略Minor GC(频繁)
  • 老年代对象回收策略Full GC/Major GC(慢)
  • 大对象直接进入老年代:超过3m的对象直接进入老年区 -XX:PretenureSizeThreshold=3145728(3M)
  • 长期存货对象进入老年区: Survivor区中的对象经历一次Minor GC年龄增长一岁,超过15岁进入老年区 -XX:MaxTenuringThreshold=15
  • 动态对象年龄断定:设置Survivor区对象占用一半空间以上的对象进入老年区
算法

垃圾收集有以下经常使用的算法:

  • 标记-清除
  • 复制
  • 标记-整理
  • 分代收集(新生用复制,老年用标记-整理)

5.2.2 垃圾回收器

分类
  • serial收集器:单线程,主要用于client模式
  • ParNew收集器:多线程版的serial,主要用于server模式
  • Parallel Scavenge收集器:线程可控吞吐量(用户代码时间/用户代码时间+垃圾收集时间),自动调节吞吐量,用户新生代内存区
  • Serial Old收集器:老年版本serial
  • Parallel Old收集器:老年版本Parallel Scavenge
  • CMS(Concurrent Mark Sweep)收集器:停顿时间短,并发收集
  • G1收集器:分块标记整理,不产生碎片
配置
  • 串行GC(-XX:+ UseSerialGC):串行GC使用简单的标记-扫描-整理方法,用于新生代和老年代的垃圾收集,即Minor和Major GC
  • 并行GC(-XX:+ UseParallelGC):并行GC与串行GC相同,不一样之处在于它为新生代垃圾收集生成N个线程,其中N是系统中的CPU核心数。咱们能够使用-XX:ParallelGCThreads = n JVM选项来控制线程数
  • 并行旧GC(-XX:+ UseParallelOldGC):这与Parallel GC相同,只是它为新生代和老年代垃圾收集使用多个线程
  • 并发标记扫描(CMS)收集器(-XX:+ UseConcMarkSweepGC):CMS也称为并发低暂停收集器。它为老年代作垃圾收集。CMS收集器尝试经过在应用程序线程内同时执行大多数垃圾收集工做来最小化因为垃圾收集而致使的暂停。年轻一代的CMS收集器使用与并行收集器相同的算法。咱们能够使用-XX限制CMS收集器中的线程数 :ParallelCMSThreads = n
  • G1垃圾收集器(-XX:+ UseG1GC):G1从长远看要是替换CMS收集器。G1收集器是并行,并发和递增紧凑的低暂停垃圾收集器。G1收集器不像其余收集器那样工做,而且没有年轻和老一代空间的概念。它将堆空间划分为多个大小相等的堆区域。当调用垃圾收集器时,它首先收集具备较少实时数据的区域,所以称为“Garbage First”也便是G1

6.执行引擎

6.1执行流程

类加载器加载的类文件字节码数据流由基于JVM指令集架构的执行引擎来执行。执行引擎以指令为单位读取Java字节码。咱们知道汇编执行的流程是CPU执行每一行的汇编指令,一样JVM执行引擎就像CPU一个接一个地执行机器命令。字节码的每一个命令都包含一个1字节的OpCode和附加的操做数。执行引擎获取一个OpCode并使用操做数执行任务,而后执行下一个OpCode。但Java是用人们能够理解的语言编写的,而不是用机器直接执行的语言编写的。所以执行引擎必须将字节码更改成JVM中的机器能够执行的语言。字节码能够经过如下两种方式之一转化为合适的语言。

  • 解释器:逐个读取,解释和执行字节码指令。当它逐个解释和执行指令时,它能够快速解释一个字节码,可是同时也只能相对缓慢的地执行解释结果,这是解释语言的缺点。
  • JIT(实时)编译器:引入了JIT编译器来弥补解释器的缺点。执行引擎首先做为解释器运行,并在适当的时候,JIT编译器编译整个字节码以将其更改成本机代码。以后,执行引擎再也不解释该方法,而是直接使用本机代码执行。本地代码中的执行比逐个解释指令要快得多。因为本机代码存储在高速缓存中,所以能够快速执行编译的代码。

可是,JIT编译器编译代码须要花费更多的时间,而不是解释器逐个解释代码。所以,若是代码只执行一次,最好是选择解释而不是编译。所以,使用JIT编译器的JVM在内部检查方法执行的频率,并仅在频率高于某个级别时编译方法。

ŒJIT-compiler.png

JVM规范中未定义执行引擎的运行方式。所以,JVM厂商使用各类技术改进其执行引擎,并引入各类类型的JIT编译器。 大多数JIT编译器运行以下图所示:

JIT-compiler.png

JIT编译器将字节码转换为中间级表达式IR,以执行优化,而后将表达式转换为本机代码。Oracle Hotspot VM使用名为Hotspot Compiler的JIT编译器。它被称为Hotspot,由于Hotspot Compiler经过分析搜索须要以最高优先级进行编译的“Hotspot”,而后将热点编译为本机代码。若是再也不频繁调用编译了字节码的方法,换句话说,若是该方法再也不是热点,则Hotspot VM将从缓存中删除本机代码并以解释器模式运行。Hotspot VM分为服务器VM和客户端VM,两个VM使用不一样的JIT编译器。

大多数Java性能改进都是经过改进执行引擎来实现的。除了JIT编译器以外,还引入了各类优化技术,所以能够不断改进JVM性能。初始JVM和最新JVM之间的最大区别是执行引擎。

下面咱们经过下图能够看出JAVA执行的流程。

img

6.2栈帧结构

每一个方法调用开始到执行完成的过程,对应这一个栈帧在虚拟机栈里面从入栈到出栈的过程。

  • 栈帧包含:局部变量表,操做数栈,动态链接,方法返回
  • 方法调用:方法调用不等于方法执行,并且肯定调用方法的版本。
  • 方法调用字节码指令:invokestatic,invokespecial,invokevirtual,invokeinterface
  • 静态分派:静态类型,实际类型,编译器重载时经过参数的静态类型来肯定方法的版本。(选方法)
  • 动态分派:invokevirtual指令把类方法符号引用解析到不一样直接引用上,来肯定栈顶的实际对象(选对象)
  • 单分派:静态多分派,相同指令有多个方法版本。
  • 多分派:动态单分派,方法接受者只能肯定惟一一个。

下图是JVM实例执行方法是的内存布局。

configuration.png

6.3早期编译

  • javac编译器:解析与符号表填充,注解处理,生成字节码
  • java语法糖:语法糖有助于代码开发,可是编译后就会解开糖衣,还原到基础语法的class二进制文件 重载要求方法具有不一样的特征签名(不包括返回值),可是class文件中,只要描述不是彻底一致的方法就能够共存,如:
public String foo(List<String> arg){
    final int var = 0;
    return "";
}
public int foo(List<Integer> arg){
    int var = 0;
    return 0;
}

6.4晚期编译

HotSpot虚拟机内的即时编译
解析模式 -Xint
编译模式 -Xcomp
混合模式 Mixed mode
分层编译:解释执行 -> C1(Client Compiler)编译 -> C2编译(Server Compiler)
触发条件:基于采样的热点探测,基于计数器的热点探测

7.性能调优

7.1调优原则

咱们知道调优的前提是,程序没有达到咱们的预期要求,那么第一步要作的是衡量咱们的预期。程序不可能十全十美,咱们要作的是经过各类指标来衡量系统的性能,最终总体达到咱们的要求。

7.1.1 环境

首先咱们要了解系统的运行环境,包括操做系统层面的差别,JVM版本,位数,乃至于硬件的时钟周期,总线设计甚至机房温度,均可能是咱们须要考虑的前置条件。

7.1.2 度量

首先咱们要先给出系统的预期指标,在特定的硬件/软件的配置,而后给出目标指标,好比系统总体输出接口的QPS,RT,或者更进一层,IO读写,cpu的load指标,内存的使用率,GC状况都是咱们须要预先考察的对象。

7.1.3 监测

肯定了环境前置条件,分析了度量指标,第三步是经过工具来监测指标,下一节提供了经常使用JVM调优工具,能够经过不一样工具的组合来发现定位问题,结合JVM的工做机制已经操做系统层面的调度流程,按图索骥来发现问题,找出问题后才能进行优化。

7.1.4 原则

整体的调优原则以下图

process-tuning-performance-java-applications.png

图片来源《Java Performance》

7.2 调优参数

上节给出了JVM性能调优的原则,咱们理清思路后应用不一样的JVM工具来发现系统存在的问题,下面列举的是经常使用的JVM参数,经过这些参数指标能够更快的帮助咱们定位出问题所在。

7.2.1内存查询

最多见的与性能相关的作法之一是根据应用程序要求初始化堆内存。这就是咱们应该指定最小和最大堆大小的缘由。如下参数可用于实现它:

-Xms<heap size>[unit] -Xmx<heap size>[unit]

unit表示要初始化内存(由堆大小表示)的单元。单位能够标记为GB的“g”,MB的*“m”和KB的“k”*。例如JVM分配最小2 GB和最大5 GB:

-Xms2G -Xmx5G

从Java 8开始Metaspace的大小未被定义,一旦达到限制JVM会自动增长它,为了不没必要要的不稳定性,咱们能够设置*Metaspace*大小:

-XX:MaxMetaspaceSize=<metaspace size>[unit]

默认状况下YG的最小大小为1310 MB,最大大小不受限制,咱们能够明确地指定它们:

-XX:NewSize=<young size>[unit] 
-XX:MaxNewSize=<young size>[unit]

7.2.2垃圾回收

JVM有四种类型的*GC*实现:

  • 串行垃圾收集器
  • 并行垃圾收集器
  • CMS垃圾收集器
  • G1垃圾收集器

能够使用如下参数声明这些实现:

-XX:+UseSerialGC
-XX:+UseParallelGC
-XX:+USeParNewGC
-XX:+UseG1GC

7.2.3GC记录

要严格监视应用程序运行情况,咱们应始终检查JVM的垃圾收集性能,使用如下参数,咱们能够记录*GC*活动:

-XX:+UseGCLogFileRotation 
-XX:NumberOfGCLogFiles=< number of log files > 
-XX:GCLogFileSize=< file size >[ unit ]
-Xloggc:/path/to/gc.log

UseGCLogFileRotation指定日志文件滚动的政策,就像log4j的,s4lj等 NumberOfGCLogFiles表示单个应用程序记录生命周期日志文件的最大数量。GCLogFileSize指定文件的最大大小。 loggc表示其位置。这里要注意的是,还有两个可用的JVM参数(*-XX:+ PrintGCTimeStamps-XX:+ PrintGCDateStamps),可用于在*GC日志中打印日期时间戳。

7.2.4内存溢出

大型应用程序面临内存不足的错误是很常见的,这是一个很是关键的场景,很难复制以解决问题。

这就是JVM带有一些参数的缘由,这些参数将堆内存转储到一个物理文件中,之后能够用它来查找泄漏:

-XX:+HeapDumpOnOutOfMemoryError 
-XX:HeapDumpPath=./java_pid<pid>.hprof
-XX:OnOutOfMemoryError="< cmd args >;< cmd args >" 
-XX:+UseGCOverheadLimit

这里有几点须要注意:

  • 在OutOfMemoryError的状况下, HeapDumpOnOutOfMemoryError指示JVM将堆转储到物理文件中
  • HeapDumpPath表示要写入文件的路径; 任何文件名均可以给出; 可是若是JVM在名称中找到 标记,则致使内存不足错误的进程ID将以 .hprof格式附加到文件名
  • OnOutOfMemoryError用于发出紧急命令,以便在出现内存不足错误时执行; 应该在cmd args的空间中使用正确的命令。例如,若是咱们想在内存不足时从新启动服务器,咱们能够设置参数:
-XX:OnOutOfMemoryError="shutdown -r"
  • UseGCOverheadLimit是一种策略,用于限制在抛出 OutOfMemory错误以前在GC中花费的VM时间的比例

7.2.5其余配置

  • -server:启用“Server Hotspot VM”; 默认状况下,此参数在64位JVM中使用
  • -XX:+ UseStringDeduplication: Java 8引入了这个JVM参数,经过建立相同 String的太多实例来减小没必要要的内存使用 ; 这经过将重复的 String值减小到单个全局char []数组来优化堆内存
  • -XX:+ UseLWPSynchronization:设置基于 LWP(*轻量级进程*)的同步策略而不是基于线程的同步
  • -XX:LargePageSizeInBytes:设置用于Java堆的大页面大小; 它采用GB / MB / KB的参数; 经过更大的页面大小,咱们能够更好地利用虚拟内存硬件资源; 可是这可能会致使 PermGen的空间大小增长,从而能够强制减少Java堆空间的大小
  • -XX:MaxHeapFreeRatio:设置 GC后堆的最大自由百分比,以免收缩
  • -XX:MinHeapFreeRatio:设置 GC后堆的最小自由百分比以免扩展,监视堆使用状况
  • -XX:SurvivorRatio:Eden区 /幸存者空间大小的比例
  • -XX:+ UseLargePages:若是系统支持,则使用大页面内存; 若是使用此JVM参数,OpenJDK 7每每会崩溃
  • -XX:+ UseStringCache:启用*字符串*池中可用的经常使用分配字符串的缓存
  • -XX:+ UseCompressedStrings:对 String对象使用 byte []类型,能够用纯ASCII格式表示
  • -XX:+ OptimizeStringConcat:它尽量优化*字符串*链接操做

7.3 调优工具

7.3.1命令行工具

  • 虚拟机进程情况工具:jps -lvm

  • 诊断命令工具:jcmd

    用来发送诊断命令请求到JVM,这些请求是控制Java的运行记录,它必须在运行JVM的同一台机器上使用,而且具备用于启动JVM的相同有效用户和分组,能够使用如下命令建立堆转储(hprof转储):

    jcmd <java_pid>GC.heap_dump filename =

  • 虚拟机统计信息监视工具:jstat

    提供有关运行的应用程序的性能和资源消耗的信息。在诊断性能问题时,能够使用该工具,特别是与堆大小调整和垃圾回收相关的问题。jstat不须要虚拟机启动任何特殊配置。

    jstat -gc pid interval count

  • java配置信息工具:jinfo

    jinfo -flag pid

  • java内存映像工具:jmap

    用于生成堆转储文件

    jmap -dump:format=b,file=java.bin pid

  • 虚拟机堆转储快照分析工具:jhat

    jhat file 分析堆转储文件,经过浏览器访问分析文件

  • java堆栈跟踪工具:jstack

    用于生成虚拟机当前时刻的线程快照threaddump或者Javacore

    jstack [ option ] vmid

  • 堆和CPU分析工具:HPROF

    HPROF是每一个JDK版本附带的堆和CPU分析工具。它是一个动态连接库(DLL),它使用Java虚拟机工具接口(JVMTI)与JVM链接。该工具将分析信息以ASCII或二进制格式写入文件或套接字。HPROF工具可以显示CPU使用状况,堆分配统计信息和监视争用配置文件。此外,它还能够报告JVM中全部监视器和线程的完整堆转储和状态。在诊断问题方面,HPROF在分析性能,锁争用,内存泄漏和其余问题时很是有用。

    java -agentlib:hprof = heap = sites target.class

7.3.2可视化工具

  • jconsole
  • jvisualvm

8.字节加强

咱们从类加载的应用介绍了热部署和类隔离两大应用场景,可是基于类加载器的技术始终只是独立于JVM内核功能而存在的,也就是全部实现都只是基于最基础的类加载机制,并没有应用其余JVM 高级特性,本章节咱们开始从字节加强的层面介绍JVM的一些高级特性。

说到字节加强咱们最早想到的是字节码,也就是本文最开头所要研究的class文件,任何合法的源码编译成class后被类加载器加载进JVM的方法区,也就是以字节码的形态存活在JVM的内存空间。这也就是咱们为何现有讲明白类的结构和加载过程,而字节码加强技术不仅是在内存里面对class的字节码进行操纵,更为复杂的是class联动的上下游对象生命周期的管理。

首先咱们回忆一下咱们开发过程当中最为熟悉的一个场景就是本地debug调试代码。可能不少同窗都已经习惯在IDE上对某句代码打上断点,而后逐步往下追踪代码执行的步骤。咱们进一步想一想,这个是怎么实现的,是一股什么样的力量能把已经跑起来的线程踩下刹车,一步一步往前挪?咱们知道线程运行其实就是在JVM的栈空间上不断的把代码对应的JVM指令集不断的送到CPU执行。那能阻止这个流程的力量也确定是发生在JVM范围内,因此咱们能够很轻松的预测到这确定是JVM提供的机制,而不是IDE真的有这样的能力,只不过是JVM把这种能力封装成接口暴露出去,而后提供给IDE调用,而IDE只不过是经过界面交互来调用这些接口而已。那么下面咱们就来介绍JVM这种重要的能力。

8.1JPDA

上面所讲的JVM提供的程序运行断点能力,其实JVM提供的一个工具箱JVMTI(JVM TOOL Interface)提供的接口,而这个工具箱是一套叫作JPDA的架构定义的,本节咱们就来聊聊JPDA。

JPDA(Java Platform Debugger Architecture)Java平台调试架构,既不是一个应用程序,也不是调试工具,而是定义了一系列设计良好的接口和协议用于调试java代码,咱们将会从三个层面来说解JPDA。

8.1.1概念

  • JVMTI

    JVMTI(Java Virtual Machine Tool Interface)Java 虚拟机调试接口,处于最底层,是咱们上文所提到的JVM开放的能力,JPDA规定了JDK必须提供一个叫作JVMTI(Java6以前是由JVMPI和JVMDI组成,Java6开始废弃掉统一为JVMTI)的工具箱,也就是定义了一系列接口能力,好比获取栈帧、设置断点、断点响应等接口,具体开放的能力参考JVMDI官方API文档

  • JDWP

    JDWP(Java Debug Wire Protocol)Java 调试连线协议,存在在中间层,定义信息格式,定义调试者和被调试程序之间请求的协议转换,位于JDI下一层,JDI更为抽象,JDWP则关注实现。也就是说JVM定义好提供的能力,可是如何调用JVM提供的接口也是须要规范的,就好比咱们Servlet容器也接收正确合法的HTTP请求就能够成功调用接口。JPDA一样也规范了调用JVMTI接口须要传入数据的规范,也就是请求包的格式,类别HTTP的数据包格式。可是JPDA并不关心请求来源,也就是说只要调用JVMTI的请求方式和数据格式对了就能够,不管是来作远程调用仍是本地调用。JDWP制定了调试者和被调试应用的字节流动机制,但没有限定具体实现,能够是远程的socket链接,或者本机的共享内存,固然还有自定义实现的通讯协议。既然只是规范了调用协议,并不局限请求来源,并且也没限制语言限制,因此非java语言只要发起调用符合规范就能够,这个大大丰富了异构应用场景,具体的协议细节能够参考JDWP官方规范文档

  • JDI

    JDI(Java Debug Interface)Java调试接口处在最上层,基于Java开发的调试接口,也就是咱们调试客户端,客户端代码封装在jdk下面tools.jar的com.sun.jdi包里面,java程序能够直接调用的接口集合,具体提供的功能能够参考JDI官方API文档

JPDA 架构

8.1.2原理

介绍完JPDA的架构体系后,咱们了解到JAVA调试平台各个层级的做用,这一节咱们更近一步讲解JPDA各个层面的工做原理,以及三个层级结合起来时如何交互的。

JVMTI

咱们JVMTI是JVM提供的一套本地接口,包含了很是丰富的功能,咱们调试和优化代码须要操做JVM,多数状况下就是调用到JVMTI,从官网咱们能够看到,JVMTI包含了对JVM线程/内存/堆/栈/类/方法/变量/事件/定时器处理等的20多项功能。但其实咱们一般不是直接调用JVMTI,而是建立一个代理客户端,咱们能够自由的定义对JVMTI的操做而后打包到代理客户端里面如libagent.so。当目标程序执行时会启动JVM,这个时候在目标程序运行前会加载代理客户端,因此代理客户端是跟目标程序运行在同一个进程上。这样一来外部请求就经过代理客户端间接调用到JVMTI,这样的好处是咱们能够在客户端Agent里面定制高级功能,并且代理客户端编译打包成一个动态连接库以后能够复用,提升效率。咱们简单描述一下代理客户端Agent的工做流程。

创建代理客户端首先须要定义Agent的入口函数,犹如Java类的main方法同样:

JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved); 

而后JVM在启动的时候就会把JVMTI的指针JavaVM传给代理的入口函数,options则是传参,有了这个指针后代理就能够充分调用JVMTI的函数了。

//设置断点,参数是调试目标方法和行数位置 jvmtiError SetBreakpoint(jvmtiEnv* env,jmethodID method,jlocation location); //当目标程序执行到指定断点,目标线程则被挂起 jvmtiError SuspendThread(jvmtiEnv* env,jthread thread); 

固然除了JVM启动时能够加载代理,运行过程当中也是能够的,这个下文咱们讲字节码加强还会再说到。

JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm, char *options, void *reserved); 

有兴趣的同窗能够本身动手写一个Agent试试,经过调用JVMTI接口能够实现本身定制化的调试工具。

JDWP

上文咱们知道调用JVMTI须要创建一个代理客户端,可是假如我创建了包含通用功能的Agent想开发出去给全部调试器使用,有一种方式是资深开发者经过阅读个人文档后进行开发调用,还有另一种方式就是我在个人Agent里面加入了JDWP协议模块,这样调试器就能够不用关心个人接口细节,只需按照阅读的协议发起请求便可。JDWP是调试器和JVM中间的协议规范,相似HTTP协议同样,JDWP也定义规范了握手协议和报文格式。

调试器发起请求的握手流程:

1)调试器发送一段包含“JDWP-Handshake”的14个bytes的字符串

2)JVM回复一样的内容“JDWP-Handshake”

完成握手流程后就能够像HTTP同样向JVM的代理客户端发送请求数据,同时回复所需参数。请求和回复的数据帧也有严格的结构,请求的数据格式为Command Packet,回复的格式为Reply Packet,包含包头和数据两部分,具体格式参考官网。实际上JDWP倒是也是经过创建代理客户端来实现报文格式的规范,也就是JDWP Agent 里面的JDWPTI实现了JDWP对协议的定义。JDWP的功能是由JDWP传输接口(Java Debug Wire Protocol Transport Interface)实现的,具体流程其实跟JVMTI差很少,也是讲JDWPTI编译打包成代理库后,在JVM启动的时候加载到目标进程。那么调试器调用的过程就是JDWP Agent接收到请求后,调用JVMTI Agent,JDWP负责定义好报文数据,而JDWPTI则是具体的执行命令和响应事件。

JDI

前面已经解释了JVMTI和JDWP的工做原理和交互机制,剩下的就是搞清楚面向用户的JDI是如何运行的。首先JDI位于JPDA的最顶层入口,它的实现是经过JAVA语言编写的,因此能够理解为Java调试客户端对JDI接口的封装调用,好比咱们熟悉的IDE界面启动调试,或者JAVA的命令行调试客户端JDB。

一般咱们设置好目标程序的断点以后启动程序,而后经过调试器启动程序以前,调试器会先获取JVM管理器,而后经过JVM管理器对象virtualMachineManager获取链接器Connector,调试器与虚拟机得到连接后就能够启动目标程序了。以下代码:

VirtualMachineManager virtualMachineManager = Bootstrap.virtualMachineManager(); 

JDI完成调试须要实现的功能有三个模块:数据、连接、事件

  • 数据

    调试器要调试的程序在目标JVM上,那么调试以前确定须要将目标程序的执行环境同步过来,否则咱们压根就不知道要调试什么,因此须要一种镜像机制,把目标程序的堆栈方法区包含的数据以及接收到的事件请求都映射到调试器上面。那么JDI的底层接口Mirror就是干这样的事,具体数据结构能够查询文档。

  • 连接

    咱们知道调试器跟目标JVM直接的通信是双向的,因此连接双方均可以发起。一个调试器能够连接多个目标JVM,可是一个目标虚拟机只能提供给一个调试器,否则就乱套了不知道听谁指令了。JDI定义三种连接器:启动连接器(LaunchingConnector)、依附连接器(AttachingConnector)、监听连接器(ListeningConnector)和。分别对应的场景是目标程序JVM启动时发起连接、调试器中途请求接入目标程序JVM和调试器监听到被调试程序返回请求时发起的连接。

  • 事件

    也就是调试过程当中对目标JVM返回请求的响应。

讲解完JPDA体系的实现原理,咱们再次梳理一下调试的整个流程:

调试器 —> JDI客户端 —> JDWP Agent—> JVMTI Agent —>> JVMTI —> Application

8.1.3 实现

如今咱们已经对整个JPDA结构有了深刻理解,接下来咱们就经过对这些朴素的原理来实现程序的断点调试。固然咱们不会在这里介绍从IDE的UI断点调试的过程,由于对这套是使用已经很是熟悉了,咱们知道IDE的UI断点调试本质上是调试器客户端对JDI的调用,那咱们就经过一个调试的案例来解释一下这背后的原理。

搭建服务

首先咱们须要先搭建一个可供调试的web服务,这里我首选springboot+来搭建,经过官网生成样例project或者maven插件均可以,具体的太基础的就不在这里演示,该服务只提供一个Controller包含的一个简单方法。若是使用Tomcat部署,则能够经过自有的开关catalina jpda start来启动debug模式。

package com.zooncool.debug.rest; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * @author linzhenhua */ @RestController("/debug") public class DebugController { @GetMapping public String ask(@RequestParam("name") String name) { String message = "are you ok?" + name; return message; } } 
启动服务

搭建好服务以后咱们先启动服务,咱们经过maven来启动服务,其中涉及到的一些参数下面解释。

mvn spring-boot:run -Drun.jvmArguments="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8001"
或者
mvn spring-boot:run -Drun.jvmArguments="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8001"
  • mvn:maven的脚本命令这个不用解释

  • Spring-boot:run:启动springboot工程

  • -Drun.jvmArguments:执行jvm环境的参数,里面的参数值才是关键

  • -Xdebug

    Xdebug开启调试模式,为非标准参数,也就是可能在其余JVM上面是不可用的,Java5以后提供了标准的执行参数agentlib,下面两种参数一样能够开启debug模式,可是在JIT方面有所差别,这里先不展开。

    java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8001

    java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8001

  • Xrunjdwp/jdwp=transport:表示链接模式是本地内存共享仍是远程socket链接

  • server:y表示打开socket监听调试器的请求;n表示被调试程序像客户端同样主动链接调试器

  • suspend:y表示被调试程序须要等到调试器的链接请求以后才能启动运行,在此以前都是挂起的,n表示被调试程序无需等待直接运行。

  • address:被调试程序启动debug模式后监听请求的地址和端口,地址缺省为本地。

执行完上述命令后,就等着咱们调试器的请求接入到目标程序了。

调试接入

咱们知道java的调试器客户端为jdb,下面咱们就使用jdb来接入咱们的目标程序。

#jdb 经过attach参数选择本地目标程序,同时附上目标程序的源码,回想以前咱们讲到的JDI的镜像接口,就是把目标程序的堆栈结构同步过来,若是能咱们提供的源码对应上,那就能够在源码上面显示断点标志
$ jdb -attach localhost:8001 -sourcepath /Users/linzhenhua/Documents/repositories/practice/stackify-master/remote-debugging/src/main/java/
设置未捕获的java.lang.Throwable
设置延迟的未捕获的java.lang.Throwable
正在初始化jdb...

#stop,选择对应方法设置断点
> stop in com.zooncool.debug.rest.DebugController.ask(java.lang.String)
设置断点com.zooncool.debug.rest.DebugController.ask(java.lang.String)

#若是咱们设置不存在的方法为断点,则会有错误提示
> stop in com.zooncool.debug.rest.DebugController.ask2(java.lang.String)
没法设置断点com.zooncool.debug.rest.DebugController.ask2(java.lang.String): com.zooncool.debug.rest.DebugController中没有方法ask2

#这时候咱们已经设置完断点,就能够发起个HTTP请求
#http://localhost:7001/remote-debugging/debug/ask?name=Jack
#发起请求后咱们回到jdb控制台,观察是否命中断点
> 断点命中: "线程=http-nio-7001-exec-5", com.zooncool.debug.rest.DebugController.ask(), 行=14 bci=0
14            String message = "are you ok?" + name;

#list,对照源码,确实是进入ask方法第一行命中断点,也就是14行,这时候咱们能够查看源码
http-nio-7001-exec-5[1] list
10    @RestController("/debug")
11    public class DebugController {
12        @GetMapping
13        public String ask(@RequestParam("name") String name) {
14 =>         String message = "are you ok?" + name;
15            return message;
16        }
17    }

#locals,观察完源码,咱们想获取name的传参,跟URL传入的一致
http-nio-7001-exec-5[1] locals
方法参数:
name = "Jack"
本地变量:

#print name,打印入参
http-nio-7001-exec-5[1] print name
 name = "Jack"

#where,查询方法调用的栈帧,从web容器入口调用方法到目标方法的调用链路
http-nio-7001-exec-5[1] where
  [1] com.zooncool.debug.rest.DebugController.ask (DebugController.java:14)
  [2] sun.reflect.NativeMethodAccessorImpl.invoke0 (本机方法)
  [3] sun.reflect.NativeMethodAccessorImpl.invoke (NativeMethodAccessorImpl.java:62)
  [4] sun.reflect.DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorImpl.java:43)
  [5] java.lang.reflect.Method.invoke (Method.java:498)
  ...
  [50] org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun (NioEndpoint.java:1,459)
  [51] org.apache.tomcat.util.net.SocketProcessorBase.run (SocketProcessorBase.java:49)
  [52] java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1,149)
  [53] java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:624)
  [54] org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run (TaskThread.java:61)
  [55] java.lang.Thread.run (Thread.java:748)

#step,下一步到下一行代码
http-nio-7001-exec-5[1] step
> 已完成的步骤: "线程=http-nio-7001-exec-5", com.zooncool.debug.rest.DebugController.ask(), 行=15 bci=20
15            return message;

#step up,完成当前方法的调用
http-nio-7001-exec-5[1] step up
> 已完成的步骤: "线程=http-nio-7001-exec-5", sun.reflect.NativeMethodAccessorImpl.invoke(), 行=62 bci=103

#cont,结束调试,执行完毕
http-nio-7001-exec-5[1] cont
> 

#clear,完成调试任务,清除断点
> clear
断点集:
        断点com.zooncool.debug.rest.DebugController.ask(java.lang.String)
        断点com.zooncool.debug.rest.DebugController.ask2(java.lang.String)
#选择一个断点删除
> clear com.zooncool.debug.rest.DebugController.ask(java.lang.String)
已删除: 断点com.zooncool.debug.rest.DebugController.ask(java.lang.String)

咱们已经完成了命令行调试的所有流程,stop/list/locals/print name/where/step/step up/cont/clear这些命令其实就是IDE的UI后台调用的脚本。而这些脚本就是基于JDI层面的接口所提供的能力,下面咱们还有重点观察一个核心功能,先从头再设置一下断点。

#stop,选择对应方法设置断点
> stop in com.zooncool.debug.rest.DebugController.ask(java.lang.String)
设置断点com.zooncool.debug.rest.DebugController.ask(java.lang.String)

#这时候咱们已经设置完断点,就能够发起个HTTP请求
#http://localhost:7001/remote-debugging/debug/ask?name=Jack
#发起请求后咱们回到jdb控制台,观察是否命中断点
> 断点命中: "线程=http-nio-7001-exec-5", com.zooncool.debug.rest.DebugController.ask(), 行=14 bci=0
14            String message = "are you ok?" + name;

#print name,打印入参
http-nio-7001-exec-5[1] print name
 name = "Jack"

#若是这个时候咱们想替换掉Jack,换成Lucy
http-nio-7001-exec-6[1] set name = "Lucy"   
 name = "Lucy" = "Lucy"

#进入下一步
http-nio-7001-exec-6[1] step
> 已完成的步骤: "线程=http-nio-7001-exec-6", com.zooncool.debug.rest.DebugController.ask(), 行=15 bci=20
15            return message;

#查看变量,咱们发现name的值已经被修改了
http-nio-7001-exec-6[1] locals
方法参数:
name = "Lucy"
本地变量:
message = "are you ok?Lucy"

至此咱们已经完成了JPDA的原理解析到调试实践,也理解了JAVA调试的工做机制,其中留下一个重要的彩蛋就是经过JPDA进入调试模式,咱们能够动态的修改JVM内存对象和类的内容,这也讲引出下文咱们要介绍的字节码加强技术。

8.2 热替换

8.2.1概念

终于来到热替换这节了,前文咱们作了好多铺垫,介绍热替换以前咱们稍稍回顾一下热部署。咱们知道热部署是“独立”于JVM以外的一门对类加载器应用的技术,一般是应用容器借助自定义类加载器的迭代,无需重启JVM缺能更新代码从而达到热部署,也就是说热部署是JVM以外容器提供的一种能力。而本节咱们介绍的热替换技术是实打实JVM提供的能力,是JVM提供的一种可以实时更新内存类结构的一种能力,这种实时更新JVM方法区类结构的能力固然也是无需重启JVM实例。

热替换HotSwap是Sun公司在Java 1.4版本引入的一种新实验性技术,也就是上一节咱们介绍JPDA提到的调试模式下能够动态替换类结构的彩蛋,这个功能被集成到JPDA框架的接口集合里面,首先咱们定义好热替换的概念。

热替换(HotSwap):使用字节码加强技术替换JVM内存里面类的结构,包括对应类的对象,而不须要重启虚拟机。

8.2.2原理

前文从宏观上介绍了JVM实例的内存布局和垃圾回收机制,微观上也解释了类的结构和类加载机制,上一节又学习了JAVA的调试框架,基本上咱们对JVM的核心模块都已经摸透了,剩下的就是攻克字节码加强的技术了。而以前讲的字节码加强技术也仅仅是放在JPDA里面做为实验性技术,并且仅仅局限在方法体和变量的修改,没法动态修改方法签名或者增删方法,由于字节码加强涉及到垃圾回收机制,类结构变动,对象引用,即时编译等复杂问题。在HotSwap被引进后至今,JCP也未能经过正式的字节码加强实现。

JAVA是一门静态语言,而字节码加强所要达的效果就是让Java像动态语言同样跑起来,无需重启服务器。下面咱们介绍字节码加强的基本原理。

  • 反射代理

    反射代理不能直接修改内存方法区的字节码,可是能够抽象出一层代理,经过内存新增实例来实现类的更新

  • 原生接口

    jdk上层提供面向java语言的字节码加强接口java.lang.instrument,经过实现ClassFileTransformer接口来操做JVM方法区的类文件字节码。

  • JVMTI代理

    JVM的JVMTI接口包含了操做方法区类文件字节码的函数,经过建立代理,将JVMTI的指针JavaVM传给代理,从而拥有JVM 本地操做字节码的方法引用。

  • 类加载器织入

    字节码加强接口加上类加载器的织入,结合起来也是一种热替换技术。

  • JVM加强

    直接新增JVM分支,增长字节码加强功能。

8.2.3实现

可是尽管字节码加强是一门复杂的技术,这并不妨碍咱们进一步的探索,下面咱们介绍几种常见的实现方案。

  • Instrumentation

  • AspectJ

  • ASM

  • DCEVM

  • JREBEL

  • CGLIB

  • javassist

  • BCEL

具体的我会挑两个具备表明性的工具深刻讲解,篇幅所限,这里就补展开了。

9.总结

JVM是程序发展至今的一颗隗宝,是程序设计和工程实现的完美结合。JVM做为做为三大工业级程序语言为首JAVA的根基,本文试图在瀚如烟海的JVM海洋中找出其中最耀眼的冰山,并力求用简洁的逻辑线索把各个冰山串起来,在脑海中对JVM的观感有更加立体的认识。更近一步的认识JVM对程序设计的功力提示大有裨益,而本文也只是将海平面上的冰山连接起来,但这只是冰山一角,JVM更多的底层设计和实现细节还远远没有涉及到,并且也不乏知识盲区而没有说起到的,路漫漫其修远兮,JVM自己也在不断的推陈出新,借此机会总结出JVM的核心体系,以此回顾对JVM知识的查漏补缺,也是一次JVM的认知升级。最后仍是例牌来两张图结束JVM的介绍,但愿对更的同窗有帮助。

image-20190310152910056

JVM体系

相关文章
相关标签/搜索