diff --git a/core/src/main/java/com/taobao/arthas/core/command/Constants.java b/core/src/main/java/com/taobao/arthas/core/command/Constants.java index a65fd534705..4d9d4824b00 100644 --- a/core/src/main/java/com/taobao/arthas/core/command/Constants.java +++ b/core/src/main/java/com/taobao/arthas/core/command/Constants.java @@ -19,6 +19,7 @@ public interface Constants { " throwExp : the throw exception of method\n" + " isReturn : the method ended by return\n" + " isThrow : the method ended by throwing exception\n" + + " #ref : global object reference store (weak reference)\n" + " #cost : the execution time in ms of method invocation"; String EXAMPLE = "\nEXAMPLES:\n"; diff --git a/core/src/main/java/com/taobao/arthas/core/command/express/ExpressFactory.java b/core/src/main/java/com/taobao/arthas/core/command/express/ExpressFactory.java index 886c910f1bb..4581b1616f9 100644 --- a/core/src/main/java/com/taobao/arthas/core/command/express/ExpressFactory.java +++ b/core/src/main/java/com/taobao/arthas/core/command/express/ExpressFactory.java @@ -30,13 +30,13 @@ public static Express threadLocalExpress(Object object) { express = new OgnlExpress(); expressRef.set(new WeakReference(express)); } - return express.reset().bind(object); + return express.reset().bind(object).bind("ref", ObjectRefStore.ref()); } public static Express unpooledExpress(ClassLoader classloader) { if (classloader == null) { classloader = ClassLoader.getSystemClassLoader(); } - return new OgnlExpress(new ClassLoaderClassResolver(classloader)); + return new OgnlExpress(new ClassLoaderClassResolver(classloader)).bind("ref", ObjectRefStore.ref()); } } diff --git a/core/src/main/java/com/taobao/arthas/core/command/express/ObjectRefStore.java b/core/src/main/java/com/taobao/arthas/core/command/express/ObjectRefStore.java new file mode 100644 index 00000000000..6ac9adf3143 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/express/ObjectRefStore.java @@ -0,0 +1,336 @@ +package com.taobao.arthas.core.command.express; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; + +public final class ObjectRefStore { + private static final int DEFAULT_MAX_ENTRIES = 1024; + private static final String DEFAULT_NAMESPACE = "default"; + + private static final ObjectRefStore INSTANCE = new ObjectRefStore(DEFAULT_MAX_ENTRIES); + + private final ReferenceQueue referenceQueue = new ReferenceQueue(); + private final LinkedHashMap entries; + private final int maxEntries; + private final AtomicLong idGenerator = new AtomicLong(0); + + private final Ref rootRef; + + private ObjectRefStore(int maxEntries) { + this.maxEntries = maxEntries; + this.entries = new LinkedHashMap(16, 0.75f, true); + this.rootRef = new Ref(this, DEFAULT_NAMESPACE); + } + + public static Ref ref() { + return INSTANCE.rootRef; + } + + private synchronized void drainReferenceQueue() { + Reference reference; + while ((reference = referenceQueue.poll()) != null) { + if (!(reference instanceof KeyedWeakReference)) { + continue; + } + KeyedWeakReference keyedReference = (KeyedWeakReference) reference; + Entry current = entries.get(keyedReference.key); + if (current != null && current.id == keyedReference.id) { + entries.remove(keyedReference.key); + } + } + } + + private static String normalizeNamespace(String namespace) { + if (namespace == null) { + return DEFAULT_NAMESPACE; + } + String trimmed = namespace.trim(); + if (trimmed.isEmpty()) { + return DEFAULT_NAMESPACE; + } + return trimmed; + } + + private static String requireKey(String key) { + if (key == null) { + throw new IllegalArgumentException("key is null"); + } + String trimmed = key.trim(); + if (trimmed.isEmpty()) { + throw new IllegalArgumentException("key is blank"); + } + return trimmed; + } + + private synchronized Object put(String namespace, String key, Object value) { + drainReferenceQueue(); + + String normalizedNamespace = normalizeNamespace(namespace); + String normalizedKey = requireKey(key); + + if (value == null) { + remove(normalizedNamespace, normalizedKey); + return null; + } + + long now = System.currentTimeMillis(); + StoreKey storeKey = new StoreKey(normalizedNamespace, normalizedKey); + long id = idGenerator.incrementAndGet(); + Entry entry = Entry.create(id, storeKey, value, now, referenceQueue); + entries.put(storeKey, entry); + evictIfNecessary(); + return value; + } + + private synchronized Object get(String namespace, String key) { + drainReferenceQueue(); + + String normalizedNamespace = normalizeNamespace(namespace); + String normalizedKey = requireKey(key); + + StoreKey storeKey = new StoreKey(normalizedNamespace, normalizedKey); + Entry entry = entries.get(storeKey); + if (entry == null) { + return null; + } + + Object value = entry.reference.get(); + if (value == null) { + entries.remove(storeKey); + return null; + } + entry.lastAccessTime = System.currentTimeMillis(); + return value; + } + + private synchronized Object remove(String namespace, String key) { + drainReferenceQueue(); + + String normalizedNamespace = normalizeNamespace(namespace); + String normalizedKey = requireKey(key); + + StoreKey storeKey = new StoreKey(normalizedNamespace, normalizedKey); + Entry entry = entries.remove(storeKey); + if (entry == null) { + return null; + } + return entry.reference.get(); + } + + private synchronized void clearNamespace(String namespace) { + drainReferenceQueue(); + + String normalizedNamespace = normalizeNamespace(namespace); + Iterator> it = entries.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry mapEntry = it.next(); + if (normalizedNamespace.equals(mapEntry.getKey().namespace)) { + it.remove(); + } + } + } + + private synchronized void clearAll() { + drainReferenceQueue(); + entries.clear(); + } + + private synchronized List> list(String namespace) { + drainReferenceQueue(); + + String normalizedNamespace = normalizeNamespace(namespace); + List> result = new ArrayList>(); + + long now = System.currentTimeMillis(); + Iterator> it = entries.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry mapEntry = it.next(); + StoreKey storeKey = mapEntry.getKey(); + if (!normalizedNamespace.equals(storeKey.namespace)) { + continue; + } + + Entry entry = mapEntry.getValue(); + Object value = entry.reference.get(); + if (value == null) { + it.remove(); + continue; + } + + Map item = new LinkedHashMap(); + item.put("namespace", storeKey.namespace); + item.put("name", storeKey.key); + item.put("class", entry.className); + item.put("identityHash", entry.identityHashHex); + item.put("classLoader", entry.classLoader); + item.put("createTime", entry.createTime); + item.put("lastAccessTime", entry.lastAccessTime); + item.put("ageMillis", now - entry.createTime); + item.put("idleMillis", now - entry.lastAccessTime); + result.add(item); + } + + return result; + } + + private synchronized List namespaces() { + drainReferenceQueue(); + + Set namespaces = new LinkedHashSet(); + Iterator> it = entries.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry mapEntry = it.next(); + Entry entry = mapEntry.getValue(); + if (entry.reference.get() == null) { + it.remove(); + continue; + } + namespaces.add(mapEntry.getKey().namespace); + } + List result = new ArrayList(namespaces); + Collections.sort(result); + return result; + } + + private void evictIfNecessary() { + while (entries.size() > maxEntries) { + Iterator> it = entries.entrySet().iterator(); + if (!it.hasNext()) { + return; + } + it.next(); + it.remove(); + } + } + + public static final class Ref { + private final ObjectRefStore store; + private final String namespace; + + private Ref(ObjectRefStore store, String namespace) { + this.store = store; + this.namespace = namespace; + } + + public Ref ns(String namespace) { + return new Ref(store, normalizeNamespace(namespace)); + } + + public String namespace() { + return namespace; + } + + public Object put(String key, Object value) { + return store.put(namespace, key, value); + } + + public Object get(String key) { + return store.get(namespace, key); + } + + public Object remove(String key) { + return store.remove(namespace, key); + } + + public void clear() { + store.clearNamespace(namespace); + } + + public void clearAll() { + store.clearAll(); + } + + public List> ls() { + return store.list(namespace); + } + + public List namespaces() { + return store.namespaces(); + } + } + + private static final class StoreKey { + private final String namespace; + private final String key; + private final int hash; + + private StoreKey(String namespace, String key) { + this.namespace = namespace; + this.key = key; + this.hash = 31 * namespace.hashCode() + key.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof StoreKey)) { + return false; + } + StoreKey other = (StoreKey) obj; + return namespace.equals(other.namespace) && key.equals(other.key); + } + + @Override + public int hashCode() { + return hash; + } + } + + private static final class Entry { + private final long id; + private final KeyedWeakReference reference; + private final String className; + private final String classLoader; + private final String identityHashHex; + private final long createTime; + private volatile long lastAccessTime; + + private Entry(long id, KeyedWeakReference reference, String className, String classLoader, String identityHashHex, + long createTime, long lastAccessTime) { + this.id = id; + this.reference = reference; + this.className = className; + this.classLoader = classLoader; + this.identityHashHex = identityHashHex; + this.createTime = createTime; + this.lastAccessTime = lastAccessTime; + } + + private static Entry create(long id, StoreKey key, Object value, long now, ReferenceQueue queue) { + Class clazz = value.getClass(); + String className = clazz.getName(); + + ClassLoader loader = clazz.getClassLoader(); + String classLoader = loader == null ? "bootstrap" + : loader.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(loader)); + + String identityHashHex = Integer.toHexString(System.identityHashCode(value)); + KeyedWeakReference reference = new KeyedWeakReference(value, queue, key, id); + return new Entry(id, reference, className, classLoader, identityHashHex, now, now); + } + } + + private static final class KeyedWeakReference extends WeakReference { + private final StoreKey key; + private final long id; + + private KeyedWeakReference(Object referent, ReferenceQueue q, StoreKey key, long id) { + super(referent, q); + this.key = key; + this.id = id; + } + } +} + diff --git a/core/src/test/java/com/taobao/arthas/core/command/express/ObjectRefStoreTest.java b/core/src/test/java/com/taobao/arthas/core/command/express/ObjectRefStoreTest.java new file mode 100644 index 00000000000..77f459d6878 --- /dev/null +++ b/core/src/test/java/com/taobao/arthas/core/command/express/ObjectRefStoreTest.java @@ -0,0 +1,73 @@ +package com.taobao.arthas.core.command.express; + +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +public class ObjectRefStoreTest { + + @Test + public void testPutGetRemoveWithNamespace() throws ExpressException { + ObjectRefStore.ref().clearAll(); + + Object value = new Object(); + + Express express1 = ExpressFactory.unpooledExpress(ObjectRefStoreTest.class.getClassLoader()); + express1.bind("v", value).bind(new Object()); + Object putResult = express1.get("#ref.ns(\"ns1\").put(\"k\", #v)"); + Assert.assertSame(value, putResult); + + Express express2 = ExpressFactory.unpooledExpress(ObjectRefStoreTest.class.getClassLoader()); + express2.bind(new Object()); + Object getResult = express2.get("#ref.ns(\"ns1\").get(\"k\")"); + Assert.assertSame(value, getResult); + + Object removeResult = express2.get("#ref.ns(\"ns1\").remove(\"k\")"); + Assert.assertSame(value, removeResult); + + Object afterRemove = express2.get("#ref.ns(\"ns1\").get(\"k\")"); + Assert.assertNull(afterRemove); + } + + @Test + public void testNamespaceIsolation() throws ExpressException { + ObjectRefStore.ref().clearAll(); + + Object valueA = new Object(); + Object valueB = new Object(); + + Express express = ExpressFactory.unpooledExpress(ObjectRefStoreTest.class.getClassLoader()); + express.bind("a", valueA).bind("b", valueB).bind(new Object()); + + Assert.assertSame(valueA, express.get("#ref.ns(\"a\").put(\"k\", #a)")); + Assert.assertSame(valueB, express.get("#ref.ns(\"b\").put(\"k\", #b)")); + + Assert.assertSame(valueA, express.get("#ref.ns(\"a\").get(\"k\")")); + Assert.assertSame(valueB, express.get("#ref.ns(\"b\").get(\"k\")")); + Assert.assertNull(express.get("#ref.ns(\"c\").get(\"k\")")); + } + + @Test + public void testLsAndNamespaces() throws ExpressException { + ObjectRefStore.ref().clearAll(); + + Object value = new Object(); + + Express express = ExpressFactory.unpooledExpress(ObjectRefStoreTest.class.getClassLoader()); + express.bind("v", value).bind(new Object()); + express.get("#ref.ns(\"ns-list\").put(\"k\", #v)"); + + List ls = (List) express.get("#ref.ns(\"ns-list\").ls()"); + Assert.assertEquals(1, ls.size()); + + Map item = (Map) ls.get(0); + Assert.assertEquals("ns-list", item.get("namespace")); + Assert.assertEquals("k", item.get("name")); + + List namespaces = (List) express.get("#ref.namespaces()"); + Assert.assertTrue(namespaces.contains("ns-list")); + } +} + diff --git a/site/docs/doc/advice-class.md b/site/docs/doc/advice-class.md index 623fb108c58..284bb904f22 100644 --- a/site/docs/doc/advice-class.md +++ b/site/docs/doc/advice-class.md @@ -37,6 +37,20 @@ public class Advice { | isThrow | 辅助判断标记,当前的方法调用以抛异常的形式结束。 | | isReturn | 辅助判断标记,当前的方法调用以正常返回的形式结束。 | +## 额外上下文变量 + +除了上面 `Advice` 里的字段,Arthas 还会在 OGNL 上下文里额外注入一些变量: + +| 变量名 | 说明 | +| ------: | :------------------------------------- | +| `#cost` | 本次调用耗时(毫秒) | +| `#ref` | 对象引用存储器(弱引用,支持命名空间) | + +示例: + +- 存入:`#ref.ns("case-123").put("obj", returnObj)` +- 取出:`#ref.ns("case-123").get("obj")`(对象可能已被 GC,返回 `null` 是正常行为) + 所有变量都可以在表达式中直接使用,如果在表达式中编写了不符合 OGNL 脚本语法或者引入了不在表格中的变量,则退出命令的执行;用户可以根据当前的异常信息修正`条件表达式`或`观察表达式` - 特殊用法请参考:[https://github.com/alibaba/arthas/issues/71](https://github.com/alibaba/arthas/issues/71) diff --git a/site/docs/doc/ognl.md b/site/docs/doc/ognl.md index 055c47efc5e..61fa0a030b6 100644 --- a/site/docs/doc/ognl.md +++ b/site/docs/doc/ognl.md @@ -22,6 +22,28 @@ - OGNL 特殊用法请参考:[https://github.com/alibaba/arthas/issues/71](https://github.com/alibaba/arthas/issues/71) - OGNL 表达式官方指南:[https://commons.apache.org/dormant/commons-ognl/language-guide.html](https://commons.apache.org/dormant/commons-ognl/language-guide.html) +## 对象引用存储器(#ref) + +Arthas 在 OGNL 上下文里内置了 `#ref` 变量,用于在多次命令之间共享对象引用。 + +`#ref` 保存的是**弱引用**,不会阻止应用 JVM 回收对象,所以 `get()` 可能返回 `null`(对象已被 GC),这是正常行为。 + +`#ref` 是全局共享的(同一个 Arthas 进程内所有连接可见)。建议使用命名空间隔离/协作: + +- 为避免 key 无限制增长,`#ref` 内部有容量上限,超过后会按 LRU(最近最少使用)策略淘汰。 +- 存入:`#ref.ns("case-123").put("name", obj)` +- 取出:`#ref.ns("case-123").get("name")` +- 列表:`#ref.ns("case-123").ls()` +- 删除:`#ref.ns("case-123").remove("name")` +- 清空命名空间:`#ref.ns("case-123").clear()` + +示例:配合 `watch` 暂存返回值,后续 `ognl` 再取出: + +```bash +$ watch demo.MathGame primeFactors '{#ref.ns("case-123").put("ret", returnObj), returnObj}' -x 2 -n 1 +$ ognl '#ref.ns("case-123").get("ret")' +``` + 调用静态函数: ```bash diff --git a/site/docs/doc/vmtool.md b/site/docs/doc/vmtool.md index 9bbb252f424..6ae30e940ef 100644 --- a/site/docs/doc/vmtool.md +++ b/site/docs/doc/vmtool.md @@ -82,6 +82,13 @@ vmtool --action getInstances -c 19469ea2 --className org.springframework.context vmtool --action getInstances --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader --className org.springframework.context.ApplicationContext --express 'instances[0].getBeanDefinitionNames()' ``` +`--express` 的 OGNL 上下文里同样支持 `#ref` 对象引用存储器(弱引用),可以把中间对象保存下来,后续在其它命令里再取出: + +```bash +vmtool --action getInstances --className org.springframework.context.ApplicationContext --express '#ref.ns("case-123").put("ctx", instances[0])' +ognl '#ref.ns("case-123").get("ctx")' +``` + ## 强制 GC ```bash diff --git a/site/docs/en/doc/advice-class.md b/site/docs/en/doc/advice-class.md index f78f99cfe60..a59f2dae37f 100644 --- a/site/docs/en/doc/advice-class.md +++ b/site/docs/en/doc/advice-class.md @@ -35,6 +35,20 @@ Description for the variables in the class `Advice`: | isThrow | flag to indicate the method call ends with exception thrown | | isReturn | flag to indicate the method call ends normally without exception thrown | +## Extra Context Variables + +Besides the fields in `Advice`, Arthas also injects extra variables into OGNL context: + +| Name | Specification | +| ------: | :------------------------------------------------------- | +| `#cost` | time cost in milliseconds | +| `#ref` | object reference store (weak reference, with namespaces) | + +Examples: + +- Put: `#ref.ns("case-123").put("obj", returnObj)` +- Get: `#ref.ns("case-123").get("obj")` (may return `null` if GC'ed, which is expected) + All variables listed above can be used directly in the [OGNL expression](https://commons.apache.org/dormant/commons-ognl/language-guide.html). The command will not execute and exit if there's illegal OGNL grammar or unexpected variable in the expression. - [typical use cases](https://github.com/alibaba/arthas/issues/71); diff --git a/site/docs/en/doc/ognl.md b/site/docs/en/doc/ognl.md index 9c6308a6ff1..b2cfb39379c 100644 --- a/site/docs/en/doc/ognl.md +++ b/site/docs/en/doc/ognl.md @@ -22,6 +22,28 @@ Since 3.0.5. - [Special usages](https://github.com/alibaba/arthas/issues/71) - [OGNL official guide](https://commons.apache.org/dormant/commons-ognl/language-guide.html) +## Object Reference Store (#ref) + +Arthas provides a built-in `#ref` variable in OGNL context, which can be used to share object references across multiple commands. + +`#ref` stores **weak references**, so it will not prevent the target JVM from garbage collecting the objects. `get()` may return `null` (the object has been GC'ed), which is expected. + +`#ref` is global and shared across all connections in the same Arthas process. Use namespaces to isolate/share: + +- To avoid unbounded key growth, `#ref` has a size limit and uses LRU eviction. +- Put: `#ref.ns("case-123").put("name", obj)` +- Get: `#ref.ns("case-123").get("name")` +- List: `#ref.ns("case-123").ls()` +- Remove: `#ref.ns("case-123").remove("name")` +- Clear namespace: `#ref.ns("case-123").clear()` + +Example: store `returnObj` from `watch`, then fetch it later in `ognl`: + +```bash +$ watch demo.MathGame primeFactors '{#ref.ns("case-123").put("ret", returnObj), returnObj}' -x 2 -n 1 +$ ognl '#ref.ns("case-123").get("ret")' +``` + Call static method: ```bash diff --git a/site/docs/en/doc/vmtool.md b/site/docs/en/doc/vmtool.md index 19ac2b0a133..0b698f052e4 100644 --- a/site/docs/en/doc/vmtool.md +++ b/site/docs/en/doc/vmtool.md @@ -82,6 +82,13 @@ The return result of the `getInstances` action is bound to the `instances` varia vmtool --action getInstances --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader --className org.springframework.context.ApplicationContext --express'instances[0].getBeanDefinitionNames()' ``` +`#ref` (weak reference object store) is also available in `--express` OGNL context, so you can store intermediate objects and reuse them in other commands: + +```bash +vmtool --action getInstances --className org.springframework.context.ApplicationContext --express '#ref.ns("case-123").put("ctx", instances[0])' +ognl '#ref.ns("case-123").get("ctx")' +``` + ## Force GC ```bash