├── .gitignore ├── LICENSE ├── Readme.md ├── pom.xml └── src ├── main ├── java │ └── de │ │ └── codecentric │ │ └── performance │ │ └── agent │ │ └── allocation │ │ ├── AgentLogger.java │ │ ├── AllocationProfilingAgent.java │ │ ├── AllocationTrackerClassFileTransformer.java │ │ ├── ClassCounter.java │ │ ├── ConstructorVisitor.java │ │ ├── TrackerConfig.java │ │ └── mbean │ │ ├── Agent.java │ │ └── AgentMBean.java ├── java6 │ └── de │ │ └── codecentric │ │ └── performance │ │ └── agent │ │ └── allocation │ │ └── Tracker.java ├── java8 │ └── de │ │ └── codecentric │ │ └── performance │ │ └── agent │ │ └── allocation │ │ └── Tracker.java └── resources │ └── META-INF │ └── MANIFEST.MF └── test └── java └── de └── codecentric ├── performance └── agent │ └── allocation │ └── AllocationTrackerClassFileTransformerTest.java └── test └── TestBean.java /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | /build 3 | .settings 4 | /target 5 | /.project 6 | /.classpath 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2014 Fabian Lange, codecentric AG 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a 5 | copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # object allocation tracker 2 | 3 | ## Purpose and Scope 4 | The purpose of this agent is to fix excessive object allocation issues. While most profilers do offer allocation tracking, they all struggle heavily when the garbage issue is really out of hand. Examples are for example gigabytes of object allocations per second. 5 | 6 | This single purpose agent is extremely lightweight and quite optimized (improvement PRs are welcome at any time!). 7 | 8 | It is intended to be inserted into production or load test environments and will give you a report of the creation count of all classes matching the configured package prefix. 9 | 10 | Usually you will know where you create instances of your classes and you can improve the situation right after the agent tells you what are your most frequently created objects. 11 | 12 | The agent does not differentiate between live and garbage objects, issues with live objects can usually be solved with a heap dump, finding out which objects create garbage is usually much harder. 13 | 14 | ## Limitations 15 | By design you have to give a non empty prefix which limits the classes which are instrumented. You should use your own package only. Trying to track JVM/platform packages is not recommended. 16 | 17 | If you create more than `Long.MAX_VALUE` instances of a tracked class the counter will roll over without you noticing it, but that scenario is unlikely. The agent uses `AtomicLong` to track the instantiations for broad JVM support. A special JDK 8 build will use `LongAdder` which has better performance under contention. 18 | 19 | The agent is controlled by a `PlatformMBeanServer` MBean. This might require activation on certain application servers. 20 | 21 | For performance reasons the agent does not try to figure out if a class is a superclass or not. Thus superclasses will be included in the report and common base classes will likely appear at the top. 22 | 23 | ## Build 24 | The agent currently supports JDK 6+7 as well as JDK8. 25 | 26 | By default the Maven build will activate the profile `java-6`, which will build an agent that works from JDK 6 onwards. 27 | 28 | Build it with `mvn clean package`. 29 | 30 | To build an optimized agent for Java8 (which will use LongAdder as mentioned above) run: 31 | `mvn -P java-8 clean package` 32 | 33 | Please always use `clean` with your build commands when building both versions, as they might overwrite / leave behind classes you do not want. 34 | 35 | The same profile applies also to the Eclipse project generation. If you switch between both it is recommended to use `eclipse:clean` like this: 36 | `mvn -P java-8 eclipse:clean eclipse:eclipse` 37 | Note that the Eclipse project generation does not know which JDK you use to compile inside Eclipse. Make sure you use the correct one. 38 | 39 | 40 | ## Usage 41 | Run your application with `-javaagent:/path/to/agent.jar=package.prefix` 42 | 43 | E.g if your name is Fabian and you are working on a codecentric project on your Macbook: 44 | `-javaagent:/Users/fabian/work/allocation-tracker/target/allocation-tracker-agent-0.0.1-SNAPSHOT.jar=de.codecentric` 45 | 46 | After the agent has printed `codecentric allocation agent - Registered Agent MBean.` the agent is ready to use. 47 | Connect to the JVM with any JMX client and use the `de.codecentric.Agent` MBean to `start` tracking allocations. Use `stop` to stop tracking allocations. `printTop`can be used any time to print the most often created object instances. The parameter will control the amount of output. If the parameter is zero or less, it will default to 100. 48 | 49 | ## Where are the freaking tests? 50 | Glad that you asked. Please take a look at the source files and then come up with a sensible unit test. If you manage to do that feel free to put up a PR. 51 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | 6 | 3.0.0 7 | 8 | 9 | de.codecentric.performance 10 | allocation-tracker-agent 11 | 0.0.1-SNAPSHOT 12 | jar 13 | 14 | codecentric allocation tracker 15 | 16 | 17 | UTF-8 18 | 19 | 20 | 21 | 22 | 23 | org.apache.maven.plugins 24 | maven-jar-plugin 25 | 2.5 26 | 27 | 28 | src/main/resources/META-INF/MANIFEST.MF 29 | 30 | 31 | 32 | 33 | org.apache.maven.plugins 34 | maven-eclipse-plugin 35 | 2.9 36 | 37 | 2.0 38 | true 39 | true 40 | 41 | 42 | 43 | org.apache.maven.plugins 44 | maven-shade-plugin 45 | 2.3 46 | 47 | 48 | package 49 | 50 | shade 51 | 52 | 53 | 54 | 55 | org.objectweb.asm 56 | de.codecentric.performance.agent.asm.org.objectweb.asm 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | junit 69 | junit 70 | 4.11 71 | test 72 | 73 | 74 | org.hamcrest 75 | hamcrest-all 76 | 1.3 77 | test 78 | 79 | 80 | commons-io 81 | commons-io 82 | 2.4 83 | test 84 | 85 | 86 | org.ow2.asm 87 | asm 88 | 5.0.3 89 | 90 | 91 | 92 | 93 | 94 | java-6 95 | 96 | true 97 | 98 | 99 | 1.6 100 | 1.6 101 | 102 | 103 | 104 | 105 | org.codehaus.mojo 106 | build-helper-maven-plugin 107 | 1.9 108 | 109 | 110 | add-source 111 | generate-sources 112 | 113 | add-source 114 | 115 | 116 | 117 | src/main/java6 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | java-8 128 | 129 | 1.8 130 | 1.8 131 | 132 | 133 | 134 | 135 | org.codehaus.mojo 136 | build-helper-maven-plugin 137 | 1.9 138 | 139 | 140 | add-source 141 | generate-sources 142 | 143 | add-source 144 | 145 | 146 | 147 | src/main/java8 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | Fabian Lange 161 | codecentric AG 162 | https://github.com/CodingFabian 163 | 164 | 165 | 166 | 167 | 168 | The MIT License 169 | http://opensource.org/licenses/MIT 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /src/main/java/de/codecentric/performance/agent/allocation/AgentLogger.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.performance.agent.allocation; 2 | 3 | /** 4 | * Poor mans logging wrapper. The agent does very little logging, so it can easily get lost in complex environments. 5 | * This class could be changed to write to a dedicated file if desired. 6 | */ 7 | public class AgentLogger { 8 | 9 | public static final boolean LOG_TOP_LIST = true; 10 | 11 | /** 12 | * Logs the given message to stout and stderr. Reason behind this is that in the past it has proven to be difficult to 13 | * locate the one or the other file. By writing to both outputs we increase the chance of not missing the agent 14 | * output. 15 | * 16 | * @param logMessage 17 | * message as String 18 | */ 19 | public static void log(String logMessage) { 20 | System.out.print("codecentric allocation agent - "); 21 | System.out.println(logMessage); 22 | System.err.print("codecentric allocation agent - "); 23 | System.err.println(logMessage); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/de/codecentric/performance/agent/allocation/AllocationProfilingAgent.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.performance.agent.allocation; 2 | 3 | import java.io.PrintWriter; 4 | import java.io.StringWriter; 5 | import java.lang.instrument.Instrumentation; 6 | import java.lang.management.ManagementFactory; 7 | import java.util.concurrent.TimeUnit; 8 | 9 | import javax.management.MBeanServer; 10 | import javax.management.ObjectName; 11 | 12 | import de.codecentric.performance.agent.allocation.mbean.Agent; 13 | 14 | /** 15 | * Class registered as premain hook, will add a ClassFileTransformer and register an MBean for controlling the agent. 16 | */ 17 | public class AllocationProfilingAgent { 18 | 19 | public static void premain(String agentArgs, Instrumentation inst) { 20 | String prefix = agentArgs; 21 | if (prefix == null || prefix.length() == 0) { 22 | AgentLogger.log("Agent failed to start: Please provide a package prefix to filter."); 23 | return; 24 | } 25 | // accepts both . and / notation, but will convert dots to slashes 26 | prefix = prefix.replace(".", "/"); 27 | if (!prefix.contains("/")) { 28 | AgentLogger.log("Agent failed to start: Please provide at least one package level prefix to filter."); 29 | return; 30 | } 31 | registerMBean(); 32 | inst.addTransformer(new AllocationTrackerClassFileTransformer(prefix)); 33 | } 34 | 35 | /* 36 | * Starts a new thread which will try to connect to the Platform Mbean Server. 37 | */ 38 | private static void registerMBean() { 39 | Thread thread = new Thread() { 40 | @Override 41 | public void run() { 42 | try { 43 | // retry up to a maximum of 10 minutes 44 | int retryLimit = 60; 45 | MBeanServer mbs = null; 46 | while (mbs == null) { 47 | if (retryLimit-- == 0) { 48 | AgentLogger.log("Could not register Agent MBean in 10 minutes."); 49 | return; 50 | } 51 | TimeUnit.SECONDS.sleep(10); 52 | mbs = ManagementFactory.getPlatformMBeanServer(); 53 | } 54 | mbs.registerMBean(new Agent(), new ObjectName("de.codecentric:type=Agent")); 55 | AgentLogger.log("Registered Agent MBean."); 56 | } catch (Exception e) { 57 | AgentLogger.log("Could not register Agent MBean. Exception:"); 58 | StringWriter sw = new StringWriter(); 59 | e.printStackTrace(new PrintWriter(sw)); 60 | AgentLogger.log(sw.toString()); 61 | } 62 | } 63 | }; 64 | thread.start(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/de/codecentric/performance/agent/allocation/AllocationTrackerClassFileTransformer.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.performance.agent.allocation; 2 | 3 | import java.lang.instrument.ClassFileTransformer; 4 | import java.lang.instrument.IllegalClassFormatException; 5 | import java.security.ProtectionDomain; 6 | 7 | import org.objectweb.asm.ClassReader; 8 | import org.objectweb.asm.ClassVisitor; 9 | import org.objectweb.asm.ClassWriter; 10 | import org.objectweb.asm.MethodVisitor; 11 | import org.objectweb.asm.Opcodes; 12 | 13 | /** 14 | * ClassFileTransformer implementation which will use ASM to visit the bytecode of constructors of classes matching the 15 | * given prefix. The actual modification of the constructor bytecode is performed by ConstructorVisitor. 16 | */ 17 | public class AllocationTrackerClassFileTransformer implements ClassFileTransformer { 18 | 19 | private String prefix; 20 | 21 | public AllocationTrackerClassFileTransformer(String prefix) { 22 | this.prefix = prefix; 23 | if (prefix.startsWith("java")) { 24 | AgentLogger.log("You are trying to instrument JVM core classes - this might crash the JVM"); 25 | } 26 | } 27 | 28 | @Override 29 | public byte[] transform(ClassLoader loader, final String className, Class classBeingRedefined, 30 | ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { 31 | if (className.startsWith(TrackerConfig.AGENT_PACKAGE_PREFIX)) { 32 | // Safeguard: do not instrument our own classes 33 | return classfileBuffer; 34 | } 35 | 36 | if (!className.startsWith(prefix)) { 37 | return classfileBuffer; 38 | } 39 | 40 | ClassReader classReader = new ClassReader(classfileBuffer); 41 | ClassWriter classWriter = new ClassWriter(classReader, 0); 42 | ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM5, classWriter) { 43 | 44 | @Override 45 | public MethodVisitor visitMethod(int access, String methodName, String desc, String signature, String[] exceptions) { 46 | MethodVisitor methodVisitor = super.visitMethod(access, methodName, desc, signature, exceptions); 47 | if (methodName.equals("")) { 48 | return new ConstructorVisitor(className, methodVisitor); 49 | } 50 | return methodVisitor; 51 | } 52 | 53 | }; 54 | classReader.accept(classVisitor, 0); 55 | return classWriter.toByteArray(); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/de/codecentric/performance/agent/allocation/ClassCounter.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.performance.agent.allocation; 2 | 3 | /** 4 | * Utility class for sorting classnames and counts. 5 | */ 6 | public class ClassCounter implements Comparable { 7 | 8 | public String className; 9 | public long count; 10 | 11 | public ClassCounter(String className, long count) { 12 | this.className = className; 13 | this.count = count; 14 | } 15 | 16 | @Override 17 | public int compareTo(ClassCounter that) { 18 | long x = that.count; 19 | long y = this.count; 20 | return (x < y) ? -1 : ((x == y) ? 0 : 1); 21 | } 22 | 23 | @Override 24 | public String toString() { 25 | return className + " " + count; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/de/codecentric/performance/agent/allocation/ConstructorVisitor.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.performance.agent.allocation; 2 | 3 | import org.objectweb.asm.MethodVisitor; 4 | import org.objectweb.asm.Opcodes; 5 | 6 | /** 7 | * Changes the bytecode of the visited constructor to call the static tracker. technically could be added to any method. 8 | */ 9 | public class ConstructorVisitor extends MethodVisitor { 10 | 11 | /* 12 | * reference to the class name of the visited class. 13 | */ 14 | private String className; 15 | 16 | public ConstructorVisitor(String className, MethodVisitor mv) { 17 | super(Opcodes.ASM5, mv); 18 | this.className = className; 19 | } 20 | 21 | @Override 22 | public void visitCode() { 23 | // Pass in the className used by the visitor, which is always the single class name instance from the loaded class. 24 | // No garbage is produced here. 25 | super.visitLdcInsn(className); 26 | super.visitMethodInsn(Opcodes.INVOKESTATIC, TrackerConfig.TRACKER_CLASS, TrackerConfig.TRACKER_CALLBACK, 27 | TrackerConfig.TRACKER_CALLBACK_SIGNATURE, false); 28 | super.visitCode(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/de/codecentric/performance/agent/allocation/TrackerConfig.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.performance.agent.allocation; 2 | 3 | /** 4 | * Configuration class for the Tracker being used. 5 | */ 6 | public class TrackerConfig { 7 | 8 | /* Package prefix of the agent. To be filtered to avoid potential recursions or other issues. */ 9 | public static final String AGENT_PACKAGE_PREFIX = "de/codecentric/performance/agent/"; 10 | 11 | /* Tracker class needs to be slash separated package name. */ 12 | static final String TRACKER_CLASS = "de/codecentric/performance/agent/allocation/Tracker"; 13 | 14 | /* Static method to be invoked for each construction. */ 15 | public static final String TRACKER_CALLBACK = "constructed"; 16 | 17 | /* Signature of the callback method. Should accept a single String. */ 18 | public static final String TRACKER_CALLBACK_SIGNATURE = "(Ljava/lang/String;)V"; 19 | 20 | /* 21 | * Default number of top classes returned in buildTopList when the argument is <= 0. 22 | * 23 | * Convenience only, no performance impact. 24 | */ 25 | static final int DEFAULT_AMOUNT = 100; 26 | 27 | /* 28 | * Default size is insufficient in almost all real world scenarios. Any number is a pure guess. 1000 is a good 29 | * starting point. 30 | * 31 | * Impacts memory usage. 32 | */ 33 | static final int MAP_SIZE = 1000; 34 | 35 | /* 36 | * Default load factor of 0.75 should work fine. 37 | * 38 | * Impacts memory usage. Low values impact cpu usage. 39 | */ 40 | static final float LOAD_FACTOR = 0.75f; 41 | 42 | /* 43 | * Default concurrency level of 16 threads is probably sufficient in most real world deployments. Note that the 44 | * setting is for updating threads only, thus is concerned only when a tracked class is instantiated the first time. 45 | * 46 | * Impacts memory and cpu usage. 47 | */ 48 | static final int CONCURRENCY_LEVEL = 16; 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/de/codecentric/performance/agent/allocation/mbean/Agent.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.performance.agent.allocation.mbean; 2 | 3 | import de.codecentric.performance.agent.allocation.AgentLogger; 4 | import de.codecentric.performance.agent.allocation.Tracker; 5 | 6 | public class Agent implements AgentMBean { 7 | 8 | @Override 9 | public void start() { 10 | AgentLogger.log("Agent is now tracking."); 11 | Tracker.start(); 12 | } 13 | 14 | @Override 15 | public void stop() { 16 | AgentLogger.log("Agent is no longer tracking."); 17 | Tracker.stop(); 18 | } 19 | 20 | @Override 21 | public String printTop(int amount) { 22 | String topList = Tracker.buildTopList(amount); 23 | if (AgentLogger.LOG_TOP_LIST) { 24 | AgentLogger.log("Agent saw these allocations:"); 25 | AgentLogger.log(topList); 26 | } 27 | return topList; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/de/codecentric/performance/agent/allocation/mbean/AgentMBean.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.performance.agent.allocation.mbean; 2 | 3 | public interface AgentMBean { 4 | 5 | public void start(); 6 | 7 | public void stop(); 8 | 9 | public String printTop(int amount); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java6/de/codecentric/performance/agent/allocation/Tracker.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.performance.agent.allocation; 2 | 3 | import static de.codecentric.performance.agent.allocation.TrackerConfig.CONCURRENCY_LEVEL; 4 | import static de.codecentric.performance.agent.allocation.TrackerConfig.DEFAULT_AMOUNT; 5 | import static de.codecentric.performance.agent.allocation.TrackerConfig.LOAD_FACTOR; 6 | import static de.codecentric.performance.agent.allocation.TrackerConfig.MAP_SIZE; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.Map.Entry; 11 | import java.util.Set; 12 | import java.util.concurrent.ConcurrentHashMap; 13 | import java.util.concurrent.atomic.AtomicLong; 14 | 15 | /** 16 | * Main class, which is notified by BCI inserted code when an object is constructed. This class keeps a 17 | * ConcurrentHashMap with class names as keys. This is "leaking" the class name by design, so that the class name string 18 | * is kept even when the class has been unloaded. For each class name the ConcurrentHashMap will store an AtmomicLong 19 | * instance. 20 | * 21 | * Compatibility: Java 6-7 22 | */ 23 | public class Tracker { 24 | 25 | private static ConcurrentHashMap counts = new ConcurrentHashMap(MAP_SIZE, 26 | LOAD_FACTOR, CONCURRENCY_LEVEL); 27 | 28 | /* 29 | * Toggle controlling whether the tracker should track instantiations. 30 | */ 31 | private static volatile boolean count = false; 32 | 33 | /** 34 | * Call back invoked by BCI inserted code when a class is instantiated. The class name must be an interned/constant 35 | * value to avoid leaking! 36 | * 37 | * @param className 38 | * name of the class that has just been instantiated. 39 | */ 40 | public static void constructed(String className) { 41 | if (!count) { 42 | return; 43 | } 44 | AtomicLong atomicLong = counts.get(className); 45 | // for most cases the long should exist already. 46 | if (atomicLong == null) { 47 | atomicLong = new AtomicLong(); 48 | AtomicLong oldValue = counts.putIfAbsent(className, atomicLong); 49 | if (oldValue != null) { 50 | // if the put returned an existing value that one is used. 51 | atomicLong = oldValue; 52 | } 53 | } 54 | atomicLong.incrementAndGet(); 55 | } 56 | 57 | /** 58 | * Clears recorded data and starts recording. 59 | */ 60 | public static void start() { 61 | counts.clear(); 62 | count = true; 63 | } 64 | 65 | /** 66 | * Stops recording. 67 | */ 68 | public static void stop() { 69 | count = false; 70 | } 71 | 72 | /** 73 | * Builds a human readable list of class names and instantiation counts. 74 | * 75 | * Note: this method will create garbage while building and sorting the top list. The amount of garbage created is 76 | * dictated by the amount of classes tracked, not by the amount requested. 77 | * 78 | * @param amount 79 | * controls how many results are included in the top list. If <= 0 will default to DEFAULT_AMOUNT. 80 | * @return a newline separated String containing class names and invocation counts. 81 | */ 82 | public static String buildTopList(final int amount) { 83 | Set> entrySet = counts.entrySet(); 84 | ArrayList cc = new ArrayList(entrySet.size()); 85 | 86 | for (Entry entry : entrySet) { 87 | cc.add(new ClassCounter(entry.getKey(), entry.getValue().longValue())); 88 | } 89 | Collections.sort(cc); 90 | StringBuilder sb = new StringBuilder(); 91 | int max = Math.min(amount <= 0 ? DEFAULT_AMOUNT : amount, cc.size()); 92 | for (int i = 0; i < max; i++) { 93 | sb.append(cc.get(i).toString()); 94 | sb.append('\n'); 95 | } 96 | return sb.toString(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java8/de/codecentric/performance/agent/allocation/Tracker.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.performance.agent.allocation; 2 | 3 | import static de.codecentric.performance.agent.allocation.TrackerConfig.CONCURRENCY_LEVEL; 4 | import static de.codecentric.performance.agent.allocation.TrackerConfig.DEFAULT_AMOUNT; 5 | import static de.codecentric.performance.agent.allocation.TrackerConfig.LOAD_FACTOR; 6 | import static de.codecentric.performance.agent.allocation.TrackerConfig.MAP_SIZE; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.Map.Entry; 11 | import java.util.Set; 12 | import java.util.concurrent.ConcurrentHashMap; 13 | import java.util.concurrent.atomic.LongAdder; 14 | 15 | /** 16 | * Main class, which is notified by BCI inserted code when an object is constructed. This class keeps a 17 | * ConcurrentHashMap with class names as keys. This is "leaking" the class name by design, so that the class name string 18 | * is kept even when the class has been unloaded. For each class name the ConcurrentHashMap will store an LongAdder 19 | * Instance. 20 | * 21 | * Compatibility: Java 8+ 22 | */ 23 | public class Tracker { 24 | 25 | private static ConcurrentHashMap counts = new ConcurrentHashMap(MAP_SIZE, 26 | LOAD_FACTOR, CONCURRENCY_LEVEL); 27 | 28 | /* 29 | * Toggle controlling whether the tracker should track instantiations. 30 | */ 31 | private static volatile boolean count = false; 32 | 33 | /** 34 | * Call back invoked by BCI inserted code when a class is instantiated. The class name must be an interned/constant 35 | * value to avoid leaking! 36 | * 37 | * @param className 38 | * name of the class that has just been instantiated. 39 | */ 40 | public static void constructed(String className) { 41 | if (!count) { 42 | return; 43 | } 44 | LongAdder longAdder = counts.get(className); 45 | // for most cases the long should exist already. 46 | if (longAdder == null) { 47 | longAdder = new LongAdder(); 48 | LongAdder oldValue = counts.putIfAbsent(className, longAdder); 49 | if (oldValue != null) { 50 | // if the put returned an existing value that one is used. 51 | longAdder = oldValue; 52 | } 53 | } 54 | longAdder.increment(); 55 | } 56 | 57 | /** 58 | * Clears recorded data and starts recording. 59 | */ 60 | public static void start() { 61 | counts.clear(); 62 | count = true; 63 | } 64 | 65 | /** 66 | * Stops recording. 67 | */ 68 | public static void stop() { 69 | count = false; 70 | } 71 | 72 | /** 73 | * Builds a human readable list of class names and instantiation counts. 74 | * 75 | * Note: this method will create garbage while building and sorting the top list. The amount of garbage created is 76 | * dictated by the amount of classes tracked, not by the amount requested. 77 | * 78 | * @param amount 79 | * controls how many results are included in the top list. If <= 0 will default to DEFAULT_AMOUNT. 80 | * @return a newline separated String containing class names and invocation counts. 81 | */ 82 | public static String buildTopList(final int amount) { 83 | Set> entrySet = counts.entrySet(); 84 | ArrayList cc = new ArrayList(entrySet.size()); 85 | 86 | for (Entry entry : entrySet) { 87 | cc.add(new ClassCounter(entry.getKey(), entry.getValue().longValue())); 88 | } 89 | Collections.sort(cc); 90 | StringBuilder sb = new StringBuilder(); 91 | int max = Math.min(amount <= 0 ? DEFAULT_AMOUNT : amount, cc.size()); 92 | for (int i = 0; i < max; i++) { 93 | sb.append(cc.get(i).toString()); 94 | sb.append('\n'); 95 | } 96 | return sb.toString(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Class-Path: 3 | Premain-class: de.codecentric.performance.agent.allocation.AllocationProfilingAgent 4 | Can-Redefine-Classes: true 5 | -------------------------------------------------------------------------------- /src/test/java/de/codecentric/performance/agent/allocation/AllocationTrackerClassFileTransformerTest.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.performance.agent.allocation; 2 | 3 | import static org.hamcrest.Matchers.greaterThan; 4 | import static org.hamcrest.Matchers.is; 5 | import static org.junit.Assert.assertSame; 6 | import static org.junit.Assert.assertThat; 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | 11 | import org.apache.commons.io.IOUtils; 12 | import org.junit.Assert; 13 | import org.junit.Before; 14 | import org.junit.Test; 15 | 16 | import de.codecentric.test.TestBean; 17 | 18 | public class AllocationTrackerClassFileTransformerTest { 19 | 20 | /** 21 | * The object under test 22 | */ 23 | private AllocationTrackerClassFileTransformer transformer; 24 | 25 | @Before 26 | public void setUp() { 27 | transformer = new AllocationTrackerClassFileTransformer("de.codecentric.test"); 28 | } 29 | 30 | @Test 31 | public void allocationTrackerClassesAreIgnored() throws Exception { 32 | byte[] classBytes = getClassBytes(ConstructorVisitor.class); 33 | byte[] actual = transformer.transform(null, ConstructorVisitor.class.getName(), ConstructorVisitor.class, null, 34 | classBytes); 35 | 36 | assertSame(classBytes, actual); 37 | } 38 | 39 | @Test 40 | public void notMatchingClassPrefixesAreIgnored() throws Exception { 41 | byte[] classBytes = getClassBytes(Assert.class); 42 | byte[] actual = transformer.transform(null, Assert.class.getName(), Assert.class, null, classBytes); 43 | 44 | assertSame(classBytes, actual); 45 | } 46 | 47 | @Test 48 | public void matchingClassesAreExtended() throws Exception { 49 | byte[] byteArray = getClassBytes(TestBean.class); 50 | byte[] actual = transformer.transform(null, TestBean.class.getName(), TestBean.class, null, byteArray); 51 | 52 | // the class has been changed. What else can we do? 53 | assertThat(actual.length, is(greaterThan(byteArray.length))); 54 | } 55 | 56 | private byte[] getClassBytes(Class clazz) throws IOException { 57 | String className = clazz.getName(); 58 | String classAsPath = className.replace('.', '/') + ".class"; 59 | InputStream stream = clazz.getClassLoader().getResourceAsStream(classAsPath); 60 | 61 | return IOUtils.toByteArray(stream); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/de/codecentric/test/TestBean.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.test; 2 | 3 | /** 4 | * Test class for testing byte code manipulation 5 | */ 6 | public class TestBean { 7 | 8 | } 9 | --------------------------------------------------------------------------------