diff --git a/bytekit/pom.xml b/bytekit/pom.xml index 1577094b0..fed34dee7 100644 --- a/bytekit/pom.xml +++ b/bytekit/pom.xml @@ -69,6 +69,12 @@ test true + + com.taobao.arthas + arthas-demo + ${project.version} + test + diff --git a/bytekit/src/main/java/com/taobao/arthas/bytekit/utils/AsmOpUtils.java b/bytekit/src/main/java/com/taobao/arthas/bytekit/utils/AsmOpUtils.java index d557c3323..81f1a7f85 100644 --- a/bytekit/src/main/java/com/taobao/arthas/bytekit/utils/AsmOpUtils.java +++ b/bytekit/src/main/java/com/taobao/arthas/bytekit/utils/AsmOpUtils.java @@ -62,6 +62,15 @@ public class AsmOpUtils { private static final Method DOUBLE_VALUE = Method.getMethod("double doubleValue()"); + public static boolean isBoxType(final Type type) { + if (BYTE_TYPE.equals(type) || BOOLEAN_TYPE.equals(type) || SHORT_TYPE.equals(type) + || CHARACTER_TYPE.equals(type) || INTEGER_TYPE.equals(type) || FLOAT_TYPE.equals(type) + || LONG_TYPE.equals(type) || DOUBLE_TYPE.equals(type)) { + return true; + } + return false; + } + public static Type getBoxedType(final Type type) { switch (type.getSort()) { case Type.BYTE: diff --git a/common/src/main/java/com/taobao/arthas/common/concurrent/ConcurrentWeakKeyHashMap.java b/common/src/main/java/com/taobao/arthas/common/concurrent/ConcurrentWeakKeyHashMap.java new file mode 100644 index 000000000..a77770b29 --- /dev/null +++ b/common/src/main/java/com/taobao/arthas/common/concurrent/ConcurrentWeakKeyHashMap.java @@ -0,0 +1,1473 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +/* + * Written by Doug Lea with assistance from members of JCP JSR-166 + * Expert Group and released to the public domain, as explained at + * http://creativecommons.org/licenses/publicdomain + */ +package com.taobao.arthas.common.concurrent; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.AbstractCollection; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.ConcurrentModificationException; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.ReentrantLock; + + +/** + * An alternative weak-key {@link ConcurrentMap} which is similar to + * {@link ConcurrentHashMap}. + * @param the type of keys maintained by this map + * @param the type of mapped values + */ +public final class ConcurrentWeakKeyHashMap extends AbstractMap implements ConcurrentMap { + + /* + * The basic strategy is to subdivide the table among Segments, + * each of which itself is a concurrently readable hash table. + */ + + /** + * The default initial capacity for this table, used when not otherwise + * specified in a constructor. + */ + static final int DEFAULT_INITIAL_CAPACITY = 16; + + /** + * The default load factor for this table, used when not otherwise specified + * in a constructor. + */ + static final float DEFAULT_LOAD_FACTOR = 0.75f; + + /** + * The default concurrency level for this table, used when not otherwise + * specified in a constructor. + */ + static final int DEFAULT_CONCURRENCY_LEVEL = 16; + + /** + * The maximum capacity, used if a higher value is implicitly specified by + * either of the constructors with arguments. MUST be a power of two + * <= 1<<30 to ensure that entries are indexable using integers. + */ + static final int MAXIMUM_CAPACITY = 1 << 30; + + /** + * The maximum number of segments to allow; used to bound constructor + * arguments. + */ + static final int MAX_SEGMENTS = 1 << 16; // slightly conservative + + /** + * Number of unsynchronized retries in size and containsValue methods before + * resorting to locking. This is used to avoid unbounded retries if tables + * undergo continuous modification which would make it impossible to obtain + * an accurate result. + */ + static final int RETRIES_BEFORE_LOCK = 2; + + /* ---------------- Fields -------------- */ + + /** + * Mask value for indexing into segments. The upper bits of a key's hash + * code are used to choose the segment. + */ + final int segmentMask; + + /** + * Shift value for indexing within segments. + */ + final int segmentShift; + + /** + * The segments, each of which is a specialized hash table + */ + final Segment[] segments; + + Set keySet; + Set> entrySet; + Collection values; + + /* ---------------- Small Utilities -------------- */ + + /** + * Applies a supplemental hash function to a given hashCode, which defends + * against poor quality hash functions. This is critical because + * ConcurrentReferenceHashMap uses power-of-two length hash tables, that + * otherwise encounter collisions for hashCodes that do not differ in lower + * or upper bits. + */ + private static int hash(int h) { + // Spread bits to regularize both segment and index locations, + // using variant of single-word Wang/Jenkins hash. + h += h << 15 ^ 0xffffcd7d; + h ^= h >>> 10; + h += h << 3; + h ^= h >>> 6; + h += (h << 2) + (h << 14); + return h ^ h >>> 16; + } + + /** + * Returns the segment that should be used for key with given hash. + * + * @param hash the hash code for the key + * @return the segment + */ + Segment segmentFor(int hash) { + return segments[hash >>> segmentShift & segmentMask]; + } + + private static int hashOf(Object key) { + return hash(key.hashCode()); + } + + /* ---------------- Inner Classes -------------- */ + + /** + * A weak-key reference which stores the key hash needed for reclamation. + */ + static final class WeakKeyReference extends WeakReference { + + final int hash; + + WeakKeyReference(K key, int hash, ReferenceQueue refQueue) { + super(key, refQueue); + this.hash = hash; + } + + public int keyHash() { + return hash; + } + + public Object keyRef() { + return this; + } + } + + /** + * ConcurrentReferenceHashMap list entry. Note that this is never exported + * out as a user-visible Map.Entry. + * + * Because the value field is volatile, not final, it is legal wrt + * the Java Memory Model for an unsynchronized reader to see null + * instead of initial value when read via a data race. Although a + * reordering leading to this is not likely to ever actually + * occur, the Segment.readValueUnderLock method is used as a + * backup in case a null (pre-initialized) value is ever seen in + * an unsynchronized access method. + */ + static final class HashEntry { + final Object keyRef; + final int hash; + volatile Object valueRef; + final HashEntry next; + + HashEntry( + K key, int hash, HashEntry next, V value, + ReferenceQueue refQueue) { + this.hash = hash; + this.next = next; + keyRef = new WeakKeyReference(key, hash, refQueue); + valueRef = value; + } + + @SuppressWarnings("unchecked") + K key() { + return ((Reference) keyRef).get(); + } + + V value() { + return dereferenceValue(valueRef); + } + + @SuppressWarnings("unchecked") + V dereferenceValue(Object value) { + if (value instanceof WeakKeyReference) { + return ((Reference) value).get(); + } + + return (V) value; + } + + void setValue(V value) { + valueRef = value; + } + + @SuppressWarnings("unchecked") + static HashEntry[] newArray(int i) { + return new HashEntry[i]; + } + } + + /** + * Segments are specialized versions of hash tables. This subclasses from + * ReentrantLock opportunistically, just to simplify some locking and avoid + * separate construction. + */ + static final class Segment extends ReentrantLock { + /* + * Segments maintain a table of entry lists that are ALWAYS kept in a + * consistent state, so can be read without locking. Next fields of + * nodes are immutable (final). All list additions are performed at the + * front of each bin. This makes it easy to check changes, and also fast + * to traverse. When nodes would otherwise be changed, new nodes are + * created to replace them. This works well for hash tables since the + * bin lists tend to be short. (The average length is less than two for + * the default load factor threshold.) + * + * Read operations can thus proceed without locking, but rely on + * selected uses of volatiles to ensure that completed write operations + * performed by other threads are noticed. For most purposes, the + * "count" field, tracking the number of elements, serves as that + * volatile variable ensuring visibility. This is convenient because + * this field needs to be read in many read operations anyway: + * + * - All (unsynchronized) read operations must first read the + * "count" field, and should not look at table entries if + * it is 0. + * + * - All (synchronized) write operations should write to + * the "count" field after structurally changing any bin. + * The operations must not take any action that could even + * momentarily cause a concurrent read operation to see + * inconsistent data. This is made easier by the nature of + * the read operations in Map. For example, no operation + * can reveal that the table has grown but the threshold + * has not yet been updated, so there are no atomicity + * requirements for this with respect to reads. + * + * As a guide, all critical volatile reads and writes to the count field + * are marked in code comments. + */ + + private static final long serialVersionUID = -8328104880676891126L; + + /** + * The number of elements in this segment's region. + */ + transient volatile int count; + + /** + * Number of updates that alter the size of the table. This is used + * during bulk-read methods to make sure they see a consistent snapshot: + * If modCounts change during a traversal of segments computing size or + * checking containsValue, then we might have an inconsistent view of + * state so (usually) must retry. + */ + int modCount; + + /** + * The table is rehashed when its size exceeds this threshold. + * (The value of this field is always (capacity * loadFactor).) + */ + int threshold; + + /** + * The per-segment table. + */ + transient volatile HashEntry[] table; + + /** + * The load factor for the hash table. Even though this value is same + * for all segments, it is replicated to avoid needing links to outer + * object. + */ + final float loadFactor; + + /** + * The collected weak-key reference queue for this segment. This should + * be (re)initialized whenever table is assigned, + */ + transient volatile ReferenceQueue refQueue; + + Segment(int initialCapacity, float lf) { + loadFactor = lf; + setTable(HashEntry.newArray(initialCapacity)); + } + + @SuppressWarnings("unchecked") + static Segment[] newArray(int i) { + return new Segment[i]; + } + + private static boolean keyEq(Object src, Object dest) { + return src.equals(dest); + } + + /** + * Sets table to new HashEntry array. Call only while holding lock or in + * constructor. + */ + void setTable(HashEntry[] newTable) { + threshold = (int) (newTable.length * loadFactor); + table = newTable; + refQueue = new ReferenceQueue(); + } + + /** + * Returns properly casted first entry of bin for given hash. + */ + HashEntry getFirst(int hash) { + HashEntry[] tab = table; + return tab[hash & tab.length - 1]; + } + + HashEntry newHashEntry( + K key, int hash, HashEntry next, V value) { + return new HashEntry( + key, hash, next, value, refQueue); + } + + /** + * Reads value field of an entry under lock. Called if value field ever + * appears to be null. This is possible only if a compiler happens to + * reorder a HashEntry initialization with its table assignment, which + * is legal under memory model but is not known to ever occur. + */ + V readValueUnderLock(HashEntry e) { + lock(); + try { + removeStale(); + return e.value(); + } finally { + unlock(); + } + } + + /* Specialized implementations of map methods */ + + V get(Object key, int hash) { + if (count != 0) { // read-volatile + HashEntry e = getFirst(hash); + while (e != null) { + if (e.hash == hash && keyEq(key, e.key())) { + Object opaque = e.valueRef; + if (opaque != null) { + return e.dereferenceValue(opaque); + } + + return readValueUnderLock(e); // recheck + } + e = e.next; + } + } + return null; + } + + boolean containsKey(Object key, int hash) { + if (count != 0) { // read-volatile + HashEntry e = getFirst(hash); + while (e != null) { + if (e.hash == hash && keyEq(key, e.key())) { + return true; + } + e = e.next; + } + } + return false; + } + + boolean containsValue(Object value) { + if (count != 0) { // read-volatile + for (HashEntry e: table) { + for (; e != null; e = e.next) { + Object opaque = e.valueRef; + V v; + + if (opaque == null) { + v = readValueUnderLock(e); // recheck + } else { + v = e.dereferenceValue(opaque); + } + + if (value.equals(v)) { + return true; + } + } + } + } + return false; + } + + boolean replace(K key, int hash, V oldValue, V newValue) { + lock(); + try { + removeStale(); + HashEntry e = getFirst(hash); + while (e != null && (e.hash != hash || !keyEq(key, e.key()))) { + e = e.next; + } + + boolean replaced = false; + if (e != null && oldValue.equals(e.value())) { + replaced = true; + e.setValue(newValue); + } + return replaced; + } finally { + unlock(); + } + } + + V replace(K key, int hash, V newValue) { + lock(); + try { + removeStale(); + HashEntry e = getFirst(hash); + while (e != null && (e.hash != hash || !keyEq(key, e.key()))) { + e = e.next; + } + + V oldValue = null; + if (e != null) { + oldValue = e.value(); + e.setValue(newValue); + } + return oldValue; + } finally { + unlock(); + } + } + + V put(K key, int hash, V value, boolean onlyIfAbsent) { + lock(); + try { + removeStale(); + int c = count; + if (c ++ > threshold) { // ensure capacity + int reduced = rehash(); + if (reduced > 0) { + count = (c -= reduced) - 1; // write-volatile + } + } + + HashEntry[] tab = table; + int index = hash & tab.length - 1; + HashEntry first = tab[index]; + HashEntry e = first; + while (e != null && (e.hash != hash || !keyEq(key, e.key()))) { + e = e.next; + } + + V oldValue; + if (e != null) { + oldValue = e.value(); + if (!onlyIfAbsent) { + e.setValue(value); + } + } else { + oldValue = null; + ++ modCount; + tab[index] = newHashEntry(key, hash, first, value); + count = c; // write-volatile + } + return oldValue; + } finally { + unlock(); + } + } + + int rehash() { + HashEntry[] oldTable = table; + int oldCapacity = oldTable.length; + if (oldCapacity >= MAXIMUM_CAPACITY) { + return 0; + } + + /* + * Reclassify nodes in each list to new Map. Because we are using + * power-of-two expansion, the elements from each bin must either + * stay at same index, or move with a power of two offset. We + * eliminate unnecessary node creation by catching cases where old + * nodes can be reused because their next fields won't change. + * Statistically, at the default threshold, only about one-sixth of + * them need cloning when a table doubles. The nodes they replace + * will be garbage collectable as soon as they are no longer + * referenced by any reader thread that may be in the midst of + * traversing table right now. + */ + + HashEntry[] newTable = HashEntry.newArray(oldCapacity << 1); + threshold = (int) (newTable.length * loadFactor); + int sizeMask = newTable.length - 1; + int reduce = 0; + for (HashEntry e: oldTable) { + // We need to guarantee that any existing reads of old Map can + // proceed. So we cannot yet null out each bin. + if (e != null) { + HashEntry next = e.next; + int idx = e.hash & sizeMask; + + // Single node on list + if (next == null) { + newTable[idx] = e; + } else { + // Reuse trailing consecutive sequence at same slot + HashEntry lastRun = e; + int lastIdx = idx; + for (HashEntry last = next; last != null; last = last.next) { + int k = last.hash & sizeMask; + if (k != lastIdx) { + lastIdx = k; + lastRun = last; + } + } + newTable[lastIdx] = lastRun; + // Clone all remaining nodes + for (HashEntry p = e; p != lastRun; p = p.next) { + // Skip GC'd weak references + K key = p.key(); + if (key == null) { + reduce++; + continue; + } + int k = p.hash & sizeMask; + HashEntry n = newTable[k]; + newTable[k] = newHashEntry(key, p.hash, n, p.value()); + } + } + } + } + table = newTable; + return reduce; + } + + /** + * Remove; match on key only if value null, else match both. + */ + V remove(Object key, int hash, Object value, boolean refRemove) { + lock(); + try { + if (!refRemove) { + removeStale(); + } + int c = count - 1; + HashEntry[] tab = table; + int index = hash & tab.length - 1; + HashEntry first = tab[index]; + HashEntry e = first; + // a reference remove operation compares the Reference instance + while (e != null && key != e.keyRef && + (refRemove || hash != e.hash || !keyEq(key, e.key()))) { + e = e.next; + } + + V oldValue = null; + if (e != null) { + V v = e.value(); + if (value == null || value.equals(v)) { + oldValue = v; + // All entries following removed node can stay in list, + // but all preceding ones need to be cloned. + ++ modCount; + HashEntry newFirst = e.next; + for (HashEntry p = first; p != e; p = p.next) { + K pKey = p.key(); + if (pKey == null) { // Skip GC'd keys + c --; + continue; + } + + newFirst = newHashEntry( + pKey, p.hash, newFirst, p.value()); + } + tab[index] = newFirst; + count = c; // write-volatile + } + } + return oldValue; + } finally { + unlock(); + } + } + + @SuppressWarnings("rawtypes") + void removeStale() { + WeakKeyReference ref; + while ((ref = (WeakKeyReference) refQueue.poll()) != null) { + remove(ref.keyRef(), ref.keyHash(), null, true); + } + } + + void clear() { + if (count != 0) { + lock(); + try { + HashEntry[] tab = table; + for (int i = 0; i < tab.length; i ++) { + tab[i] = null; + } + ++ modCount; + // replace the reference queue to avoid unnecessary stale + // cleanups + refQueue = new ReferenceQueue(); + count = 0; // write-volatile + } finally { + unlock(); + } + } + } + } + + /* ---------------- Public operations -------------- */ + + /** + * Creates a new, empty map with the specified initial capacity, load factor + * and concurrency level. + * + * @param initialCapacity the initial capacity. The implementation performs + * internal sizing to accommodate this many elements. + * @param loadFactor the load factor threshold, used to control resizing. + * Resizing may be performed when the average number of + * elements per bin exceeds this threshold. + * @param concurrencyLevel the estimated number of concurrently updating + * threads. The implementation performs internal + * sizing to try to accommodate this many threads. + * @throws IllegalArgumentException if the initial capacity is negative or + * the load factor or concurrencyLevel are + * nonpositive. + */ + public ConcurrentWeakKeyHashMap( + int initialCapacity, float loadFactor, int concurrencyLevel) { + if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0) { + throw new IllegalArgumentException(); + } + + if (concurrencyLevel > MAX_SEGMENTS) { + concurrencyLevel = MAX_SEGMENTS; + } + + // Find power-of-two sizes best matching arguments + int sshift = 0; + int ssize = 1; + while (ssize < concurrencyLevel) { + ++ sshift; + ssize <<= 1; + } + segmentShift = 32 - sshift; + segmentMask = ssize - 1; + segments = Segment.newArray(ssize); + + if (initialCapacity > MAXIMUM_CAPACITY) { + initialCapacity = MAXIMUM_CAPACITY; + } + int c = initialCapacity / ssize; + if (c * ssize < initialCapacity) { + ++ c; + } + int cap = 1; + while (cap < c) { + cap <<= 1; + } + + for (int i = 0; i < segments.length; ++ i) { + segments[i] = new Segment(cap, loadFactor); + } + } + + /** + * Creates a new, empty map with the specified initial capacity and load + * factor and with the default reference types (weak keys, strong values), + * and concurrencyLevel (16). + * + * @param initialCapacity The implementation performs internal sizing to + * accommodate this many elements. + * @param loadFactor the load factor threshold, used to control resizing. + * Resizing may be performed when the average number of + * elements per bin exceeds this threshold. + * @throws IllegalArgumentException if the initial capacity of elements is + * negative or the load factor is + * nonpositive + */ + public ConcurrentWeakKeyHashMap(int initialCapacity, float loadFactor) { + this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL); + } + + /** + * Creates a new, empty map with the specified initial capacity, and with + * default reference types (weak keys, strong values), load factor (0.75) + * and concurrencyLevel (16). + * + * @param initialCapacity the initial capacity. The implementation performs + * internal sizing to accommodate this many elements. + * @throws IllegalArgumentException if the initial capacity of elements is + * negative. + */ + public ConcurrentWeakKeyHashMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL); + } + + /** + * Creates a new, empty map with a default initial capacity (16), reference + * types (weak keys, strong values), default load factor (0.75) and + * concurrencyLevel (16). + */ + public ConcurrentWeakKeyHashMap() { + this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL); + } + + /** + * Creates a new map with the same mappings as the given map. The map is + * created with a capacity of 1.5 times the number of mappings in the given + * map or 16 (whichever is greater), and a default load factor (0.75) and + * concurrencyLevel (16). + * + * @param m the map + */ + public ConcurrentWeakKeyHashMap(Map m) { + this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, + DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR, + DEFAULT_CONCURRENCY_LEVEL); + putAll(m); + } + + /** + * Returns true if this map contains no key-value mappings. + * + * @return true if this map contains no key-value mappings + */ + @Override + public boolean isEmpty() { + final Segment[] segments = this.segments; + /* + * We keep track of per-segment modCounts to avoid ABA problems in which + * an element in one segment was added and in another removed during + * traversal, in which case the table was never actually empty at any + * point. Note the similar use of modCounts in the size() and + * containsValue() methods, which are the only other methods also + * susceptible to ABA problems. + */ + int[] mc = new int[segments.length]; + int mcsum = 0; + for (int i = 0; i < segments.length; ++ i) { + if (segments[i].count != 0) { + return false; + } else { + mcsum += mc[i] = segments[i].modCount; + } + } + // If mcsum happens to be zero, then we know we got a snapshot before + // any modifications at all were made. This is probably common enough + // to bother tracking. + if (mcsum != 0) { + for (int i = 0; i < segments.length; ++ i) { + if (segments[i].count != 0 || mc[i] != segments[i].modCount) { + return false; + } + } + } + return true; + } + + /** + * Returns the number of key-value mappings in this map. If the map contains + * more than Integer.MAX_VALUE elements, returns + * Integer.MAX_VALUE. + * + * @return the number of key-value mappings in this map + */ + @Override + public int size() { + final Segment[] segments = this.segments; + long sum = 0; + long check = 0; + int[] mc = new int[segments.length]; + // Try a few times to get accurate count. On failure due to continuous + // async changes in table, resort to locking. + for (int k = 0; k < RETRIES_BEFORE_LOCK; ++ k) { + check = 0; + sum = 0; + int mcsum = 0; + for (int i = 0; i < segments.length; ++ i) { + sum += segments[i].count; + mcsum += mc[i] = segments[i].modCount; + } + if (mcsum != 0) { + for (int i = 0; i < segments.length; ++ i) { + check += segments[i].count; + if (mc[i] != segments[i].modCount) { + check = -1; // force retry + break; + } + } + } + if (check == sum) { + break; + } + } + if (check != sum) { // Resort to locking all segments + sum = 0; + for (Segment segment: segments) { + segment.lock(); + } + for (Segment segment: segments) { + sum += segment.count; + } + for (Segment segment: segments) { + segment.unlock(); + } + } + if (sum > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } else { + return (int) sum; + } + } + + /** + * Returns the value to which the specified key is mapped, or {@code null} + * if this map contains no mapping for the key. + * + *

More formally, if this map contains a mapping from a key {@code k} to + * a value {@code v} such that {@code key.equals(k)}, then this method + * returns {@code v}; otherwise it returns {@code null}. (There can be at + * most one such mapping.) + * + * @throws NullPointerException if the specified key is null + */ + @Override + public V get(Object key) { + int hash = hashOf(key); + return segmentFor(hash).get(key, hash); + } + + /** + * Tests if the specified object is a key in this table. + * + * @param key possible key + * @return true if and only if the specified object is a key in + * this table, as determined by the equals method; + * false otherwise. + * @throws NullPointerException if the specified key is null + */ + @Override + public boolean containsKey(Object key) { + int hash = hashOf(key); + return segmentFor(hash).containsKey(key, hash); + } + + /** + * Returns true if this map maps one or more keys to the specified + * value. Note: This method requires a full internal traversal of the hash + * table, and so is much slower than method containsKey. + * + * @param value value whose presence in this map is to be tested + * @return true if this map maps one or more keys to the specified + * value + * @throws NullPointerException if the specified value is null + */ + + @Override + public boolean containsValue(Object value) { + if (value == null) { + throw new NullPointerException(); + } + + // See explanation of modCount use above + + final Segment[] segments = this.segments; + int[] mc = new int[segments.length]; + + // Try a few times without locking + for (int k = 0; k < RETRIES_BEFORE_LOCK; ++ k) { + int mcsum = 0; + for (int i = 0; i < segments.length; ++ i) { + mcsum += mc[i] = segments[i].modCount; + if (segments[i].containsValue(value)) { + return true; + } + } + boolean cleanSweep = true; + if (mcsum != 0) { + for (int i = 0; i < segments.length; ++ i) { + if (mc[i] != segments[i].modCount) { + cleanSweep = false; + break; + } + } + } + if (cleanSweep) { + return false; + } + } + // Resort to locking all segments + for (Segment segment: segments) { + segment.lock(); + } + boolean found = false; + try { + for (Segment segment: segments) { + if (segment.containsValue(value)) { + found = true; + break; + } + } + } finally { + for (Segment segment: segments) { + segment.unlock(); + } + } + return found; + } + + /** + * Legacy method testing if some key maps into the specified value in this + * table. This method is identical in functionality to + * {@link #containsValue}, and exists solely to ensure full compatibility + * with class {@link Hashtable}, which supported this method prior to + * introduction of the Java Collections framework. + * + * @param value a value to search for + * @return true if and only if some key maps to the value + * argument in this table as determined by the equals + * method; false otherwise + * @throws NullPointerException if the specified value is null + */ + public boolean contains(Object value) { + return containsValue(value); + } + + /** + * Maps the specified key to the specified value in this table. Neither the + * key nor the value can be null. + * + *

The value can be retrieved by calling the get method with a + * key that is equal to the original key. + * + * @param key key with which the specified value is to be associated + * @param value value to be associated with the specified key + * @return the previous value associated with key, or null + * if there was no mapping for key + * @throws NullPointerException if the specified key or value is null + */ + @Override + public V put(K key, V value) { + if (value == null) { + throw new NullPointerException(); + } + int hash = hashOf(key); + return segmentFor(hash).put(key, hash, value, false); + } + + /** + * @return the previous value associated with the specified key, or + * null if there was no mapping for the key + * @throws NullPointerException if the specified key or value is null + */ + public V putIfAbsent(K key, V value) { + if (value == null) { + throw new NullPointerException(); + } + int hash = hashOf(key); + return segmentFor(hash).put(key, hash, value, true); + } + + /** + * Copies all of the mappings from the specified map to this one. These + * mappings replace any mappings that this map had for any of the keys + * currently in the specified map. + * + * @param m mappings to be stored in this map + */ + @Override + public void putAll(Map m) { + for (Map.Entry e: m.entrySet()) { + put(e.getKey(), e.getValue()); + } + } + + /** + * Removes the key (and its corresponding value) from this map. This method + * does nothing if the key is not in the map. + * + * @param key the key that needs to be removed + * @return the previous value associated with key, or null + * if there was no mapping for key + * @throws NullPointerException if the specified key is null + */ + @Override + public V remove(Object key) { + int hash = hashOf(key); + return segmentFor(hash).remove(key, hash, null, false); + } + + /** + * @throws NullPointerException if the specified key is null + */ + public boolean remove(Object key, Object value) { + int hash = hashOf(key); + if (value == null) { + return false; + } + return segmentFor(hash).remove(key, hash, value, false) != null; + } + + /** + * @throws NullPointerException if any of the arguments are null + */ + public boolean replace(K key, V oldValue, V newValue) { + if (oldValue == null || newValue == null) { + throw new NullPointerException(); + } + int hash = hashOf(key); + return segmentFor(hash).replace(key, hash, oldValue, newValue); + } + + /** + * @return the previous value associated with the specified key, or + * null if there was no mapping for the key + * @throws NullPointerException if the specified key or value is null + */ + public V replace(K key, V value) { + if (value == null) { + throw new NullPointerException(); + } + int hash = hashOf(key); + return segmentFor(hash).replace(key, hash, value); + } + + /** + * Removes all of the mappings from this map. + */ + @Override + public void clear() { + for (Segment segment: segments) { + segment.clear(); + } + } + + /** + * Removes any stale entries whose keys have been finalized. Use of this + * method is normally not necessary since stale entries are automatically + * removed lazily, when blocking operations are required. However, there are + * some cases where this operation should be performed eagerly, such as + * cleaning up old references to a ClassLoader in a multi-classloader + * environment. + * + * Note: this method will acquire locks, one at a time, across all segments + * of this table, so if it is to be used, it should be used sparingly. + */ + public void purgeStaleEntries() { + for (Segment segment: segments) { + segment.removeStale(); + } + } + + /** + * Returns a {@link Set} view of the keys contained in this map. The set is + * backed by the map, so changes to the map are reflected in the set, and + * vice-versa. The set supports element removal, which removes the + * corresponding mapping from this map, via the Iterator.remove, + * Set.remove, removeAll, retainAll, and + * clear operations. It does not support the add or + * addAll operations. + * + *

The view's iterator is a "weakly consistent" iterator that + * will never throw {@link ConcurrentModificationException}, and guarantees + * to traverse elements as they existed upon construction of the iterator, + * and may (but is not guaranteed to) reflect any modifications subsequent + * to construction. + */ + @Override + public Set keySet() { + Set ks = keySet; + return ks != null? ks : (keySet = new KeySet()); + } + + /** + * Returns a {@link Collection} view of the values contained in this map. + * The collection is backed by the map, so changes to the map are reflected + * in the collection, and vice-versa. The collection supports element + * removal, which removes the corresponding mapping from this map, via the + * Iterator.remove, Collection.remove, removeAll, + * retainAll, and clear operations. It does not support + * the add or addAll operations. + * + *

The view's iterator is a "weakly consistent" iterator that + * will never throw {@link ConcurrentModificationException}, and guarantees + * to traverse elements as they existed upon construction of the iterator, + * and may (but is not guaranteed to) reflect any modifications subsequent + * to construction. + */ + @Override + public Collection values() { + Collection vs = values; + return vs != null? vs : (values = new Values()); + } + + /** + * Returns a {@link Set} view of the mappings contained in this map. + * The set is backed by the map, so changes to the map are reflected in the + * set, and vice-versa. The set supports element removal, which removes the + * corresponding mapping from the map, via the Iterator.remove, + * Set.remove, removeAll, retainAll, and + * clear operations. It does not support the add or + * addAll operations. + * + *

The view's iterator is a "weakly consistent" iterator that + * will never throw {@link ConcurrentModificationException}, and guarantees + * to traverse elements as they existed upon construction of the iterator, + * and may (but is not guaranteed to) reflect any modifications subsequent + * to construction. + */ + @Override + public Set> entrySet() { + Set> es = entrySet; + return es != null? es : (entrySet = new EntrySet()); + } + + /** + * Returns an enumeration of the keys in this table. + * + * @return an enumeration of the keys in this table + * @see #keySet() + */ + public Enumeration keys() { + return new KeyIterator(); + } + + /** + * Returns an enumeration of the values in this table. + * + * @return an enumeration of the values in this table + * @see #values() + */ + public Enumeration elements() { + return new ValueIterator(); + } + + /* ---------------- Iterator Support -------------- */ + + abstract class HashIterator { + int nextSegmentIndex; + int nextTableIndex; + HashEntry[] currentTable; + HashEntry nextEntry; + HashEntry lastReturned; + K currentKey; // Strong reference to weak key (prevents gc) + + HashIterator() { + nextSegmentIndex = segments.length - 1; + nextTableIndex = -1; + advance(); + } + + public void rewind() { + nextSegmentIndex = segments.length - 1; + nextTableIndex = -1; + currentTable = null; + nextEntry = null; + lastReturned = null; + currentKey = null; + advance(); + } + + public boolean hasMoreElements() { + return hasNext(); + } + + final void advance() { + if (nextEntry != null && (nextEntry = nextEntry.next) != null) { + return; + } + + while (nextTableIndex >= 0) { + if ((nextEntry = currentTable[nextTableIndex --]) != null) { + return; + } + } + + while (nextSegmentIndex >= 0) { + Segment seg = segments[nextSegmentIndex --]; + if (seg.count != 0) { + currentTable = seg.table; + for (int j = currentTable.length - 1; j >= 0; -- j) { + if ((nextEntry = currentTable[j]) != null) { + nextTableIndex = j - 1; + return; + } + } + } + } + } + + public boolean hasNext() { + while (nextEntry != null) { + if (nextEntry.key() != null) { + return true; + } + advance(); + } + + return false; + } + + HashEntry nextEntry() { + do { + if (nextEntry == null) { + throw new NoSuchElementException(); + } + + lastReturned = nextEntry; + currentKey = lastReturned.key(); + advance(); + } while (currentKey == null); // Skip GC'd keys + + return lastReturned; + } + + public void remove() { + if (lastReturned == null) { + throw new IllegalStateException(); + } + ConcurrentWeakKeyHashMap.this.remove(currentKey); + lastReturned = null; + } + } + + final class KeyIterator + extends HashIterator implements ReusableIterator, Enumeration { + + public K next() { + return nextEntry().key(); + } + + public K nextElement() { + return nextEntry().key(); + } + } + + final class ValueIterator + extends HashIterator implements ReusableIterator, Enumeration { + + public V next() { + return nextEntry().value(); + } + + public V nextElement() { + return nextEntry().value(); + } + } + + /* + * This class is needed for JDK5 compatibility. + */ + static class SimpleEntry implements Entry { + + private final K key; + + private V value; + + public SimpleEntry(K key, V value) { + this.key = key; + this.value = value; + } + + public SimpleEntry(Entry entry) { + key = entry.getKey(); + value = entry.getValue(); + } + + public K getKey() { + return key; + } + + public V getValue() { + return value; + } + + public V setValue(V value) { + V oldValue = this.value; + this.value = value; + return oldValue; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Map.Entry)) { + return false; + } + @SuppressWarnings("rawtypes") + Map.Entry e = (Map.Entry) o; + return eq(key, e.getKey()) && eq(value, e.getValue()); + } + + @Override + public int hashCode() { + return (key == null? 0 : key.hashCode()) ^ (value == null? 0 : value.hashCode()); + } + + @Override + public String toString() { + return key + "=" + value; + } + + private static boolean eq(Object o1, Object o2) { + return o1 == null? o2 == null : o1.equals(o2); + } + } + + /** + * Custom Entry class used by EntryIterator.next(), that relays setValue + * changes to the underlying map. + */ + final class WriteThroughEntry extends SimpleEntry { + + WriteThroughEntry(K k, V v) { + super(k, v); + } + + /** + * Set our entry's value and write through to the map. The value to + * return is somewhat arbitrary here. Since a WriteThroughEntry does not + * necessarily track asynchronous changes, the most recent "previous" + * value could be different from what we return (or could even have been + * removed in which case the put will re-establish). We do not and can + * not guarantee more. + */ + @Override + public V setValue(V value) { + + if (value == null) { + throw new NullPointerException(); + } + V v = super.setValue(value); + put(getKey(), value); + return v; + } + } + + final class EntryIterator extends HashIterator implements + ReusableIterator> { + public Map.Entry next() { + HashEntry e = nextEntry(); + return new WriteThroughEntry(e.key(), e.value()); + } + } + + final class KeySet extends AbstractSet { + @Override + public Iterator iterator() { + return new KeyIterator(); + } + + @Override + public int size() { + return ConcurrentWeakKeyHashMap.this.size(); + } + + @Override + public boolean isEmpty() { + return ConcurrentWeakKeyHashMap.this.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return containsKey(o); + } + + @Override + public boolean remove(Object o) { + return ConcurrentWeakKeyHashMap.this.remove(o) != null; + } + + @Override + public void clear() { + ConcurrentWeakKeyHashMap.this.clear(); + } + } + + final class Values extends AbstractCollection { + @Override + public Iterator iterator() { + return new ValueIterator(); + } + + @Override + public int size() { + return ConcurrentWeakKeyHashMap.this.size(); + } + + @Override + public boolean isEmpty() { + return ConcurrentWeakKeyHashMap.this.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return containsValue(o); + } + + @Override + public void clear() { + ConcurrentWeakKeyHashMap.this.clear(); + } + } + + final class EntrySet extends AbstractSet> { + @Override + public Iterator> iterator() { + return new EntryIterator(); + } + + @Override + public boolean contains(Object o) { + if (!(o instanceof Map.Entry)) { + return false; + } + Map.Entry e = (Map.Entry) o; + V v = get(e.getKey()); + return v != null && v.equals(e.getValue()); + } + + @Override + public boolean remove(Object o) { + if (!(o instanceof Map.Entry)) { + return false; + } + Map.Entry e = (Map.Entry) o; + return ConcurrentWeakKeyHashMap.this.remove(e.getKey(), e.getValue()); + } + + @Override + public int size() { + return ConcurrentWeakKeyHashMap.this.size(); + } + + @Override + public boolean isEmpty() { + return ConcurrentWeakKeyHashMap.this.isEmpty(); + } + + @Override + public void clear() { + ConcurrentWeakKeyHashMap.this.clear(); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/com/taobao/arthas/common/concurrent/ReusableIterator.java b/common/src/main/java/com/taobao/arthas/common/concurrent/ReusableIterator.java new file mode 100644 index 000000000..f146a029c --- /dev/null +++ b/common/src/main/java/com/taobao/arthas/common/concurrent/ReusableIterator.java @@ -0,0 +1,22 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.taobao.arthas.common.concurrent; + +import java.util.Iterator; + +public interface ReusableIterator extends Iterator { + void rewind(); +} \ No newline at end of file diff --git a/core/pom.xml b/core/pom.xml index 8190e7ead..53fb17147 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -221,6 +221,12 @@ mockito-core test + + com.taobao.arthas + arthas-demo + ${project.version} + test + org.benf diff --git a/core/src/main/java/com/taobao/arthas/core/advisor/AdviceListenerManager.java b/core/src/main/java/com/taobao/arthas/core/advisor/AdviceListenerManager.java new file mode 100644 index 000000000..0143e390f --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/advisor/AdviceListenerManager.java @@ -0,0 +1,203 @@ +package com.taobao.arthas.core.advisor; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; + +import com.taobao.arthas.common.concurrent.ConcurrentWeakKeyHashMap; +import com.taobao.arthas.core.server.ArthasBootstrap; +import com.taobao.arthas.core.shell.system.ExecStatus; +import com.taobao.arthas.core.shell.system.ProcessAware; + +/** + * + * TODO line 的记录 listener方式? 还是有string为key,不过 classname|method|desc|num 这样子? 判断是否已插入了,可以在两行中间查询,有没有 SpyAPI 的invoke? + * + * TODO trace的怎么搞? trace 只记录一次就可以了 classname|method|desc|trace ? 怎么避免 trace 到 SPY的invoke ?直接忽略? + * + * TODO trace命令可以动态的增加 新的函数进去不?只要关联上同一个 Listener应该是可以的。 + * + * TODO 在SPY里放很多的 Object数组,然后动态的设置进去? 比如有新的 Listener来的时候。 这样子连查表都不用了。 甚至可以动态生成 存放这些 Listener数组的类? + * 这样子的话,只要有 Binding那里,查询到一个具体分配好的类, 这样子就可以了? 甚至每个ClassLoader里都动态生成这样子的 存放类,那么这样子不可以避免查 ClassLoader了么? + * + * 动态为每一个增强类,生成一个新的类,新的类里,有各种的 ID 数组,保存每一个类的每一种 trace 点的信息?? + * + * 多个 watch命令 对同一个类,现在的逻辑是,每个watch都有一个自己的 TransForm,但不会重复增强,因为做了判断。 watch命令停止时,也没有去掉增强的代码。 + * 只有reset时 才会去掉。 + * + * 其实用户想查看局部变量,并不是想查看哪一行! 而是想看某个函数里子调用时的 局部变量的值! 所以实际上是想要一个新的命令,比如 watchinmethod , + * 可以 在某个子调用里, + * + * TODO 现在的trace 可以输出行号,可能不是很精确,但是可以对应上的。 这个在新的方式里怎么支持? 增加一个 linenumber binding? 从mehtodNode,向上查找到最近的行号? + * + * TODO 防止重复增强,最重要的应该还是动态增加 annotation,这个才是真正可以做到某一行,某一个子 invoke 都能识别出来的! 无论是 transform多少次! + * 字节码怎么动态加 annotation ? annotation里签名用 url ?的key/value方式表达! 这样子可以有效还原信息 + * + * TODO 是否考虑一个 trace /watch命令之后,得到一个具体的 Listener ID, 允许在另外的窗口里,再次 trace/watch时指定这个ID,就会查找到,并处理。 + * 这样子的话,真正达到了动态灵活的,一层一层增加的trace ! + * + * + * @author hengyunabc 2020-04-24 + * + */ +public class AdviceListenerManager { + + private static Timer timer = ArthasBootstrap.getInstance().getTimer(); + + static { + timer.scheduleAtFixedRate(new TimerTask() { + + @Override + public void run() { + if (adviceListenerMap != null) { + for (Entry entry : adviceListenerMap.entrySet()) { + ClassLoaderAdviceListenerManager adviceListenerManager = entry.getValue(); + synchronized (adviceListenerManager) { + for (Entry> eee : adviceListenerManager.map.entrySet()) { + List listeners = eee.getValue(); + List newResult = new ArrayList(); + for (AdviceListener listener : listeners) { + if (listener instanceof ProcessAware) { + ProcessAware processAware = (ProcessAware) listener; + ExecStatus status = processAware.getProcess().status(); + if (!status.equals(ExecStatus.TERMINATED)) { + newResult.add(listener); + } + } + } + + if (newResult.size() != listeners.size()) { + adviceListenerManager.map.put(eee.getKey(), newResult); + } + + } + } + } + } + } + + }, 3000, 3000); + } + + static private ConcurrentWeakKeyHashMap adviceListenerMap = new ConcurrentWeakKeyHashMap(); + + static class ClassLoaderAdviceListenerManager { + private ConcurrentHashMap> map = new ConcurrentHashMap>(); + + private String key(String className, String methodName, String methodDesc) { + return className + methodName + methodDesc; + } + + private String keyForTrace(String className, String owner, String methodName, String methodDesc) { + return className + owner + methodName + methodDesc; + } + + public void registerAdviceListener(String className, String methodName, String methodDesc, + AdviceListener listener) { + synchronized (this) { + className = className.replace('/', '.'); + String key = key(className, methodName, methodDesc); + + List listeners = map.get(key); + if (listeners == null) { + listeners = new ArrayList(); + map.put(key, listeners); + } + if(!listeners.contains(listener)) { + listeners.add(listener); + } + } + } + + public List queryAdviceListeners(String className, String methodName, String methodDesc) { + className = className.replace('/', '.'); + String key = key(className, methodName, methodDesc); + + List listeners = map.get(key); + + return listeners; + } + + + public void registerTraceAdviceListener(String className, String owner, String methodName, String methodDesc, + AdviceListener listener) { + + className = className.replace('/', '.'); + String key = keyForTrace(className, owner, methodName, methodDesc); + + List listeners = map.get(key); + if (listeners == null) { + listeners = new ArrayList(); + map.put(key, listeners); + } + if(!listeners.contains(listener)) { + listeners.add(listener); + } + } + + public List queryTraceAdviceListeners(String className, String owner, String methodName, String methodDesc) { + className = className.replace('/', '.'); + String key = keyForTrace(className, owner, methodName, methodDesc); + + List listeners = map.get(key); + + return listeners; + } + } + + public static void registerAdviceListener(ClassLoader classLoader, String className, String methodName, + String methodDesc, AdviceListener listener) { + className = className.replace('/', '.'); + + ClassLoaderAdviceListenerManager manager = adviceListenerMap.get(classLoader); + + if (manager == null) { + manager = new ClassLoaderAdviceListenerManager(); + adviceListenerMap.put(classLoader, manager); + } + manager.registerAdviceListener(className, methodName, methodDesc, listener); + } + + public static void updateAdviceListeners() { + + } + + public static List queryAdviceListeners(ClassLoader classLoader, String className, + String methodName, String methodDesc) { + className = className.replace('/', '.'); + ClassLoaderAdviceListenerManager manager = adviceListenerMap.get(classLoader); + + if (manager != null) { + return manager.queryAdviceListeners(className, methodName, methodDesc); + } + + return null; + } + + public static void registerTraceAdviceListener(ClassLoader classLoader, String className, String owner, String methodName, + String methodDesc, AdviceListener listener) { + className = className.replace('/', '.'); + + ClassLoaderAdviceListenerManager manager = adviceListenerMap.get(classLoader); + + if (manager == null) { + manager = new ClassLoaderAdviceListenerManager(); + adviceListenerMap.put(classLoader, manager); + } + manager.registerTraceAdviceListener(className, owner, methodName, methodDesc, listener); + } + public static List queryTraceAdviceListeners(ClassLoader classLoader, String className, String owner, + String methodName, String methodDesc) { + className = className.replace('/', '.'); + ClassLoaderAdviceListenerManager manager = adviceListenerMap.get(classLoader); + + if (manager != null) { + return manager.queryTraceAdviceListeners(className, owner, methodName, methodDesc); + } + + return null; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/advisor/Enhancer.java b/core/src/main/java/com/taobao/arthas/core/advisor/Enhancer.java index 9fedcdeca..8a0becfd4 100644 --- a/core/src/main/java/com/taobao/arthas/core/advisor/Enhancer.java +++ b/core/src/main/java/com/taobao/arthas/core/advisor/Enhancer.java @@ -23,6 +23,7 @@ import java.util.WeakHashMap; import com.alibaba.arthas.deps.org.objectweb.asm.Opcodes; import com.alibaba.arthas.deps.org.objectweb.asm.Type; +import com.alibaba.arthas.deps.org.objectweb.asm.tree.AbstractInsnNode; import com.alibaba.arthas.deps.org.objectweb.asm.tree.ClassNode; import com.alibaba.arthas.deps.org.objectweb.asm.tree.MethodInsnNode; import com.alibaba.arthas.deps.org.objectweb.asm.tree.MethodNode; @@ -44,10 +45,9 @@ import com.taobao.arthas.bytekit.asm.location.filter.GroupLocationFilter; import com.taobao.arthas.bytekit.asm.location.filter.InvokeCheckLocationFilter; import com.taobao.arthas.bytekit.asm.location.filter.InvokeContainLocationFilter; import com.taobao.arthas.bytekit.asm.location.filter.LocationFilter; +import com.taobao.arthas.bytekit.utils.AsmOpUtils; import com.taobao.arthas.bytekit.utils.AsmUtils; import com.taobao.arthas.core.GlobalOptions; -import com.taobao.arthas.core.bytecode.AdviceListenerManager; -import com.taobao.arthas.core.bytecode.SpyImpl; import com.taobao.arthas.core.server.ArthasBootstrap; import com.taobao.arthas.core.util.ArthasCheckUtils; import com.taobao.arthas.core.util.FileUtils; @@ -248,25 +248,47 @@ public class Enhancer implements ClassFileTransformer { groupLocationFilter.addFilter(invokeExceptionFilter); for (MethodNode methodNode : matchedMethods) { - MethodProcessor methodProcessor = new MethodProcessor(classNode, methodNode, groupLocationFilter); - for (InterceptorProcessor interceptor : interceptorProcessors) { - try { - List locations = interceptor.process(methodProcessor); - for (Location location : locations) { - if (location instanceof MethodInsnNodeWare) { - MethodInsnNodeWare methodInsnNodeWare = (MethodInsnNodeWare) location; - MethodInsnNode methodInsnNode = methodInsnNodeWare.methodInsnNode(); - - AdviceListenerManager.registerTraceAdviceListener(inClassLoader, className, - methodInsnNode.owner, methodInsnNode.name, methodInsnNode.desc, listener); + // 先查找是否有 atBeforeInvoke 函数,如果有,则说明已经有trace了,则直接不再尝试增强,直接插入 listener + if(AsmUtils.containsMethodInsnNode(methodNode, Type.getInternalName(SpyAPI.class), "atBeforeInvoke")) { + for (AbstractInsnNode insnNode = methodNode.instructions.getFirst(); insnNode != null; insnNode = insnNode + .getNext()) { + if (insnNode instanceof MethodInsnNode) { + final MethodInsnNode methodInsnNode = (MethodInsnNode) insnNode; + if(this.skipJDKTrace) { + if(methodInsnNode.owner.startsWith("java/")) { + continue; + } } + // 原始类型的box类型相关的都跳过 + if(AsmOpUtils.isBoxType(Type.getObjectType(methodInsnNode.owner))) { + continue; + } + AdviceListenerManager.registerTraceAdviceListener(inClassLoader, className, + methodInsnNode.owner, methodInsnNode.name, methodInsnNode.desc, listener); } + } + }else { + MethodProcessor methodProcessor = new MethodProcessor(classNode, methodNode, groupLocationFilter); + for (InterceptorProcessor interceptor : interceptorProcessors) { + try { + List locations = interceptor.process(methodProcessor); + for (Location location : locations) { + if (location instanceof MethodInsnNodeWare) { + MethodInsnNodeWare methodInsnNodeWare = (MethodInsnNodeWare) location; + MethodInsnNode methodInsnNode = methodInsnNodeWare.methodInsnNode(); - } catch (Throwable e) { - e.printStackTrace(); + AdviceListenerManager.registerTraceAdviceListener(inClassLoader, className, + methodInsnNode.owner, methodInsnNode.name, methodInsnNode.desc, listener); + } + } + + } catch (Throwable e) { + logger.error("enhancer error, class: {}, method: {}, interceptor: {}", classNode.name, methodNode.name, interceptor.getClass().getName(), e); + } } } + // enter/exist 总是要插入 listener AdviceListenerManager.registerAdviceListener(inClassLoader, className, methodNode.name, methodNode.desc, listener); affect.mCnt(1); diff --git a/core/src/main/java/com/taobao/arthas/core/advisor/ReflectAdviceListenerAdapter.java b/core/src/main/java/com/taobao/arthas/core/advisor/ReflectAdviceListenerAdapter.java index 2723cb3a7..308a9366d 100644 --- a/core/src/main/java/com/taobao/arthas/core/advisor/ReflectAdviceListenerAdapter.java +++ b/core/src/main/java/com/taobao/arthas/core/advisor/ReflectAdviceListenerAdapter.java @@ -3,6 +3,8 @@ package com.taobao.arthas.core.advisor; import com.taobao.arthas.core.command.express.ExpressException; import com.taobao.arthas.core.command.express.ExpressFactory; import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.shell.system.Process; +import com.taobao.arthas.core.shell.system.ProcessAware; import com.taobao.arthas.core.util.ArthasCheckUtils; import com.taobao.arthas.core.util.Constants; import com.taobao.arthas.core.util.StringUtils; @@ -16,7 +18,8 @@ import java.lang.reflect.Method; * 通过反射拿到对应的Class/Method类,而不是原始的ClassName/MethodName * 当然性能开销要比普通监听器高许多 */ -public abstract class ReflectAdviceListenerAdapter implements AdviceListener { +public abstract class ReflectAdviceListenerAdapter implements AdviceListener , ProcessAware{ + private Process process; @Override public void create() { @@ -28,6 +31,14 @@ public abstract class ReflectAdviceListenerAdapter implements AdviceListener { // default no-op } + public Process getProcess() { + return process; + } + + public void setProcess(Process process) { + this.process = process; + } + private ClassLoader toClassLoader(ClassLoader loader) { return null != loader ? loader diff --git a/core/src/main/java/com/taobao/arthas/core/advisor/SpyImpl.java b/core/src/main/java/com/taobao/arthas/core/advisor/SpyImpl.java new file mode 100644 index 000000000..175ed9f1d --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/advisor/SpyImpl.java @@ -0,0 +1,210 @@ +package com.taobao.arthas.core.advisor; + +import java.arthas.SpyAPI.AbstractSpy; +import java.util.List; +import java.util.regex.Pattern; + +import com.alibaba.arthas.deps.org.slf4j.Logger; +import com.alibaba.arthas.deps.org.slf4j.LoggerFactory; +import com.taobao.arthas.core.shell.system.ExecStatus; +import com.taobao.arthas.core.shell.system.ProcessAware; + +/** + *

+ * 怎么从 className|methodDesc 到 id 对应起来??
+ * 当id少时,可以id自己来判断是否符合?
+ * 
+ * 如果是每个 className|methodDesc 为 key ,是否
+ * 
+ * + * @author hengyunabc 2020-04-24 + * + */ +public class SpyImpl extends AbstractSpy { + private static final Logger logger = LoggerFactory.getLogger(SpyImpl.class); + + @Override + public void atEnter(Class clazz, String methodInfo, Object target, Object[] args) { + ClassLoader classLoader = clazz.getClassLoader(); + + String[] info = splitMethodInfo(methodInfo); + String methodName = info[0]; + String methodDesc = info[1]; + // TODO listener 只用查一次,放到 thread local里保存起来就可以了! + List listeners = AdviceListenerManager.queryAdviceListeners(classLoader, clazz.getName(), + methodName, methodDesc); + if (listeners != null) { + for (AdviceListener adviceListener : listeners) { + try { + if (skipAdviceListener(adviceListener)) { + continue; + } + adviceListener.before(classLoader, clazz.getName(), methodName, methodDesc, target, args); + } catch (Throwable e) { + if (logger.isDebugEnabled()) { + logger.error("class: {}, methodInfo: {}", clazz.getName(), methodInfo, e); + } + } + } + } + + } + + @Override + public void atExit(Class clazz, String methodInfo, Object target, Object[] args, Object returnObject) { + ClassLoader classLoader = clazz.getClassLoader(); + + String[] info = splitMethodInfo(methodInfo); + String methodName = info[0]; + String methodDesc = info[1]; + + List listeners = AdviceListenerManager.queryAdviceListeners(classLoader, clazz.getName(), + methodName, methodDesc); + if (listeners != null) { + for (AdviceListener adviceListener : listeners) { + try { + if (skipAdviceListener(adviceListener)) { + continue; + } + adviceListener.afterReturning(classLoader, clazz.getName(), methodName, methodDesc, target, args, + returnObject); + } catch (Throwable e) { + if (logger.isDebugEnabled()) { + logger.error("class: {}, methodInfo: {}", clazz.getName(), methodInfo, e); + } + } + } + } + } + + @Override + public void atExceptionExit(Class clazz, String methodInfo, Object target, Object[] args, Throwable throwable) { + ClassLoader classLoader = clazz.getClassLoader(); + + String[] info = splitMethodInfo(methodInfo); + String methodName = info[0]; + String methodDesc = info[1]; + + List listeners = AdviceListenerManager.queryAdviceListeners(classLoader, clazz.getName(), + methodName, methodDesc); + if (listeners != null) { + for (AdviceListener adviceListener : listeners) { + try { + if (skipAdviceListener(adviceListener)) { + continue; + } + adviceListener.afterThrowing(classLoader, clazz.getName(), methodName, methodDesc, target, args, + throwable); + } catch (Throwable e) { + if (logger.isDebugEnabled()) { + logger.error("class: {}, methodInfo: {}", clazz.getName(), methodInfo, e); + } + } + } + } + } + + @Override + public void atBeforeInvoke(Class clazz, String invokeInfo, Object target) { + ClassLoader classLoader = clazz.getClassLoader(); + String[] info = splitInvokeInfo(invokeInfo); + String owner = info[0]; + String methodName = info[1]; + String methodDesc = info[2]; + + List listeners = AdviceListenerManager.queryTraceAdviceListeners(classLoader, clazz.getName(), + owner, methodName, methodDesc); + + if (listeners != null) { + for (AdviceListener adviceListener : listeners) { + try { + if (skipAdviceListener(adviceListener)) { + continue; + } + final InvokeTraceable listener = (InvokeTraceable) adviceListener; + listener.invokeBeforeTracing(owner, methodName, methodDesc, Integer.parseInt(info[3])); + } catch (Throwable e) { + if (logger.isDebugEnabled()) { + logger.error("class: {}, invokeInfo: {}", clazz.getName(), invokeInfo, e); + } + } + } + } + } + + @Override + public void atAfterInvoke(Class clazz, String invokeInfo, Object target) { + ClassLoader classLoader = clazz.getClassLoader(); + String[] info = splitInvokeInfo(invokeInfo); + String owner = info[0]; + String methodName = info[1]; + String methodDesc = info[2]; + List listeners = AdviceListenerManager.queryTraceAdviceListeners(classLoader, clazz.getName(), + owner, methodName, methodDesc); + + if (listeners != null) { + for (AdviceListener adviceListener : listeners) { + try { + if (skipAdviceListener(adviceListener)) { + continue; + } + final InvokeTraceable listener = (InvokeTraceable) adviceListener; + listener.invokeAfterTracing(owner, methodName, methodDesc, Integer.parseInt(info[3])); + } catch (Throwable e) { + if (logger.isDebugEnabled()) { + logger.error("class: {}, invokeInfo: {}", clazz.getName(), invokeInfo, e); + } + } + } + } + + } + + @Override + public void atInvokeException(Class clazz, String invokeInfo, Object target, Throwable throwable) { + ClassLoader classLoader = clazz.getClassLoader(); + String[] info = splitInvokeInfo(invokeInfo); + String owner = info[0]; + String methodName = info[1]; + String methodDesc = info[2]; + + List listeners = AdviceListenerManager.queryTraceAdviceListeners(classLoader, clazz.getName(), + owner, methodName, methodDesc); + + if (listeners != null) { + for (AdviceListener adviceListener : listeners) { + try { + if (skipAdviceListener(adviceListener)) { + continue; + } + final InvokeTraceable listener = (InvokeTraceable) adviceListener; + listener.invokeThrowTracing(owner, methodName, methodDesc, Integer.parseInt(info[3])); + } catch (Throwable e) { + if (logger.isDebugEnabled()) { + logger.error("class: {}, invokeInfo: {}", clazz.getName(), invokeInfo, e); + } + } + } + } + } + + private String[] splitMethodInfo(String methodInfo) { + return methodInfo.split(Pattern.quote("|")); + } + + private String[] splitInvokeInfo(String invokeInfo) { + return invokeInfo.split(Pattern.quote("|")); + } + + private boolean skipAdviceListener(AdviceListener adviceListener) { + if (adviceListener instanceof ProcessAware) { + ProcessAware processAware = (ProcessAware) adviceListener; + ExecStatus status = processAware.getProcess().status(); + if (status.equals(ExecStatus.TERMINATED) || status.equals(ExecStatus.STOPPED)) { + return true; + } + } + return false; + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/taobao/arthas/core/advisor/TransformerManager.java b/core/src/main/java/com/taobao/arthas/core/advisor/TransformerManager.java new file mode 100644 index 000000000..492cfb085 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/advisor/TransformerManager.java @@ -0,0 +1,72 @@ +package com.taobao.arthas.core.advisor; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.lang.instrument.Instrumentation; +import java.security.ProtectionDomain; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * + * @author hengyunabc 2020-05-18 + * + */ +public class TransformerManager { + + private Instrumentation instrumentation; + private List watchTransformers = new CopyOnWriteArrayList(); + private List traceTransformers = new CopyOnWriteArrayList(); + + private ClassFileTransformer classFileTransformer; + + public TransformerManager(Instrumentation instrumentation) { + this.instrumentation = instrumentation; + + classFileTransformer = new ClassFileTransformer() { + + @Override + public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, + ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { + + for (ClassFileTransformer classFileTransformer : watchTransformers) { + byte[] transformResult = classFileTransformer.transform(loader, className, classBeingRedefined, + protectionDomain, classfileBuffer); + if (transformResult != null) { + classfileBuffer = transformResult; + } + } + + for (ClassFileTransformer classFileTransformer : traceTransformers) { + byte[] transformResult = classFileTransformer.transform(loader, className, classBeingRedefined, + protectionDomain, classfileBuffer); + if (transformResult != null) { + classfileBuffer = transformResult; + } + } + + return classfileBuffer; + } + + }; + instrumentation.addTransformer(classFileTransformer, true); + } + + public void addTransformer(ClassFileTransformer transformer, boolean isTracing) { + if (isTracing) { + traceTransformers.add(transformer); + } else { + watchTransformers.add(transformer); + } + } + + public void removeTransformer(ClassFileTransformer transformer) { + watchTransformers.remove(transformer); + traceTransformers.remove(transformer); + } + + public void destroy() { + instrumentation.removeTransformer(classFileTransformer); + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/EnhancerCommand.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/EnhancerCommand.java index cebd52c54..e7b1e8ec0 100644 --- a/core/src/main/java/com/taobao/arthas/core/command/monitor200/EnhancerCommand.java +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/EnhancerCommand.java @@ -133,7 +133,7 @@ public abstract class EnhancerCommand extends AnnotatedCommand { // 这里做个补偿,如果在enhance期间,unLock被调用了,则补偿性放弃 if (session.getLock() == lock) { // 注册通知监听器 - process.register(lock, listener); + process.register(lock, listener, effect.getTransformer()); if (process.isForeground()) { process.echoTips(Constants.Q_OR_CTRL_C_ABORT_MSG + "\n"); } diff --git a/core/src/main/java/com/taobao/arthas/core/env/PropertySource.java b/core/src/main/java/com/taobao/arthas/core/env/PropertySource.java index c86153d8c..314cff3ee 100644 --- a/core/src/main/java/com/taobao/arthas/core/env/PropertySource.java +++ b/core/src/main/java/com/taobao/arthas/core/env/PropertySource.java @@ -18,8 +18,6 @@ package com.taobao.arthas.core.env; import java.util.Arrays; -import com.sun.tools.javac.util.Log; - /** * Abstract base class representing a source of name/value property pairs. The * underlying {@linkplain #getSource() source object} may be of any type diff --git a/core/src/main/java/com/taobao/arthas/core/server/ArthasBootstrap.java b/core/src/main/java/com/taobao/arthas/core/server/ArthasBootstrap.java index 0e45762fe..33d3a9ade 100644 --- a/core/src/main/java/com/taobao/arthas/core/server/ArthasBootstrap.java +++ b/core/src/main/java/com/taobao/arthas/core/server/ArthasBootstrap.java @@ -27,6 +27,7 @@ import com.alibaba.arthas.tunnel.client.TunnelClient; import com.taobao.arthas.common.AnsiLog; import com.taobao.arthas.common.PidUtils; import com.taobao.arthas.core.advisor.AdviceWeaver; +import com.taobao.arthas.core.advisor.TransformerManager; import com.taobao.arthas.core.command.BuiltinCommandPack; import com.taobao.arthas.core.config.BinderUtils; import com.taobao.arthas.core.config.Configure; @@ -80,6 +81,8 @@ public class ArthasBootstrap { private Timer timer = new Timer("arthas-timer", true); + private TransformerManager transformerManager; + private ArthasBootstrap(Instrumentation instrumentation, String args) throws Throwable { this.instrumentation = instrumentation; @@ -114,6 +117,7 @@ public class ArthasBootstrap { } }; + transformerManager = new TransformerManager(instrumentation); Runtime.getRuntime().addShutdownHook(shutdown); } @@ -330,6 +334,7 @@ public class ArthasBootstrap { } } executorService.shutdownNow(); + transformerManager.destroy(); UserStatUtil.destroy(); // clear the reference in Spy class. cleanUpSpyReference(); @@ -395,6 +400,14 @@ public class ArthasBootstrap { return this.timer; } + public Instrumentation getInstrumentation() { + return this.instrumentation; + } + + public TransformerManager getTransformerManager() { + return this.transformerManager; + } + private Logger logger() { return LoggerFactory.getLogger(this.getClass()); } diff --git a/core/src/main/java/com/taobao/arthas/core/shell/command/CommandProcess.java b/core/src/main/java/com/taobao/arthas/core/shell/command/CommandProcess.java index 6f85ca0b4..b3a12dbef 100644 --- a/core/src/main/java/com/taobao/arthas/core/shell/command/CommandProcess.java +++ b/core/src/main/java/com/taobao/arthas/core/shell/command/CommandProcess.java @@ -7,6 +7,7 @@ import com.taobao.arthas.core.shell.session.Session; import com.taobao.arthas.core.shell.term.Tty; import com.taobao.middleware.cli.CommandLine; +import java.lang.instrument.ClassFileTransformer; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @@ -125,7 +126,7 @@ public interface CommandProcess extends Tty { * @param lock the lock for enhance class * @param listener */ - void register(int lock, AdviceListener listener); + void register(int lock, AdviceListener listener, ClassFileTransformer transformer); /** * Unregister listener diff --git a/core/src/main/java/com/taobao/arthas/core/shell/system/ProcessAware.java b/core/src/main/java/com/taobao/arthas/core/shell/system/ProcessAware.java new file mode 100644 index 000000000..9dc4baf2e --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/system/ProcessAware.java @@ -0,0 +1,14 @@ +package com.taobao.arthas.core.shell.system; + +/** + * + * @author hengyunabc 2020-05-18 + * + */ +public interface ProcessAware { + + public Process getProcess(); + + public void setProcess(Process process); + +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/system/impl/ProcessImpl.java b/core/src/main/java/com/taobao/arthas/core/shell/system/impl/ProcessImpl.java index 606c67a7d..b28b7875a 100644 --- a/core/src/main/java/com/taobao/arthas/core/shell/system/impl/ProcessImpl.java +++ b/core/src/main/java/com/taobao/arthas/core/shell/system/impl/ProcessImpl.java @@ -14,8 +14,8 @@ import com.taobao.arthas.core.shell.handlers.Handler; import com.taobao.arthas.core.shell.session.Session; import com.taobao.arthas.core.shell.system.ExecStatus; import com.taobao.arthas.core.shell.system.Process; +import com.taobao.arthas.core.shell.system.ProcessAware; import com.taobao.arthas.core.shell.term.Tty; -import com.taobao.arthas.core.shell.term.impl.httptelnet.HttpTelnetTermServer; import com.taobao.arthas.core.util.usage.StyledUsageFormatter; import com.taobao.middleware.cli.CLIException; import com.taobao.middleware.cli.CommandLine; @@ -24,6 +24,7 @@ import com.taobao.text.Color; import io.termd.core.function.Function; +import java.lang.instrument.ClassFileTransformer; import java.util.Date; import java.util.LinkedList; import java.util.List; @@ -348,7 +349,7 @@ public class ProcessImpl implements Process { return; } - process = new CommandProcessImpl(args2, tty, cl); + process = new CommandProcessImpl(this, args2, tty, cl); if (cacheLocation() != null) { process.echoTips("job id : " + this.jobId + "\n"); process.echoTips("cache location : " + cacheLocation() + "\n"); @@ -379,15 +380,17 @@ public class ProcessImpl implements Process { } private class CommandProcessImpl implements CommandProcess { - + private final Process process; private final List args2; private final Tty tty; private final CommandLine commandLine; private int enhanceLock = -1; private AtomicInteger times = new AtomicInteger(); private AdviceListener suspendedListener = null; + private ClassFileTransformer transformer; - public CommandProcessImpl(List args2, Tty tty, CommandLine commandLine) { + public CommandProcessImpl(Process process, List args2, Tty tty, CommandLine commandLine) { + this.process = process; this.args2 = args2; this.tty = tty; this.commandLine = commandLine; @@ -525,13 +528,23 @@ public class ProcessImpl implements Process { } @Override - public void register(int enhanceLock, AdviceListener listener) { + public void register(int enhanceLock, AdviceListener listener, ClassFileTransformer transformer) { this.enhanceLock = enhanceLock; + + if (listener instanceof ProcessAware) { + ((ProcessAware) listener).setProcess(this.process); + } AdviceWeaver.reg(enhanceLock, listener); + + this.transformer = transformer; } @Override public void unregister() { + if (transformer != null) { + ArthasBootstrap.getInstance().getTransformerManager().removeTransformer(transformer); + } + AdviceWeaver.unReg(enhanceLock); } diff --git a/core/src/main/java/com/taobao/arthas/core/util/affect/EnhancerAffect.java b/core/src/main/java/com/taobao/arthas/core/util/affect/EnhancerAffect.java index 131ec24df..81111c444 100644 --- a/core/src/main/java/com/taobao/arthas/core/util/affect/EnhancerAffect.java +++ b/core/src/main/java/com/taobao/arthas/core/util/affect/EnhancerAffect.java @@ -3,6 +3,7 @@ package com.taobao.arthas.core.util.affect; import com.taobao.arthas.core.GlobalOptions; import java.io.File; +import java.lang.instrument.ClassFileTransformer; import java.util.ArrayList; import java.util.Collection; import java.util.concurrent.atomic.AtomicInteger; @@ -18,19 +19,13 @@ public final class EnhancerAffect extends Affect { private final AtomicInteger cCnt = new AtomicInteger(); private final AtomicInteger mCnt = new AtomicInteger(); - + private ClassFileTransformer transformer; /** * dumpClass的文件存放集合 */ private final Collection classDumpFiles = new ArrayList(); public EnhancerAffect() { - - } - - public EnhancerAffect(int cCnt, int mCnt) { - this.cCnt(cCnt); - this.mCnt(mCnt); } /** @@ -80,6 +75,14 @@ public final class EnhancerAffect extends Affect { return classDumpFiles; } + public ClassFileTransformer getTransformer() { + return transformer; + } + + public void setTransformer(ClassFileTransformer transformer) { + this.transformer = transformer; + } + @Override public String toString() { final StringBuilder infoSB = new StringBuilder();