blob: 1ead9ca1cb098f193d46bfcb273a8555a87cb030 [file] [log] [blame]
/*
* 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;
}
}
}