diff --git a/core/src/main/java/lucee/commons/lang/PhysicalClassLoader.java b/core/src/main/java/lucee/commons/lang/PhysicalClassLoader.java index b0e8cb4b8ea..baae28b1cd2 100644 --- a/core/src/main/java/lucee/commons/lang/PhysicalClassLoader.java +++ b/core/src/main/java/lucee/commons/lang/PhysicalClassLoader.java @@ -24,8 +24,10 @@ import java.lang.instrument.UnmodifiableClassException; import java.net.URL; import java.net.URLClassLoader; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import lucee.commons.digest.HashUtil; @@ -56,6 +58,17 @@ public final class PhysicalClassLoader extends URLClassLoader implements Extenda private static final double CLASSLOADER_INSPECTION_COUNT = Caster.toIntValue(SystemUtil.getSystemPropOrEnvVar("lucee.template.classloader.inspection.count", null), 1000); private static final double CLASSLOADER_INSPECTION_RATIO = Caster.toIntValue(SystemUtil.getSystemPropOrEnvVar("lucee.template.classloader.inspection.ratio", null), 3); + // Track last flush stats for testing/debugging + private static volatile int lastFlushPagesCleared = 0; + + public static int getLastFlushPagesCleared() { + return lastFlushPagesCleared; + } + + public static void resetLastFlushPagesCleared() { + lastFlushPagesCleared = 0; + } + static { boolean res = registerAsParallelCapable(); } @@ -69,7 +82,13 @@ public final class PhysicalClassLoader extends URLClassLoader implements Extenda private Map allLoadedClasses = new ConcurrentHashMap<>(); // this includes all renames private Map unavaiClasses = new ConcurrentHashMap<>(); - private PageSourcePool pageSourcePool; + private final Set pageSourcePools = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + public void registerPageSourcePool( PageSourcePool pool ) { + if ( pool != null ) { + pageSourcePools.add( pool ); + } + } private boolean rpc; @@ -77,11 +96,11 @@ public final class PhysicalClassLoader extends URLClassLoader implements Extenda public final String id; - PhysicalClassLoader(Config c, List resources, Resource directory, ClassLoader parentClassLoader, ClassLoader addionalClassLoader, PageSourcePool pageSourcePool, + PhysicalClassLoader(Config c, List resources, Resource directory, ClassLoader parentClassLoader, ClassLoader addionalClassLoader, boolean rpc) throws IOException { this(c, PhysicalClassLoaderFactory.doURLs(resources), resources, directory, - parentClassLoader == null ? (parentClassLoader = SystemUtil.getCombinedClassLoader()) : parentClassLoader, addionalClassLoader, pageSourcePool, rpc); + parentClassLoader == null ? (parentClassLoader = SystemUtil.getCombinedClassLoader()) : parentClassLoader, addionalClassLoader, rpc); // check directory if (!directory.exists()) directory.mkdirs(); @@ -90,13 +109,12 @@ public final class PhysicalClassLoader extends URLClassLoader implements Extenda } private PhysicalClassLoader(Config c, URL[] urls, List resources, Resource directory, ClassLoader parentClassLoader, ClassLoader addionalClassLoader, - PageSourcePool pageSourcePool, boolean rpc) { + boolean rpc) { super(urls, parentClassLoader == null ? (parentClassLoader = SystemUtil.getCombinedClassLoader()) : parentClassLoader); this.resources = resources; config = (ConfigPro) c; this.addionalClassLoader = addionalClassLoader; this.birthplace = ExceptionUtil.getStacktrace(new Throwable(), false); - this.pageSourcePool = pageSourcePool; this.directory = directory; this.rpc = rpc; @@ -112,9 +130,15 @@ private PhysicalClassLoader(Config c, URL[] urls, List resources, Reso } public static PhysicalClassLoader flush(PhysicalClassLoader existing, Config config) { - if (existing.pageSourcePool != null) existing.pageSourcePool.clearPages(existing); + int pagesCleared = 0; + for (PageSourcePool pool : existing.pageSourcePools) { + pagesCleared += pool.clearPages(existing); + } + lastFlushPagesCleared = pagesCleared; PhysicalClassLoader clone = new PhysicalClassLoader(config, existing.getURLs(), existing.resources, existing.directory, existing.getParent(), existing.addionalClassLoader, - null, existing.rpc); + existing.rpc); + // copy registered pools to the new classloader + clone.pageSourcePools.addAll(existing.pageSourcePools); DynamicInvoker instance = DynamicInvoker.getExistingInstance(); int count = 0; if (instance != null) count += instance.remove(existing); @@ -125,17 +149,17 @@ public static PhysicalClassLoader flush(PhysicalClassLoader existing, Config con for (Integer i: existing.allLoadedClasses.values()) { allClassesBytes += i.intValue(); } - LogUtil.log(Log.LEVEL_INFO, "physical-classloader", - "flush physical classloader [" + existing.getDirectory() + "] because we reached the size limit (all loaded classes count/size: " + all + "/" - + StringUtil.byteFormat(allClassesBytes) + "; unique loaded classes: " + unique + "; ratio: " + (all / unique) + "), removed " + count - + " cache elements from dynamic invoker"); + int level = (pagesCleared > 0 || count > 0) ? Log.LEVEL_INFO : Log.LEVEL_DEBUG; + LogUtil.log(level, "physical-classloader", + "flush physical classloader [" + existing.getDirectory() + "] (classes: " + all + "/" + unique + ", " + StringUtil.byteFormat(allClassesBytes) + + ", pages cleared: " + pagesCleared + ", dynamic invoker: " + count + ")"); return clone; } public static PhysicalClassLoader flushIfNecessary(PhysicalClassLoader existing, Config config) { double all; - if (LogUtil.does(Log.LEVEL_DEBUG)) { + if (LogUtil.does(Log.LEVEL_TRACE)) { int allClasses = existing.allLoadedClasses.size(); int allClassesBytes = 0; int uniqueClasses = existing.loadedClasses.size(); @@ -145,7 +169,10 @@ public static PhysicalClassLoader flushIfNecessary(PhysicalClassLoader existing, allClassesBytes += i.intValue(); } - LogUtil.log(Log.LEVEL_DEBUG, "physical-classloader", + boolean willFlush = allClasses > CLASSLOADER_INSPECTION_SIZE && ratio > CLASSLOADER_INSPECTION_RATIO; + int level = willFlush ? Log.LEVEL_DEBUG : Log.LEVEL_TRACE; + + LogUtil.log(level, "physical-classloader", "checking if flush necessary for physical classloader [" + existing.getDirectory() + "]: " + "all loaded classes: " + allClasses + " (" + StringUtil.byteFormat(allClassesBytes) + "), " + "unique loaded classes: " + uniqueClasses + ", " + "ratio: " + String.format("%.2f", ratio) + ", " + "inspection size threshold: " + Caster.toString(CLASSLOADER_INSPECTION_COUNT) + "/" + Caster.toString(CLASSLOADER_INSPECTION_SIZE) + ", " @@ -439,7 +466,11 @@ private void clear() { } private void clear(boolean clearPagePool) { - if (clearPagePool && pageSourcePool != null) pageSourcePool.clearPages(this); + if (clearPagePool) { + for (PageSourcePool pool : pageSourcePools) { + pool.clearPages(this); + } + } this.loadedClasses.clear(); this.allLoadedClasses.clear(); this.unavaiClasses.clear(); diff --git a/core/src/main/java/lucee/commons/lang/PhysicalClassLoaderFactory.java b/core/src/main/java/lucee/commons/lang/PhysicalClassLoaderFactory.java index d93832c98cd..e475bdf3998 100644 --- a/core/src/main/java/lucee/commons/lang/PhysicalClassLoaderFactory.java +++ b/core/src/main/java/lucee/commons/lang/PhysicalClassLoaderFactory.java @@ -54,7 +54,7 @@ public static PhysicalClassLoader getPhysicalClassLoader(Config c, Resource dire PhysicalClassLoader existing = classLoaders.get(key); if (existing != null) PhysicalClassLoader.flush(existing, c); } - classLoaders.put(key, rpccl = new PhysicalClassLoader(c, new ArrayList(), directory, SystemUtil.getCombinedClassLoader(), null, null, false)); + classLoaders.put(key, rpccl = new PhysicalClassLoader(c, new ArrayList(), directory, SystemUtil.getCombinedClassLoader(), null, false)); return rpccl; } } @@ -70,9 +70,16 @@ public static PhysicalClassLoader getPhysicalClassLoader(Config c, Resource dire public static PhysicalClassLoader getRPCClassLoader(Config c, JavaSettings js, boolean reload, ClassLoader parent) throws IOException { String key = js == null ? "orphan" : ((JavaSettingsImpl) js).id(); + String parentInfo = "null"; if (parent != null) { - if (parent instanceof PhysicalClassLoader) key += "_" + ((PhysicalClassLoader) parent).id; - else key += "_" + parent.hashCode(); + if (parent instanceof PhysicalClassLoader) { + key += "_" + ((PhysicalClassLoader) parent).id; + parentInfo = "PhysicalClassLoader[id=" + ((PhysicalClassLoader) parent).id + "]"; + } + else { + key += "_" + parent.hashCode(); + parentInfo = parent.getClass().getName() + "@" + parent.hashCode(); + } } PhysicalClassLoader rpccl = reload ? null : classLoaders.get(key); @@ -93,14 +100,15 @@ public static PhysicalClassLoader getRPCClassLoader(Config c, JavaSettings js, b resources = toSortedList(((JavaSettingsImpl) js).getAllResources()); } Resource dir = storeResourceMeta(c, key, js, resources); - // (Config config, String key, JavaSettings js, Collection _resources) - classLoaders.put(key, rpccl = new PhysicalClassLoader(c, resources, dir, parent != null ? parent : SystemUtil.getCombinedClassLoader(), null, null, true)); + lucee.aprint.o( "PhysicalClassLoaderFactory.getRPCClassLoader: Creating new RPC classloader: key=[" + key + "], parent=[" + parentInfo + "], jsId=[" + (js == null ? "null" : ((JavaSettingsImpl) js).id()) + "], totalClassLoaders=" + (classLoaders.size() + 1) ); + classLoaders.put(key, rpccl = new PhysicalClassLoader(c, resources, dir, parent != null ? parent : SystemUtil.getCombinedClassLoader(), null, true)); return rpccl; } } } // at this point we know we had an existing one + lucee.aprint.o( "PhysicalClassLoaderFactory.getRPCClassLoader: Reusing existing RPC classloader: key=[" + key + "], parent=[" + parentInfo + "], pclId=[" + rpccl.id + "], totalClassLoaders=" + classLoaders.size() ); PhysicalClassLoader flushed = PhysicalClassLoader.flushIfNecessary(rpccl, c); if (flushed != null) { classLoaders.put(key, rpccl = flushed); @@ -122,8 +130,7 @@ public static PhysicalClassLoader getRPCClassLoader(Config c, BundleClassLoader } Resource dir = c.getClassDirectory().getRealResource("RPC/" + key); if (!dir.exists()) ResourceUtil.createDirectoryEL(dir, true); - // (Config config, String key, JavaSettings js, Collection _resources) - classLoaders.put(key, rpccl = new PhysicalClassLoader(c, new ArrayList(), dir, SystemUtil.getCombinedClassLoader(), bcl, null, true)); + classLoaders.put(key, rpccl = new PhysicalClassLoader(c, new ArrayList(), dir, SystemUtil.getCombinedClassLoader(), bcl, true)); return rpccl; } } diff --git a/core/src/main/java/lucee/runtime/MappingImpl.java b/core/src/main/java/lucee/runtime/MappingImpl.java index 9cc7508b1dd..3a063dfae79 100755 --- a/core/src/main/java/lucee/runtime/MappingImpl.java +++ b/core/src/main/java/lucee/runtime/MappingImpl.java @@ -266,6 +266,7 @@ public Class loadClass(String className) { private Class loadClass(String className, byte[] code) throws IOException, ClassNotFoundException { PhysicalClassLoader pcl = PhysicalClassLoaderFactory.getPhysicalClassLoader(config, getClassRootDirectory(), false); + pcl.registerPageSourcePool( pageSourcePool ); /* * PhysicalClassLoaderReference pclr = loaders.get(className); PhysicalClassLoader pcl = pclr == * null ? null : pclr.get(); if (pcl == null || code != null) {// || pcl.getSize(true) > 3 if (pcl @@ -280,6 +281,7 @@ private Class loadClass(String className, byte[] code) throws IOException, Cl } catch (UnmodifiableClassException e) { pcl = PhysicalClassLoaderFactory.getPhysicalClassLoader(config, getClassRootDirectory(), true); + pcl.registerPageSourcePool( pageSourcePool ); try { return pcl.loadClass(className, code); } diff --git a/core/src/main/java/lucee/runtime/PageSourceImpl.java b/core/src/main/java/lucee/runtime/PageSourceImpl.java index d2c43b97766..f5af69e7983 100755 --- a/core/src/main/java/lucee/runtime/PageSourceImpl.java +++ b/core/src/main/java/lucee/runtime/PageSourceImpl.java @@ -351,7 +351,7 @@ && isLoad(LOAD_PHYSICAL)) // synchronized (SystemUtil.createToken("PageSource", getRealpathWithVirtual())) { // new class if (flush || !classFile.exists()) { - LogUtil.log(config, Log.LEVEL_DEBUG, "compile", "compile [" + getDisplayPath() + "] no previous class file or flush"); + LogUtil.log(config, Log.LEVEL_TRACE, "compile", "compile [" + getDisplayPath() + "] no previous class file or flush"); pcn.set(page = compile(config, classRootDir, null, false, pci != null && pci.ignoreScopes())); flush = false; @@ -423,7 +423,7 @@ public boolean releaseWhenOutdatted() { // synchronized (SystemUtil.createToken("PageSource", getRealpathWithVirtual())) { if (srcLastModified == 0 || srcLastModified != page.getSourceLastModified()) {// || (page instanceof PagePro && ((PagePro) page).getSourceLength() != // srcFile.length()) - if (LogUtil.doesDebug(mapping.getLog())) mapping.getLog().debug("page-source", "release [" + getDisplayPath() + "] from page source pool"); + if (LogUtil.doesTrace(mapping.getLog())) mapping.getLog().trace("page-source", "release [" + getDisplayPath() + "] from page source pool"); resetLoaded(); flush(); return true; @@ -435,7 +435,7 @@ public boolean releaseWhenOutdatted() { } public void flush() { - if (LogUtil.doesDebug(mapping.getLog())) mapping.getLog().debug("page-source", "flush [" + getDisplayPath() + "]"); + if (LogUtil.doesTrace(mapping.getLog())) mapping.getLog().trace("page-source", "flush [" + getDisplayPath() + "]"); pcn.page = null; flush = true; } @@ -1075,11 +1075,13 @@ public void clear() { * * @param cl */ - public void clear(ClassLoader cl) { + public boolean clear(ClassLoader cl) { Page page = pcn.page; if (page != null && page.getClass().getClassLoader().equals(cl)) { pcn.page = null; + return true; } + return false; } public boolean isLoad() { @@ -1163,7 +1165,7 @@ public boolean executable() { } public void resetLoaded() { - if (LogUtil.doesDebug(mapping.getLog())) mapping.getLog().debug("page-source", "reset loaded [" + getDisplayPath() + "]"); + if (LogUtil.doesTrace(mapping.getLog())) mapping.getLog().trace("page-source", "reset loaded [" + getDisplayPath() + "]"); Page p = pcn.page; if (p != null) p.setLoadType((byte) 0); } diff --git a/core/src/main/java/lucee/runtime/PageSourcePool.java b/core/src/main/java/lucee/runtime/PageSourcePool.java index 3c53963589d..c0994b870a4 100644 --- a/core/src/main/java/lucee/runtime/PageSourcePool.java +++ b/core/src/main/java/lucee/runtime/PageSourcePool.java @@ -275,16 +275,22 @@ public DumpData toDumpData(PageContext pageContext, int maxlevel, DumpProperties * * @param cl */ - public void clearPages(ClassLoader cl) { + public int clearPages(ClassLoader cl) { Iterator> it = this.pageSources.values().iterator(); PageSourceImpl psi; SoftReference sr; + int count = 0; while (it.hasNext()) { sr = it.next(); psi = sr == null ? null : (PageSourceImpl) sr.get(); if (psi == null) continue; - if (cl != null) psi.clear(cl); - else psi.clear(); + if (cl != null) { + if (psi.clear(cl)) count++; + } + else { + psi.clear(); + count++; + } } if (cl == null) { @@ -292,6 +298,7 @@ public void clearPages(ClassLoader cl) { } resetWatcherWhenEmpty(false, true); + return count; } public void resetPages(ClassLoader cl) { diff --git a/core/src/main/java/lucee/transformer/dynamic/meta/dynamic/ClazzDynamic.java b/core/src/main/java/lucee/transformer/dynamic/meta/dynamic/ClazzDynamic.java index bb9c4cbe22f..6722c39e64a 100644 --- a/core/src/main/java/lucee/transformer/dynamic/meta/dynamic/ClazzDynamic.java +++ b/core/src/main/java/lucee/transformer/dynamic/meta/dynamic/ClazzDynamic.java @@ -86,7 +86,7 @@ public static Clazz getInstance(Class clazz, Resource dir, Log log) { synchronized (clazz) { cd = classes.get(clazz); if (cd == null) { - if (log != null) log.debug("dynamic", "extract metadata from [" + clazz.getName() + "]"); + if (log != null) log.trace("dynamic", "extract metadata from [" + clazz.getName() + "]"); try { cd = new ClazzDynamic(clazz, log); } diff --git a/test/tickets/LDEV5903_3.cfc b/test/tickets/LDEV5903_3.cfc new file mode 100644 index 00000000000..bf7c74c2c4d --- /dev/null +++ b/test/tickets/LDEV5903_3.cfc @@ -0,0 +1,101 @@ +component extends="org.lucee.cfml.test.LuceeTestCase" labels="classloader,memory,leak" { + + function beforeAll() { + variables.testPrefix = "LDEV5903_3_tmp"; + variables.testWorkingDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "/" & variables.testPrefix & "/"; + if ( directoryExists( variables.testWorkingDir ) ) { + directoryDelete( variables.testWorkingDir, true ); + } + directoryCreate( variables.testWorkingDir ); + pagePoolClear( force=true ); + } + + function afterAll() { + if ( directoryExists( variables.testWorkingDir ) ) { + directoryDelete( variables.testWorkingDir, true ); + } + } + + function run( testResults, testBox ) { + describe( "LDEV-5903 - PageSourcePool leak when classloader flushes", function() { + + it( "PageSourcePool.clearPages() should be called when classloader is flushed", function() { + // Get the mapping's PhysicalClassLoader + var pageContext = getPageContext(); + var ps = pageContext.getCurrentPageSource(); + var mapping = ps.getMapping(); + var PhysicalClassLoader = createObject( "java", "lucee.commons.lang.PhysicalClassLoader" ); + var PhysicalClassLoaderFactory = createObject( "java", "lucee.commons.lang.PhysicalClassLoaderFactory" ); + var config = pageContext.getConfig(); + var classRootDir = mapping.getClassRootDirectory(); + + var pcl = PhysicalClassLoaderFactory.getPhysicalClassLoader( config, classRootDir, false ); + var initialPclHash = pcl.hashCode(); + var initialPagesCleared = PhysicalClassLoader.getLastFlushPagesCleared(); + var initialAllSize = pcl.getSize( true ); + var initialUniqueSize = pcl.getSize( false ); + + systemOutput( "LDEV5903_3: initial state - pclHash=#initialPclHash#, lastFlushPagesCleared=#initialPagesCleared#, allSize=#initialAllSize#, uniqueSize=#initialUniqueSize#, classRootDir=#classRootDir#", true ); + + // Reset flush stats before test + PhysicalClassLoader.resetLastFlushPagesCleared(); + systemOutput( "LDEV5903_3: after reset - lastFlushPagesCleared=#PhysicalClassLoader.getLastFlushPagesCleared()#", true ); + + // Create and modify component to trigger classloader flushes + // Default thresholds: count=1000, ratio=3 + // Need >1000 classes with ratio>3 to trigger flush + var componentPath = variables.testWorkingDir & "_LDEV5903Test.cfc"; + var componentName = variables.testPrefix & "._LDEV5903Test"; + + var pclHashesSeen = { "#initialPclHash#": true }; + var maxPagesCleared = 0; + var flushCount = 0; + + // Strategy: Create and repeatedly modify 5 unique components + // Each component gets modified many times, accumulating renamed classes + // With 5 components modified 1000 times each = 5000 renames, ratio = 5000/5 = 1000 + var numComponents = 5; + var modifyCount = 1000; + var totalIterations = numComponents * modifyCount; + + loop from=1 to=totalIterations index="local.i" { + var compIndex = ( ( local.i - 1 ) mod numComponents ) + 1; + var compPath = variables.testWorkingDir & "_LDEV5903Test#compIndex#.cfc"; + var compName = variables.testPrefix & "._LDEV5903Test#compIndex#"; + + fileWrite( compPath, "component { function getVersion() { return '#local.i#'; } }" ); + createObject( "component", compName ); + + var currentPcl = PhysicalClassLoaderFactory.getPhysicalClassLoader( config, classRootDir, false ); + var currentPclHash = currentPcl.hashCode(); + + // Log progress at intervals + if ( local.i == 500 || local.i == 1000 || local.i == 2000 || local.i == 3000 ) { + var currentAllSize = currentPcl.getSize( true ); + var currentUniqueSize = currentPcl.getSize( false ); + var ratio = currentUniqueSize > 0 ? currentAllSize / currentUniqueSize : 0; + systemOutput( "LDEV5903_3: iteration #local.i# - allSize=#currentAllSize#, uniqueSize=#currentUniqueSize#, ratio=#numberFormat( ratio, '0.00' )#, pclHash=#currentPclHash#", true ); + } + + if ( !structKeyExists( pclHashesSeen, currentPclHash ) ) { + pclHashesSeen[ currentPclHash ] = true; + flushCount++; + // A flush happened - check if pages were cleared + var lastPagesCleared = PhysicalClassLoader.getLastFlushPagesCleared(); + systemOutput( "LDEV5903_3: flush ###flushCount# at iteration #local.i# - newPclHash=#currentPclHash#, pagesCleared=#lastPagesCleared#", true ); + if ( lastPagesCleared > maxPagesCleared ) { + maxPagesCleared = lastPagesCleared; + } + } + } + + systemOutput( "LDEV5903_3: final - flushCount=#flushCount#, pclHashesSeen=#structCount( pclHashesSeen )#, maxPagesCleared=#maxPagesCleared#", true ); + + // Multiple classloaders means flushes happened + expect( structCount( pclHashesSeen ) ).toBeGT( 1, "Flush should have created multiple PhysicalClassLoaders, got #structCount( pclHashesSeen )#" ); + // Pages should have been cleared during at least one flush + expect( maxPagesCleared ).toBeGT( 0, "PageSourcePool.clearPages() should have cleared pages during flush, got #maxPagesCleared#" ); + }); + }); + } +} diff --git a/test/tickets/LDEV5903_4.cfc b/test/tickets/LDEV5903_4.cfc new file mode 100644 index 00000000000..da6484ea8ed --- /dev/null +++ b/test/tickets/LDEV5903_4.cfc @@ -0,0 +1,92 @@ +component extends="org.lucee.cfml.test.LuceeTestCase" labels="classloader,memory,leak" { + + function beforeAll() { + variables.testPrefix = "LDEV5903_4_tmp"; + variables.testWorkingDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "/" & variables.testPrefix & "/"; + if ( directoryExists( variables.testWorkingDir ) ) { + directoryDelete( variables.testWorkingDir, true ); + } + directoryCreate( variables.testWorkingDir ); + } + + function afterAll() { + if ( directoryExists( variables.testWorkingDir ) ) { + directoryDelete( variables.testWorkingDir, true ); + } + } + + function run( testResults, testBox ) { + describe( "LDEV-5903 - Application mapping SoftReference causes repeated class loading", function() { + + it( "static components should not cause class renames when application mapping is evicted", function() { + var PhysicalClassLoaderFactory = createObject( "java", "lucee.commons.lang.PhysicalClassLoaderFactory" ); + var pageContext = getPageContext(); + var config = pageContext.getConfig(); + + // Create a STATIC component (file never changes) + var componentPath = variables.testWorkingDir & "StaticComponent.cfc"; + var componentName = variables.testPrefix & ".StaticComponent"; + fileWrite( componentPath, "component { function getValue() { return 'static'; } }" ); + + // First load - this creates the application mapping and loads the class + var obj1 = createObject( "component", componentName ); + expect( obj1.getValue() ).toBe( "static" ); + + // Get the class name - should NOT have a rename suffix like $1, $2 + var className1 = obj1.getComponentPage().getClass().getName(); + systemOutput( "LDEV5903_4: first load - className=#className1#", true ); + + // Get initial classloader state + var ps = pageContext.getCurrentPageSource(); + var mapping = ps.getMapping(); + var classRootDir = mapping.getClassRootDirectory(); + var pcl = PhysicalClassLoaderFactory.getPhysicalClassLoader( config, classRootDir, false ); + var initialAllSize = pcl.getSize( true ); + var initialUniqueSize = pcl.getSize( false ); + systemOutput( "LDEV5903_4: initial classloader state - all=#initialAllSize#, unique=#initialUniqueSize#", true ); + + // Force GC to try to evict SoftReferences (application mappings) + var System = createObject( "java", "java.lang.System" ); + System.gc(); + sleep( 500 ); + System.gc(); + sleep( 500 ); + + // Now load the SAME component again - file hasn't changed + // If SoftReference was evicted, a new mapping is created with empty PageSourcePool + // This would cause the class to be "loaded" again, triggering a rename + var renameCount = 0; + var classNames = { "#className1#": true }; + + loop from=1 to=100 index="local.i" { + // Force more GC pressure + if ( local.i mod 10 == 0 ) { + System.gc(); + } + + var obj = createObject( "component", componentName ); + var className = obj.getComponentPage().getClass().getName(); + + if ( !structKeyExists( classNames, className ) ) { + classNames[ className ] = true; + renameCount++; + systemOutput( "LDEV5903_4: iteration #local.i# - NEW className=#className# (rename detected!)", true ); + } + } + + // Check final classloader state + pcl = PhysicalClassLoaderFactory.getPhysicalClassLoader( config, classRootDir, false ); + var finalAllSize = pcl.getSize( true ); + var finalUniqueSize = pcl.getSize( false ); + systemOutput( "LDEV5903_4: final classloader state - all=#finalAllSize#, unique=#finalUniqueSize#", true ); + systemOutput( "LDEV5903_4: total unique classNames seen=#structCount( classNames )#, renames=#renameCount#", true ); + + // With STATIC files and proper caching, there should be NO renames + // All 100 iterations should return the exact same class + expect( renameCount ).toBe( 0, "Static component should not cause class renames, but got #renameCount# renames. ClassNames seen: #structKeyList( classNames )#" ); + }); + + + }); + } +} diff --git a/test/tickets/LDEV5903_5.cfc b/test/tickets/LDEV5903_5.cfc new file mode 100644 index 00000000000..a2e5ce6b8e4 --- /dev/null +++ b/test/tickets/LDEV5903_5.cfc @@ -0,0 +1,47 @@ +component extends="org.lucee.cfml.test.LuceeTestCase" labels="classloader,memory,leak" { + + function run( testResults, testBox ) { + describe( "LDEV-5903 - Multiple RPC classloaders created across requests", function() { + + it( "should reuse RPC classloader across multiple requests with same loadPaths", function() { + var PhysicalClassLoaderFactory = createObject( "java", "lucee.commons.lang.PhysicalClassLoaderFactory" ); + + // Get initial classloader count via reflection + var factoryClass = PhysicalClassLoaderFactory.getClass(); + var classLoadersField = factoryClass.getDeclaredField( "classLoaders" ); + classLoadersField.setAccessible( true ); + var classLoadersMap = classLoadersField.get( javacast( "null", 0 ) ); + var initialCount = classLoadersMap.size(); + + systemOutput( "LDEV5903_5: Initial RPC classloader count: #initialCount#", true ); + + // Make multiple internal requests - this mimics production where each request + // might call createObject with the same loadPaths (like QR code generation) + // Need to actually reproduce the bug - 149 classloaders suggests something + // about the loadPaths array or parent classloader is changing each request + var iterations = 150; + var workerPath = "/test/tickets/LDEV5903_5/worker.cfm"; + loop from=1 to=iterations index="local.i" { + internalRequest( + template: workerPath, + method: "GET" + ); + + if ( local.i mod 10 == 0 ) { + var currentCount = classLoadersMap.size(); + systemOutput( "LDEV5903_5: After #local.i# requests - RPC classloader count: #currentCount#", true ); + } + } + + var finalCount = classLoadersMap.size(); + var newClassLoaders = finalCount - initialCount; + + systemOutput( "LDEV5903_5: Final RPC classloader count: #finalCount# (created #newClassLoaders# new)", true ); + + // We should only create ONE new RPC classloader for these libs across all requests + // Allow for maybe 2-3 due to test framework overhead, but NOT 100 + expect( newClassLoaders ).toBeLT( 10, "Should not create #newClassLoaders# RPC classloaders across #iterations# requests. Expected < 10" ); + }); + }); + } +} diff --git a/test/tickets/LDEV5903_5/worker.cfm b/test/tickets/LDEV5903_5/worker.cfm new file mode 100644 index 00000000000..7b9fdf277c1 --- /dev/null +++ b/test/tickets/LDEV5903_5/worker.cfm @@ -0,0 +1,11 @@ + + // Mimic QrCodeGenerator pattern: DirectoryList to get paths + libDir = expandPath( "{lucee-server}/context/lib" ); + // Get actual JAR files, not directories + libs = directoryList( path=libDir, recurse=true, listinfo="path", filter="*.jar" ); + + // Use createObject with loadPaths like QR code generator does + obj = createObject( "java", "java.util.HashMap", libs ); + + writeOutput( "OK" ); +