Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions core/src/main/java/org/jruby/Ruby.java
Original file line number Diff line number Diff line change
Expand Up @@ -3369,21 +3369,23 @@ private void systemTeardown(final ThreadContext context) {
}
}

// Tear down LoadService before terminating ThreadService, since LoadedFeatures.clear requires context.
loadService.tearDown();

// Shut down and replace thread service after all other hooks and finalizers have run
threadService.teardown();
threadService = new ThreadService(this);

// Release classloader resources
// Release classloader resources once all other runtime state has been torn down.
releaseClassLoader();

// Tear down LoadService
loadService.tearDown();

// Clear runtime tables to aid GC
boundMethods.clear();
allModules.clear();
constantNameInvalidators.clear();
symbolTable.clear();

javaSupport.tearDown();
javaSupport = loadJavaSupport();
}

Expand Down
52 changes: 52 additions & 0 deletions core/src/main/java/org/jruby/embed/internal/LocalContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@

import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.HashMap;
import java.util.Map;

Expand All @@ -50,6 +52,19 @@ public class LocalContext {
private BiVariableMap varMap;
private Map<AttributeName, Object> attributes;

/**
* Termination mark for removed LocalContext instances.
*/
private volatile boolean terminated;
private static final VarHandle TERMINATED;
static {
try {
TERMINATED = MethodHandles.lookup().findVarHandle(LocalContext.class, "terminated", boolean.class);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}

public LocalContext(RubyInstanceConfig config, LocalVariableBehavior behavior) {
this(config, behavior, false);
}
Expand All @@ -68,6 +83,8 @@ public Ruby getThreadSafeRuntime() {
}

public BiVariableMap getVarMap(LocalContextProvider provider) {
checkTerminated();

if (varMap == null) {
synchronized(this) {
if (varMap == null) {
Expand All @@ -84,6 +101,8 @@ public LocalVariableBehavior getLocalVariableBehavior() {

@SuppressWarnings("MapReplaceableByEnumMap")
public Map<?, Object> getAttributeMap() {
checkTerminated();

if (attributes == null) {
synchronized(this) {
if (attributes == null) {
Expand All @@ -98,15 +117,25 @@ public Map<?, Object> getAttributeMap() {
}

public void remove() {
// Ensure the context is terminated only once
if (!TERMINATED.compareAndSet(this, false, true)) {
throw terminatedException();
}

if (attributes != null) {
synchronized(this) { attributes.clear(); }
}
if (varMap != null) {
synchronized(this) { varMap.clear(); }
}

// Clear the runtime reference to assist GC
runtime = null;
}

Ruby getRuntime() {
checkTerminated();

if (runtime == null) {
synchronized(this) {
if (runtime == null) {
Expand All @@ -117,6 +146,29 @@ Ruby getRuntime() {
return runtime;
}

/**
* Check if this LocalContext has been terminated and raise error if so.
*/
private void checkTerminated() {
if (isTerminated()) throw terminatedException();
}

/**
* Construct an exception to indicate this LocalContext has been terminated.
*
* @return the exception
*/
private static IllegalStateException terminatedException() {
return new IllegalStateException("LocalContext is terminated");
}

boolean isInitialized() { return runtime != null; }

/**
* Return the termination state of this LocalContext.
*
* @return true if terminated, false otherise
*/
boolean isTerminated() { return terminated; }

}
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,13 @@ public void teardown() {
mainContext = null;

// clear thread map
for (Map.Entry<Thread, RubyThread> entry : rubyThreadMap.entrySet()) {
entry.getValue().clearContext();
}
rubyThreadMap.clear();

// remove the terminating thread's context
remove();
}

public void initMainThread() {
Expand Down
45 changes: 38 additions & 7 deletions core/src/main/java/org/jruby/javasupport/JavaSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import org.jruby.util.collections.ClassValue;
import org.jruby.util.collections.ClassValueCalculator;

import java.lang.ref.WeakReference;
import java.lang.reflect.Member;
import java.util.Map;
import java.util.Set;
Expand All @@ -63,12 +64,17 @@

public abstract class JavaSupport {

protected final Ruby runtime;
protected Ruby runtime;

@Deprecated(since = "9.4.3.0")
private final ClassValue<JavaClass> javaClassCache;
private final ClassValue<RubyModule> proxyClassCache;

/**
* A lock to be used for proxy initialization.
*/
private final ReentrantLock lock = new ReentrantLock();

static final class UnfinishedProxy extends ReentrantLock {
final RubyModule proxy;
UnfinishedProxy(RubyModule proxy) {
Expand Down Expand Up @@ -112,23 +118,48 @@ public IRubyObject allocateProxy(Object javaObject, RubyClass clazz) {
public JavaSupport(final Ruby runtime) {
this.runtime = runtime;

this.javaClassCache = ClassValue.newInstance(klass -> new JavaClass(runtime, getJavaClassClass(), klass));
WeakReference<Ruby> runtimeRef = new WeakReference<>(runtime);
WeakReference<JavaSupport> javaSupportRef = new WeakReference<>(this);

this.proxyClassCache = ClassValue.newInstance(this::computeProxyClass);
this.javaClassCache = ClassValue.newInstance(javaClassCalculator(runtimeRef, javaSupportRef));

this.proxyClassCache = ClassValue.newInstance(proxyCalculator(runtimeRef, lock));

// Proxy creation is synchronized (see above) so a HashMap is fine for recursion detection.
this.unfinishedProxies = new ConcurrentHashMap<>(8, 0.75f, 1);
}

private static ClassValueCalculator<JavaClass> javaClassCalculator(WeakReference<Ruby> runtimeRef, WeakReference<JavaSupport> javaSupportRef) {
return klass -> new JavaClass(runtimeRef.get(), javaSupportRef.get().getJavaClassClass(), klass);
}

private static ClassValueCalculator<RubyModule> proxyCalculator(WeakReference<Ruby> runtimeRef, ReentrantLock lock) {
return klass -> computeProxyClass(runtimeRef.get(), lock, klass);
}

public void tearDown() {
javaClassCache.clear();
proxyClassCache.clear();
unfinishedProxies.clear();
runtime = null;
}

/**
* Because of the complexity of processing a given class and all its dependencies,
* we opt to synchronize this logic. Creation of all proxies goes through here,
* allowing us to skip some threading work downstream.
*
* This method is made static to avoid any circular reference via JavaSupport back to the ClassValue this supports.
*/
private synchronized RubyModule computeProxyClass(Class<?> klass) {
RubyModule proxyKlass = Java.createProxyClassForClass(runtime, klass);
JavaExtensions.define(runtime, klass, proxyKlass); // (lazy) load extensions
return proxyKlass;
private static RubyModule computeProxyClass(Ruby runtime, ReentrantLock lock, Class<?> klass) {
lock.lock();
try {
RubyModule proxyKlass = Java.createProxyClassForClass(runtime, klass);
JavaExtensions.define(runtime, klass, proxyKlass); // (lazy) load extensions
return proxyKlass;
} finally {
lock.unlock();
}
}

@Deprecated(since = "9.4.0.0")
Expand Down
6 changes: 6 additions & 0 deletions core/src/main/java/org/jruby/javasupport/JavaSupportImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ public JavaSupportImpl(final Ruby runtime) {
super(runtime);
}

public void tearDown() {
super.tearDown();
staticAssignedNames.clear();
instanceAssignedNames.clear();
}

private static Map<String, AssignedName> newAssignedNames(Class<?> klass) {
return new HashMap<>(8, 1);
}
Expand Down
12 changes: 11 additions & 1 deletion core/src/main/java/org/jruby/util/collections/ClassValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,17 @@ public ClassValue(ClassValueCalculator<T> calculator) {

public abstract T get(Class<?> cls);

protected final ClassValueCalculator<T> calculator;
/**
* Make a best effort to clear all references.
*
* Best effort here is sometimes not easily achievable, if for example values are opaquely contained
* in a {@link java.lang.ClassValue} instance that does not directly support global clearing.
*/
public void clear() {
calculator = null;
}

protected volatile ClassValueCalculator<T> calculator;

public static <T> ClassValue<T> newInstance(ClassValueCalculator<T> calculator) {
if (Options.INVOKEDYNAMIC_CLASS_VALUES.load()) return newJava7Instance(calculator);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ public T get(Class<?> cls) {
return proxy.get(cls).get();
}

public void clear() {
// nothing to clear and values are weakly referenced by the ClassValue anyway
}

// If we put any objects that reference an org.jruby.Ruby runtime
// (like RubyClass instances) in here we run into a circular
// reference situation that GC isn't handling where they will
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ public T get(Class<?> cls) {
return obj;
}

public void clear() {
cache.clear();
}

// There's not a compelling reason to keep JavaClass instances in a weak map
// (any proxies created are [were] kept in a non-weak map, so in most cases they will
// stick around anyway), and some good reasons not to (JavaClass creation is
Expand Down
53 changes: 42 additions & 11 deletions core/src/main/java/org/jruby/util/collections/StableClassValue.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package org.jruby.util.collections;

import java.util.WeakHashMap;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;

/**
* An implementation of JRuby's ClassValue, using java.lang.ClassValue directly but ensuring computation of each value
* occurs at most once.
*/
final class StableClassValue<T> extends ClassValue<T> {
private final ReentrantLock lock = new ReentrantLock();
private final WeakHashMap<StableValue<Class<?>, T>, Object> stableValues = new WeakHashMap<>();

public StableClassValue(ClassValueCalculator<T> calculator) {
super(calculator);
Expand All @@ -18,6 +22,15 @@ public T get(Class<?> cls) {
return proxy.get(cls).get(cls);
}

public void clear() {
synchronized (stableValues) {
for (StableValue<Class<?>, T> value : stableValues.keySet()) {
value.clear();
}
stableValues.clear();
}
}

/**
* Represents a stable value, which is computed at most once and then stored forever.
*
Expand All @@ -27,35 +40,53 @@ public T get(Class<?> cls) {
* @param <Input> input of the computation
* @param <Result> result of the computation
*/
private class StableValue<Input, Result> {
private final Function<Input, Result> calculator;
private static class StableValue<Input, Result> {
private final ReentrantLock lock;
private volatile Function<Input, Result> calculator;
private volatile Result result;
StableValue(Function<Input, Result> calculator) {
StableValue(ReentrantLock lock, Function<Input, Result> calculator) {
this.lock = lock;
this.calculator = calculator;
}
Result get(Input input) {
Result result = this.result;

if (result != null) return result;

// lock on the StableClassValue so there are not multiple locks potentially in different orders
synchronized (StableClassValue.this) {
// Use shared lock to ensure only one StableValue can initialize at a time
lock.lock();
try {
result = this.result;

if (result != null) return result;

result = this.calculator.apply(input);
this.result = result;
} finally {
lock.unlock();
}

return result;
}
}

private final java.lang.ClassValue<StableValue<Class<?>, T>> proxy = new java.lang.ClassValue<StableValue<Class<?>, T>>() {
@Override
protected StableValue<Class<?>, T> computeValue(Class<?> type) {
return new StableValue<>(calculator);
void clear() {
this.calculator = null;
this.result = null;
}
};
}

private final java.lang.ClassValue<StableValue<Class<?>, T>> proxy = createStableValueProxy(lock, calculator, stableValues);

private static <U> java.lang.ClassValue<StableValue<Class<?>, U>> createStableValueProxy(ReentrantLock lock, Function<Class<?>, U> calculator, WeakHashMap<StableValue<Class<?>, U>, Object> stableValues){
return new java.lang.ClassValue<>() {
@Override
protected StableValue<Class<?>, U> computeValue(Class<?> type) {
StableValue<Class<?>, U> stableValue = new StableValue<>(lock, calculator);
synchronized (stableValues) {
stableValues.put(stableValue, null);
}
return stableValue;
}
};
}
}
Loading
Loading