├── .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 |
--------------------------------------------------------------------------------