一 。深入了解Java的ClassLoader机制
为了深入了解Java的ClassLoader机制,我们先来做以下实验: package java.lang; public class Test { public static void main(String[] args) { char[] c = "1234567890".toCharArray(); String s = new String(0, 10, c); } } String类有一个Package权限的构造函数String(int offset, int length, char[] array),按照默认的访问权限,由于Test属于java.lang包,因此理论上应该可以访问String的这个构造函数。编译通过!执行时结果如下: Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang at java.lang.ClassLoader.defineClass(Unknown Source) at java.security.SecureClassLoader.defineClass(Unknown Source) at java.net.URLClassLoader.defineClass(Unknown Source) at java.net.URLClassLoader.access$100(Unknown Source) at java.net.URLClassLoader$1.run(Unknown Source) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(Unknown Source) at java.lang.ClassLoader.loadClass(Unknown Source) at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source) at java.lang.ClassLoader.loadClass(Unknown Source) at java.lang.ClassLoader.loadClassInternal(Unknown Source) 奇怪吧?要弄清为什么会有SecurityException,就必须搞清楚ClassLoader的机制。 Java的ClassLoader就是用来动态装载class的,ClassLoader对一个class只会装载一次,JVM使用的ClassLoader一共有4种: 启动类装载器,标准扩展类装载器,类路径装载器和网络类装载器。 这4种ClassLoader的优先级依次从高到低,使用所谓的“双亲委派模型”。确切地说,如果一个网络类装载器被请求装载一个java.lang.Integer,它会首先把请求发送给上一级的类路径装载器,如果返回已装载,则网络类装载器将不会装载这个java.lang.Integer,如果上一级的类路径装载器返回未装载,它才会装载java.lang.Integer。 类似的,类路径装载器收到请求后(无论是直接请求装载还是下一级的ClassLoader上传的请求),它也会先把请求发送到上一级的标准扩展类装载器,这样一层一层上传,于是启动类装载器优先级最高,如果它按照自己的方式找到了java.lang.Integer,则下面的ClassLoader都不能再装载java.lang.Integer,尽管你自己写了一个java.lang.Integer,试图取代核心库的java.lang.Integer是不可能的,因为自己写的这个类根本无法被下层的ClassLoader装载。 再说说Package权限。Java语言规定,在同一个包中的class,如果没有修饰符,默认为Package权限,包内的class都可以访问。但是这还不够准确。确切的说,只有由同一个ClassLoader装载的class才具有以上的Package权限。比如启动类装载器装载了java.lang.String,类路径装载器装载了我们自己写的java.lang.Test,它们不能互相访问对方具有Package权限的方法。这样就阻止了恶意代码访问核心类的Package权限方法。
end
二 .一个简单的自定义classloader的实现
一个简单的自定义classloader的实现 kert 原创 (参与分:57651,专家分:885) 发表:2002-07-18 17:55 更新:2002-07-18 20:05 版本:1.0 阅读:16355 次
很多时候人们会使用一些自定义的classloader ,而不是使用系统的class loader。大多数时候人们这样做的原因是,他们在编译时无法预知运行时会需要那些class。特别是在那些appserver中,比如tomcat,avalon-phonix,jboss中。或是程序提供一些plug-in的功能,用户可以在程序编译好之后再添加自己的功能,比如ant, jxta-shell等。定制一个classloader很简单,一般只需要理解很少的几个方法就可以完成。 一个最简单的自定义的classloader从classloader类继承而来。这里我们要做一个可以在运行时指定路径,加载这个路径下的class的classloader。 通常我们使用classloader.loadclass(string):class方法,通过给出一个类名,就会得到一个相应的class实例。因此只要小小的改动这个方法,就可以实现我们的愿望了。 源码:
protected synchronized class loadclass(string name, boolean resolve) throws classnotfoundexception { / first, check if the class has already been loaded class c = findloadedclass(name); if (c == null ) { try { if (parent != null ) { c = parent.loadclass(name, false ); }else { c = findbootstrapclass0(name); } }catch (classnotfoundexception e){ / if still not found, then call findclass in order / to find the class. c = findclass(name); } } if (resolve) { resolveclass(c); } return c; }
source from classloader.java first,check javaapi doc:上面指出了缺省的loadclass方法所做的几个步骤。 1. 调用findloadedclass(string):class 检查一下这个class是否已经被加载过了,由于jvm 规范规定classloader可以cache它所加载的class,因此如果一个class已经被加载过的话,直接从cache中获取即可。 2. 调用它的parent 的loadclass()方法,如果parent为空,这使用jvm内部的class loader(即著名的bootstrap classloader)。 3. 如果上面两步都没有找到,调用findclass(string)方法来查找并加载这个class。 后面还有一句话,在java 1.2版本以后,鼓励用户通过继承findclass(string)方法实现自己的class loader而不是继承loadclass(string)方法。 既然如此,那么我们就先这么做:)
public class anotherclassloader extends classloader { private string basedir;private static final logger log = logger .getlogger(anotherclassloader.class ); public anotherclassloader (classloader parent, string basedir) { super (parent); this .basedir = basedir; } protected class findclass(string name) throws classnotfoundexception { log.debug("findclass " + name); byte !#91;!#93; bytes = loadclassbytes(name); class theclass = defineclass(name, bytes, 0, bytes.length );/a if (theclass == null ) throw new classformaterror (); return theclass; } private byte !#91;!#93; loadclassbytes(string classname) throws classnotfoundexception { try { string classfile = getclassfile(classname); fileinputstream fis = new fileinputstream (classfile); filechannel filec = fis.getchannel(); bytearrayoutputstream baos = new bytearrayoutputstream (); writablebytechannel outc = channels .newchannel(baos); bytebuffer buffer = bytebuffer .allocatedirect(1024); while (true ) { int i = filec.read(buffer); if (i == 0 || i == -1) { break ; } buffer.flip(); outc.write(buffer); buffer.clear(); } fis.close(); return baos.tobytearray(); } catch (ioexception fnfe) { throw new classnotfoundexception (classname); } } private string getclassfile(string name) { stringbuffer sb = new stringbuffer (basedir); name = name.replace('.', file .separatorchar) + ".class" ; sb.append(file .separator + name); return sb.tostring(); } }
[i]ps:这里使用了一些jdk1.4的nio的代码:)[/i] 很简单的代码,关键的地方就在a处,我们使用了defineclass方法,目的在于把从class文件中得到的二进制数组转换为相应的class实例。defineclass是一个native的方法,它替我们识别class文件格式,分析读取相应的数据结构,并生成一个class实例。 还没完呢,我们只是找到了发布在某个目录下的class,还有资源呢。我们有时会用class.getresource():url来获取相应的资源文件。如果仅仅使用上面的classloader是找不到这个资源的,相应的返回值为null。 同样我们看一下原来的classloader内部的结构。
public java.net.url getresource(string name) { name = resolvename(name); classloader cl = getclassloader0();/这里 if (cl==null ) { / a system class. return classloader .getsystemresource(name); } return cl.getresource(name);}
原来是使用加载这个class的那个classloader获取得资源。
public url getresource(string name) { url url; if (parent != null ) { url = parent.getresource(name); } else { url = getbootstrapresource(name); } if (url == null ) { url = findresource(name);/这里 } return url; }
这样看来只要继承findresource(string)方法就可以了。修改以下我们的代码:
/新增的一个findresource方法 protected url findresource(string name) { log.debug("findresource " + name); try { url url = super .findresource(name); if (url != null ) return url; url = new url ("file://" + convername(name)); /简化处理,所有资源从文件系统中获取 return url; } catch (malformedurlexception mue) { log.error("findresource" , mue); return null ; } } private string convername(string name) { stringbuffer sb = new stringbuffer (basedir); name = name.replace('.', file .separatorchar); sb.append(file .separator + name); return sb.tostring(); }
好了,到这里一个简单的自定义的classloader就做好了,你可以添加其他的调料(比如安全检查,修改class文件等),以满足你自己的口味:)
.jar文件?
end
三.tomcat reload,不得不说的故事
tomcat reload,不得不说的故事 kert 原创 (参与分:57509,专家分:885) 发表:2002-09-15 18:18 更新:2002-09-15 19:15 版本:0.5 阅读:10250 次
tomcat reload,不得不说的故事 [i]我们知道在使用tomcat时,如果设置了reload后,tomcat会自动侦测web-inf目录下修改过的资源。如果发现有变化(通常是依据文件的lastmodified值),便会自动重新载入所有 的资源。表面上看,似乎是个很好的主意:不用重新启动tomcat便可以更新我们的web应。尤其是在调试阶段,只需简单的更新我们的代码,就可以重新测试了。然而美丽的表面总是隐藏着不可测的秘密。[/i] ---------------- 最近在使用tomcat时,就遇到了一个有趣的问题,简单当时困扰了我很久(也许是因为我比较苯:))。到这里和大家分享一下。 我在webapp应用中有一个daemon 线程,用来定时监视某个状态的改变。如果没有改变就sleep一段时间,否则进行某些相应的处理。类似如下的代码:
public class testreload{ private static final logger log = logger .getlogger(commonreload.class .getname()); private testreload(){ log.info("constructing " + getclass() + " : " + getclass().hashcode()); new thread (){ public void run(){ while (true ){ work();/相应的处理工作 try { sleep(10000); }catch (throwable t){ }; } } }.start(); } private final static testreload instance = new testreload(); }
[i]在constructor中构造这个线程,每隔10秒钟工作一次[/i] 这个类作为某个webapp中的一个组件,因此最初的入口还是一个servlet。当我为了debug,而重新编译代码并重新发布我的webapp后,发现原先生成的线程仍旧在工作,而同时tomcat也将新编译的代码载入内存,因此这时jvm中有了两个监视的线程在工作,因此会有不可预料的问题。但是这不仅仅是两个独立的工作线程的问题,虽然表面上如此。我修改了一下代码,添加了一个测试用的work方法,如下:
public class testreload{ ...... private void work(){ log.info("testreload " + testreload.class .hashcode()); final classloader cl = getclass().getclassloader(); log.info("the class loader is " + cl.getclass().getname()+ " : " + cl.hashcode()); } }
这里有三行输出信息,用来跟踪一些jvm内部的信息。
log.info("testreload " +testreload.class .hashcode());
用来输出testreload的class的hashcode值。
log.info("the class loader is " + cl.getclass().getname()+ " : " + cl.hashcode())
用来输出加载这个testclass的class的classloader的名字和hashcode。 然后用一个简单的servlet作为程序的入口:
protected void doget(httpservletrequest httpservletrequest, httpservletresponse httpservletresponse) throws servletexception, ioexception { class reload = testreload.class ; writer w = httpservletresponse.getwriter(); w.write("working...\r\n" ); w.write(reload.getname() + ": " + reload.hashcode()); w.flush(); }
这个servlet只是简单的要求classloader载入testclass的class并且进行class的初始化和相应的静态初始化。 我们来看一下试验的输出。 当webapp第一次运行时,屏幕输出入下: ...... 2002-9-15 16:00:01 kert.reload.testreload <init> 信息: constructing class kert.reload.testreload : 2737550 2002-9-15 16:00:01 kert.reload.testreload work 信息: testreload 2737550 2002-9-15 16:00:01 kert.reload.testreload work 信息: the class loader is org.apache.catalina.loader.webappclassloader : 23414511 2002-9-15 16:00:11 kert.reload.testreload work 信息: testreload 2737550 2002-9-15 16:00:11 kert.reload.testreload work 信息: the class loader is org.apache.catalina.loader.webappclassloader : 23414511 我重新编译代码并发布后,tomcat reload相应的代码后并在此运行这个webapp: ...... 2002-9-15 16:01:59 kert.reload.testreload <init> 信息: constructing class kert.reload.testreload : 9104244 2002-9-15 16:01:59 kert.reload.testreload work 信息: testreload 9104244 2002-9-15 16:01:59 kert.reload.testreload work 信息: the class loader is org.apache.catalina.loader.webappclassloader : 13754931 2002-9-15 16:02:01 kert.reload.testreload work 信息: testreload 2737550 2002-9-15 16:02:01 kert.reload.testreload work 信息: the class loader is org.apache.catalina.loader.webappclassloader : 23414511 2002-9-15 16:02:09 kert.reload.testreload work 信息: testreload 9104244 2002-9-15 16:02:09 kert.reload.testreload work 信息: the class loader is org.apache.catalina.loader.webappclassloader : 13754931 2002-9-15 16:02:11 kert.reload.testreload work 信息: testreload 2737550 2002-9-15 16:02:11 kert.reload.testreload work 信息: the class loader is org.apache.catalina.loader.webappclassloader : 23414511 ...... 可以很明显的看到,在tomcat reload后jvm中同时存在了两个工作线程。并且不仅仅如此,两个线程输出有着明显的不同。
两个testreload的class的hashcode不同,说明jvm内存中存在着两个不同的testreload的class的实例。 每个testreload的class的对应的classloader也不相同。 照理说,tomcat reload在reload一个webapp时,应该清除原先的所有载入的数据。包括已生成的对象和相应的class对象,然后交个gc来处理(回收所有的对象,包括class对象和classloader)。 但是由于有一个无法终止的线程,tomcat reload无法让线程停止,因此也无法回收相应的class。这样,在先前生成的所有class都会仍旧保存在内存中。并且与reload后的class同名,虽然由于加载的classloader不同,这两组class是无法互相访问的,因为他们属于不同的runtime package。 但是这种状况仍旧会导致很多问题。
1.重复工作:有多个线程在做同样的工作。 2.访问限制:由于runtime package的限制,原来在编译期互相可见的变量或是方法,在运行期可能无法互相访问。 3.classnotfound:显而易见。 ...... 显然,tomcat reload并不能像我们想象的那样很够很好的完成我们的工作。虽然这不是tomcat的错,我猜想在其他的container中也会有这样的现象发生,如jboss。container并不能够终止我们的精灵线程,而我们也无法介入到container的reload机制中去,如何reload(remove)我们先前的代码。如果tomcat在reload之前,在remove旧的代码的时候可以定义一个回调函数,或是有一个event机制通知我们的应用,那么我们可以采取某些措施。 现在为止,我还没有想到一个比较好的方式来处理这种情况(还是比较笨的缘故)。暂时还是重启tomcat。或是把这个后台线程做成一个mbean,使用jmx来管理它。 如果各位有好解决方法或是相应的pattern,欢迎回贴。 如果各位发现某些错误,更是欢迎“砖头”。
相關評論:
评论人:crazycode 参与分: 101 专家分: 0 发表时间: 2003-01-14 14:42 你可以用single设计模式来保证只创建一个实例,再在启动线程时做一下判断,如果已经有线程在运行,中止它,再启动新的线程。
评论人:kert 参与分: 57509 专家分: 885 发表时间: 2003-01-14 16:08 呵呵,由于代码是由tomcat的内置classloader加载,它们根本不再同一个runtime package中, 因此无法判断。
评论人:quake_wang 参与分: 179 专家分: 130 发表时间: 2003-01-15 09:12 你可以把这个daemon线程的类放在tomcat的classpath下,而不是放在webapp下,这样应该就可以了.
评论人:banq 参与分: 86 专家分: 10 发表时间: 2003-01-15 09:51 我不理解的问题是: servlet一般情况下本质是线程的,你为什么还在其内部直接编制启动一个线程? 我觉得如果你可以在servlet容器外编制一个线程不停的访问servlet 这样可以实现你的目的
评论人:banq 参与分: 86 专家分: 10 发表时间: 2003-01-15 10:09 如果一定要在servlet容器内部做,可以借鉴jive使用java.util.timertask
评论人:wuyu 参与分: 80 专家分: 50 发表时间: 2003-02-14 16:49 精灵线程start的时候将其hashcode或时间戳记录在一个handle文件中,定期检查这个文件内容是否相符,如果不符则中止线程?
评论人:panwen 参与分: 77 专家分: 0 发表时间: 2003-02-15 13:23 我觉得任何线程都应该有一个退出机制,例如: public void run() { while(flag) { try { work(); thread.sleep(6000); }catch(exception e){} } } 在servlet的destroy方法里 设置flag=false就可以让线程退出。
评论人:fridaychen 参与分: 59 专家分: 10 发表时间: 2003-02-18 14:09 在tomcat中可以注册servletcontextlistener,这是一个标准的机制。 public void contextinitialized(servletcontextevent sce); public void contextdestroyed(servletcontextevent sce); 允许程序在系统启动和关闭的时候作一些工作。我把线程的启动和关闭都放在这里了,这样系统在reload的时候,也会调用servletcontextlistener的方法。
评论人:wuyu 参与分: 80 专家分: 50 发表时间: 2003-02-18 16:20 非常感谢楼上的所有朋友们,特别是fridaychen。 为了测试fridaychen所说的servletcontextlistener,我特意做了一个小测试,然后在tomcat4.1.18的/manager/html/list里面stop/start/reload /market这个web app,结果,在tomcat load webapp的时,contextinitialized中的代码被执行,系统开始定时执行继续了java.util.timertask的守护线程程序,stop时,contextdestroyed中的代码被执行,所有正在执行的守护线程程序均正常中止。tomcat控制台的输出信息 startup init start d:\www\web\market_version\ timertask run... timertask run... timertask run... timertask run... timertask run... timertask run... timertask run... timertask run... timertask run... timertask run... timertask run... timertask run... destorymarket.marketlistener源代码 package market; /** * 侦听器程序测试 */ public class marketlistener implements javax.servlet.servletcontextlistener { private java.util.timer timer; public marketlistener() { system.out.println( "startup init" ); timer = new java.util.timer( true ); } public void contextdestroyed( javax.servlet.servletcontextevent event ) { system.out.println( "destory" ); timer.cancel(); } public void contextinitialized( javax.servlet.servletcontextevent event ) { &