共计 16556 个字符,预计需要花费 42 分钟才能阅读完成。
一 class 的热替换
ClassLoader 中重要的办法
loadClassClassLoader.loadClass(...)
是 ClassLoader 的入口点。当一个类没有指明用什么加载器加载的时候,JVM 默认采纳 AppClassLoader 加载器加载没有加载过的 class,调用的办法的入口就是 loadClass(…)。如果一个 class 被自定义的 ClassLoader 加载,那么 JVM 也会调用这个自定义的 ClassLoader.loadClass(…)办法来加载 class 外部援用的一些别的 class 文件。重载这个办法,能实现自定义加载 class 的形式,摈弃双亲委托机制,然而即便不采纳双亲委托机制,比方 java.lang 包中的相干类还是不能自定义一个同名的类来代替,次要因为 JVM 解析、验证 class 的时候,会进行相干判断。
defineClass
零碎自带的 ClassLoader,默认加载程序的是 AppClassLoader,ClassLoader 加载一个 class,最终调用的是 defineClass(…)办法,这时候就在想是否能够反复调用 defineClass(…)办法加载同一个类(或者批改过),最初发现调用屡次的话会有相干谬误:
java.lang.LinkageError
attempted duplicate class definition
所以 一个 class 被一个 ClassLoader 实例加载过的话,就不能再被这个 ClassLoader 实例再次加载 (这里的加载指的是,调用了 defileClass(…) 放办法,从新加载字节码、解析、验证。)。而零碎默认的 AppClassLoader 加载器,他们外部会缓存加载过的 class,从新加载的话,就间接取缓存。所与对于热加载的话,只能从新创立一个 ClassLoader,而后再去加载曾经被加载过的 class 文件。
二 class 卸载
在 Java 中 class 也是能够 unload。JVM 中 class 和 Meta 信息寄存在 PermGen space 区域。如果加载的 class 文件很多,那么可能导致 PermGen space 区域空间溢出。引起:java.lang.OutOfMemoryErrorPermGen space. 对于有些 Class 咱们可能只须要应用一次,就不再须要了,也可能咱们批改了 class 文件,咱们须要从新加载 newclass,那么 oldclass 就不再须要了。那么 JVM 怎么样能力卸载 Class 呢。
JVM 中的 Class 只有满足以下三个条件,能力被 GC 回收,也就是该 Class 被卸载(unload):
- 该类所有的实例都曾经被 GC。
- 加载该类的 ClassLoader 实例曾经被 GC。
- 该类的 java.lang.Class 对象没有在任何中央被援用。
GC 的机会咱们是不可控的,那么同样的咱们对于 Class 的卸载也是不可控的。
1、有启动类加载器加载的类型在整个运行期间是不可能被卸载的(jvm 和 jls 标准).
2、被零碎类加载器和规范扩大类加载器加载的类型在运行期间不太可能被卸载,因为零碎类加载器实例或者规范扩大类的实例基本上在整个运行期间总能间接或者间接的拜访的到,其达到 unreachable 的可能性极小.(当然,在虚拟机快退出的时候能够,因为不论 ClassLoader 实例或者 Class(java.lang.Class)实例也都是在堆中存在,同样遵循垃圾收集的规定).
3、被开发者自定义的类加载器实例加载的类型只有在很简略的上下文环境中能力被卸载,而且个别还要借助于强制调用虚拟机的垃圾收集性能才能够做到. 能够料想,略微简单点的利用场景中(尤其很多时候,用户在开发自定义类加载器实例的时候采纳缓存的策略以进步零碎性能),被加载的类型在运行期间也是简直不太可能被卸载的(至多卸载的工夫是不确定的).
综合以上三点,一个曾经加载的类型被卸载的几率很小至多被卸载的工夫是不确定的. 同时,咱们能够看的进去,开发者在开发代码时候,不应该对虚拟机的类型卸载做任何假如的前提下来实现零碎中的特定性能.
三 Tomcat 中对于类的加载与卸载
Tomcat 中与其说有热加载,还不如说是热部署来的精确些。因为对于一个利用,其中 class 文件被批改过,那么 Tomcat 会先卸载这个利用(Context),而后从新加载这个利用,其中要害就在于自定义 ClassLoader 的利用。这里有篇文章很好的介绍了 tomcat 中对于 ClassLoader 的利用。
Tomcat 启动的时候,ClassLoader 加载的流程:
1 Tomcat 启动的时候,用 system classloader 即 AppClassLoader 加载
{catalina.home}/bin
外面的 jar 包,也就是 tomcat 启动相干的 jar 包。2 Tomcat 启动类 Bootstrap 中有 3 个 classloader 属性,catalinaLoader、commonLoader、sharedLoader 在 Tomcat7 中默认他们初始化都为同一个 StandardClassLoader 实例。具体的也能够在{catalina.home}/bin/bootstrap.jar
包中的 catalina.properites 中进行配置。3 StandardClassLoader 加载{catalina.home}/lib
上面的所有 Tomcat 用到的 jar 包。4 一个 Context 容器,代表了一个 app 利用。Context–>WebappLoader–>WebClassLoader。并且 Thread.contextClassLoader=WebClassLoader。应用程序中的 jsp 文件、class 类、lib/*.jar 包,都是 WebClassLoader 加载的。
当 Jsp 文件批改的时候,Tomcat 更新步骤:
1 但拜访 1.jsp 的时候,1.jsp 的包装类 JspServletWrapper 会去比拟 1.jsp 文件最新批改工夫和上次的批改工夫,以此判断 1.jsp 是否批改过。2 1.jsp 批改过的话,那么 jspservletWrapper 会革除相干援用,包含 1.jsp 编译后的 servlet 实例和加载这个 servlet 的 JasperLoader 实例。3 从新创立一个 JasperLoader 实例,从新加载批改过后的 1.jsp,从新生成一个 Servlet 实例。4 返回批改后的 1.jsp 内容给用户。
当 app 上面的 class 文件批改的时候,Tomcat 更新步骤:
1 Context 容器会有专门线程监控 app 上面的类的批改状况。2 如果发现有类被批改了。那么调用 Context.reload()。分明一系列相干的援用和资源。3 而后翻新创立一个 WebClassLoader 实例,从新加载 app 上面须要的 class。
在一个有肯定规模的利用中,如果文件批改屡次,重启屡次的话,java.lang.OutOfMemoryErrorPermGen space
这个谬误的的呈现十分频繁。次要就是因为每次重启从新加载大量的 class,超过了 PermGen space 设置的大小。两种状况可能导致 PermGen space 溢出。一、GC(Garbage Collection)在主程序运行期对 PermGen space 没有进行清理(GC 的不可控行),二、重启之前 WebClassLoader 加载的 class 在别的中央还存在着援用。
原文地址:http://www.blogjava.net/heave…
在 Java 开发畛域,热部署始终是一个难以解决的问题,目前的 Java 虚拟机只能实现办法体的批改热部署,对于整个类的构造批改,依然须要重启虚拟机,对类从新加载能力实现更新操作。对于某些大型的利用来说,每次的重启都须要破费大量的工夫老本。尽管 osgi 架构的呈现,让模块重启成为可能,然而如果模块之间有调用关系的话,这样的操作仍然会让利用呈现短暂的功能性休克。本文将摸索如何在不毁坏 Java 虚拟机现有行为的前提下,实现某个单一类的热部署,让零碎无需重启就实现某个类的更新。
类加载的摸索
首先谈一下何为热部署(hotswap),热部署是在不重启 Java 虚拟机的前提下,能主动侦测到 class 文件的变动,更新运行时 class 的行为 。Java 类是通过 Java 虚拟机加载的,某个类的 class 文件在被 classloader 加载后,会生成对应的 Class 对象,之后就能够创立该类的实例。 默认的虚拟机行为只会在启动时加载类,如果前期有一个类须要更新的话,单纯替换编译的 class 文件,Java 虚拟机是不会更新正在运行的 class。如果要实现热部署,最基本的形式是批改虚拟机的源代码,扭转 classloader 的加载行为,使虚拟机能监听 class 文件的更新,从新加载 class 文件,这样的行为破坏性很大,为后续的 JVM 降级埋下了一个大坑。
另一种敌对的办法是创立本人的 classloader
来加载须要监听的 class,这样就能管制类加载的机会,从而实现热部署。本文将具体摸索如何实现这个计划。首先须要理解一下 Java 虚拟机现有的加载机制。目前的加载机制,称为 双亲委派 ,零碎在应用一个 classloader
来加载类时,会先询问以后 classloader
的父类是否有能力加载,如果父类无奈实现加载操作,才会将工作下放到该 classloader
来加载。这种自上而下的加载形式的益处是,让每个 classloader
执行本人的加载工作,不会反复加载类。 然而这种形式却使加载程序十分难扭转,让自定义 classloader
领先加载须要监听扭转的类成为了一个难题。
不过咱们能够换一个思路,尽管无奈领先加载该类,然而依然能够用自定义 classloader 创立一个性能雷同的类,让每次实例化的对象都指向这个新的类。当这个类的 class 文件产生扭转的时候,再次创立一个更新的类,之后如果零碎再次收回实例化申请,创立的对象讲指向这个全新的类。
上面来简略列举一下须要做的工作。
创立自定义的 classloader,加载须要监听扭转的类,在 class 文件产生扭转的时候,从新加载该类。扭转创建对象的行为,使他们在创立时应用自定义 classloader 加载的 class。
自定义加载器的实现
自定义加载器依然须要执行类加载的性能。这里却存在一个问题,同一个类加载器无奈同时加载两个雷同名称的类,因为不管类的构造如何发生变化,生成的类名不会变,而 classloader 只能在虚拟机进行前销毁曾经加载的类,这样 classloader 就无奈加载更新后的类了。这里有一个小技巧,让每次加载的类都保留成一个带有版本信息的 class,比方加载 Test.class 时,保留在内存中的类是 Test_v1.class,当类产生扭转时,从新加载的类名是 Test_v2.class。然而真正执行加载 class 文件创建 class 的 defineClass 办法是一个 native 的办法,批改起来又变得很艰难。所以背后还剩一条路,那就是间接批改编译生成的 class 文件。
利用 ASM 批改 class 文件
能够批改字节码的框架有很多,比方 ASM,CGLIB。本文应用的是 ASM。先来介绍一下 class 文件的构造,class 文件蕴含了以下几类信息:
第一个是类的根本信息,蕴含了拜访权限信息,类名信息,父类信息,接口信息。
第二个是类的变量信息。
第三个是办法的信息。
ASM 会先加载一个 class 文件,而后严格程序读取类的各项信息,用户能够依照本人的志愿定义加强组件批改这些信息,最初输入成一个新的 class。
首先看一下如何利用 ASM 批改类信息。
清单 1. 利用 ASM 批改字节码
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassReader cr = null;
String enhancedClassName = classSource.getEnhancedName();
try {
cr = new ClassReader(new FileInputStream(classSource.getFile()));
} catch (IOException e) {e.printStackTrace();
return null;
}
ClassVisitor cv = new EnhancedModifier(cw,
className.replace(".", "/"),
enhancedClassName.replace(".", "/"));
cr.accept(cv, 0);
ASM 批改字节码文件的流程是一个责任链模式,首先应用一个 ClassReader 读入字节码,而后利用 ClassVisitor 做个性化的批改,最初利用 ClassWriter 输入批改后的字节码。
之前提过,须要将读取的 class 文件的类名做一些批改,加载成一个全新名字的派生类。这里将之分为了 2 个步骤。
第一步,先将原来的类变成接口。
清单 2. 重定义的原始类
public Class<?> redefineClass(String className){ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassReader cr = null;
ClassSource cs = classFiles.get(className);
if(cs==null){return null;}
try {cr = new ClassReader(new FileInputStream(cs.getFile()));
} catch (IOException e) {e.printStackTrace();
return null;
}
ClassModifier cm = new ClassModifier(cw);
cr.accept(cm, 0);
byte[] code = cw.toByteArray();
return defineClass(className, code, 0, code.length);
}
首先 load 原始类的 class 文件,此处定义了一个加强组件 ClassModifier,作用是批改原始类的类型,将它转换成接口。原始类的所有办法逻辑都会被去掉。
第二步,生成的派生类都实现这个接口,即原始类,并且复制原始类中的所有办法逻辑。之后如果该类须要更新,会生成一个新的派生类,也会实现这个接口。这样做的目标是不论如何批改,同一个 class 的派生类都有一个独特的接口,他们之间的转换变得对外不通明。
清单 3. 定义一个派生类
// 在 class 文件产生扭转时从新定义这个类
private Class<?> redefineClass(String className, ClassSource classSource){ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassReader cr = null;
classSource.update();
String enhancedClassName = classSource.getEnhancedName();
try {
cr = new ClassReader(new FileInputStream(classSource.getFile()));
} catch (IOException e) {e.printStackTrace();
return null;
}
EnhancedModifier em = new EnhancedModifier(cw, className.replace(".", "/"),
enhancedClassName.replace(".", "/"));
ExtendModifier exm = new ExtendModifier(em, className.replace(".", "/"),
enhancedClassName.replace(".", "/"));
cr.accept(exm, 0);
byte[] code = cw.toByteArray();
classSource.setByteCopy(code);
Class<?> clazz = defineClass(enhancedClassName, code, 0, code.length);
classSource.setClassCopy(clazz);
return clazz;
}
再次 load 原始类的 class 文件,此处定义了两个加强组件,一个是 EnhancedModifier,这个加强组件的作用是扭转原有的类名。第二个加强组件是 ExtendModifier,这个加强组件的作用是扭转原有类的父类,让这个批改后的派生类可能实现同一个原始类(此时原始类曾经转成接口了)。
自定义 classloader 还有一个作用是监听会产生扭转的 class 文件,classloader 会治理一个定时器,定时顺次扫描这些 class 文件是否扭转。
扭转创建对象的行为
Java 虚拟机常见的创建对象的办法有两种,一种是动态创立,间接 new 一个对象,一种是动态创建,通过反射的办法,创建对象。
因为曾经在自定义加载器中更改了原有类的类型,把它从类改成了接口,所以这两种创立办法都无奈成立。咱们要做的是将实例化原始类的行为变成实例化派生类。
对于第一种办法,须要做的是将动态创立,变为通过 classloader 获取 class,而后动态创建该对象。
清单 4. 替换后的指令集所对应的逻辑
// 原始逻辑
Greeter p = new Greeter();
// 扭转后的逻辑
IGreeter p = (IGreeter)MyClassLoader.getInstance().
findClass(“com.example.Greeter”).newInstance();
这里又须要用到 ASM 来批改 class 文件了。查找到所有 new 对象的语句,替换成通过 classloader 的模式来获取对象的模式。
清单 5. 利用 ASM 批改办法体
@Override
public void visitTypeInsn(int opcode, String type) {if(opcode==Opcodes.NEW && type.equals(className)){
List<LocalVariableNode> variables = node.localVariables;
String compileType = null;
for(int i=0;i<variables.size();i++){LocalVariableNode localVariable = variables.get(i);
compileType = formType(localVariable.desc);
if(matchType(compileType)&&!valiableIndexUsed[i]){valiableIndexUsed[i] = true;
break;
}
}
mv.visitMethodInsn(Opcodes.INVOKESTATIC, CLASSLOAD_TYPE,
"getInstance", "()L"+CLASSLOAD_TYPE+";");
mv.visitLdcInsn(type.replace("/", "."));
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, CLASSLOAD_TYPE,
"findClass", "(Ljava/lang/String;)Ljava/lang/Class;");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class",
"newInstance", "()Ljava/lang/Object;");
mv.visitTypeInsn(Opcodes.CHECKCAST, compileType);
flag = true;
} else {mv.visitTypeInsn(opcode, type);
}
}
对于第二种创立办法,须要通过批改 Class.forName()和 ClassLoader.findClass()的行为,使他们通过自定义加载器加载类。
应用 JavaAgent 拦挡默认加载器的行为
之前实现的类加载器曾经解决了热部署所须要的性能,可是 JVM 启动时,并不会用自定义的加载器加载 classpath 下的所有 class 文件,取而代之的是通过利用加载器去加载。如果在其之后用自定义加载器从新加载曾经加载的 class,有可能会呈现 LinkageError 的 exception。所以必须在利用启动之前,从新替换曾经加载的 class。如果在 jdk1.4 之前,能应用的办法只有一种,扭转 jdk 中 classloader 的加载行为,使它指向自定义加载器的加载行为。好在 jdk5.0 之后,咱们有了另一种侵略性更小的方法,这就是 JavaAgent 办法,JavaAgent 能够在 JVM 启动之后,利用启动之前的短暂间隙,提供空间给用户做一些非凡行为。比拟常见的利用,是利用 JavaAgent 做面向方面的编程,在办法间退出监控日志等。
JavaAgent 的实现很容易,只有在一个类外面,定义一个 premain 的办法。
清单 6. 一个简略的 JavaAgent
public class ReloadAgent {public static void premain(String agentArgs, Instrumentation inst){GeneralTransformer trans = new GeneralTransformer();
inst.addTransformer(trans);
}
}
而后编写一个 manifest 文件,将 Premain-Class 属性设置成定义一个领有 premain 办法的类名即可。
生成一个蕴含这个 manifest 文件的 jar 包。
manifest-Version: 1.0
Premain-Class: com.example.ReloadAgent
Can-Redefine-Classes: true
最初须要在执行利用的参数中减少 -javaagent 参数 , 退出这个 jar。同时能够为 Javaagent 减少参数,下图中的参数是测试代码中 test project 的绝对路径。这样在执行利用的之前,会优先执行 premain 办法中的逻辑,并且预解析须要加载的 class。
这里利用 JavaAgent 替换原始字节码,阻止原始字节码被 Java 虚拟机加载。只须要实现 一个 ClassFileTransformer 的接口,利用这个实现类实现 class 替换的性能。
清单 7. 替换 class
@Override
public byte [] transform(ClassLoader paramClassLoader, String paramString,
Class<?> paramClass, ProtectionDomain paramProtectionDomain,
byte [] paramArrayOfByte) throws IllegalClassFormatException {String className = paramString.replace("/", ".");
if(className.equals("com.example.Test")){MyClassLoader cl = MyClassLoader.getInstance();
cl.defineReference(className, "com.example.Greeter");
return cl.getByteCode(className);
}else if(className.equals("com.example.Greeter")){MyClassLoader cl = MyClassLoader.getInstance();
cl.redefineClass(className);
return cl.getByteCode(className);
}
return null;
}
至此,所有的工作功败垂成,观赏一下 hotswap 的后果吧。
原文地址:http://www.ibm.com/developerw…
java 的热部署和热加载
ps: 热部署和热加载其实是两个相似但不同的概念,之前了解不深,so,这篇文章重构了下。
一、热部署与热加载
在利用运行的时降级软件,无需重新启动的形式有两种,热部署和热加载。
对于 Java 应用程序来说,热部署就是在服务器运行时重新部署我的项目,热加载即在在运行时从新加载 class,从而降级利用。
二、实现原理
热加载的实现原理次要依赖 java 的类加载机制,在实现形式能够概括为在容器启动的时候起一条后盾线程,定时的检测类文件的工夫戳变动,如果类的工夫戳变掉了,则将类从新载入。
比照反射机制,反射是在运行时获取类信息,通过动静的调用来改变程序行为;
热加载则是在运行时通过从新加载扭转类信息,间接改变程序行为。
热部署原理相似,但它是间接从新加载整个利用,这种形式会开释内存,比热加载更加洁净彻底,但同时也更费时间。
三、在 java 中利用
1. 生产环境
热部署作为一个比拟灵便的机制,在理论的生产上使用还是有,但绝对很少,热加载则根本没有利用。剖析如下
- 一、安全性
热加载这种间接批改 jvm 中字节码的形式是难以监控的,不同于 sql 等执行能够记录日志,间接字节码的批改简直无奈记录代码逻辑的变动,对既有代码行为的影响难以管制,对于越重视平安的利用,热加载带来的危险越大, 这好比给航行中的飞机更换发动机。
- 二、实用的情景
技术大部分是跟需要挂钩的,而须要热部署的情景很少。
- 频繁的部署并且启动耗时长的利用
- 无奈进行服务的利用
在生产中,并没有须要频繁部署的利用,即便是麻利,再快也是一周一次的迭代,并且通过业务划分和模块化编程,部署的代价齐全能够忽略不计,对于现有的利用,启动耗时再长,也并非长到无法忍受,如果真的这么长,那更应该思考的是如何进行模块拆分,分布式部署了。
对于无奈进行服务的利用,比方当初的云计算平台这样分布式应用,采纳分批上线也能够满足需要,相似热部署计划应该是放在最初思考的解决方案。
2. 开发环境
在生产中,不会有频繁的部署并且启动耗时长的利用,但因为云计算的衰亡,热部署还是有其利用。
而热加载有点玩火,太危险了。但在开发和 debug 中,频繁启动利用却随处可见,热加载机制能够极大的晋升开发效率。这两种机制,在开发中还有另外一种称说—开发者模式。
对于大型项目:往往启 / 停须要期待几分钟工夫。更浪费时间的是,对于一个类中的办法的调试过程,如果批改屡次,须要重复的启停服务器,节约的工夫更多。
以目前的 crm 我的项目为例,其启动工夫为 5m,以一天 debug 重启十次,一个月工作 20 天来算,每年重启耗时 25 人日,如果能齐全应用热加载,每年节俭重启工夫近 1 人月。
crm pool 启动耗时
1.struts2 热加载
在 struts2 中热加载即开发者模式,在 struts.xml 配置
<constant name="struts.devMode" value="true" />
从而当更改 struts.xml 文件后不须要重新启动服务器就能够进行程序调试。
2. 开发时应用 tomcat 热加载
tomcat 自身默认开启了热部署形式,但热部署是间接从新加载整个利用,耗时跟重启服务器差不多,咱们须要的其实是热加载,即批改了哪个 class,只从新加载这一个 class,这样耗时简直为 0。
对于 tomcat5.x 以上版本,均已反对肯定水平上得热加载,但这种形式只针对代码行级别的,也就是说如果新删办法,注解,类,或者变量时是有效的,只能重启,这是我目前在公司开发时用的形式,能够显著升高 debug 时的重启次数,进步开发效率
1. 将 tomcat server.xml 文件的context reloadable 值置为false 或者在 web modules 中编辑勾销 Auto reloading 选项。
<Context reloadable="false"/>
2. 批改 eclipse 中的 server 配置
这样做能够在在批改代码之后,不会主动重启服务器,而只加载代码,新增一行 java 代码 ctrl+ s 后间接刷新页面或调用接口即可看到成果,无需重启 tomcat。
3. 近程 debug 中应用 tomcat 热加载
tomcat 的热加载机制不仅能够在本地 debug 时,tomcat 的近程调试也反对热部署,通过 eclipse debug 近程到近程 tomcat 上,批改本地代码,ctrl+ s 后间接刷新页面后调用接口,即可发现近程 tomcat 已将本地代码进行了热加载。
4.jrebel 插件形式
jrebel 插件能够进行更彻底的热加载,不仅包含类,甚至反对 spring 等配置文件的热加载,但公司我的项目开发环境简单,目前在 eclipse 中配置始终没有胜利,只能应用 tomcat 自带的热加载机制。
总结
在理论生产中热部署在云计算中使用挺多,但热加载没有,而在开发中,热加载能够显著的晋升工作效率,强烈推荐应用热加载形式,不仅 tomcat,大多数其余 servlet 容器也反对这种形式,大家能够自行搜寻相干技巧。
参考文档:
1.Tomcat 热部署实现形式源码剖析总结
2. 进步开发效率 -jrebel 插件装置
HotSwap 和 JRebel 原理
HotSwap 和 Instrumentation
在 2002 年的时候,Sun 在 Java 1.4 的 JVM 中引入了一种新的被称作 HotSwap
的实验性技术,这一技术被合成到了 Debugger API
外部,其容许调试者应用同一个类标识来更新类的字节码。这意味着所有对象都能够援用一个更新后的类,并在它们的办法被调用的时候执行新的代码,这就防止了无论何时只有有类的字节码被批改就要重载容器的这种要求。所有旧式的 IDE(包含 Eclipse、IDEA 和 NetBeans)都反对这一技术,从 Java 5 开始,这一性能还通过 Instrumentation API
间接提供给 Java 利用应用。
可怜的是,这种重定义仅限于批改办法体——除了办法体之外,它既不能增加办法或域,也不能批改其余任何货色。这限度了 HotSwap 的实用性,且其还因其余的一些问题而变得更糟:
Java 编译器经常会创立合成的办法或是域,只管你仅是批改了一个办法体(比如说,在增加一个类字面常量(class literal)、匿名的和外部的类的时候等等)。在调试模式下运行经常会升高利用的速度或是引入其余的问题。
这些状况导致了 HotSwap 很少被应用,较之应该可能被应用的频度要低。
为什么 HotSwap 仅限于对办法体起作用?
自从引入了 HotSwap 之后,在最近的 10 年,这一问题曾经被问了十分屡次。在反对做整组扭转的 JVM 调用的 bug 中,这是一个得票率最高的 bug,但到目前为止,这一问题始终没有被落实。
一个申明:我不能说是一个 JVM 专家,我对 JVM 是如何实现的在总体上有着一个很好的了解,这几年来我有和少数几个(前)Sun 工程师谈过,不过我并没有验证我在这里说的每一件事件。不过话虽如此,对于这个 bug 仍然处开发状态的起因我的确是有一些想法的(不过如果你更分明其中的起因的话,欢送斧正)。
JVM 是一种做了重度优化的软件,运行在多个平台上。性能和稳定性是其最高的优先事项。为了在不同的环境中反对这些事项,Sun 的 JVM 提供了这样的性能特色:
两个重度优化的即时编译器(-client 和 -server)几个多代(multi-generational)垃圾收集器
这些性能个性使得类模式(schema)的倒退变成了一个相当大的挑战。为了了解这其中的起因,咱们须要略微凑近一点看一看,到底是须要用什么来反对办法和域的增加操作(甚至更深刻一些,批改继承的层次结构)。
在被加载到 JVM 中时,对象是由内存中的构造来示意的,构造占据了某个特定大小(它的域加上元数据)的间断的内存区域。为了增加一个域,咱们须要调整结构的大小,但因为邻近的区域可能已被占用,咱们就须要把整个构造重新分配到一个不同的区域中,这一区域中有足够可用的空间来把它填写进来。当初,因为咱们实际上是更新了一个类(并不仅是某个对象),所以咱们不得不对该类的每一个对象都做这样的一件事。
这自身并不难实现——Java 垃圾收集器就曾经是随时都在做重调配对象的工作的了。问题是,一个“堆”的形象就仅是一个形象而已。内存的理论布局取决于以后流动的垃圾收集器,而且,为了能与所有这些对象兼容,重调配应该有可能会被委派给流动的垃圾收集器。JVM 在重调配期间还须要挂起,因而其在此期间同时进行 GC 工作也是正当的。
增加一个办法并不要求更新对象的构造,但的确是须要更新类的构造的,这也会体现在堆上。不过考虑一下这种状况:从类被载入之后的那一刻起,其从实质上来说就是被永恒解冻了的。这使得 JIT(Just-In-Time)可能实现 JVM 执行的次要优化操作——内联。应用程序热点中的大多数办法调用会被勾销,这些代码会被拷贝到对其做调用的办法中。一个简略的检测会被插进来,用以确保指标对象的确是咱们所认为的对象。
于是就有了这样可笑的事:在咱们可能增加办法到类中的时候,这种“简略的查看”是不够的。咱们须要的是一个相当简单的查看,须要这样更简单的查看来确保没有应用了雷同名字的办法被增加到指标类以及指标类的超类中。另外,咱们也能够跟踪所有的内联点和它们的依赖,并在类被更新时,解除对它们所做的优化。两种形式可抉择,或是付出性能方面的代价,或是带来更高的复杂性。
最重要的是,思考到咱们正在探讨的是有着不同的内存模型和指令集的多个平台,它们可能多多少少须要一些特定的解决,因而你给本人带来的是一个代价过高而没有太多投资回报的问题。
JRebel 介绍
2007 年,ZeroTurnaround 发表提供一种被称作 JRebel(过后是 JavaRebel)的工具,该工具能够在无需动静类加载器的状况下更新类,且只做极少的限度。不像 HotSwap 要依赖于 IDE 的集成,这一工具的工作形式是,监控磁盘上理论已编译的.class 文件,无论何时只有有文件被更新就更新类。这意味着如果违心的话,你能够把 JRebel 和文本编辑器、命令行的编译器放在一起应用。当然,它也被奇妙地整合到了 Eclipse、InteliJ 和 NetBeans 中。与动静的类加载器不一样,JRebel 保留了所有现有的对象和类的标识和状态,容许开发者持续应用他们的利用而不会产生提早。
如何使之失效?
对于初学者来说,JRebel 工作在与 HotSwap 不同的一个形象层面上。鉴于 HotSwap 是工作在虚拟机层面上,且依赖于 JVM 的外部运作,JRebel 用到了 JVM 的两个显著的性能特色——形象的字节码和类加载器。类加载器容许 JRebel 分别出类被加载的时刻,而后实时地翻译字节码,用以在虚拟机和可执行代码之间创立另一个形象层。
也有人应用这一性能个性来提供分析器、性能监控、后续(continuation)、软件事务性内存以及甚至是分布式的堆。把字节码形象和类加载器联合在一起,这是一种弱小的组合,可被用来实现各种比类重载还要不寻常的性能。当咱们越是深刻地钻研这一问题,咱们就会看到面临的挑战并不仅是在类重载这件事上,而且是还要在性能和兼容性方面没有显著进化的状况下来做这件事件,
正如咱们在 Reloading Java Classes 101 一文中所做的回顾一样,重载类存在的问题是,一旦类被载入,它就不能被卸载或是扭转;然而只有咱们违心,咱们就能够自在地加载新的类。为了了解在实践上咱们是如何重载类的,让咱们来钻研一下 Java 平台上的动静语言。具体来说,让咱们先来看一看 JRudy(咱们做了许多的简化,免得对任何重要人物造成折磨)。
只管 JRuby 以“类(class)”作为其性能个性,但在运行时,其每个对象都是动静的,任何时候都能够退出新的域和办法。这意味着 JRuby 对象与 Map 没有什么两样,有着从办法名字到办法实现的映射,以及域名到其值的映射。这些办法的实现被蕴含在匿名的类中,在遇到办法时这些类就会被生成。如果你增加了一个办法,则所有 JRuby 要做的事件就是生成一个新的匿名类,该类蕴含了这一办法的办法体。因为每个匿名类都有一个惟一的名称,因而在加载该类是不会有问题的,而这样做的后果是,利用被实时动静地更新了。
从实践上来说,因为字节码翻译通常是用来批改类的字节码,因而若仅仅是为了依据须要创立足够多的类来履行类的性能的话,咱们没有什么理由不能应用类中的信息。这样的话,咱们就能够应用如 JRuby 所做的雷同转换来把所有的 Java 类宰割成持有者类和办法体类。可怜的是,这样的一种做法会蒙受(至多是)如下的问题:
性能。这样的设置将意味着,每个办法调用都会遭逢重定向。咱们能够做优化,但应用程序的速度将会变慢至多一个数量级,内存的应用也会扶摇直上,因为有这么多的类被创立。Java 的 SDK 类 。Java SDK 中的类显著地比利用或是库中的类更加难以解决。此外它们通常会以本地的代码来实现,因而不能以“JRuby”的形式做转换。然而,如果咱们让它们放弃原样的话,那么就会引发各种的不兼容性谬误,这些谬误有可能是无奈绕开的。 兼容性。只管 Java 是一种动态的语言,然而它蕴含了一些动静的个性,比如说反射和动静代理等。如果咱们采纳了“JRuby”式的转换的话,这些性能个性就会生效,除非咱们应用本人的类来替换掉 Reflection API,而这些类晓得这些要做的转换。
因而,JRebel 并没有采纳这样的做法。相同,其应用了一种更简单的办法,基于先进的编译技术,留给咱们一个主类和几个匿名的反对类,这些类由 JIT 的转换运行时做反对,其容许所进行的批改不会带来任何显著的性能或是兼容性的进化。它还
留有尽可能多残缺的办法调用,这意味着 JRebel 把性能开销升高到了最小,使其轻量级化。防止了改编(instrument)Java SDK,除了少数几个须要放弃兼容性的中央外。调整 Reflection API 的后果,这样咱们就可能把这些后果中已增加 / 已删除的成员正确地蕴含进来。这也意味着注解(Annotation)的扭转对于利用来说是可见的。
除了类重载之外——还有归档文件
重载类是一件 Java 开发者曾经埋怨了很久的事件,不过一旦咱们解决了它之后,另外的一些问题就随之而来了。
Java EE 规范的制订并未怎么关注开发的周转期(Turnaround)(指的是从对代码做批改到察看到扭转在利用中造成的影响这一过程所破费的工夫)。其构想的是,所有的利用和它们的模块都被打包到归档文件(JAR、WAR 和 EAR)中,这意味着在可能更新利用中的任何文件之前,你须要更新归档文件——这通常是一个代价昂扬的操作,波及了诸如 Ant 或是 Maven 这一类的构建零碎。正如咱们在 Reloading Java Classes 301 所做的探讨那样,能够通过应用展开式的开发和增量的 IDE 构建来尽量减少花销,不过对于大型的利用来说,这种做法通常不是一个可行的抉择。
为了解决这一问题,在 JRebel 2.x 中,咱们为用户开发了一种形式来把归档的利用和模块映射回到工作区中——用户在每个利用和模块中创立一个 rebel.xml 配置文件,该文件通知 JRebel 在哪里能够找到源文件。JRebel 与应用服务器整合在一起,当某个类或是资源被更新时,其被从工作区中而不是从归档文件中读入。
这一做法不仅容许类的即时更新,且容许诸如 HTML、XML、JSP、CSS、.properties 等之类的任何类型的资源的即时更新。Maven 用户甚至不须要创立一个 rebel.xml 文件,因为 Maven 插件会主动地生成该文件。
除了类重载之外——还有配置和元数据
在打消周转期的这一过程中,另一个问题变得显著起来:现如今的利用已不仅仅是类和资源,它们还通过大量的配置和元数据绑定在一起。当配置产生扭转时,扭转应该被反映到那个正在运行的利用上。然而,仅把对配置文件的批改变成是可见的是不够的,具体的框架必须要要重载配置,把扭转反映到利用中才行。
为了在 JRebel 中反对这些类型的扭转,咱们开发了一个开源的 API,该 API 容许咱们的团队和第三方的捐献者应用框架特有的插件来应用 JRebel 的性能个性,把配置中所做的扭转流传到框架中。例如,咱们反对动静实时地在 Spring 中增加 bean 和依赖,以及反对在其余框架中所做的各种各样的扭转。
论断
本文总结了在未应用动静类加载器状况下的各种重载 Java 类的办法。咱们还探讨了导致 HotSwap 局限性的起因,揭示了 JRebel 幕后的工作形式,以及探讨了在解决类重载问题时呈现的其余问题。
原文地址:http://article.yeeyan.org/vie…