Java Reload (experimental) simple Support - JReload

Introduction and usage, plus some notes on java classes unloading and internalTablesImpl option

The "jreload" module is not the definitive word about java classes reloading under jython. It is still experimental and its interface may improve or change to become more pythonic.

"jreload" cannot cover all the possible reload-flavors, and its goal is to offer a simple interface for the most common cases, e.g. quick trial-and-error experimenting.

Java classes reloading in jython is not enabled by "jreload", some of the modifications occurred to jython run-time have made it possible. Now jython can deal with java classes with the same name (without clashes) and run-time supports unloading of java classes by different ways, which is a must feature for some uses of reloading

[The expert user can now play directly with class-loaders and reloading as he would from java.]

The main idea of the "jreload" interface is that of a load-set. A load-set is a package-like object that can host a complete hierarchy of java packages, which can be reloaded as a whole

Why there is no support for reloading a single class? Java classes are loaded through class-loaders, actually there is no support in java to redefine an already loaded class through its class-loader. In order to reload a class one should create a new class-loader, but classes from different class-loaders cannot interoperate, so we need to reload all related classes through the new class-loader. Note: The normal python reload built-in does nothing for java classes and simply returns the old version.

The "jreload" module exports the following functions:

makeLoadSet(name, path)

makeLoadSet create a new load-set with the given name. The created load-set will behave like a package and import statement related to it can be issued. name should be a valid python identifier like any python module name and should not clash with any module that has been or will be imported. Internally the created load-set will be added to sys.modules, the same way it happens to modules on import. You can issue the same makeLoadSet from many places, it is idempotent like modules imports are.

path should be a list of directory or jar paths. The created load-set will enable importing the classes present there. path should be disjoint from both sys.path and java classpath, otherwise one can get very confusing results.

reload(loadSet) reloads all the classes in the package hierarchy hosted by loadSet.

Note: The current version of "jreload" (jreload.__version__=='0.3') does not support removing or substituting jars on the fly.

The following example should help make things clearer: (its files should be present in the jython Demo dir)

  • Demo/jreload/example.jar contains example.Version *lnk to src* and example.PrintVer *lnk to src*
  • Demo/jreload/_xample contains a slightly modified version of example.Version *lnk to src*
  • *paths should be fixed to use xp*
    >>> import sys
    >>> import os
    >>> import jreload
    >>> def xp(name): return os.path.join(sys.prefix,'Demo/jreload/'+name)
    >>> X=jreload.makeLoadSet('X',['rel2','rel2/example.jar'])
    >>> from X import example
    >>> dir(example)
    ['PrintVer', 'Version', '__name__']
    >>> X.example.Version
    <jclass example.Version at 6781345>
    >>> from X.example import *
    >>> v=Version(1)
    >>> PrintVer.print(v)
    version 1
    >>> os.rename('rel2/_xample','rel2/example') # _xample becomes example, hiding and "patching" jar contents 
    >>> jreload.reload(X) # (re)loads example dir example.Version and jar example.PrintVer
    <java load-set X>
    >>> nv2=example.Version(2)
    >>> example.PrintVer.print(nv2)
    new version 2
    >>> PrintVer.print(nv2)
    Traceback (innermost last):
      File "<console>", line 1, in ?
    TypeError: print(): 1st arg can't be coerced to example.Version
    >>> example.PrintVer.print(v)
    Traceback (innermost last):
      File "<console>", line 1, in ?
    TypeError: print(): 1st arg can't be coerced to example.Version
    >>> os.rename('rel2/example','rel2/_xample')

    Note: Differently from python packages reload, load-sets reload the complete hosted hierarchy.
    Note: Class versions across reloads are not interoperable.

    Like for python classes and python reload, old versions are kept around, if there are still references to them. But what happens if they are no longer used?

    Java Classes Unloading

    One would expect that no longer referenced java classes would be unloaded, but the situation is not that simple.

    In order to give a python-class-like view on python side and for implementation reasons jython wraps java classes (in instances of org.python.core.PyJavaClass). Clearly the mapping from java classes to their wrapped version should be unique (e.g. to guarantee == and 'is' semantic). So jython keeps this mapping in an internal table. This is also good because building the wrappers is expensive.

    Note: Unless one cannot predict the amount of reloading that will be performed by an application or for the case of long-running highly dynamic apps, the unloading issue can simply be ignored.

    Clearly the entries somehow block unloading. On the other hand java classes unloading is just a memory consumption optimization (and as such is it presented in Java Language Specification). Actual jvms clearly support this. JPython simply kept the entries in the table forever but Jython and "jreload" try to make unloading possible.

    Note: java never unloads system classes (java.* etc) nor classes from classpath. Further Jython cannot unload sys.path java classes. So the whole unload issue makes sense only with "jreload" or custom class-loaders.

    Java 2 and jython internalTablesImpl option

    Under java2 jython offers table implementations that exploit soft/weak references in order to discard entries (when this is OK) for unloading.

    A possible policy would be to keep an entry as long as the corresponding java class is still referenced outside the table (both by java or jython code). But this one cannot be implemented. [One cannot add fields to final java class Class!] So entries are kept as long as the wrapped version is still in use.

    These implementations can be chosen trough python.options.internalTablesImpl registry option. Note: they only influence classes unloading, there is no need and reason to use them, unless one depends on class unloading to avoid memory leakage.

    internalTablesImpl = weak -- Sets implementation using weak-refs. Table entries for not referenced (outside the table) wrapped versions are "discarded" at garbage collection points. If a class or some of its instances are continuously passed from java to jython side, but no long-living reference to it is kept from jython side, this can imply a performance penalty (rebuilding the wrapped version is expensive). On the other hand this is a good setting for testing if unloading actually happens or some references hunting around prevent it.

    [Note: With jdk 1.3 java -verbose:class can help tracking class unloads, and System.gc forces class unloading. With jdk 1.2 java -verbose:gc should give some information on class unloading, but unloading of classes happen at unpredictable points and System.gc does not trigger it. Also weak-refs allow testing for unloading and gc.]

    internalTablesImpl = soft --Sets implementation using soft-refs. Table entries for not referenced (outside the table) wrapped versions are "discarded" on memory shortage, given soft-reference definition. Soft-references behavior is not specified in full details, so the actual behavior will depend on the concrete jvm. But if actual (jvm) implementations are not too bad, this should be a good setting for production code, which relies on unloading to avoid out of memory failures.

    Java 1.1

    To be honest the unloading support that jython can offer under java 1.1 (given the absence of weak/soft-refs) is error-prone and anything serious would require "too much" caution, but this should not be a real issue. Support is offered only for "jreload" needs in these forms:

  • Before reload(X) one can issue X.unload(). X.unload() discards all the entries for the old versions of the classes in X. This is safe only if all python subclasses and all instances of them have been destroyed.
  • One can "extract" the information needed in order to discard the entries for the versions actually present in X at a later point (after a whole series of reloads):
    u_t1=X.unload # extract unload info for time t1 versions
    ... reloads ...
    u_t1() # discard entries for time t1 versions
    u_t1() is safe only if at that point all subclasses/instances of the involved versions have been destroyed.