Groovy实现代码热载的机制和原理
前言:
真的很久没在博客园上更新博客了, 现在趁这段空闲的时间, 对之前接触的一些工程知识做下总结. 先来讲下借用Groovy如何来实现代码的热载, 以及其中涉及到的原理和需要注意的点.
总的来说, Groovy作为一本动态编译语言, 其对标应该是c/c++体系中的lua, 在一些业务逻辑变动频繁的场景, 其意义非常的重大.
简单入门:
本文的主题是Groovy实现代码热载, 其他大背景是java实现主干代码, groovy实现易变动的逻辑代码. 先来看下java是如何调用的groovy脚本的.
import groovy.lang.Binding;
import groovy.lang.GroovyShell; public class GroovyTest { public static void main(String[] args) {
// *) groovy 代码
String script = "println 'hello'; 'name = ' + name;"; // *) 传入参数
Binding binding = new Binding();
binding.setVariable("name", "lilei"); // *) 执行脚本代码
GroovyShell shell = new GroovyShell(binding);
Object res = shell.evaluate(script);
System.out.println(res);
} }
这段代码的输出为:
hello
name = lilei
Binding类主要用于传递参数集, 而GroovyShell则主要用于编译执行Groovy代码. 是不是比想象中的要简答, ^_^.
当然java调用groovy还有其他的方式, 下文会涉及到.
原理分析:
下面这段其实大有文章.
GroovyShell shell = new GroovyShell(binding);
Object res = shell.evaluate(script);
对于函数evaluate, 我们追踪进去, 会有不少的重新认识.
public Object evaluate(GroovyCodeSource codeSource) throws CompilationFailedException {
Script script = this.parse(codeSource);
return script.run();
} public Script parse(GroovyCodeSource codeSource) throws CompilationFailedException {
return InvokerHelper.createScript(this.parseClass(codeSource), this.context);
}
其大致的思路, 为Groovy脚本代码包装生成class, 然后产生该类实例对象, 在具体执行其包装的逻辑代码.
但是这边需要注意的情况:
public Class parseClass(String text) throws CompilationFailedException {
return this.parseClass(text, "script" + System.currentTimeMillis() + Math.abs(text.hashCode()) + ".groovy");
}
对于groovy脚本, 它默认会生成名字为script + System.currentTimeMillis() + Math.abs(text.hashCode())的class类, 也就是说传入脚本, 它都会生成一个新类, 就算同一段groovy脚本代码, 每调用一次, 都会生成一个新类.
陷阱评估:
原理我们基本上理解了, 但是让我们来构造一段代码, 看看是否有哪些陷阱.
import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.lang.Script; import java.util.Map;
import java.util.TreeMap; public class GroovyTest2 { private static GroovyShell shell = new GroovyShell(); public static Object handle(String script, Map<String, Object> params) {
Binding binding = new Binding();
for ( Map.Entry<String, Object> ent : params.entrySet() ) {
binding.setVariable(ent.getKey(), ent.getValue());
}
Script sci = shell.parse(script);
sci.setBinding(binding);
return sci.run();
} public static void main(String[] args) {
String script = "println 'hello'; 'name = ' + name;";
Map<String, Object> params = new TreeMap<String, Object>();
params.put("name", "lilei");
while(true) {
handle(script, params);
}
} }
这段代码执行到最后的结果为, 频繁触发full gc, 究其原因为PermGen区爆满. 这是为何呢?
如上所分析的, 虽然是同一份脚本代码, 但是都为其每次调用, 间接生成了一个class类. 对于full gc, 除了清理老年代, 也会顺便清理永久代(PermGen), 但为何不清理这些一次性的class呢? 答案是gc条件不成立.
引用下class被gc, 需满足的三个条件:
1). 该类所有的实例都已经被GC
2). 加载该类的ClassLoader已经被GC
3). 该类的java.lang.Class对象没有在任何地方被引用
加载类的ClassLoader实例被GroovyShell所持有, 作为静态变量(gc root), 条件2不成立, GroovyClassLoader有个map成员, 会缓存编译的class, 因此条件3都不成立.
有人会问, 为何不把GroovyShell对象, 作为一个临时变量呢?
public static Object handle(String script, Map<String, Object> params) {
Binding binding = new Binding();
for ( Map.Entry<String, Object> ent : params.entrySet() ) {
binding.setVariable(ent.getKey(), ent.getValue());
}
GroovyShell shell = new GroovyShell();
Script sci = shell.parse(script);
sci.setBinding(binding);
return sci.run();
}
实际上, 还是治标不治本, 只是说class能被gc掉, 但是清理的速度可能赶不上产生的速度, 依旧频繁触发full gc.
推荐做法:
解决上述问题很简单, 就是引入缓存, 当然缓存的对象不上Script实例(在多线程环境下, 会遇到数据混乱的问题, 对象有状态), 而是Script.class本身. 对应的key为脚本代码的指纹.
大致的代码如下所示:
private static ConcurrentHashMap<String, Class<Script>> zlassMaps
= new ConcurrentHashMap<String, Class<Script>>(); public static Object invoke(String scriptText, Map<String, Object> params) {
String key = fingerKey(scriptText);
Class<Script> script = zlassMaps.get(key);
if ( script == null ) {
synchronized (key.intern()) {
// Double Check
script = zlassMaps.get(key);
if ( script == null ) {
GroovyClassLoader classLoader = new GroovyClassLoader();
script = classLoader.parseClass(scriptText);
zlassMaps.put(key, script);
}
}
} Binding binding = new Binding();
for ( Map.Entry<String, Object> ent : params.entrySet() ) {
binding.setVariable(ent.getKey(), ent.getValue());
}
Script scriptObj = InvokerHelper.createScript(script, binding);
return scriptObj.run(); } // *) 为脚本代码生成md5指纹
public static String fingerKey(String scriptText) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] bytes = md.digest(scriptText.getBytes("utf-8")); final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray();
StringBuilder ret = new StringBuilder(bytes.length * 2);
for (int i=0; i<bytes.length; i++) {
ret.append(HEX_DIGITS[(bytes[i] >> 4) & 0x0f]);
ret.append(HEX_DIGITS[bytes[i] & 0x0f]);
}
return ret.toString();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
这边会为每个新类单独创建一个GroovyClassLoader对象, 也是巧妙地回避之前的陷阱.
总结:
这边没有深入研究java中类的加载机制, 只是涉及class被gc的先决条件, 同时提供了一种思路, 如何借助groovy实现代码热加载, 同时又规避其中的陷阱.
Groovy实现代码热载的机制和原理的更多相关文章
- 最简破解-java代码热加载热部署IDEA插件JRebel
如果经济实力允许的话,还是建议大家去购买收费版.支持原创作者,才能有更好的产品出现. 一.Jrebel插件介绍 JRebel一款帮助我们在开发过程中实现热加载的插件,目前来说,在IDEA中实现热加载最 ...
- 深入探索Android热修复技术原理读书笔记 —— 代码热修复技术
在前一篇文章 深入探索Android热修复技术原理读书笔记 -- 热修复技术介绍中,对热修复技术进行了介绍,下面将详细介绍其中的代码修复技术. 1 底层热替换原理 在各种 Android 热修复方案中 ...
- SpringBoot开发 - 什么是热部署和热加载?devtool的原理是什么?
在SpringBoot开发调试中,如果我每行代码的修改都需要重启启动再调试,可能比较费时间:SpringBoot团队针对此问题提供了spring-boot-devtools(简称devtools)插件 ...
- [转载] Android动态加载Dex机制解析
本文转载自: http://blog.csdn.net/wy353208214/article/details/50859422 1.什么是类加载器? 类加载器(class loader)是 Java ...
- 前端通信:SSE设计方案(二)--- 服务器推送技术的实践以及一些应用场景的demo(包括在线及时聊天系统以及线上缓存更新,代码热修复案例)
距离上一篇博客,这篇文章的发布大概过了整整三个月.我也从饿了么度过了试用期,成为了正式员工.刚进来恰好遇到项目底层改造和迁移,将项目从angular全部迁移到vue上,所以适应这边的节奏和业务的开发任 ...
- Java提高篇——JVM加载class文件的原理机制
在面试java工程师的时候,这道题经常被问到,故需特别注意. 1.JVM 简介 JVM 是我们Javaer 的最基本功底了,刚开始学Java 的时候,一般都是从“Hello World ”开始的,然后 ...
- IntelliJ IDEA配置Springboot2.x 通过devtools实现代码热部署,提高调试效率
1.pom.xml添加依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifa ...
- JVM加载class文件的原理机制(转)
JVM加载class文件的原理机制 1.Java中的所有类,必须被装载到jvm中才能运行,这个装载工作是由jvm中的类装载器完成的,类装载器所做的工作实质是把类文件从硬盘读取到内存中 2.java中的 ...
- Unity3D热更新之LuaFramework篇[09]--资源热更新与代码热更新的具体实现
前言 在上一篇文章 Unity3D热更新之LuaFramework篇[08]--热更新原理及热更服务器搭建 中,我介绍了热更新的基本原理,并且着手搭建一台服务器. 本篇就做一个实战练习,真正的来实现热 ...
随机推荐
- linux上udev的配置(转载)
udev配置文件主要的udev配置文件是/etc/udev/udev.conf.这个文件通常很短,他可能只是包含几行#开头的注释,然后有几行选项:udev_rules=”/etc/udev/rules ...
- 安天透过北美DDoS事件解读IoT设备安全——Mirai的主要感染对象是linux物联网设备,包括:路由器、网络摄像头、DVR设备,入侵主要通过telnet端口进行流行密码档暴力破解,或默认密码登陆,下载DDoS功能的bot,运行控制物联网设备
安天透过北美DDoS事件解读IoT设备安全 安天安全研究与应急处理中心(安天CERT)在北京时间10月22日下午启动高等级分析流程,针对美国东海岸DNS服务商Dyn遭遇DDoS攻击事件进行了跟进分析. ...
- 把旧系统迁移到.Net Core 2.0 日记(1) - Startup.cs 解析
因为自己到开发电脑转到Mac Air,之前的Webform/MVC应用在Mac 跑不起来,而且.Net Core 2.0 已经比较稳定了. 1. 为什么会有跨平台的.Net Core 近年来,我们已 ...
- css 让div 的高度和屏幕的高度一样
<html><head><title>无标题文档</title><style type="text/css">html, ...
- 002-linux——控制台的使用:
1.桌面控制台: 2.字符控制台: .默认6个字符控制台. .独立运行 互不影响 .多用户 多任务 tty-控制台的使用: .开始进入的是图形图面:tty1 就是图形界面. .图形界面切换到字符界面 ...
- 适应c++ 新特性 - 与我 - 多年传统方式开发(新特性参考微软标准:https://msdn.microsoft.com/zh-cn/library/hh279654.aspx)
公司同事都在积极使用c++的新特性,并对其赞不绝口,而自己一直做着传统的c++开发方式,到底这些新特性如何,又是怎么提高开发效率的,我依然在疑问当中,从同事的说法和实际代码操练里,确实在减少代码量,集 ...
- Uva LA 3177 - Beijing Guards 贪心,特例分析,判断器+二分,记录区间内状态数目来染色 难度: 3
题目 https://icpcarchive.ecs.baylor.edu/index.php?option=com_onlinejudge&Itemid=8&page=show_pr ...
- Jdbc连接数据库基本步骤
Jdbc连接数据库的基本步骤: package demo.jdbc; import java.sql.Connection; import java.sql.DriverManager; import ...
- java新随笔
1.纯随机数发生器 Xn+1=(aXn + c)mod m Modulus=231-1=int.MaxValue Multiplier=75=16807 C=0 当显示过2^31-2个数之后,才可能重 ...
- java语句的控制流程
if(布尔表达式 ){ 程序执行语句1 }else { 程序执行语句2 } while(布尔表达式){ 程序执行语句 } do{ 程序执行语句 }while(布尔表达式); for(初始化语句,条件语 ...