diff options
Diffstat (limited to 'java-allocation-instrumenter/src/main/java/com/google/monitoring/runtime/instrumentation/AllocationRecorder.java')
-rw-r--r-- | java-allocation-instrumenter/src/main/java/com/google/monitoring/runtime/instrumentation/AllocationRecorder.java | 260 |
1 files changed, 260 insertions, 0 deletions
diff --git a/java-allocation-instrumenter/src/main/java/com/google/monitoring/runtime/instrumentation/AllocationRecorder.java b/java-allocation-instrumenter/src/main/java/com/google/monitoring/runtime/instrumentation/AllocationRecorder.java new file mode 100644 index 0000000..1ead9ca --- /dev/null +++ b/java-allocation-instrumenter/src/main/java/com/google/monitoring/runtime/instrumentation/AllocationRecorder.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2009 Google Inc. + * + * Licensed 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.google.monitoring.runtime.instrumentation; + +import java.lang.instrument.Instrumentation; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentMap; + +import com.google.common.collect.ForwardingMap; +import com.google.common.collect.MapMaker; + +/** + * The logic for recording allocations, called from bytecode rewritten by + * {@link AllocationInstrumenter}. + * + * @author jeremymanson@google.com (Jeremy Manson) + * @author fischman@google.com (Ami Fischman) + */ +public class AllocationRecorder { + static { + // Sun's JVMs in 1.5.0_06 and 1.6.0{,_01} have a bug where calling + // Instrumentation.getObjectSize() during JVM shutdown triggers a + // JVM-crashing assert in JPLISAgent.c, so we make sure to not call it after + // shutdown. There can still be a race here, depending on the extent of the + // JVM bug, but this seems to be good enough. + // instrumentation is volatile to make sure the threads reading it (in + // recordAllocation()) see the updated value; we could do more + // synchronization but it's not clear that it'd be worth it, given the + // ambiguity of the bug we're working around in the first place. + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + setInstrumentation(null); + } + }); + } + + // See the comment above the addShutdownHook in the static block above + // for why this is volatile. + private static volatile Instrumentation instrumentation = null; + + static Instrumentation getInstrumentation() { + return instrumentation; + } + + static void setInstrumentation(Instrumentation inst) { + instrumentation = inst; + } + + // Mostly because, yes, arrays are faster than collections. + private static volatile Sampler [] additionalSamplers; + + // Protects mutations of additionalSamplers. Reads are okay because + // the field is volatile, so anyone who reads additionalSamplers + // will get a consistent view of it. + private static final Object samplerLock = new Object(); + + // List of packages that can add samplers. + private static final List<String> classNames = new ArrayList<String>(); + + static { + classNames.add("com.google.monitoring.runtime."); + } + + // Used for reentrancy checks + private static final ThreadLocal<Boolean> recordingAllocation = new ThreadLocal<Boolean>(); + + // Stores the object sizes for the last ~100000 encountered classes + private static final ForwardingMap<Class<?>, Long> classSizesMap = + new ForwardingMap<Class<?>, Long>() { + private final ConcurrentMap<Class<?>, Long> map = new MapMaker() + .weakKeys() + .makeMap(); + + @Override public Map<Class<?>, Long> delegate() { + return map; + } + + // The approximate maximum size of the map + private static final int MAX_SIZE = 100000; + + // The approximate current size of the map; since this is not an AtomicInteger + // and since we do not synchronize the updates to this field, it will only be + // an approximate size of the map; it's good enough for our purposes though, + // and not synchronizing the updates saves us some time + private int approximateSize = 0; + + @Override + public Long put(Class<?> key, Long value) { + // if we have too many elements, delete about 10% of them + // this is expensive, but needs to be done to keep the map bounded + // we also need to randomize the elements we delete: if we remove the same + // elements all the time, we might end up adding them back to the map + // immediately after, and then remove them again, then add them back, etc. + // which will cause this expensive code to be executed too often + if (approximateSize >= MAX_SIZE) { + for (Iterator<Class<?>> it = keySet().iterator(); it.hasNext(); ) { + it.next(); + if (Math.random() < 0.1) { + it.remove(); + } + } + + // get the exact size; another expensive call, but we need to correct + // approximateSize every once in a while, or the difference between + // approximateSize and the actual size might become significant over time; + // the other solution is synchronizing every time we update approximateSize, + // which seems even more expensive + approximateSize = size(); + } + + approximateSize++; + return super.put(key, value); + } + }; + + /** + * Adds a {@link Sampler} that will get run <b>every time an allocation is + * performed from Java code</b>. Use this with <b>extreme</b> judiciousness! + * + * @param sampler The sampler to add. + */ + public static void addSampler(Sampler sampler) { + synchronized (samplerLock) { + Sampler[] samplers = additionalSamplers; + /* create a new list of samplers from the old, adding this sampler */ + if (samplers != null) { + Sampler [] newSamplers = new Sampler[samplers.length + 1]; + System.arraycopy(samplers, 0, newSamplers, 0, samplers.length); + newSamplers[samplers.length] = sampler; + additionalSamplers = newSamplers; + } else { + Sampler[] newSamplers = new Sampler[1]; + newSamplers[0] = sampler; + additionalSamplers = newSamplers; + } + } + } + + /** + * Removes the given {@link Sampler}. + * + * @param sampler The sampler to remove. + */ + public static void removeSampler(Sampler sampler) { + synchronized (samplerLock) { + Sampler[] samplers = additionalSamplers; + List<Sampler> l = Arrays.asList(samplers); + while (l.remove(sampler)); + additionalSamplers = l.toArray(new Sampler[0]); + } + } + + /** + * Returns the size of the given object. If the object is not an array, we + * check the cache first, and update it as necessary. + * + * @param obj the object. + * @param isArray indicates if the given object is an array. + * @return the size of the given object. + */ + private static long getObjectSize(Object obj, boolean isArray) { + if (isArray) { + return instrumentation.getObjectSize(obj); + } + + Class<?> clazz = obj.getClass(); + Long classSize = classSizesMap.get(clazz); + if (classSize == null) { + classSize = instrumentation.getObjectSize(obj); + classSizesMap.put(clazz, classSize); + } + + return classSize; + } + + public static void recordAllocation(Class<?> cls, Object newObj) { + // The use of replace makes calls to this method relatively ridiculously + // expensive. + String typename = cls.getName().replace(".", "/"); + recordAllocation(-1, typename, newObj); + } + + /** + * Records the allocation. This method is invoked on every allocation + * performed by the system. + * + * @param count the count of how many instances are being + * allocated, if an array is being allocated. If an array is not being + * allocated, then this value will be -1. + * @param desc the descriptor of the class/primitive type + * being allocated. + * @param newObj the new <code>Object</code> whose allocation is being + * recorded. + */ + public static void recordAllocation(int count, String desc, Object newObj) { + if (recordingAllocation.get() == Boolean.TRUE) { + return; + } else { + recordingAllocation.set(Boolean.TRUE); + } + + // NB: This could be smaller if the defaultSampler were merged with the + // optional samplers. However, you don't need the optional samplers in + // the common case, so I thought I'd save some space. + if (instrumentation != null) { + // calling getObjectSize() could be expensive, + // so make sure we do it only once per object + long objectSize = -1; + + Sampler[] samplers = additionalSamplers; + if (samplers != null) { + if (objectSize < 0) { + objectSize = getObjectSize(newObj, (count >= 0)); + } + for (Sampler sampler : samplers) { + sampler.sampleAllocation(count, desc, newObj, objectSize); + } + } + } + + recordingAllocation.set(Boolean.FALSE); + } + + /** + * Helper method to force recording; for unit tests only. + */ + public static void recordAllocationForceForTest(int count, String desc, + Object newObj) { + // Make sure we get the right number of elided frames + recordAllocationForceForTestReal(count, desc, newObj, 2); + } + + public static void recordAllocationForceForTestReal( + int count, String desc, Object newObj, int recurse) { + if (recurse != 0) { + recordAllocationForceForTestReal(count, desc, newObj, recurse - 1); + return; + } + } +} |