├── .gitignore ├── README.md ├── license.txt ├── pom.xml ├── src └── org │ └── github │ └── jamm │ ├── CannotAccessFieldException.java │ ├── CannotMeasureObjectException.java │ ├── FieldAndClassFilter.java │ ├── FieldFilter.java │ ├── Filters.java │ ├── IdentityHashSet.java │ ├── Measurable.java │ ├── MeasurementStack.java │ ├── MemoryLayoutSpecification.java │ ├── MemoryMeter.java │ ├── MemoryMeterListener.java │ ├── MemoryMeterStrategy.java │ ├── Unmetered.java │ ├── VM.java │ ├── accessors │ ├── FieldAccessor.java │ ├── JpmsAccessor.java │ └── PlainReflectionAccessor.java │ ├── listeners │ ├── NoopMemoryMeterListener.java │ └── TreePrinter.java │ ├── strategies │ ├── ContendedUtils.java │ ├── ContentionGroupCounter.java │ ├── DoesNotUseEmptySlotInSuperSpecStrategy.java │ ├── InstrumentationAndSpecStrategy.java │ ├── InstrumentationStrategy.java │ ├── MemoryLayoutBasedStrategy.java │ ├── MemoryMeterStrategies.java │ ├── PreJava15SpecStrategy.java │ ├── PreJava15UnsafeStrategy.java │ ├── SpecStrategy.java │ └── UnsafeStrategy.java │ ├── string │ ├── PlainReflectionStringMeter.java │ ├── PreJava9StringMeter.java │ ├── StringMeter.java │ └── UnsafeStringMeter.java │ └── utils │ ├── ArrayMeasurementUtils.java │ ├── ByteBufferMeasurementUtils.java │ ├── MathUtils.java │ └── MethodHandleUtils.java ├── test └── org │ └── github │ └── jamm │ ├── GuessTest.java │ ├── IdentityHashSetTest.java │ ├── MathUtilsTest.java │ ├── MemoryMeterTest.java │ ├── jmh │ ├── BenchmarkMeasureArray.java │ ├── BenchmarkMeasureInstance.java │ ├── BenchmarkMeasureString.java │ └── BenchmarkObjectGraphTraversal.java │ ├── strategies │ ├── MemoryMeterStrategiesTest.java │ └── MemoryMeterStrategyTest.java │ ├── string │ └── StringMeterTest.java │ └── testedclasses │ ├── PackageProtectedClass.java │ └── PublicClassWithPackageProtectedClassField.java └── toolchains.example.xml /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | out 3 | .gitignore 4 | .idea/ 5 | target 6 | *.iml 7 | *.ipr 8 | *.iws 9 | *.orig 10 | *.rej 11 | .classpath 12 | .project 13 | /classes/ 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | Jamm provides `MemoryMeter`, a Java agent for all Java versions to 4 | measure actual object memory use including JVM overhead. 5 | 6 | Jamm assume that the JVM running the code is an HotSpot JVM. It has not been tested with other type of JVMs. 7 | 8 | # Building 9 | 10 | "mvn package"; optionally, "mvn install" 11 | 12 | # Setup your toolchains.xml 13 | 14 | We want to compile and test using different JVM versions. Configuration option jdkToolchain can be used to supply an alternate 15 | toolchain specification. To achieve our goal we need first to setup `toolchains.xml`. 16 | The `toolchains.xml` file is the configuration file where you set the installation paths of your toolchains. This file 17 | should be put in your `${user.home}/.m2` directory. When the maven-toolchains-plugin executes, it looks for the `toolchains.xml` 18 | file, reads it and looks for a toolchain matching the toolchains requirements configured in the plugin. 19 | Jamm repo contains a `toolchains.example.xml` which you can use as a baseline for your own `toolchains.xml`. You need it 20 | to be able to run the tests. Copy `toolchains.example.xml` to `${user.home}/.m2`, rename the file to `toolchains.xml`. 21 | In `toolchains.xml`, check to update your vendor and jdkHome for JDK8, JDK11 and JDK17 which you have installed on your machine. 22 | 23 | # Running Tests 24 | 25 | The tests can be run with "mvn test". The `JvmArgs` property can be used to specify the JVM arguments that can be used for running the tests. 26 | For example: 27 | 28 | ``` 29 | mvn test -DjvmArgs="-Xmx64g" 30 | mvn test -DjvmArgs="-Xmx64g -XX:ObjectAlignmentInBytes=16 -XX:-UseCompressedClassPointers" 31 | ``` 32 | 33 | `mvn test` runs all tests with JDK8, JDK11, and then with JDK17 34 | To run the tests with only one particular JDK version run: 35 | * for JDK8: 36 | ``` 37 | mvn surefire:test@test-default 38 | ``` 39 | or 40 | ``` 41 | mvn surefire:test 42 | ``` 43 | * for JDK11: 44 | ``` 45 | mvn surefire:test@test-jdk11 46 | ``` 47 | * for JDK17: 48 | ``` 49 | mvn surefire:test@test-jdk17 50 | ``` 51 | 52 | # Use 53 | 54 | 55 | The best way to use `MemoryMeter` is to start the JVM with "-javaagent:/jamm.jar" in order to use the 56 | `Instrumentation` strategy to guess objects' sizes. 57 | 58 | `MemoryMeter` can be used in your code like this: 59 | 60 | MemoryMeter meter = MemoryMeter.builder().build(); 61 | meter.measure(object); 62 | meter.measureDeep(object); 63 | 64 | 65 | If you would like to use `MemoryMeter` in a web application, make sure 66 | that you do NOT put this jar in `WEB-INF/lib`, as that may cause problems 67 | since your code is accessing a `MemoryMeter` from a different class loader 68 | than the one loaded by the `-javaagent` and won't see it as initialized. 69 | 70 | If you want `MemoryMeter` not to measure some specific classes, you can 71 | mark the classes (or interfaces) using the `Unmetered` annotation. 72 | 73 | # The Maven coordinates for the latest version of Jamm 74 | 75 | ``` 76 | com.github.jbellis 77 | jamm 78 | 0.4.0 79 | ``` 80 | 81 | # 0.4.0 breaking changes 82 | 83 | The 0.4.0 version comes with speed improvements and support java versions up to Java 17 but also some breaking 84 | changes at the API level. 85 | * The `MemoryMeter` constructor and the static methods used to configure the different options (`withGuessing`, `ignoreOuterClassReference`, `ignoreKnownSingletons`, `ignoreNonStrongReferences`, `enableDebug`) have been removed. Instead `MemoryMeter` instances must be created through a `Builder`. 86 | * The `omitSharedBufferOverhead` option has been removed. Instead a `ByteBufferMode` can be provided as an argument to `measureDeep`. The provided mode must match the way the application is creating SLABs for accurate results. 87 | * The ability to provide a tracker for visited object has been removed. 88 | * `Guess` values have been changed to each represent a single strategy. Fallback strategies can be defined through the `MemoryMeter.Builder::withGuessing` method. 89 | * `MemoryMeter.countChildren` has been removed. 90 | * The `MemoryMeter.measure` and `MemoryMeter.measureDeep` now accept `null` parameters 91 | * Jamm is not trying anymore to support non Hotspot JVM (e.g. OpenJ9) 92 | * By default `MemoryMeter.measureDeep` is now ignoring the space occupied by known singletons such as `Class` objects, `enums`, `ClassLoaders`, `AccessControlContexts` as well as non-strong references 93 | (like weak/soft/phantom references). If you want `MemoryMeter` to measure them you need to enable those measurements through `MemoryMeter.builder().measureKnownSingletons()` and `MemoryMeter.builder().measureNonStrongReferences()`. 94 | * When measuring direct `ByteBuffer` objects `MemoryMeter` is ignoring some fields from the Cleaner as it might lead to some incorrect measurements by including references to other Cleaner instances 95 | * When measuring `Thread` objects `MemoryMeter` is ignoring the `group` field as it references the all the threads from the group 96 | * The behavior around non-strong references has changed. When non-strong references are ignored (the default) `MemoryMeter` will ignore all the fields from the `Reference` class as well as the `head` field from `ReferenceQueue`. 97 | 98 | # Supported Java versions 99 | 100 | The 0.4.0 release has been tested with Java 8, 11 and 17 and the following JVM arguments that can affect the memory layout: 101 | * `-Xmx` 102 | * `UseCompressedClassPointers` 103 | * `ObjectAlignmentInBytes` 104 | * `UseCompressedOops` 105 | * `RestrictContended` 106 | * `EnableContended` 107 | * `ContendedPaddingWidth` 108 | * `UseEmptySlotsInSupers` 109 | 110 | The `Specification` strategy does not work correctly with `UseEmptySlotsInSupers` disabled for some classes (like direct `ByteBuffer`) 111 | that interleave fields from different classes when they should not. 112 | 113 | The `ContendedPaddingWidth` and `EnableContended` arguments are broken in Java 17. Changing the padding width has no effect and disabling @Contended 114 | (`-XX:-EnableContended`) has 2 bugs: 115 | * It does not work for contended annotation on fields 116 | * It does not work for the `ConcurrentHashMap` use of the class Contended annotation 117 | 118 | Those bugs might caused the `Unsafe` and `Specification` strategies to return wrong results when the classes are using `@Contended` and those JVM arguments are used. 119 | 120 | # The fine print 121 | 122 | ## Measurement strategies 123 | 124 | `MemoryMeter` can use different strategies to guess the objects sizes. We have tried to ensure that the output of the strategies is the same. 125 | 126 | ### Instrumentation 127 | 128 | If the JVM has been started with `-javaagent`, `MemoryMeter` will use 129 | `java.lang.instrument.Instrumentation.getObjectSize` to get an estimate of the space required to store 130 | the given object. It is the safest strategy. 131 | 132 | ### Instrumentation and specification 133 | 134 | This strategy requires `java.lang.instrument.Instrumentation` as the `Instrumentation` strategy and will use it 135 | to measure non array object. For measuring arrays it will use the `Specification` strategy way. 136 | This strategy tries to combine the best of both strategies the accuracy and speed of `Instrumentation` for non array object 137 | and the speed of `Specification` for measuring array objects for which all strategy are accurate. For some reason `Instrumentation` is slower for arrays before Java 17. 138 | 139 | ### Unsafe 140 | 141 | `MemoryMeter` will use `Unsafe.objectFieldOffset` to guess the object offset. 142 | Java 14 introduced records and Java 15 introduced Hidden classes which are used from Java 15 onward for Lambda expressions. 143 | Unfortunately, calling `Unsafe.objectFieldOffset` on the `Field` of a record or hidden class will result into an `UnsupportedOperationException` therefore 144 | for record and hidden classes the unsafe strategy delegates the measurement to the specification strategy. 145 | 146 | ### Specification 147 | 148 | `MemoryMeter` will guess the object size based on what it knows from the JVM. 149 | 150 | ## Object graph crawling 151 | 152 | ### Default crawling approach 153 | 154 | When `measureDeep` is called by default `MemoryMeter` will use reflection to crawl the object graph. 155 | In order to prevent infinite loops due to cycles in the object graph `MemoryMeter` tracks visited objects 156 | imposing a memory cost of its own. 157 | 158 | Java 9 introduced the Java Platform Module System (JPMS) that made illegal reflective access between some modules. This is breaking 159 | the ability for Jamm to crawl the object graph. To avoid that problem, if Jamm detects that it cannot use reflection to retrieve 160 | field data it will rely on `Unsafe` to do it. Unfortunately, despite the fact that the code is designed to go around those 161 | illegal accesses the JVM might emit some warning for access that only will be illegal in future versions. The `Unsafe` approach 162 | might also fail for some scenarios as `Unsafe.objectFieldOffset` do not work for `records` or `hidden` classes such 163 | as lambda expressions. In such cases `add-exports` or `add-opens` should be used. 164 | 165 | ### Optimized crawling approach 166 | 167 | For your own classes, `MemoryMeter` provides a way to avoid the use of reflections by having the class implement the `Measurable` 168 | interface. When `MemoryMeter` encounter a class that implements the `Measurable` interface it will call the `addChildrenTo` to let 169 | the class adds its fields to the stack of objects that need to be measured instead of using reflection. Therefore avoiding the reflection cost. 170 | 171 | ### Filtering 172 | 173 | By default `MemoryMeter.measureDeep` is ignoring known singletons such as `Class` objects, `enums`, `ClassLoaders`, `AccessControlContexts` as well as non-strong references 174 | (like weak/soft/phantom references). If you want `MemoryMeter` to measure them you need to enable those measurements through 175 | `MemoryMeter.builder().measureKnownSingletons()` and `MemoryMeter.builder().measureNonStrongReferences()`. 176 | 177 | ## Skipping objects 178 | 179 | If you want `MemoryMeter` not to measure some specific classes or fields, you can 180 | mark the classes/interfaces or fields using the 181 | [`@Unmetered`](./src/org/github/jamm/Unmetered.java) annotation. 182 | 183 | ``` 184 | public class WithAnnotationField { 185 | 186 | @Unmetered 187 | private String s; 188 | 189 | public WithAnnotationField(String s) { 190 | this.s = s; 191 | } 192 | ... 193 | } 194 | ``` 195 | 196 | ``` 197 | @Unmetered 198 | private static class WithTypeAnnotation { 199 | private String s; 200 | 201 | public WithTypeAnnotation(String s) { 202 | this.s = s; 203 | } 204 | ... 205 | } 206 | ``` 207 | 208 | For a finer control on which classes and fields should be filtered out it is possible to use the `MemoryMeter(MemoryMeterStrategy, FieldAndClassFilter, FieldFilter , boolean, MemoryMeterListener.Factory)` constructor. 209 | 210 | ## ByteBuffer measurements 211 | 212 | `MemoryMeter` has 3 ways to measure ByteBuffers: `NORMAL`, `SLAB_ALLOCATION_NO_SLICE` and `SLAB_ALLOCATION_SLICE`. 213 | 214 | ### Normal (default) mode 215 | 216 | In this mode `MemoryMeter.measureDeep` will crawl the object graph and sum its different elements. 217 | For a `HeapByteBuffer` like `ByteBuffer.allocate(20)` the crawled graph will be: 218 | 219 | ``` 220 | root [java.nio.HeapByteBuffer] 96 bytes (56 bytes) 221 | | 222 | +--hb [byte[]] 40 bytes (40 bytes) 223 | ``` 224 | For a `DirectByteBuffer` like `ByteBuffer.allocateDirect(20)` the crawled graph will be: 225 | 226 | ``` 227 | root [java.nio.DirectByteBuffer] 136 bytes (64 bytes) 228 | | 229 | +--cleaner [jdk.internal.ref.Cleaner] 72 bytes (40 bytes) 230 | | 231 | +--thunk [java.nio.DirectByteBuffer$Deallocator] 32 bytes (32 bytes) 232 | ``` 233 | 234 | In reality the `cleaner` field has some extra fields that `MemoryMeter` is excluding as they might result in an incorrect 235 | measurement. Those fields are: 236 | * `queue` as it is a dummy queue referenced by all `Cleaner` instances 237 | * `next` and `prev` as they are used to create a doubly-linked list of live cleaners and therefore refer to other Cleaners instances 238 | 239 | If `slice`, `duplicate` or `asReadOnlyBuffer` is used to create a new buffer from a heap buffer, the resulting buffer will have a reference to the original buffer array, 240 | whereas if the buffer was a direct buffer, the new buffer would have a direct reference to the original direct buffer: 241 | 242 | ``` 243 | root [java.nio.DirectByteBuffer] 200 bytes (64 bytes) 244 | | 245 | +--att [java.nio.DirectByteBuffer] 136 bytes (64 bytes) 246 | | 247 | +--cleaner [jdk.internal.ref.Cleaner] 72 bytes (40 bytes) 248 | | 249 | +--thunk [java.nio.DirectByteBuffer$Deallocator] 32 bytes (32 bytes) 250 | ``` 251 | In the `NORMAL` mode those underlying arrays and direct buffer size will always be included in the final total size. 252 | 253 | ### SLAB allocation no slice mode 254 | 255 | The goal of this mode is to omit the size of the shared data in slabs when the slabs are allocated through the use of: `duplicate().position(x).limit(y)`. 256 | This is done by comparing the number of remaining bytes with the buffer capacity and considering only the remaining bytes for the size when the number of remaining bytes is smaller than the capacity. 257 | 258 | ### SLAB allocation slice mode 259 | 260 | The goal of this mode is to omit the size of the shared data in slabs when the slabs are allocated through the use of: `duplicate().position(x).limit(y).slice()`. 261 | This is done by comparing the buffer's capacity with the array'size for heap buffers or with the size of the underlying buffer for direct buffers. If a buffer is considered a slab, only its capacity will considered for the size. 262 | 263 | ## @Contended 264 | 265 | `@Contended` was introduced in Java 8 as `sun.misc.Contended` but was repackaged in the `jdk.internal.vm.annotation` package in Java 9. 266 | Therefore, in Java 9+ unless `-XX:-RestrictContended` or `--add-exports java.base/jdk.internal.vm.annotation=ALL-UNNAMED` are specified `MemoryMeter` will not have access 267 | to the `value()` method of `@Contended` and will be unable to retrieve the contention group tags. Making it potentially unable to computes the correct sizes with the `Unsafe` or `Spec` strategies. 268 | As it also means that only the internal Java classes will use that annotation, `MemoryMeter` will rely on its knowledge of those internal classes to try to go around that problem. 269 | 270 | Moreover as specified in the `Supported Java versions` section the `ContendedPaddingWidth` and `EnableContended` arguments logics are broken in Java 17. Therefore the use of the `ContendedPaddingWidth` argument or of `-XX:-EnableContended` might caused the `Unsafe` and `Specification` strategies to return wrong results when the classes are using `@Contended`. 271 | 272 | ## Non-strong references 273 | 274 | By default `MemoryMeter` will ignore non-strong reference objects. It will also ignore all the Reference's fields and the `head` field of `ReferenceQueue`. 275 | The Reference fields `next` and `discovered` are used by `ReferenceQueue` instances to create a linked list, and taking them into account could lead to measuring a much larger part of the heap than what is usually intended. 276 | The `queue` field is either a singleton `ReferenceQueue.NULL` or a provided queue that users hold a reference to and therefore should be ignored too. 277 | To be consistent, the `head` field of `ReferenceQueue` is also ignored to fully decouple queue and references. 278 | This means that calling measureDeep on a `ReferenceQueue` will by default only measure the `ReferenceQueue` instance and its related objects but not the References that are part of the queue. 279 | 280 | If `measureNonStrongReferences` is set, `MemoryMeter` will measure referenced objects but also all the fields from `Reference` objects and `ReferenceQueue` object. 281 | 282 | ## Debugging 283 | 284 | ### Layout and JVM information 285 | 286 | The `org.github.jamm.strategies.LogInfoAtStartup` system property can be set to `true` to request Jamm to log at startup in `System.out` information about the JVM and the detected memory layout. 287 | 288 | Example of output: 289 | 290 | ``` 291 | Jamm starting with: java.version='1.8.0_144', java.vendor='Oracle Corporation', instrumentation=true, unsafe=true, Memory Layout: [objectHeaderSize=12 , arrayHeaderSize=16, objectAlignment=8, referenceSize=4, contendedPaddingWidth=128] 292 | ``` 293 | ### Visited object tree 294 | 295 | In order to see the object tree visited when calling `MemoryMeter.measureDeep` and ensuring that it matches your 296 | expectations you can build the `MemoryMeter` instance using `printVisitedTree`: 297 | 298 | ``` 299 | MemoryMeter meter = MemoryMeter.builder().printVisitedTree().build(); 300 | ``` 301 | 302 | If a problem occurs while crawling the graph, `MemoryMeter` will not print the graph in the `System.out` but instead will 303 | print in `System.err` the stack from the object that could not be accessed up to the original input object. 304 | 305 | ## JMH Benchmarks 306 | 307 | The Jamm JMH benchmarks can be run using: 308 | 309 | ``` 310 | mvn jmh:benchmark 311 | ``` 312 | 313 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ossrh 6 | https://oss.sonatype.org/content/repositories/snapshots 7 | 8 | 9 | 4.0.0 10 | com.github.jbellis 11 | jamm 12 | jar 13 | 0.4.1-SNAPSHOT 14 | Java Agent for Memory Measurements 15 | 16 | Jamm provides MemoryMeter, a java agent to measure actual object memory use including JVM overhead. 17 | 18 | https://github.com/jbellis/jamm/ 19 | 20 | scm:git:git://github.com/jbellis/jamm.git 21 | scm:git:git@github.com:jbellis/jamm.git 22 | http://github.com/jbellis/jamm/tree/master/ 23 | 24 | 25 | github 26 | http://github.com/jbellis/jamm/issues 27 | 28 | 29 | 30 | The Apache Software License, Version 2.0 31 | http://www.apache.org/licenses/LICENSE-2.0 32 | repo 33 | 34 | 35 | 36 | 37 | junit 38 | junit 39 | 4.12 40 | test 41 | 42 | 43 | org.openjdk.jmh 44 | jmh-core 45 | 1.36 46 | test 47 | 48 | 49 | 50 | 51 | 52 | stephenc 53 | Stephen Connolly 54 | 55 | publisher 56 | 57 | 58 | 59 | jbellis 60 | Jonathan Ellis 61 | 62 | architect 63 | 64 | 65 | 66 | belliottsmith 67 | Benedict Elliott Smith 68 | 69 | developer 70 | 71 | 72 | 73 | mebigfatguy 74 | Dave Brosius 75 | 76 | developer 77 | 78 | 79 | 80 | mshuler 81 | Michael Shuler 82 | 83 | developer 84 | 85 | 86 | 87 | snazy 88 | Robert Stupp 89 | 90 | developer 91 | 92 | 93 | 94 | blerer 95 | Benjamin Lerer 96 | 97 | developer 98 | 99 | 100 | 101 | ekaterinadimitrova2 102 | Ekaterina Dimitrova 103 | 104 | developer 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | ${basedir}/src 113 | ${basedir}/test 114 | 115 | 116 | 117 | maven-clean-plugin 118 | 3.2.0 119 | 120 | 121 | maven-compiler-plugin 122 | 3.11.0 123 | 124 | 125 | maven-deploy-plugin 126 | 3.1.1 127 | 128 | 129 | maven-enforcer-plugin 130 | 3.3.0 131 | 132 | 133 | maven-install-plugin 134 | 3.1.1 135 | 136 | 137 | maven-jar-plugin 138 | 3.3.0 139 | 140 | 141 | maven-resources-plugin 142 | 3.3.0 143 | 144 | 145 | maven-surefire-plugin 146 | 3.0.0-M7 147 | 148 | 149 | 150 | 151 | 152 | maven-compiler-plugin 153 | 154 | 1.8 155 | 1.8 156 | 157 | 158 | 159 | maven-jar-plugin 160 | 161 | 162 | 163 | org.github.jamm.MemoryMeter 164 | 165 | 166 | 167 | 168 | 169 | generate-test-resources 170 | 171 | test-jar 172 | 173 | 174 | 175 | 176 | 177 | maven-surefire-plugin 178 | 179 | 180 | 1.8 181 | Oracle Corporation 182 | 183 | 1 184 | false 185 | ${jvmArgs} -javaagent:${project.build.directory}/${project.build.finalName}-tests.${project.packaging} 186 | 187 | 188 | 189 | test-jdk11 190 | 191 | test 192 | 193 | 194 | 195 | 11 196 | 197 | 198 | 199 | 200 | test-jdk17 201 | 202 | test 203 | 204 | 205 | 206 | 17 207 | 208 | 209 | 210 | 211 | 212 | 213 | pw.krejci 214 | jmh-maven-plugin 215 | 0.2.2 216 | 217 | 1.8 218 | 1.8 219 | 220 | 221 | 222 | 223 | 224 | 225 | release 226 | 227 | 228 | 229 | org.sonatype.plugins 230 | nexus-staging-maven-plugin 231 | 1.6.13 232 | true 233 | 234 | ossrh 235 | https://oss.sonatype.org/ 236 | true 237 | 238 | 239 | 240 | org.apache.maven.plugins 241 | maven-source-plugin 242 | 3.3.0 243 | 244 | 245 | attach-sources 246 | 247 | jar-no-fork 248 | 249 | 250 | 251 | 252 | 253 | org.apache.maven.plugins 254 | maven-javadoc-plugin 255 | 3.5.0 256 | 257 | 258 | attach-javadocs 259 | 260 | jar 261 | 262 | 263 | 264 | 265 | 266 | org.apache.maven.plugins 267 | maven-gpg-plugin 268 | 1.5 269 | 270 | 271 | sign-artifacts 272 | verify 273 | 274 | sign 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | -------------------------------------------------------------------------------- /src/org/github/jamm/CannotAccessFieldException.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm; 2 | 3 | /** 4 | * {@code RuntimeException} thrown when Jamm cannot access successfully one of the fields from an object of the measured graph. 5 | */ 6 | public class CannotAccessFieldException extends RuntimeException 7 | { 8 | private static final long serialVersionUID = 8558265261116386533L; 9 | 10 | public CannotAccessFieldException(String message) { 11 | super(message); 12 | } 13 | 14 | public CannotAccessFieldException(String message, Throwable cause) { 15 | super(message, cause); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/org/github/jamm/CannotMeasureObjectException.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm; 2 | 3 | /** 4 | * {@code RuntimeException} thrown when Jamm fail to measure an object. 5 | */ 6 | public class CannotMeasureObjectException extends RuntimeException 7 | { 8 | private static final long serialVersionUID = -3880720440309336955L; 9 | 10 | public CannotMeasureObjectException(String message) 11 | { 12 | super(message); 13 | } 14 | 15 | public CannotMeasureObjectException(String message, Throwable cause) 16 | { 17 | super(message, cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/org/github/jamm/FieldAndClassFilter.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm; 2 | 3 | import java.lang.reflect.Field; 4 | 5 | /** 6 | * Filter for fields and classes. 7 | */ 8 | @FunctionalInterface 9 | public interface FieldAndClassFilter extends FieldFilter 10 | { 11 | @Override 12 | default boolean ignore(Class cls, Field field) { 13 | return ignore(field.getType()); 14 | } 15 | 16 | /** 17 | * Checks whether a {@code Class} must be ignored or not. 18 | * 19 | * @param clazz the class to check 20 | * @return {@code true} if the class must be ignored {@code false} otherwise. 21 | */ 22 | boolean ignore(Class clazz); 23 | } 24 | -------------------------------------------------------------------------------- /src/org/github/jamm/FieldFilter.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm; 2 | 3 | import java.lang.reflect.Field; 4 | 5 | /** 6 | * A filter for class fields. 7 | */ 8 | @FunctionalInterface 9 | public interface FieldFilter 10 | { 11 | /** 12 | * Checks whether a {@code Field} must be ignored or not. 13 | * 14 | * @param cls the class to which the field belong. Which might be different from the declaring class if the field is 15 | * from a superclass. 16 | * @param field the field to check 17 | * @return {@code true} if the field must be ignored {@code false} otherwise. 18 | */ 19 | boolean ignore(Class cls, Field field); 20 | } 21 | -------------------------------------------------------------------------------- /src/org/github/jamm/Filters.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm; 2 | 3 | import java.lang.ref.Reference; 4 | import java.lang.ref.ReferenceQueue; 5 | import java.lang.reflect.Field; 6 | import java.lang.reflect.Modifier; 7 | import java.nio.ByteBuffer; 8 | import java.security.AccessControlContext; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | 12 | /** 13 | * Utility class providing the different filters used by {@code MemoryMeter} 14 | * 15 | */ 16 | public final class Filters 17 | { 18 | /** 19 | * Name pattern for outer class reference 20 | */ 21 | private static final String OUTER_CLASS_REFERENCE = "this\\$[0-9]+"; 22 | 23 | private static final List CLEANER_FIELDS_TO_IGNORE = Arrays.asList("queue", "prev", "next"); 24 | 25 | private static final Class CLEANER_CLASS = getCleanerClass(); 26 | 27 | private static Class getCleanerClass() { 28 | try { 29 | return ByteBuffer.allocateDirect(0) 30 | .getClass() 31 | .getDeclaredField("cleaner") 32 | .getType(); 33 | 34 | } catch (Exception e) { 35 | System.out.print("WARN: Jamm could not load the sun.misc.Cleaner Class. This might lead to overestimating DirectByteBuffer size."); 36 | return null; 37 | } 38 | } 39 | 40 | /** 41 | * Filter excluding static and primitive fields 42 | */ 43 | public static final FieldFilter IGNORE_STATIC_AND_PRIMITIVE_FIELDS = (c, f) -> Modifier.isStatic(f.getModifiers()) || f.getType().isPrimitive(); 44 | 45 | /** 46 | * Filter excluding class such as {@code Enum}, {@code Class}, {@code ClassLoader} and {@code AccessControlContext} 47 | */ 48 | public static final FieldAndClassFilter IGNORE_KNOWN_SINGLETONS = c -> Class.class.equals(c) 49 | || Enum.class.isAssignableFrom(c) 50 | || ClassLoader.class.isAssignableFrom(c) 51 | || AccessControlContext.class.isAssignableFrom(c); 52 | 53 | /** 54 | * Filter excluding all the Reference's fields and the {@code head} field of {@code ReferenceQueue}. 55 | * The Reference fields {@code next} and {@code discovered} are used by {@code ReferenceQueue} instances to create a linked list and are not part of what 56 | * we want to measure. The {@code queue} field is either a singleton {@code ReferenceQueue.NULL} or a provided queue that user hold a reference to and therefore 57 | * should be ignored too. To be consistent, the {@code head} field of {@code ReferenceQueue} is also ignored to fully decouple queue and references. 58 | */ 59 | public static final FieldFilter IGNORE_NON_STRONG_REFERENCES = (c, f) -> 60 | f.getDeclaringClass().equals(Reference.class) || (ReferenceQueue.class.isAssignableFrom(c) && "head".equals(f.getName())); 61 | 62 | /** 63 | * Filter excluding some of the fields from sun.misc.Cleaner as they should not be taken into account. 64 | * The fields being excluded are: 65 | * 69 | */ 70 | public static final FieldFilter IGNORE_CLEANER_FIELDS = (c, f) -> c.equals(CLEANER_CLASS) && CLEANER_FIELDS_TO_IGNORE.contains(f.getName()) ; 71 | 72 | /** 73 | * Filter excluding the {@code group} field from thread classes as that field holds the references to all the other threads from the group to which the thread belongs. 74 | */ 75 | public static final FieldFilter IGNORE_THREAD_FIELDS = (c, f) -> c.equals(Thread.class) && "group".equals(f.getName()) ; 76 | 77 | /** 78 | * Filter excluding the outer class reference from non-static inner classes. 79 | * In practice that filter is only useful if the top class is an inner class, and we wish to ignore the outer class in the measurement. 80 | */ 81 | public static final FieldFilter IGNORE_OUTER_CLASS_REFERENCES = (c, f) -> f.getName().matches(OUTER_CLASS_REFERENCE); 82 | 83 | /** 84 | * Filter excluding fields and class annotated with {@code Unmetered} 85 | */ 86 | public static final FieldAndClassFilter IGNORE_UNMETERED_FIELDS_AND_CLASSES = new FieldAndClassFilter() 87 | { 88 | @Override 89 | public boolean ignore(Class cls, Field field) { 90 | return field.isAnnotationPresent(Unmetered.class) || ignore(field.getType()); 91 | } 92 | 93 | @Override 94 | public boolean ignore(Class cls) { 95 | // The @Inherited annotation only causes annotations to be inherited from superclasses. Therefore we need to check the interfaces manually 96 | return cls != null && (cls.isAnnotationPresent(Unmetered.class) || isAnnotationPresentOnInterfaces(cls)); 97 | } 98 | 99 | /** 100 | * Checks if any of the implemented interfaces has the {@code @Unmetered} annotation 101 | * @param cls the class for which the interfaces must be checked 102 | * @return {@code true} if any of the interfaces is annotated with {@code @Unmetered}. {@code false} otherwise. 103 | */ 104 | private boolean isAnnotationPresentOnInterfaces(Class cls) { 105 | Class[] interfaces = cls.getInterfaces(); 106 | for (int i = 0; i < interfaces.length; i++) { 107 | if (cls.getInterfaces()[i].isAnnotationPresent(Unmetered.class)) 108 | return true; 109 | } 110 | 111 | return false; 112 | } 113 | }; 114 | 115 | public static FieldAndClassFilter getClassFilters(boolean ignoreKnownSingletons) { 116 | 117 | if (ignoreKnownSingletons) 118 | return new FieldAndClassFilter() { 119 | 120 | @Override 121 | public boolean ignore(Class cls, Field field) { 122 | return IGNORE_KNOWN_SINGLETONS.ignore(cls, field) || IGNORE_UNMETERED_FIELDS_AND_CLASSES.ignore(cls, field); 123 | } 124 | 125 | @Override 126 | public boolean ignore(Class cls) 127 | { 128 | return IGNORE_KNOWN_SINGLETONS.ignore(cls) || IGNORE_UNMETERED_FIELDS_AND_CLASSES.ignore(cls); 129 | } 130 | }; 131 | 132 | return IGNORE_UNMETERED_FIELDS_AND_CLASSES; 133 | } 134 | 135 | public static FieldFilter getFieldFilters(boolean ignoreKnownSingletons, 136 | boolean ignoreOuterClassReference, 137 | boolean ignoreNonStrongReferences) { 138 | 139 | if (ignoreOuterClassReference) { 140 | 141 | if (ignoreNonStrongReferences) 142 | return (c, f) -> IGNORE_STATIC_AND_PRIMITIVE_FIELDS.ignore(c, f) 143 | || getClassFilters(ignoreKnownSingletons).ignore(c, f) 144 | || IGNORE_CLEANER_FIELDS.ignore(c, f) 145 | || IGNORE_THREAD_FIELDS.ignore(c, f) 146 | || IGNORE_NON_STRONG_REFERENCES.ignore(c, f) 147 | || IGNORE_OUTER_CLASS_REFERENCES.ignore(c, f); 148 | 149 | return (c, f) -> IGNORE_STATIC_AND_PRIMITIVE_FIELDS.ignore(c, f) 150 | || getClassFilters(ignoreKnownSingletons).ignore(c, f) 151 | || IGNORE_CLEANER_FIELDS.ignore(c, f) 152 | || IGNORE_THREAD_FIELDS.ignore(c, f) 153 | || IGNORE_OUTER_CLASS_REFERENCES.ignore(c, f); 154 | } 155 | 156 | if (ignoreNonStrongReferences) 157 | return (c, f) -> IGNORE_STATIC_AND_PRIMITIVE_FIELDS.ignore(c, f) 158 | || getClassFilters(ignoreKnownSingletons).ignore(c, f) 159 | || IGNORE_CLEANER_FIELDS.ignore(c, f) 160 | || IGNORE_THREAD_FIELDS.ignore(c, f) 161 | || IGNORE_NON_STRONG_REFERENCES.ignore(c, f); 162 | 163 | return (c, f) -> IGNORE_STATIC_AND_PRIMITIVE_FIELDS.ignore(c, f) 164 | || getClassFilters(ignoreKnownSingletons).ignore(c, f) 165 | || IGNORE_CLEANER_FIELDS.ignore(c, f) 166 | || IGNORE_THREAD_FIELDS.ignore(c, f); 167 | } 168 | 169 | /** 170 | * The class should not be instantiated. 171 | */ 172 | private Filters() { 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/org/github/jamm/IdentityHashSet.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm; 2 | 3 | /** 4 | * Simple set that use object equality to compare elements. This set is used in {@code MemoryMeter} to keep track of 5 | * the objects already visited to avoid circular dependencies. 6 | * 7 | *

This class provides constant-time performance for the {@code add} operation, 8 | * assuming the system identity hash function ({@link System#identityHashCode(Object)}) 9 | * disperses elements properly among the buckets.

10 | * 11 | *

{@code IdentityHashSet} uses linear probing to resolve hash collisions. When a hash collision occurs it will look 12 | * for the next {@code null} bucket available. To minimize the risk of clustering impacting the performance, 13 | * {@code IdentityHashSet} will ensure that the underlying array is at most 2/3 full by resizing the array when this 14 | * limit is reached.

15 | */ 16 | public final class IdentityHashSet 17 | { 18 | int size; 19 | // Open-addressing table for this set. 20 | // This table will never be fully populated (2/3) to keep enough "spare slots" that are `null` 21 | // so a loop checking for an element would not have to check too many slots (iteration stops 22 | // when an entry in the table is `null`). 23 | Object[] table = new Object[32]; // 32 2/3 populated = 21 elements 24 | 25 | boolean add(Object o) 26 | { 27 | // no need for a null-check here, see call-sites 28 | for (; true; resize()) 29 | { 30 | Object[] tab = table; 31 | int len = tab.length; 32 | int mask = len - 1; 33 | int i = index(o, mask); 34 | 35 | while (true) 36 | { 37 | Object item = tab[i]; 38 | if (item == null) 39 | break; 40 | if (item == o) 41 | return false; 42 | i = inc(i, len); 43 | } 44 | 45 | int s = size + 1; 46 | // Ensure that the array is only at most 2/3 full 47 | // if ( s <= ((2 * len) / 3) 48 | // if ((3 * s) <= (2 * len)) 49 | // if ((s + (2 * s)) <= (2 * len)) 50 | if (s + (s << 1) <= (len << 1)) 51 | { 52 | size = s; 53 | tab[i] = o; 54 | return true; 55 | } 56 | } 57 | } 58 | 59 | private void resize() 60 | { 61 | Object[] tab = table; 62 | 63 | int newLength = tab.length << 1; 64 | if (newLength < 0) 65 | throw new IllegalStateException("too many objects visited"); 66 | 67 | Object[] n = new Object[newLength]; 68 | int mask = newLength - 1; 69 | int i; 70 | for (Object o : tab) 71 | { 72 | if (o != null) 73 | { 74 | i = index(o, mask); 75 | while (n[i] != null) 76 | i = inc(i, newLength); 77 | n[i] = o; 78 | } 79 | } 80 | table = n; 81 | } 82 | 83 | private static int index(Object o, int mask) 84 | { 85 | return System.identityHashCode(o) & mask; 86 | } 87 | 88 | private static int inc(int i, int len) 89 | { 90 | int n = i + 1; 91 | return n >= len ? 0 : n; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/org/github/jamm/Measurable.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm; 2 | 3 | /** 4 | * Interface that allows users to avoid crawling via reflection by adding children manually to the stack, therefore 5 | * speeding up the computation. The downside of this approach is that it pushes the responsibility to the user to ensure 6 | * that all the children are provided. 7 | */ 8 | public interface Measurable { 9 | 10 | /** 11 | * Allow the implementation to pre-compute and cache the {@code Measurable} shallow size. 12 | * 13 | * @param strategy the {@code MemoryMeterStrategy} 14 | * @return the object shallow size 15 | */ 16 | default long shallowSize(MemoryMeterStrategy strategy) { 17 | return strategy.measure(this); 18 | } 19 | 20 | /** 21 | * Adds the children that should be part of the measurement to the stack 22 | * @param stack the stack to which the children must be added 23 | */ 24 | void addChildrenTo(MeasurementStack stack); 25 | } 26 | -------------------------------------------------------------------------------- /src/org/github/jamm/MeasurementStack.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm; 2 | 3 | import java.util.ArrayDeque; 4 | import java.util.Deque; 5 | 6 | /** 7 | * 8 | */ 9 | public final class MeasurementStack { 10 | 11 | /** 12 | * Tracker used to ensure that we do not visit the same instance twice. 13 | */ 14 | private final IdentityHashSet tracker = new IdentityHashSet(); 15 | 16 | /** 17 | * The listener 18 | */ 19 | private final MemoryMeterListener listener; 20 | 21 | /** 22 | * Filter used to determine which classes should be ignored. 23 | */ 24 | private final FieldAndClassFilter classFilter; 25 | 26 | /** 27 | * Stack of objects that need to be measured. 28 | */ 29 | private final Deque stack = new ArrayDeque(); 30 | 31 | MeasurementStack(FieldAndClassFilter classFilter, MemoryMeterListener listener) { 32 | this.classFilter = classFilter; 33 | this.listener = listener; 34 | } 35 | 36 | /** 37 | * Push the specified object into the stack. 38 | * 39 | * @param parent the parent object 40 | * @param name the field name 41 | * @param child the child to be added 42 | */ 43 | public void pushObject(Object parent, String name, Object child) { 44 | if (child != null && tracker.add(child)) { 45 | stack.push(child); 46 | listener.fieldAdded(parent, name, child); 47 | } 48 | } 49 | 50 | /** 51 | * Push the root object into the stack. 52 | * @param object the root of the object tree to measure. 53 | */ 54 | void pushRoot(Object object) { 55 | stack.push(object); 56 | tracker.add(object); 57 | listener.started(object); 58 | } 59 | 60 | /** 61 | * Push the specified array element into the stack. 62 | * 63 | * @param array the array 64 | * @param index the element index 65 | */ 66 | void pushArrayElement(Object[] array, int index) { 67 | Object child = array[index]; 68 | if (child != null && !classFilter.ignore(child.getClass()) && tracker.add(child)) { 69 | stack.push(child); 70 | listener.arrayElementAdded(array, index, child); 71 | } 72 | } 73 | 74 | /** 75 | * Checks if this stack is empty. 76 | * @return {@code true} if the stack is empty, {@code false} otherwise. 77 | */ 78 | boolean isEmpty() { 79 | return stack.isEmpty(); 80 | } 81 | 82 | /** 83 | * Returns the listener used by this stack. 84 | * @return the listener used by this stack. 85 | */ 86 | MemoryMeterListener listener() { 87 | return listener; 88 | } 89 | 90 | /** 91 | * Pop an element from this stack. 92 | * @return the element at the top of this stack. 93 | */ 94 | Object pop() { 95 | return stack.pop(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/org/github/jamm/MemoryLayoutSpecification.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm; 2 | 3 | import org.github.jamm.utils.MathUtils; 4 | 5 | /** 6 | * Information about the memory layout used by the JVM running the code. 7 | * This code assume that the JVM is an HotSpot JVM. 8 | * 9 | *

The memory layout for normal Java objects start with an object header which consists of mark and class words plus 10 | * possible alignment paddings. After the object header, there may be zero or more references to instance fields.

11 | * 12 | *

For arrays, the header contains a 4-byte array length in addition to the mark and class word. Array headers 13 | * might also contain some padding as array base is aligned ( 14 | * https://bugs.openjdk.org/browse/JDK-8139457),.

  15 | * 16 | *

Objects are aligned: they always start at some multiple of the alignment.

17 | * 18 | */ 19 | public interface MemoryLayoutSpecification { 20 | 21 | /** 22 | * Returns the size of the array header. 23 | *

The array header is composed of the object header + the array length. 24 | * Its size in bytes is equal to {@code getObjectHeaderSize()} + 4

25 | * 26 | * @return the size of the array header. 27 | */ 28 | int getArrayHeaderSize(); 29 | 30 | /** 31 | * Returns the size of the object header (mark word + class word). 32 | * @return the size of the object header 33 | */ 34 | int getObjectHeaderSize(); 35 | 36 | /** 37 | * Returns the object alignment (padding) in bytes. 38 | *

The alignment is always a power of 2.

39 | * 40 | * @return the object alignment in bytes. 41 | */ 42 | int getObjectAlignment(); 43 | 44 | /** 45 | * Returns the size of the reference to java objects (also called oops for ordinary object pointers) 46 | * 47 | * @return the java object reference size 48 | */ 49 | int getReferenceSize(); 50 | 51 | /** 52 | * Returns the number of bytes used to pad the fields/classes annotated with {@code Contended}. 53 | * 54 | * @return the number of bytes used to pad the fields/classes annotated with {@code Contended}. 55 | */ 56 | int getContendedPaddingWidth(); 57 | 58 | static MemoryLayoutSpecification getEffectiveMemoryLayoutSpecification() { 59 | 60 | final int objectHeaderSize; 61 | final int referenceSize; 62 | final int heapWordSize; 63 | 64 | if (VM.is32Bits()) { 65 | 66 | // Running with 32-bit data model 67 | objectHeaderSize = 8; // mark word (4 bytes) + class word (4 bytes) 68 | referenceSize = 4; // reference size for 32 bit 69 | heapWordSize = 4; 70 | 71 | } else { 72 | 73 | heapWordSize = 8; 74 | referenceSize = VM.useCompressedOops() ? 4 // compressed reference 75 | : 8; // uncompressed reference (it's a 64-bit uncompressed OOPs object model) 76 | 77 | // Prior to Java 15, the use of compressed class pointers assumed the use of compressed oops. 78 | // This was changed in Java 15 by JDK-8241825 (https://bugs.openjdk.org/browse/JDK-8241825). 79 | objectHeaderSize = VM.useCompressedClassPointers() ? 12 // mark word (8 bytes) + class word (4 bytes) 80 | : 16; // mark word (8 bytes) + class word (8 bytes) 81 | } 82 | 83 | final int objectAlignment = VM.getObjectAlignmentInBytes(); 84 | final int arrayLength = 4; // space in bytes used to store the array length after the mark and class word 85 | final int arrayHeaderSize = MathUtils.roundTo(objectHeaderSize + arrayLength, heapWordSize); 86 | final int contendedPaddingWidth = VM.contendedPaddingWidth(); 87 | 88 | return new MemoryLayoutSpecification() { 89 | 90 | @Override 91 | public int getArrayHeaderSize() { 92 | return arrayHeaderSize; 93 | } 94 | 95 | @Override 96 | public int getObjectHeaderSize() { 97 | return objectHeaderSize; 98 | } 99 | 100 | @Override 101 | public int getObjectAlignment() { 102 | return objectAlignment; 103 | } 104 | 105 | @Override 106 | public int getReferenceSize() { 107 | return referenceSize; 108 | } 109 | 110 | @Override 111 | public int getContendedPaddingWidth() { 112 | return contendedPaddingWidth; 113 | } 114 | 115 | @Override 116 | public String toString() { 117 | return new StringBuilder().append("Memory Layout: [objectHeaderSize=") 118 | .append(objectHeaderSize) 119 | .append(" , arrayHeaderSize=") 120 | .append(arrayHeaderSize) 121 | .append(", objectAlignment=") 122 | .append(objectAlignment) 123 | .append(", referenceSize=") 124 | .append(referenceSize) 125 | .append(", contendedPaddingWidth=") 126 | .append(contendedPaddingWidth) 127 | .append(']') 128 | .toString(); 129 | } 130 | }; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/org/github/jamm/MemoryMeterListener.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | /** 6 | * Listener that receive notification form MemoryMeter. 7 | */ 8 | public interface MemoryMeterListener { 9 | 10 | /** 11 | * A factory for MemoryMeterListener. 12 | */ 13 | interface Factory { 14 | MemoryMeterListener newInstance(); 15 | } 16 | 17 | /** 18 | * Notification that MemoryMeter as started analyzing the specified object. 19 | * 20 | * @param obj the object being analyzed 21 | */ 22 | void started(Object obj); 23 | 24 | /** 25 | * Notification that the field from the specified object has been added. 26 | * 27 | * @param obj the object for which a field has been added 28 | * @param fieldName the field name 29 | * @param fieldValue the field value 30 | */ 31 | void fieldAdded(Object obj, String fieldName, Object fieldValue); 32 | 33 | /** 34 | * Notification that the element from the specified array has been added. 35 | * 36 | * @param array the array for which an element has been added 37 | * @param index the element index 38 | * @param elementValue the element value 39 | */ 40 | void arrayElementAdded(Object[] array, int index, Object elementValue); 41 | 42 | /** 43 | * Notification that the size of the specified object has been measured. 44 | * 45 | * @param current the object that has been measured 46 | * @param size the object size in bytes 47 | */ 48 | void objectMeasured(Object current, long size); 49 | 50 | /** 51 | * Notification that the size of the remaining bytes of a {@code ByteBuffer} have been measured. 52 | * 53 | * @param buffer the {@code ByteBuffer} 54 | * @param size the remaining bytes 55 | */ 56 | void byteBufferRemainingMeasured(ByteBuffer buffer, long size); 57 | 58 | /** 59 | * Notification that the entire graph has been measured. 60 | * @param size the size of the entire graph. 61 | */ 62 | void done(long size); 63 | 64 | /** 65 | * Notification that the graph could not be fully measured has it failed to access a field. 66 | * 67 | * @param obj the object owning the field that could not be accessed 68 | * @param fieldName the field name 69 | * @param fieldType the field type 70 | */ 71 | void failedToAccessField(Object obj, String fieldName, Class fieldType); 72 | } 73 | -------------------------------------------------------------------------------- /src/org/github/jamm/MemoryMeterStrategy.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm; 2 | 3 | /** 4 | * Represents a strategy to measure the shallow memory used by a Java object. 5 | */ 6 | @FunctionalInterface 7 | public interface MemoryMeterStrategy { 8 | 9 | /** 10 | * The memory layout detected by JAMM. 11 | */ 12 | MemoryLayoutSpecification MEMORY_LAYOUT = MemoryLayoutSpecification.getEffectiveMemoryLayoutSpecification(); 13 | 14 | /** 15 | * Measures the shallow memory used by the specified object. 16 | * 17 | * @param object the object to measure 18 | * @return the shallow memory usage of the specified object 19 | */ 20 | long measure(Object object); 21 | 22 | /** 23 | * Measures the shallow memory used by the specified array. 24 | * 25 | * @param array the array to measure 26 | * @param type the array type 27 | * @return the shallow memory usage of the specified array 28 | */ 29 | default long measureArray(Object array, Class type) { 30 | return measure(array); 31 | } 32 | 33 | /** 34 | * Measures the shallow memory used by the specified array. 35 | * 36 | * @param array the array to measure 37 | * @return the shallow memory used by the specified array. 38 | */ 39 | default long measureArray(Object[] array) { 40 | return measure(array); 41 | } 42 | 43 | /** 44 | * Measures the shallow memory used by the specified byte array. 45 | * 46 | * @param array the array to measure 47 | * @return the shallow memory used by the specified byte array. 48 | */ 49 | default long measureArray(byte[] array) { 50 | return measure(array); 51 | } 52 | 53 | /** 54 | * Measures the shallow memory used by the specified boolean array. 55 | * 56 | * @param array the boolean array to measure 57 | * @return the shallow memory used by the specified boolean array. 58 | */ 59 | default long measureArray(boolean[] array) { 60 | return measure(array); 61 | } 62 | 63 | /** 64 | * Measures the shallow memory used by the specified short array. 65 | * 66 | * @param array the short array to measure 67 | * @return the shallow memory used by the specified short array. 68 | */ 69 | default long measureArray(short[] array) { 70 | return measure(array); 71 | } 72 | 73 | /** 74 | * Measures the shallow memory used by the specified char array. 75 | * 76 | * @param array the char array to measure 77 | * @return the shallow memory used by the specified char array. 78 | */ 79 | default long measureArray(char[] array) { 80 | return measure(array); 81 | } 82 | 83 | /** 84 | * Measures the shallow memory used by the specified int array. 85 | * 86 | * @param array the int array to measure 87 | * @return the shallow memory used by the specified int array. 88 | */ 89 | default long measureArray(int[] array) { 90 | return measure(array); 91 | } 92 | 93 | /** 94 | * Measures the shallow memory used by the specified float array. 95 | * 96 | * @param array the float array to measure 97 | * @return the shallow memory used by the specified float array. 98 | */ 99 | default long measureArray(float[] array) { 100 | return measure(array); 101 | } 102 | 103 | /** 104 | * Measures the shallow memory used by the specified long array. 105 | * 106 | * @param array the long array to measure 107 | * @return the shallow memory used by the specified long array. 108 | */ 109 | default long measureArray(long[] array) { 110 | return measure(array); 111 | } 112 | 113 | /** 114 | * Measures the shallow memory used by the specified double array. 115 | * 116 | * @param array the long array to measure 117 | * @return the shallow memory used by the specified double array. 118 | */ 119 | default long measureArray(double[] array) { 120 | return measure(array); 121 | } 122 | 123 | /** 124 | * Checks if this instance supports the {@code computeArraySize} operation. 125 | * @return {@code true} if this instance support the {@code computeArraySize} operation, {@code false} otherwise. 126 | */ 127 | default boolean supportComputeArraySize() { 128 | return false; 129 | } 130 | 131 | /** 132 | * Computes an array size from its length and element size (optional operation). 133 | *

{@code supportComputeArraySize} should be used before calling this method to check if this operation is supported.

134 | * 135 | * @param length the array length 136 | * @param elementSize the size of the elements 137 | * @return the array size 138 | * @throws UnsupportedOperationException if the operation is not supported 139 | */ 140 | default long computeArraySize(int length, int elementSize) { 141 | throw new UnsupportedOperationException(); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/org/github/jamm/Unmetered.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Inherited; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | /** 10 | * Indicates that a specified field or type should not be measured or counted by MemoryMeter. 11 | */ 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Target({ElementType.TYPE, ElementType.FIELD}) 14 | @Inherited 15 | public @interface Unmetered { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/org/github/jamm/VM.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm; 2 | 3 | import java.lang.management.ManagementFactory; 4 | import java.lang.reflect.Field; 5 | import java.lang.reflect.Modifier; 6 | import java.util.Map; 7 | import java.util.TreeMap; 8 | 9 | import javax.management.MBeanServer; 10 | import javax.management.ObjectName; 11 | import javax.management.openmbean.CompositeDataSupport; 12 | 13 | import sun.misc.Unsafe; 14 | 15 | /** 16 | * Utility class for retrieving information from the JVM. 17 | */ 18 | public final class VM 19 | { 20 | private static final boolean DEFAULT_USE_COMPRESSED_OOPS = true; 21 | private static final int DEFAULT_ALIGNMENT_IN_BYTES = 8; 22 | private static final int DEFAULT_CONTENDED_PADDING_WIDTH = 128; 23 | 24 | private static final boolean IS_PRE_JAVA12_JVM = !supportStringIndentMethod(); 25 | 26 | private static final Unsafe UNSAFE = loadUnsafe(); 27 | 28 | /** 29 | * Checks if the JVM support the {@code String#indent} method added in Java 12. 30 | * @return {@code true} if the JVM support the {@code String#indent} method, {@code false} otherwise. 31 | */ 32 | private static boolean supportStringIndentMethod() { 33 | try { 34 | String.class.getMethod("indent", int.class); 35 | return true; 36 | } catch (Exception e) { 37 | return false; 38 | } 39 | } 40 | 41 | /** 42 | * Returns the value of the specified VM option 43 | * 44 | * @param option the option name 45 | * @return the value of the specified VM option or {@code null} if the value could not be retrieved. 46 | */ 47 | private static String getVMOption(String option) { 48 | try { 49 | 50 | MBeanServer server = ManagementFactory.getPlatformMBeanServer(); 51 | ObjectName mbean = new ObjectName("com.sun.management:type=HotSpotDiagnostic"); 52 | CompositeDataSupport data = (CompositeDataSupport) server.invoke(mbean, "getVMOption", new Object[]{option}, new String[]{"java.lang.String"}); 53 | return data.get("value").toString(); 54 | 55 | } catch (Exception e) { 56 | return null; 57 | } 58 | } 59 | 60 | /** 61 | * Retrieve the object alignment in bytes from the JVM. If the alignment cannot be retrieved 62 | * the default value will be returned. 63 | * 64 | * @return the object alignment in bytes if it can be retrieved or the default value (8). 65 | */ 66 | public static int getObjectAlignmentInBytes() { 67 | 68 | String alignment = getVMOption("ObjectAlignmentInBytes"); 69 | return alignment == null ? DEFAULT_ALIGNMENT_IN_BYTES : Integer.parseInt(alignment); 70 | } 71 | 72 | /** 73 | * Checks if the JVM uses compressed reference. 74 | * 75 | * @return {{@code true} if the JVM use compressed references {@code false} otherwise. 76 | */ 77 | public static boolean useCompressedOops() { 78 | 79 | String useCompressedOops = getVMOption("UseCompressedOops"); 80 | return useCompressedOops == null ? DEFAULT_USE_COMPRESSED_OOPS : Boolean.parseBoolean(useCompressedOops); 81 | } 82 | 83 | /** 84 | * Checks if the JVM uses compressed class pointers. 85 | * 86 | * @return {{@code true} if the JVM use compressed class pointers {@code false} otherwise. 87 | */ 88 | public static boolean useCompressedClassPointers() { 89 | 90 | String useCompressedClassPointers = getVMOption("UseCompressedClassPointers"); 91 | return useCompressedClassPointers == null ? useCompressedOops() : Boolean.parseBoolean(useCompressedClassPointers); 92 | } 93 | 94 | /** 95 | * Checks if the JVM uses more aggressive optimizations to avoid unused gaps in instances. 96 | * 97 | * @return {{@code true} if the JVM use empty slots in super class {@code false} otherwise. 98 | */ 99 | public static boolean useEmptySlotsInSuper() { 100 | 101 | String useEmptySlotsInSuper = getVMOption("UseEmptySlotsInSupers"); 102 | return useEmptySlotsInSuper == null ? false : Boolean.parseBoolean(useEmptySlotsInSuper); 103 | } 104 | 105 | /** 106 | * Checks if the JVM restricts the use of {@code @Contended} to internal classes. 107 | * 108 | * @return {{@code true} if the JVM restricts the use of {@code @Contended} to internal classes, {@code false} otherwise. 109 | */ 110 | public static boolean restrictContended() { 111 | 112 | String restrictContended = getVMOption("RestrictContended"); 113 | return restrictContended == null ? true : Boolean.parseBoolean(restrictContended); 114 | } 115 | 116 | /** 117 | * Checks if {@code @Contended} annotations are enabled. 118 | * 119 | * @return {{@code true} if {@code @Contended} annotations are enabled, {@code false} otherwise. 120 | */ 121 | public static boolean enableContended() { 122 | 123 | String enableContended = getVMOption("EnableContended"); 124 | return enableContended == null ? true : Boolean.parseBoolean(enableContended); 125 | } 126 | 127 | /** 128 | * Returns the number of bytes used to pad the fields/classes annotated with {@code Contended}. 129 | *

The value will be between 0 and 8192 (inclusive) and will be a multiple of 8.

130 | * 131 | * @return the number of bytes used to pad the fields/classes annotated with {@code Contended}. 132 | */ 133 | public static int contendedPaddingWidth() { 134 | 135 | String contendedPaddingWidth = getVMOption("ContendedPaddingWidth"); 136 | return contendedPaddingWidth == null ? DEFAULT_CONTENDED_PADDING_WIDTH : Integer.parseInt(contendedPaddingWidth); 137 | } 138 | 139 | /** 140 | * Checks if the JVM is a 32 bits one. 141 | * @return {@code true} if the JVM is a 32 bits version, {@code false} otherwise. 142 | */ 143 | public static boolean is32Bits() { 144 | return "32".equals(System.getProperty("sun.arch.data.model")); 145 | } 146 | 147 | /** 148 | * Checks if the JVM is a pre-Java 12 version. 149 | * @return {@code true} if the JVM is a pre-Java 12 version, {@code false} otherwise. 150 | */ 151 | public static boolean isPreJava12JVM() { 152 | return IS_PRE_JAVA12_JVM; 153 | } 154 | 155 | /** 156 | * Checks if {@code Unsafe} is available. 157 | * @return {@code true} if unsafe is available, {@code false} otherwise. 158 | */ 159 | public static boolean hasUnsafe() { 160 | return UNSAFE != null; 161 | } 162 | 163 | /** 164 | * Returns {@code Unsafe} if it is available. 165 | * @return {@code Unsafe} if it is available, {@code null} otherwise. 166 | */ 167 | public static Unsafe getUnsafe() { 168 | return UNSAFE; 169 | } 170 | 171 | /** 172 | * Utility method using {@code Unsafe} to print the field offset for debugging. 173 | * 174 | * @param obj the object to analyze 175 | */ 176 | public static void printOffsets(Object obj) { 177 | if (UNSAFE == null) 178 | throw new IllegalStateException("printOffsets relies on Unsafe which could not be loaded"); 179 | 180 | Class type = obj.getClass(); 181 | 182 | if (type.isArray()) { 183 | 184 | System.out.println("---------------------------------------------------------------------------------"); 185 | System.out.println("Memory layout for: " + obj.getClass().getComponentType().getName() + "[]"); 186 | System.out.println("arrayBaseOffset : " + UNSAFE.arrayBaseOffset(obj.getClass())); 187 | System.out.println("---------------------------------------------------------------------------------"); 188 | 189 | } else { 190 | 191 | Map fieldInfo = new TreeMap<>(); 192 | while (type != null) { 193 | for (Field f : type.getDeclaredFields()) { 194 | if (!Modifier.isStatic(f.getModifiers())) { 195 | long offset = UNSAFE.objectFieldOffset(f); 196 | Class fieldType = f.getType(); 197 | String fieldTypeAsString = fieldType.isArray() ? fieldType.getComponentType().getName() + "[]" : fieldType.getName(); 198 | fieldInfo.put(offset, "class=" + type.getName() + ", field=" + f.getName() + ", offset=" + offset + ", field type=" + fieldTypeAsString); 199 | } 200 | } 201 | type = type.getSuperclass(); 202 | } 203 | 204 | System.out.println("---------------------------------------------------------------------------------"); 205 | System.out.println("Memory layout for: " + obj.getClass().getName()); 206 | fieldInfo.forEach((k, v) -> System.out.println(v)); 207 | System.out.println("---------------------------------------------------------------------------------"); 208 | } 209 | } 210 | 211 | private static Unsafe loadUnsafe() { 212 | try { 213 | return Unsafe.getUnsafe(); 214 | } catch (final Exception ex) { 215 | try { 216 | Field field = Unsafe.class.getDeclaredField("theUnsafe"); 217 | field.setAccessible(true); 218 | return (Unsafe) field.get(null); 219 | } catch (Exception e) { 220 | return null; 221 | } 222 | } 223 | } 224 | 225 | private VM() { 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/org/github/jamm/accessors/FieldAccessor.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.accessors; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | import java.lang.reflect.Field; 5 | import java.lang.reflect.Method; 6 | 7 | import org.github.jamm.VM; 8 | 9 | import static org.github.jamm.utils.MethodHandleUtils.methodHandle; 10 | 11 | /** 12 | * Utility to retrieve {@code Field} values. 13 | */ 14 | public interface FieldAccessor { 15 | 16 | /** 17 | * Returns the field value for the given object 18 | * 19 | * @param object the object for which the field value must be returned 20 | * @param field the field to access 21 | * @return the field value for the given object 22 | */ 23 | Object getFieldValue(Object object, Field field); 24 | 25 | /** 26 | * Returns the {@code FieldAccessor} instance suitable for the JDK running this code. 27 | * @return a {@code FieldAccessor} instance 28 | */ 29 | static FieldAccessor newInstance() { 30 | 31 | try { 32 | Method method = Field.class.getMethod("trySetAccessible", new Class[0]); 33 | MethodHandle trySetAccessibleMH = methodHandle(method); 34 | 35 | // If we reached that point we are on a JDK9+ version with a Module System 36 | return new JpmsAccessor(trySetAccessibleMH, VM.getUnsafe()); 37 | } 38 | catch (NoSuchMethodException | SecurityException | IllegalAccessException e) 39 | { 40 | return new PlainReflectionAccessor(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/org/github/jamm/accessors/JpmsAccessor.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.accessors; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | import java.lang.reflect.Field; 5 | import java.lang.reflect.Modifier; 6 | 7 | import org.github.jamm.CannotAccessFieldException; 8 | 9 | import sun.misc.Unsafe; 10 | 11 | /** 12 | * {@code FieldAccessor} able to deal with the Java Platform Module System by relying on {@code Unsafe} to access object 13 | * protected by the Module System. 14 | */ 15 | final class JpmsAccessor implements FieldAccessor 16 | { 17 | /** 18 | * The {@code MethodHandle} for the {@code AccessibleObject.trySetAccessible} method introduced in JDK 9. 19 | */ 20 | private final MethodHandle trySetAccessibleMH; 21 | 22 | /** 23 | * The unsafe instance used to access object protected by the Module System 24 | */ 25 | private final Unsafe unsafe; 26 | 27 | public JpmsAccessor(MethodHandle trySetAccessibleMH, Unsafe unsafe) { 28 | this.trySetAccessibleMH = trySetAccessibleMH; 29 | this.unsafe = unsafe; 30 | } 31 | 32 | @Override 33 | public Object getFieldValue(Object object, Field field) { 34 | try { 35 | // This call will unfortunately emit a warning for some scenario (which was a weird decision from the JVM designer) 36 | if ((boolean) trySetAccessibleMH.invoke(field)) { 37 | // The field is accessible lets use reflection. 38 | return field.get(object); 39 | } 40 | 41 | // The access to the field is being restricted by the module system. Let's try to go around it through Unsafe. 42 | if (unsafe == null) 43 | throw new CannotAccessFieldException("The value of the '" + field.getName() + "' field from " + object.getClass().getName() 44 | + " cannot be retrieved as the field cannot be made accessible and Unsafe is unavailable"); 45 | 46 | long offset = unsafe.objectFieldOffset(field); 47 | 48 | boolean isFinal = Modifier.isFinal(field.getModifiers()); 49 | boolean isVolatile = Modifier.isVolatile(field.getModifiers()); 50 | 51 | return isFinal || isVolatile ? unsafe.getObjectVolatile(object, offset) : unsafe.getObject(object, offset); 52 | 53 | } catch (Throwable e) { 54 | throw new CannotAccessFieldException("The value of the '" + field.getName() + "' field from " + object.getClass().getName() + " cannot be retrieved", e); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/org/github/jamm/accessors/PlainReflectionAccessor.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.accessors; 2 | 3 | import java.lang.reflect.Field; 4 | 5 | import org.github.jamm.CannotAccessFieldException; 6 | 7 | /** 8 | * {@code FieldAccessor} relying on plain reflection to retrieve field value. 9 | *

This accessor will not work properly with JDK9+ due to the introduction of the Module System. 10 | */ 11 | final class PlainReflectionAccessor implements FieldAccessor 12 | { 13 | @Override 14 | public Object getFieldValue(Object object, Field field) { 15 | try { 16 | if (!field.isAccessible()) 17 | field.setAccessible(true); 18 | 19 | return field.get(object); 20 | 21 | } catch (Exception e) { 22 | throw new CannotAccessFieldException("The value of the " + field.getName() + " field from " + object.getClass() + " cannot be retrieved", e); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/org/github/jamm/listeners/NoopMemoryMeterListener.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.listeners; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | import org.github.jamm.MemoryMeterListener; 6 | 7 | /** 8 | * Listener that does nothing. 9 | */ 10 | public final class NoopMemoryMeterListener implements MemoryMeterListener { 11 | 12 | /** 13 | * Singleton instance. 14 | */ 15 | private static final MemoryMeterListener INSTANCE = new NoopMemoryMeterListener(); 16 | 17 | public static final Factory FACTORY = new Factory() { 18 | 19 | @Override 20 | public MemoryMeterListener newInstance() { 21 | return INSTANCE; 22 | } 23 | }; 24 | 25 | @Override 26 | public void objectMeasured(Object current, long size) { 27 | } 28 | 29 | @Override 30 | public void byteBufferRemainingMeasured(ByteBuffer buffer, long size) { 31 | } 32 | 33 | @Override 34 | public void fieldAdded(Object obj, String fieldName, Object fieldValue) { 35 | } 36 | 37 | @Override 38 | public void arrayElementAdded(Object[] array, int index, Object elementValue) { 39 | } 40 | 41 | @Override 42 | public void done(long size) { 43 | } 44 | 45 | @Override 46 | public void failedToAccessField(Object obj, String fieldName, Class fieldType) { 47 | } 48 | 49 | @Override 50 | public void started(Object obj) { 51 | } 52 | 53 | private NoopMemoryMeterListener() { 54 | } 55 | } -------------------------------------------------------------------------------- /src/org/github/jamm/listeners/TreePrinter.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.listeners; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.util.ArrayList; 5 | import java.util.Collections; 6 | import java.util.IdentityHashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | import org.github.jamm.MemoryMeterListener; 11 | 12 | /** 13 | * A memory listener that print to the System.out the class tree with the size information. 14 | */ 15 | public final class TreePrinter implements MemoryMeterListener { 16 | 17 | private static final String LINE_SEPARATOR = System.getProperty("line.separator"); 18 | 19 | private static final int ONE_KB = 1024; 20 | 21 | private static final int ONE_MB = 1024 * ONE_KB; 22 | 23 | /** 24 | * Mapping between objects and their information 25 | */ 26 | private final Map mapping = new IdentityHashMap<>(); 27 | 28 | /** 29 | * The maximum depth of the trees to be printed 30 | */ 31 | private final int maxDepth; 32 | 33 | /** 34 | * Specifies is some element will not be printed due to the depth limit. 35 | */ 36 | private boolean hasMissingElements; 37 | 38 | /** 39 | * The root object 40 | */ 41 | private Object root; 42 | 43 | public TreePrinter(int maxDepth) { 44 | this.maxDepth = maxDepth; 45 | } 46 | 47 | @Override 48 | public void started(Object obj) { 49 | root = obj; 50 | mapping.put(obj, ObjectInfo.newRoot(obj.getClass())); 51 | } 52 | 53 | @Override 54 | public void fieldAdded(Object obj, String fieldName, Object fieldValue) { 55 | ObjectInfo parent = mapping.get(obj); 56 | if (parent != null && parent.depth <= maxDepth - 1) { 57 | ObjectInfo field = parent.addChild(fieldName, fieldValue.getClass()); 58 | mapping.put(fieldValue, field); 59 | } else { 60 | hasMissingElements = true; 61 | } 62 | } 63 | 64 | @Override 65 | public void arrayElementAdded(Object[] array, int index, Object elementValue) { 66 | fieldAdded(array, Integer.toString(index) , elementValue); 67 | } 68 | 69 | @Override 70 | public void objectMeasured(Object current, long size) { 71 | ObjectInfo field = mapping.get(current); 72 | if (field != null) { 73 | field.size = size; 74 | } 75 | } 76 | 77 | @Override 78 | public void byteBufferRemainingMeasured(ByteBuffer buffer, long size) { 79 | ObjectInfo field = mapping.get(buffer); 80 | if (field != null) { 81 | field.size += size; 82 | } 83 | } 84 | 85 | @Override 86 | public void done(long size) { 87 | System.out.println(mapping.get(root).toString(!hasMissingElements)); 88 | } 89 | 90 | @Override 91 | public void failedToAccessField(Object obj, String fieldName, Class fieldType) { 92 | 93 | String fieldTypeName = ObjectInfo.className(fieldType); 94 | StringBuilder builder = new StringBuilder("The value of the ").append(fieldName) 95 | .append(" field from ") 96 | .append(fieldTypeName) 97 | .append(" could not be retrieved. Dependency stack below: ") 98 | .append(LINE_SEPARATOR); 99 | 100 | builder.append(fieldName) 101 | .append(" [") 102 | .append(fieldTypeName) 103 | .append("] "); 104 | 105 | ObjectInfo parent = mapping.get(obj); 106 | while (parent != null) { 107 | builder.append(LINE_SEPARATOR) 108 | .append('|') 109 | .append(LINE_SEPARATOR); 110 | parent.appendNameAndClassName(builder); 111 | parent = parent.parent; 112 | } 113 | 114 | System.err.println(builder); 115 | } 116 | 117 | /** 118 | * Container for the information associated to a field object. 119 | */ 120 | private static final class ObjectInfo { 121 | 122 | /** 123 | * The name for a root object 124 | */ 125 | private static final String ROOT_NAME = "#root"; // use # sign to makes sure that it could not be a valid field name 126 | 127 | /** 128 | * The object name 129 | */ 130 | private final String name; 131 | 132 | /** 133 | * The object class name 134 | */ 135 | private final String className; 136 | 137 | /** 138 | * The object maxDepth. 139 | */ 140 | private final int depth; 141 | 142 | /** 143 | * The object parent. 144 | */ 145 | private final ObjectInfo parent; 146 | 147 | /** 148 | * The field children 149 | */ 150 | private List children = Collections.emptyList(); 151 | 152 | /** 153 | * The object size. 154 | */ 155 | private long size; 156 | 157 | /** 158 | * The total size (lazy loaded) 159 | */ 160 | private long totalSize = -1; 161 | 162 | public ObjectInfo(ObjectInfo parent, String name, Class clazz, int depth) { 163 | this.parent = parent; 164 | this.name = name; 165 | this.className = className(clazz); 166 | this.depth = depth; 167 | } 168 | 169 | /** 170 | * Creates a new root ObjectInfo for the specified class. 171 | * 172 | * @param clazz the root class 173 | * @return a new root ObjectInfo 174 | */ 175 | public static ObjectInfo newRoot(Class clazz) { 176 | return new ObjectInfo(null, ROOT_NAME, clazz, 0); 177 | } 178 | 179 | /** 180 | * Adds the specified child 181 | * @param childName the name of the child 182 | * @param childClass the class of the child 183 | * @return the child information 184 | */ 185 | public ObjectInfo addChild(String childName, Class childClass) { 186 | ObjectInfo child = new ObjectInfo(this, childName, childClass, depth + 1); 187 | if (children.isEmpty()) { 188 | children = new ArrayList<>(); 189 | } 190 | children.add(child); 191 | return child; 192 | } 193 | 194 | /** 195 | * Returns the total size of the object and of its children 196 | * @return the total size of the object and of its children 197 | */ 198 | public long totalSize() { 199 | 200 | if (totalSize < 0) 201 | totalSize = computeTotalSize(); 202 | 203 | return totalSize; 204 | } 205 | 206 | /** 207 | * Computes the total size of the object and of its children 208 | * @return the total size of the object and of its children 209 | */ 210 | private long computeTotalSize() { 211 | long total = size; 212 | for (ObjectInfo child : children) { 213 | total += child.totalSize(); 214 | } 215 | return total; 216 | } 217 | 218 | @Override 219 | public String toString() 220 | { 221 | return toString(false); 222 | } 223 | 224 | public String toString(boolean printTotalSize) { 225 | return append("", 226 | true, 227 | printTotalSize, 228 | new StringBuilder().append(LINE_SEPARATOR).append(LINE_SEPARATOR)).toString(); 229 | } 230 | 231 | /** 232 | * Appends the representation of this ObjectInfo to the specified builder. 233 | * 234 | * @param indentation the indentation to use 235 | * @param isLast true if this object is the last child from its parent 236 | * @param printTotalSize true if the total size must be printed, false otherwise 237 | * @param builder the StringBuilder to append to 238 | * @return the StringBuilder 239 | */ 240 | private StringBuilder append(String indentation, 241 | boolean isLast, 242 | boolean printTotalSize, 243 | StringBuilder builder) 244 | { 245 | if (name != (ROOT_NAME)) { // Checking for instance equality and not simple equality 246 | builder.append(indentation) 247 | .append('|') 248 | .append(LINE_SEPARATOR) 249 | .append(indentation) 250 | .append("+--"); 251 | } 252 | 253 | appendNameAndClassName(builder); 254 | 255 | if (size != 0) { 256 | if (printTotalSize) { 257 | appendSizeTo(builder, totalSize()); 258 | builder.append(' '); 259 | } 260 | builder.append('('); 261 | appendSizeTo(builder, size); 262 | builder.append(')'); 263 | } 264 | return appendChildren(childIndentation(indentation, isLast), 265 | printTotalSize, 266 | builder.append(LINE_SEPARATOR)); 267 | } 268 | 269 | /** 270 | * Appends the name and class name of this ObjectInfo to the specified builder. 271 | * 272 | * @param builder the StringBuilder to append to 273 | * @return the StringBuilder 274 | */ 275 | public StringBuilder appendNameAndClassName(StringBuilder builder) { 276 | return builder.append(name.equals(ROOT_NAME) ? "root" : name) 277 | .append(" [") 278 | .append(className) 279 | .append("] "); 280 | } 281 | 282 | /** 283 | * Appends to the specified StringBuilder the String representation of the children 284 | * 285 | * @param indentation the indentation 286 | * @param printTotalSize true if the total size must be printed, false otherwise 287 | * @param builder the builder to append to 288 | */ 289 | private StringBuilder appendChildren(String indentation, boolean printTotalSize, StringBuilder builder) { 290 | for (int i = 0, m = children.size(); i < m; i++) { 291 | ObjectInfo child = children.get(i); 292 | boolean isLast = i == m - 1; 293 | child.append(indentation, isLast, printTotalSize, builder); 294 | } 295 | return builder; 296 | } 297 | 298 | /** 299 | * Returns the indentation to use for the children 300 | * 301 | * @param indentation the parent indentation 302 | * @param isLast true if the parent is the last child of its parent 303 | * @return the indentation to use for the children 304 | */ 305 | private static String childIndentation(String indentation, boolean isLast) { 306 | return isLast ? indentation + " " : indentation + "| "; 307 | } 308 | 309 | /** 310 | * Returns the name of the specified class. 311 | * 312 | * @param clazz the class 313 | * @return the name of the specified class 314 | */ 315 | public static String className(Class clazz) { 316 | 317 | if (clazz.isArray()) 318 | { 319 | return clazz.getComponentType().getName() + "[]"; 320 | } 321 | return clazz.getName(); 322 | } 323 | 324 | private static void appendSizeTo(StringBuilder builder, long size) { 325 | 326 | if (size >= ONE_MB) { 327 | builder.append(String.format("%.2f", (double) size / ONE_MB)).append(" KB"); 328 | } else if (size >= ONE_KB) { 329 | builder.append(String.format("%.2f", (double) size / ONE_KB)).append(" KB"); 330 | } else { 331 | builder.append(size).append(" bytes"); 332 | } 333 | } 334 | } 335 | 336 | /** 337 | * Factory for TreePrinter instances. 338 | */ 339 | public static class Factory implements MemoryMeterListener.Factory { 340 | 341 | /** 342 | * The maximum depth of the trees to be printed 343 | */ 344 | private final int maxDepth; 345 | 346 | /** 347 | * Creates a new Factory instance which create TreePrinter that will print the 348 | * visited trees up to the specified maxDepth. 349 | * @param maxDepth the maximum depth of the trees to be printed 350 | */ 351 | public Factory(int maxDepth) { 352 | this.maxDepth = maxDepth; 353 | } 354 | 355 | @Override 356 | public MemoryMeterListener newInstance() { 357 | return new TreePrinter(maxDepth); 358 | } 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /src/org/github/jamm/strategies/ContendedUtils.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.strategies; 2 | 3 | import java.lang.annotation.Annotation; 4 | import java.lang.invoke.MethodHandle; 5 | import java.lang.reflect.Field; 6 | import java.util.Optional; 7 | import java.util.function.Predicate; 8 | 9 | import org.github.jamm.CannotMeasureObjectException; 10 | import org.github.jamm.VM; 11 | 12 | import static org.github.jamm.utils.MethodHandleUtils.mayBeMethodHandle; 13 | 14 | /** 15 | * Utility methods to retrieve information about the use of {@code @Contended} annotations. 16 | */ 17 | final class ContendedUtils { 18 | 19 | /** 20 | * {@code true} if contended is enabled, {@code false} otherwise. 21 | */ 22 | private static final boolean CONTENDED_ENABLED = VM.enableContended(); 23 | 24 | /** 25 | * {@code true} if contended is restricted, {@code false} otherwise. 26 | */ 27 | private static final boolean CONTENDED_RESTRICTED = VM.restrictContended(); 28 | 29 | /** 30 | * The {@code Contended} annotation class. 31 | */ 32 | private static final Class CONTENDED_CLASS = loadContendedClass(); 33 | 34 | /** 35 | * The {@code MethodHandle} used to invoke the value method from {@code @Contended} if it is accessible. 36 | * 37 | * @Contended was introduced in Java 8 as {@code sun.misc.Contended} but was repackaged in the jdk.internal.vm.annotation package in Java 9. 38 | * Therefore, in Java 9+ unless '-XX:-RestrictContended' or '--add-exports java.base/jdk.internal.vm.annotation=ALL-UNNAMED' is specified we will not have access 39 | * to the value() method of @Contended and will be unable to retrieve the contention group tags and might be unable to computes the correct sizes. 40 | * Nevertheless, it also means that only the internal Java classes will use that annotation, and we know which one they are. Therefore, we can rely on this fact to mitigate the problem. 41 | */ 42 | private static final Optional MAY_BE_CONTENDED_VALUE_MH = mayBeMethodHandle(CONTENDED_CLASS, "value"); 43 | 44 | /** 45 | * The predicate used to check if a ClassLoader is a platform one. 46 | */ 47 | private static final Predicate PLATFORM_PREDICATE = platformClassLoaderPredicate(); 48 | 49 | /** 50 | * Checks if the specified type is annotated with {@code Contended} 51 | * 52 | * @param type the type to check 53 | * @return {@code true} if the specified type is annotated with {@code Contended}, {@code false} otherwise. 54 | */ 55 | public static boolean isClassAnnotatedWithContended(Class type) { 56 | return type.isAnnotationPresent(CONTENDED_CLASS); 57 | } 58 | 59 | /** 60 | * Checks if the specified field is annotated with {@code Contended} 61 | * 62 | * @param f the field to check 63 | * @return {@code true} if the specified field is annotated with {@code Contended}, {@code false} otherwise. 64 | */ 65 | public static boolean isFieldAnnotatedWithContended(Field f) { 66 | return f.isAnnotationPresent(CONTENDED_CLASS); 67 | } 68 | 69 | /** 70 | * Returns the {@code @Contended} annotation of the specified field 71 | * 72 | * @param f the field 73 | * @return the {@code @Contended} annotation of the specified field 74 | */ 75 | private static Object getContendedAnnotation(Field f) { 76 | return f.getAnnotation(CONTENDED_CLASS); 77 | } 78 | 79 | /** 80 | * Adds to the counter the contention group tag for the specified field 81 | * 82 | * @param counter the counter to add to 83 | * @param f the field 84 | * @return the provided counter or a new one if the provided one was {@code null} 85 | */ 86 | public static ContentionGroupsCounter countContentionGroup(ContentionGroupsCounter counter, Field f) { 87 | 88 | if (isFieldAnnotatedWithContended(f)) { 89 | String tag = getContentionGroupTag(f); 90 | if (counter == null) 91 | counter = new ContentionGroupsCounter(); 92 | counter.add(tag); 93 | } 94 | return counter; 95 | } 96 | 97 | /** 98 | * Returns the contention group tag of the {@code @Contended} annotation of the specified field. 99 | * 100 | * @param f the field for which the contention group tag must be retrieved 101 | * @return the contention group tag of the {@code @Contended} annotation of the specified field 102 | */ 103 | public static String getContentionGroupTag(Field f) { 104 | 105 | try { 106 | if (MAY_BE_CONTENDED_VALUE_MH.isPresent()) 107 | return (String) MAY_BE_CONTENDED_VALUE_MH.get().invoke(getContendedAnnotation(f)); 108 | } catch (Throwable e) { 109 | throw new CannotMeasureObjectException("The field " + f.getName() + " from the class " + f.getDeclaringClass() + "cannot be measured.", e); 110 | } 111 | 112 | // We cannot retrieve the contention group tag as the annotation can only be used by internal classes. 113 | // Up to Java 17 the only internal java class using @Contended on fields was Thread. 114 | if (f.getDeclaringClass().equals(Thread.class)) 115 | return "tlr"; // for ThreadLocalRandom 116 | 117 | throw new CannotMeasureObjectException("The field " + f.getName() + " from the class " + f.getDeclaringClass() 118 | + "cannot be measured as the @Contended contention group tag cannot be retrieved." 119 | + " Consider using: --add-exports java.base/jdk.internal.vm.annotation=ALL-UNNAMED to remove that problem"); 120 | } 121 | 122 | /** 123 | * Checks if a class is trusted (loaded by the root or platform (named extension in Java 8) ClassLoader) or not. 124 | * 125 | * @param cls the class to check 126 | * @return {@code true} if the class is trusted, {@code false} otherwise. 127 | */ 128 | private static boolean isTrustedClass(Class cls) { 129 | ClassLoader classLoader = cls.getClassLoader(); 130 | return classLoader == null || PLATFORM_PREDICATE.test(classLoader); 131 | } 132 | 133 | /** 134 | * Checks if @Contended is enabled for the specified class 135 | * 136 | * @param cls the class 137 | * @return {@code true} if @Contended is enabled, {@code false} otherwise. 138 | */ 139 | public static boolean isContendedEnabled(Class cls) { 140 | 141 | return CONTENDED_ENABLED && (isTrustedClass(cls) || !CONTENDED_RESTRICTED); 142 | } 143 | 144 | /** 145 | * Load the {@code Contended} class. 146 | * @return the {@code Contended} class. 147 | */ 148 | @SuppressWarnings("unchecked") 149 | private static Class loadContendedClass() { 150 | try { 151 | return (Class) Class.forName("sun.misc.Contended"); 152 | } catch (ClassNotFoundException e) { 153 | try { 154 | return (Class) Class.forName("jdk.internal.vm.annotation.Contended"); 155 | } catch (ClassNotFoundException ex) { 156 | throw new IllegalStateException("The Contended annotation class could not be loaded.", ex); 157 | } 158 | } 159 | } 160 | 161 | /** 162 | * Returns the predicate used to determine if a ClassLoader is a Platform one (Extension in Java 8). 163 | * @return the predicate used to determine if a ClassLoader is a Platform one. 164 | */ 165 | private static Predicate platformClassLoaderPredicate() { 166 | 167 | Optional mayBeMethodHandle = mayBeMethodHandle(ClassLoader.class, "getPlatformClassLoader()"); 168 | 169 | // The getPlatformClassLoader method was added in Java 9 170 | if (mayBeMethodHandle.isPresent()) { 171 | try { 172 | ClassLoader platformClassLoader = (ClassLoader) mayBeMethodHandle.get().invoke(); 173 | return cl -> platformClassLoader == cl; 174 | } catch (Throwable e) { 175 | throw new IllegalStateException(e); 176 | } 177 | } 178 | 179 | // For Java 8, we have to check the class name. A bit fragile but did not find another robust way. 180 | return cl -> cl.toString().startsWith("sun.misc.Launcher$ExtClassLoader"); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/org/github/jamm/strategies/ContentionGroupCounter.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.strategies; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | 6 | import static java.util.Collections.emptySet; 7 | 8 | /** 9 | * Utility class for counting Contention groups. 10 | * 11 | */ 12 | final class ContentionGroupsCounter 13 | { 14 | private static final String ANONYMOUS_GROUP = ""; 15 | 16 | /** 17 | * The number of anonymous groups (each counting as one) 18 | */ 19 | private int anonymousCounter; 20 | 21 | /** 22 | * The non-anonymous groups (each one counting as only one) 23 | */ 24 | private Set contentionGroups = emptySet(); 25 | 26 | /** 27 | * Adds a group tag to this counter 28 | * @param contentionGroupTag the contention group tag of a field 29 | */ 30 | public void add(String contentionGroupTag) 31 | { 32 | if (ANONYMOUS_GROUP.equals(contentionGroupTag)) { 33 | this.anonymousCounter++; 34 | } else { 35 | if (contentionGroups.isEmpty()) 36 | this.contentionGroups = new HashSet<>(); 37 | this.contentionGroups.add(contentionGroupTag); 38 | } 39 | } 40 | 41 | /** 42 | * Returns the total number of groups. 43 | * 44 | * @return the total number of groups 45 | */ 46 | public int count() { 47 | return anonymousCounter + contentionGroups.size(); 48 | } 49 | 50 | @Override 51 | public String toString() 52 | { 53 | return "ContentionGroupsCounter [anonymousCounter=" + anonymousCounter + ", contentionGroups=" 54 | + contentionGroups + "]"; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/org/github/jamm/strategies/DoesNotUseEmptySlotInSuperSpecStrategy.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.strategies; 2 | 3 | import static org.github.jamm.utils.MathUtils.roundTo; 4 | 5 | /** 6 | * {@code MemoryMeterStrategy} that computes the size of the memory occupied by an object, in a Java 15+ JVM, when the 7 | * {@code UseEmptySlotsInSupers} option is disabled. 8 | *

For backward compatibility reason, in 15+, the optimization can be disabled through the {@code UseEmptySlotsInSupers} option. 9 | * (see https://bugs.openjdk.org/browse/JDK-8237767 and 10 | * type) { 36 | return instrumentation.getObjectSize(object); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/org/github/jamm/strategies/InstrumentationStrategy.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.strategies; 2 | 3 | import java.lang.instrument.Instrumentation; 4 | 5 | import org.github.jamm.MemoryMeterStrategy; 6 | 7 | /** 8 | * {@code MemoryMeterStrategy} relying on {@code Instrumentation} to measure object size. 9 | * 10 | */ 11 | final class InstrumentationStrategy implements MemoryMeterStrategy { 12 | 13 | private final Instrumentation instrumentation; 14 | 15 | public InstrumentationStrategy(Instrumentation instrumentation) { 16 | this.instrumentation = instrumentation; 17 | } 18 | 19 | @Override 20 | public long measure(Object object) { 21 | return instrumentation.getObjectSize(object); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/org/github/jamm/strategies/MemoryLayoutBasedStrategy.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.strategies; 2 | 3 | import java.lang.reflect.Array; 4 | 5 | import org.github.jamm.MemoryMeterStrategy; 6 | 7 | import static org.github.jamm.utils.MathUtils.roundTo; 8 | 9 | /** 10 | * Base class for strategies that need access to the {@code MemoryLayoutSpecification} for computing object size. 11 | */ 12 | public abstract class MemoryLayoutBasedStrategy implements MemoryMeterStrategy { 13 | 14 | private final static int ARRAY_BASE_OFFSET = MEMORY_LAYOUT.getArrayHeaderSize(); 15 | 16 | @Override 17 | public final long measure(Object object) { 18 | Class type = object.getClass(); 19 | return type.isArray() ? measureArray(object, type) : measureInstance(object, type); 20 | } 21 | 22 | @Override 23 | public long measureArray(Object[] array) { 24 | return computeArraySize(array.length, MEMORY_LAYOUT.getReferenceSize()); 25 | } 26 | 27 | @Override 28 | public long measureArray(byte[] array) { 29 | return computeArraySize(array.length, Byte.BYTES); 30 | } 31 | 32 | @Override 33 | public long measureArray(boolean[] array) { 34 | return computeArraySize(array.length, 1); 35 | } 36 | 37 | @Override 38 | public long measureArray(short[] array) { 39 | return computeArraySize(array.length, Short.BYTES); 40 | } 41 | 42 | @Override 43 | public long measureArray(char[] array) { 44 | return computeArraySize(array.length, Character.BYTES); 45 | } 46 | 47 | @Override 48 | public long measureArray(int[] array) { 49 | return computeArraySize( array.length, Integer.BYTES); 50 | } 51 | 52 | @Override 53 | public long measureArray(float[] array) { 54 | return computeArraySize(array.length, Float.BYTES); 55 | } 56 | 57 | @Override 58 | public long measureArray(long[] array) { 59 | return computeArraySize(array.length, Long.BYTES); 60 | } 61 | 62 | @Override 63 | public long measureArray(double[] array) { 64 | return computeArraySize(array.length, Double.BYTES); 65 | } 66 | 67 | /** 68 | * Measures the shallow memory used by objects of the specified class. 69 | * 70 | * @param instance the object to measure 71 | * @param type the object type 72 | * @return the shallow memory used by the object 73 | */ 74 | protected abstract long measureInstance(Object instance, Class type); 75 | 76 | /** 77 | * Measure the shallow memory used by the specified array. 78 | * 79 | * @param instance the array instance 80 | * @param type the array type 81 | * @return the shallow memory used by the specified array 82 | */ 83 | public final long measureArray(Object instance, Class type) { 84 | int length = Array.getLength(instance); 85 | int elementSize = measureField(type.getComponentType()); 86 | return computeArraySize(length, elementSize); 87 | } 88 | 89 | @Override 90 | public boolean supportComputeArraySize() { 91 | return true; 92 | } 93 | 94 | /** 95 | * Returns the array base offset. 96 | *

Array base is aligned based on heap word. It is not visible by default as compressed references are used and the 97 | * header size is 16 but becomes visible when they are disabled. 98 | * @return the array base offset. 99 | */ 100 | protected int arrayBaseOffset() { 101 | return ARRAY_BASE_OFFSET; 102 | } 103 | 104 | /** 105 | * Computes the size of an array from its length and elementSize. 106 | * 107 | * @param length the array length 108 | * @param elementSize the size of the array elements 109 | * @return the size of the array 110 | */ 111 | public long computeArraySize(int length, int elementSize) { 112 | return roundTo(arrayBaseOffset() + length * (long) elementSize, MEMORY_LAYOUT.getObjectAlignment()); 113 | } 114 | 115 | /** 116 | * Returns the size of a field of the specified type. 117 | * 118 | * @param type the field type 119 | * @return The memory size of a field of a class of the provided type; for Objects this is the size of the reference only 120 | */ 121 | protected final int measureField(Class type) { 122 | 123 | if (!type.isPrimitive()) 124 | return MEMORY_LAYOUT.getReferenceSize(); 125 | 126 | if (type == boolean.class || type == byte.class) 127 | return 1; 128 | 129 | if (type == char.class || type == short.class) 130 | return 2; 131 | 132 | if (type == float.class || type == int.class) 133 | return 4; 134 | 135 | if (type == double.class || type == long.class) 136 | return 8; 137 | 138 | throw new IllegalStateException(); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/org/github/jamm/strategies/MemoryMeterStrategies.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.strategies; 2 | 3 | import java.lang.instrument.Instrumentation; 4 | import java.lang.invoke.MethodHandle; 5 | import java.util.LinkedList; 6 | import java.util.List; 7 | import java.util.Optional; 8 | import java.util.Queue; 9 | 10 | import org.github.jamm.MemoryMeter.Guess; 11 | 12 | import static org.github.jamm.utils.MethodHandleUtils.mayBeMethodHandle; 13 | 14 | import org.github.jamm.MemoryMeterStrategy; 15 | import org.github.jamm.VM; 16 | 17 | /** 18 | * The different strategies that can be used to measure object sizes. 19 | */ 20 | public final class MemoryMeterStrategies { 21 | 22 | public static Instrumentation instrumentation; 23 | 24 | /** 25 | * The strategies instance. 26 | */ 27 | private static MemoryMeterStrategies instance; 28 | 29 | /** 30 | * Strategy relying on instrumentation or {@code null} if the instrumentation was not provided 31 | */ 32 | private final MemoryMeterStrategy instrumentationStrategy; 33 | 34 | /** 35 | * Strategy relying on instrumentation to measure non array object and the {@code Specification} approach to measure arrays. 36 | * {@code null} if the instrumentation was not provided 37 | */ 38 | private final MemoryMeterStrategy instrumentationAndSpecStrategy; 39 | 40 | /** 41 | * Strategy relying on unsafe or {@code null} if unsafe could not be loaded 42 | */ 43 | private final MemoryMeterStrategy unsafeStrategy; 44 | 45 | /** 46 | * Strategy relying on specification 47 | */ 48 | private final MemoryMeterStrategy specStrategy; 49 | 50 | private MemoryMeterStrategies(MemoryMeterStrategy instrumentationStrategy, 51 | MemoryMeterStrategy instrumentationAndSpecStrategy, 52 | MemoryMeterStrategy unsafeStrategy, 53 | MemoryMeterStrategy specStrategy) { 54 | 55 | this.instrumentationStrategy = instrumentationStrategy; 56 | this.instrumentationAndSpecStrategy = instrumentationAndSpecStrategy; 57 | this.unsafeStrategy = unsafeStrategy; 58 | this.specStrategy = specStrategy; 59 | } 60 | 61 | public static synchronized MemoryMeterStrategies getInstance() { 62 | 63 | if (instance == null) 64 | instance = createStrategies(); 65 | 66 | return instance; 67 | } 68 | 69 | /** 70 | * Creates the strategies available based on the JVM information. 71 | * @return the strategies available 72 | */ 73 | private static MemoryMeterStrategies createStrategies() { 74 | 75 | Optional mayBeIsHiddenMH = mayBeIsHiddenMethodHandle(); 76 | 77 | MemoryMeterStrategy instrumentationStrategy = createInstrumentationStrategy(); 78 | MemoryMeterStrategy instrumentationAndSpecStrategy = createInstrumentationAndSpecStrategy(); 79 | MemoryMeterStrategy specStrategy = createSpecStrategy(mayBeIsHiddenMH); 80 | MemoryMeterStrategy unsafeStrategy = createUnsafeStrategy(mayBeIsHiddenMH, (MemoryLayoutBasedStrategy) specStrategy); 81 | 82 | if (logInformationAtStartup()) { 83 | // Logging important information once at startup for debugging purpose 84 | System.out.println("Jamm starting with: java.version='" + System.getProperty("java.version") 85 | + "', java.vendor='" + System.getProperty("java.vendor") 86 | + "', instrumentation=" + (instrumentationStrategy != null) 87 | + ", unsafe=" + (unsafeStrategy != null) 88 | + ", " + MemoryMeterStrategy.MEMORY_LAYOUT); 89 | } 90 | 91 | return new MemoryMeterStrategies(instrumentationStrategy, instrumentationAndSpecStrategy, unsafeStrategy, specStrategy); 92 | } 93 | 94 | /** 95 | * Checks if layout and JDK information should be logged at startup. 96 | *

{@code false} by default to avoid causing issues to existing applications that do not expect that message to be logged.

97 | * @return {@code true} if the information should be logged, {@code false} otherwise. 98 | */ 99 | private static boolean logInformationAtStartup() { 100 | return Boolean.parseBoolean(System.getProperty("org.github.jamm.strategies.LogInfoAtStartup", "false")); 101 | } 102 | 103 | private static MemoryMeterStrategy createSpecStrategy(Optional mayBeIsHiddenMH) { 104 | 105 | if (mayBeIsHiddenMH.isPresent() && !VM.useEmptySlotsInSuper()) 106 | System.out.println("WARNING: Jamm is starting with the UseEmptySlotsInSupers JVM option disabled." 107 | + " The memory layout created when this option is enabled cannot always be reproduced accurately by the Specification or UNSAFE strategies." 108 | + " By consequence the measured sizes when these strategies are used might be off in some cases."); 109 | 110 | // The Field layout was optimized in Java 15. For backward compatibility reasons, in 15+, the optimization can be disabled through the {@code -XX:-UseEmptySlotsInSupers} option. 111 | // (see https://bugs.openjdk.org/browse/JDK-8237767 and https://bugs.openjdk.org/browse/JDK-8239016) 112 | // Unfortunately, when {@code UseEmptySlotsInSupers} is disabled the layout resulting does not match the pre-15 versions 113 | return mayBeIsHiddenMH.isPresent() ? VM.useEmptySlotsInSuper() ? new SpecStrategy() 114 | : new DoesNotUseEmptySlotInSuperSpecStrategy() 115 | : new PreJava15SpecStrategy(); 116 | } 117 | 118 | private static MemoryMeterStrategy createUnsafeStrategy(Optional mayBeIsHiddenMH, 119 | MemoryLayoutBasedStrategy specStrategy) { 120 | if (!VM.hasUnsafe()) 121 | return null; 122 | 123 | Optional mayBeIsRecordMH = mayBeIsRecordMethodHandle(); 124 | 125 | // The hidden method was added in Java 15 so if isHidden exists we are on a version greater or equal to Java 15 126 | return mayBeIsHiddenMH.isPresent() ? new UnsafeStrategy(mayBeIsRecordMH.get(), mayBeIsHiddenMH.get(), specStrategy) 127 | : new PreJava15UnsafeStrategy(mayBeIsRecordMH, specStrategy); 128 | 129 | } 130 | 131 | /** 132 | * Returns the {@code MethodHandle} for the {@code Class.isHidden} method introduced in Java 15 if we are running 133 | * on a Java 15+ JVM. 134 | * @return an {@code Optional} for the {@code MethodHandle} 135 | */ 136 | private static Optional mayBeIsHiddenMethodHandle() { 137 | return mayBeMethodHandle(Class.class, "isHidden"); 138 | } 139 | 140 | /** 141 | * Returns the {@code MethodHandle} for the {@code Class.isRecord} method introduced in Java 14 if we are running 142 | * on a Java 14+ JVM. 143 | * @return an {@code Optional} for the {@code MethodHandle} 144 | */ 145 | private static Optional mayBeIsRecordMethodHandle() { 146 | return mayBeMethodHandle(Class.class, "isRecord"); 147 | } 148 | 149 | private static MemoryMeterStrategy createInstrumentationStrategy() { 150 | return instrumentation != null ? new InstrumentationStrategy(instrumentation) : null; 151 | } 152 | 153 | private static MemoryMeterStrategy createInstrumentationAndSpecStrategy() { 154 | return instrumentation != null ? new InstrumentationAndSpecStrategy(instrumentation) : null; 155 | } 156 | 157 | public boolean hasInstrumentation() { 158 | return instrumentationStrategy != null; 159 | } 160 | 161 | public boolean hasUnsafe() { 162 | return unsafeStrategy != null; 163 | } 164 | 165 | public MemoryMeterStrategy getStrategy(List guessList) { 166 | 167 | if (guessList.isEmpty()) 168 | throw new IllegalArgumentException("The guessList argument is empty"); 169 | 170 | Guess.checkOrder(guessList); 171 | 172 | Queue guesses = new LinkedList<>(guessList); 173 | 174 | while (true) { 175 | 176 | Guess guess = guesses.poll(); 177 | 178 | if (guess.requireInstrumentation()) { 179 | 180 | if (hasInstrumentation()) 181 | return guess == Guess.INSTRUMENTATION_AND_SPECIFICATION ? instrumentationAndSpecStrategy 182 | : instrumentationStrategy; 183 | 184 | if (guesses.isEmpty()) 185 | throw new IllegalStateException("Instrumentation is not set; Jamm must be set as -javaagent"); 186 | 187 | } else if (guess.requireUnsafe()) { 188 | 189 | if (hasUnsafe()) 190 | return unsafeStrategy; 191 | 192 | if (guesses.isEmpty()) 193 | throw new IllegalStateException("sun.misc.Unsafe could not be obtained. The SecurityManager must permit access to sun.misc.Unsafe"); 194 | 195 | } else { 196 | 197 | return specStrategy; 198 | } 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/org/github/jamm/strategies/PreJava15SpecStrategy.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.strategies; 2 | 3 | import java.lang.reflect.Field; 4 | import java.lang.reflect.Modifier; 5 | 6 | import static org.github.jamm.strategies.ContendedUtils.countContentionGroup; 7 | import static org.github.jamm.strategies.ContendedUtils.isClassAnnotatedWithContended; 8 | import static org.github.jamm.strategies.ContendedUtils.isContendedEnabled; 9 | import static org.github.jamm.utils.MathUtils.roundTo; 10 | 11 | /** 12 | * {@code MemoryMeterStrategy} that computes the size of the memory occupied by an object, in a pre-Java 15 JVM, based on 13 | * the JVM information. 14 | * 15 | *

The layout of a Java object in memory is composed of a header, composed of a mark word and a class word, 16 | * followed by the fields information. Each object is also aligned based on the value of the object alignment 17 | * (-XX:ObjectAlignmentInBytes which by default is 8 bytes). 18 | *

 19 |  *  +--------+---------+---------+-----+---------+-------------------------+
 20 |  *  | header | Field 1 | Field 2 | ... | Field n | gap to ensue alignment  |
 21 |  *  +--------+---------+---------+-----+---------+-------------------------+ 
 22 |  * 
23 | *

24 | *

25 | * The header size depends on the JVM bitness and for 64-bit JVM on the use of compressed references for Java pointers. 26 | * On a 64 bit JVM with compressed references enabled (default behavior for heaps lower than 32 GB) object headers occupy 12 bytes. 27 | * This size leave a gap of 4 bytes before the next alignment that the JVM can use to store some fields. For example, the following class: 28 | *

 29 |  * class A
 30 |  * {
 31 |  *      int i;
 32 |  *      long l;
 33 |  * }
 34 |  * 
35 | * will be stored as: 36 | *
 37 |  *  +------------------+---------------+---------------+
 38 |  *  | header (12 bytes)| int (4 bytes) | long (8 bytes)| Total size: 24 bytes
 39 |  *  +------------------+---------------+---------------+ 
 40 |  * 
41 | * As the Java Memory Model guarantees the absence of
word tearing for fields, 42 | * field alignment can cause some gaps. For example, the following class: 43 | *
 44 |  * class LongHolder
 45 |  * {
 46 |  *      long l;
 47 |  * }
 48 |  * 
49 | * will be stored as: 50 | *
 51 |  *  +------------------+---------------+---------------+
 52 |  *  | header (12 bytes)| 4 bytes gap   | long (8 bytes)| Total size: 24 bytes
 53 |  *  +------------------+---------------+---------------+ 
 54 |  * 
55 | *

56 | *

57 | * Before Java 15, super class fields were always taken care first and sub-class fields could not take the gaps left by 58 | * the super class. For example with the following classes: 59 | *

 60 |  * class Parent
 61 |  * {
 62 |  *      long l;
 63 |  * }
 64 |  *
 65 |  * class Child extends Parent
 66 |  * {
 67 |  *      int l;
 68 |  * }
 69 |  * 
70 | * the Child class will be stored as: 71 | *
 72 |  *  +------------------+---------------+---------------+--------------+---------------+
 73 |  *  | header (12 bytes)| 4 bytes gap   | long (8 bytes)| int (4 bytes)| alignment gap | Total size: 32 bytes
 74 |  *  +------------------+---------------+---------------+--------------+---------------+ 
 75 |  * 
76 | * where the gap left after the header could not be taken by the int field. 77 | *
Prior to Java 15, super class fields were also aligned using the object reference size which could cause some hierarchy gaps.
 78 |  * For example with the following definitions:
 79 |  * 
 80 |  * class A
 81 |  * {
 82 |  *      boolean b1;
 83 |  * }
 84 |  *
 85 |  * class B extends A
 86 |  * {
 87 |  *      boolean b2;
 88 |  * }
 89 |  * class C extends B
 90 |  * {
 91 |  *      boolean b3;
 92 |  * }
 93 |  * 
94 | * The class C would be represented in memory as: 95 | *
 96 |  *  +------------------+--------------+-------------+------------+-------------+-------------+---------------+
 97 |  *  | header (12 bytes)|  b1 (1 byte) | 3 bytes gap | b2 (1 byte)| 3 bytes gap | b3 (1 byte) | alignment gap | Total size: 24 bytes
 98 |  *  +------------------+--------------+-------------+------------+-------------+-------------+---------------+ 
 99 |  * 
100 | *

101 | *

For more inside on the Java Object Layout, I highly recommend the excellent Java Objects Inside Out blog post from Aleksey Shipilëv. 102 | */ 103 | class PreJava15SpecStrategy extends MemoryLayoutBasedStrategy { 104 | 105 | /** 106 | * Align the size of the fields. 107 | *

Prior to JDK 15 the field blocks for each super class were aligned based using the object reference size. 108 | * This method provides a hook for that logic.

109 | * 110 | * @param sizeOfDeclaredFields the size of the fields for a class of the hierarchy 111 | * @return the size of the class fields aligned. 112 | */ 113 | protected long alignFieldBlock(long sizeOfDeclaredFields) { 114 | return roundTo(sizeOfDeclaredFields, MEMORY_LAYOUT.getReferenceSize()); 115 | } 116 | 117 | @Override 118 | public final long measureInstance(Object instance, Class type) { 119 | 120 | long size = sizeOfFields(type, false); 121 | return roundTo(size, MEMORY_LAYOUT.getObjectAlignment()); 122 | } 123 | 124 | private long sizeOfFields(Class type, boolean useFieldBlockAlignment) { 125 | 126 | final boolean isContendedEnabled = isContendedEnabled(type); 127 | 128 | if (type == Object.class) 129 | return MEMORY_LAYOUT.getObjectHeaderSize(); 130 | 131 | long size = sizeOfFields(type.getSuperclass(), true); 132 | 133 | long sizeOfDeclaredField = 0; 134 | long sizeTakenBy8BytesFields = 0; 135 | ContentionGroupsCounter contentionGroupCounter = null; 136 | for (Field f : type.getDeclaredFields()) { 137 | if (!Modifier.isStatic(f.getModifiers())) { 138 | int fieldSize = measureField(f.getType()); 139 | 140 | sizeTakenBy8BytesFields += fieldSize & 8; // count only the 8 bytes fields 141 | sizeOfDeclaredField += fieldSize; 142 | // If some fields are annotated with @Contended we need to count the contention groups to know how much padding needs to be added 143 | if (isContendedEnabled) 144 | contentionGroupCounter = countContentionGroup(contentionGroupCounter, f); 145 | } 146 | } 147 | 148 | // Ensure that we take into account the superclass gaps 149 | if (hasSuperClassGap(size, sizeOfDeclaredField, sizeTakenBy8BytesFields)) { 150 | size = roundTo(size, 8); 151 | } 152 | 153 | // Ensure that we take into account the hierarchy gaps 154 | size += useFieldBlockAlignment ? alignFieldBlock(sizeOfDeclaredField) : sizeOfDeclaredField; 155 | 156 | /* From Contended Javadoc: 157 | * The class level {@code @Contended} annotation is not inherited and has 158 | * no effect on the fields declared in any sub-classes. The effects of all 159 | * {@code @Contended} annotations, however, remain in force for all 160 | * subclass instances, providing isolation of all the defined contention 161 | * groups. Contention group tags are not inherited, and the same tag used 162 | * in a superclass and subclass, represent distinct contention groups. 163 | */ 164 | if (isContendedEnabled && isClassAnnotatedWithContended(type)) 165 | size += (MEMORY_LAYOUT.getContendedPaddingWidth() << 1); 166 | 167 | if (contentionGroupCounter != null) 168 | size += (contentionGroupCounter.count() + 1) * MEMORY_LAYOUT.getContendedPaddingWidth(); // 1 padding before each group + 1 at the end 169 | 170 | return size; 171 | } 172 | 173 | /** 174 | * Checks if there should be a gap left at the level of the super class fields. 175 | * 176 | * @param size the memory space used by all the super classes fields so far. 177 | * @param sizeOfDeclaredField the size taken by the field of the class being looked at 178 | * @param sizeTakenBy8BytesFields the size taken only by the 8 byte fields 179 | * @return if there should be a gap left at the level of the super class fields 180 | */ 181 | protected boolean hasSuperClassGap(long size, long sizeOfDeclaredField, long sizeTakenBy8BytesFields) { 182 | return hasGapSmallerThan8Bytes(size) && hasOnly8BytesFields(sizeOfDeclaredField, sizeTakenBy8BytesFields); 183 | } 184 | 185 | /** 186 | * Checks if the fields for this field block are all 8 bytes fields. 187 | * 188 | * @param sizeOfDeclaredField the total size of the fields within this block 189 | * @param sizeTakenBy8BytesFields the total size taken by the 8 bytes fields within this block 190 | * * @return {@code true} if the fields for this block are all 8 bytes fields, {@code false} otherwise. 191 | */ 192 | protected final boolean hasOnly8BytesFields(long sizeOfDeclaredField, long sizeTakenBy8BytesFields) { 193 | return sizeOfDeclaredField != 0 && sizeOfDeclaredField == sizeTakenBy8BytesFields; 194 | } 195 | 196 | /** 197 | * Checks if the previous block has a gap at the end smaller than 8 bytes into which the JVM can store some field 198 | * to improve packing. 199 | *

We know that if there is a gap this gap will always be of 4 bytes as field blocks are always aligned by the 200 | * Object reference size which can only be 4 (compressed reference) or 8.

201 | * 202 | * @param size the size of the memory used so far. 203 | * @return {@code true} if there is some space available (4 bytes), {@code false} otherwise. 204 | */ 205 | protected final boolean hasGapSmallerThan8Bytes(long size) { 206 | return (size & 7) > 0; 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/org/github/jamm/strategies/PreJava15UnsafeStrategy.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.strategies; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | import java.lang.reflect.Field; 5 | import java.lang.reflect.Modifier; 6 | import java.util.Optional; 7 | 8 | import org.github.jamm.CannotMeasureObjectException; 9 | import org.github.jamm.VM; 10 | 11 | import static org.github.jamm.strategies.ContendedUtils.isClassAnnotatedWithContended; 12 | import static org.github.jamm.strategies.ContendedUtils.isContendedEnabled; 13 | import static org.github.jamm.strategies.ContendedUtils.isFieldAnnotatedWithContended; 14 | 15 | import sun.misc.Unsafe; 16 | 17 | import static org.github.jamm.utils.MathUtils.roundTo; 18 | 19 | /** 20 | * {@code MemoryMeterStrategy} relying on {@code Unsafe} to measure object sizes for Java version pre-15. 21 | * 22 | *

In Java 15, the JVM layout of fields across the hierarchy changed. Prior to Java 15 superclass field 23 | * always came first. From Java 15 onward it is not the case anymore. This strategy takes advantage and it looks at 24 | * the child class first to find the greatest offsets.

25 | */ 26 | final class PreJava15UnsafeStrategy extends MemoryLayoutBasedStrategy { 27 | 28 | private final static Unsafe UNSAFE = VM.getUnsafe(); 29 | 30 | private final static int ARRAY_BASE_OFFSET = UNSAFE.arrayBaseOffset(Object[].class); 31 | 32 | /** 33 | * Method Handle for the {@code Class.isRecord} method introduced in Java 14. 34 | */ 35 | private final Optional mayBeIsRecordMH; 36 | 37 | /** 38 | * The strategy used for records. 39 | */ 40 | private final MemoryLayoutBasedStrategy recordsStrategy; 41 | 42 | public PreJava15UnsafeStrategy(Optional mayBeIsRecordMH, 43 | MemoryLayoutBasedStrategy strategy) { 44 | 45 | this.mayBeIsRecordMH = mayBeIsRecordMH; 46 | this.recordsStrategy = strategy; 47 | } 48 | 49 | @Override 50 | public long measureInstance(Object instance, Class type) { 51 | 52 | try { 53 | 54 | // If the class is a record 'unsafe.objectFieldOffset(f)' will throw an UnsupportedOperationException 55 | // In those cases, rather than failing, we rely on the Spec strategy to provide the measurement. 56 | if (mayBeIsRecordMH.isPresent() && ((Boolean) mayBeIsRecordMH.get().invoke(type))) 57 | return recordsStrategy.measureInstance(instance, type); 58 | 59 | int annotatedClassesWithoutFields = 0; // Keep track of the @Contended annotated classes without fields 60 | while (type != null) { 61 | 62 | long size = 0; 63 | boolean isLastFieldWithinContentionGroup = false; 64 | for (Field f : type.getDeclaredFields()) { 65 | if (!Modifier.isStatic(f.getModifiers())) 66 | { 67 | long previousSize = size; 68 | size = Math.max(size, UNSAFE.objectFieldOffset(f) + measureField(f.getType())); 69 | if (previousSize < size) 70 | isLastFieldWithinContentionGroup = isFieldAnnotatedWithContended(f) && isContendedEnabled(type); 71 | } 72 | } 73 | 74 | // If the last field is within a contention group we need to add the end padding 75 | if (isLastFieldWithinContentionGroup) 76 | size += MEMORY_LAYOUT.getContendedPaddingWidth(); 77 | 78 | // As we know that the superclass fields always come first pre-Java 15, if the size is greater than zero 79 | // we know that all the other fields will have a smaller offset. 80 | if (size > 0) { 81 | // If the class is annotated with @Contended we need to add the end padding 82 | if (isClassAnnotatedWithContended(type) && isContendedEnabled(type)) 83 | size += MEMORY_LAYOUT.getContendedPaddingWidth(); 84 | 85 | size += annotatedClassesWithoutFields * (MEMORY_LAYOUT.getContendedPaddingWidth() << 1); 86 | 87 | return roundTo(size, MEMORY_LAYOUT.getObjectAlignment()); 88 | } 89 | 90 | // The JVM will add padding even if the annotated class does not have any fields 91 | if (isClassAnnotatedWithContended(type) && isContendedEnabled(type)) 92 | annotatedClassesWithoutFields++; 93 | 94 | type = type.getSuperclass(); 95 | } 96 | return roundTo(MEMORY_LAYOUT.getObjectHeaderSize(), MEMORY_LAYOUT.getObjectAlignment()); 97 | } 98 | catch (Throwable e) { 99 | throw new CannotMeasureObjectException("The object of type " + type + " cannot be measured by the unsafe strategy", e); 100 | } 101 | } 102 | 103 | @Override 104 | protected int arrayBaseOffset() { 105 | return ARRAY_BASE_OFFSET; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/org/github/jamm/strategies/SpecStrategy.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.strategies; 2 | 3 | import java.lang.reflect.Field; 4 | import java.lang.reflect.Modifier; 5 | 6 | import static org.github.jamm.strategies.ContendedUtils.countContentionGroup; 7 | import static org.github.jamm.strategies.ContendedUtils.isClassAnnotatedWithContended; 8 | import static org.github.jamm.strategies.ContendedUtils.isContendedEnabled; 9 | import static org.github.jamm.utils.MathUtils.roundTo; 10 | 11 | /** 12 | * {@code MemoryMeterStrategy} that computes the size of the memory occupied by an object, in a Java 15+ JVM, based on 13 | * the JVM information. 14 | *

In Java 15 the Field layout computation was optimized (https://bugs.openjdk.org/browse/JDK-8237767) to eliminate 15 | * a certain amount of the inefficiency from the previous versions (see {@link PreJava15SpecStrategy}).

16 | */ 17 | class SpecStrategy extends MemoryLayoutBasedStrategy { 18 | 19 | @Override 20 | public final long measureInstance(Object instance, Class type) { 21 | 22 | long size = MEMORY_LAYOUT.getObjectHeaderSize() + sizeOfDeclaredFields(type); 23 | while ((type = type.getSuperclass()) != Object.class && type != null) 24 | size += sizeOfDeclaredFields(type); 25 | 26 | return roundTo(size, MEMORY_LAYOUT.getObjectAlignment()); 27 | } 28 | 29 | /** 30 | * Returns the size of the declared fields of the specified class. 31 | * 32 | * @param type the class for which the size of its declared fields must be returned 33 | * @return the size of the declared fields of the specified class 34 | */ 35 | private long sizeOfDeclaredFields(Class type) { 36 | 37 | long size = 0; 38 | ContentionGroupsCounter contentionGroupCounter = null; 39 | for (Field f : type.getDeclaredFields()) 40 | { 41 | if (!Modifier.isStatic(f.getModifiers())) { 42 | size += measureField(f.getType()); 43 | // If some fields are annotated with @Contended we need to count the contention groups to know how much padding needs to be added 44 | // In Java 17, disabling Contended has no effect on field level annotations 45 | contentionGroupCounter = countContentionGroup(contentionGroupCounter, f); 46 | } 47 | } 48 | 49 | /* From Contended Javadoc: 50 | * The class level {@code @Contended} annotation is not inherited and has 51 | * no effect on the fields declared in any sub-classes. The effects of all 52 | * {@code @Contended} annotations, however, remain in force for all 53 | * subclass instances, providing isolation of all the defined contention 54 | * groups. Contention group tags are not inherited, and the same tag used 55 | * in a superclass and subclass, represent distinct contention groups. 56 | */ 57 | if (isClassAnnotatedWithContended(type) && isContendedEnabled(type)) 58 | size += (MEMORY_LAYOUT.getContendedPaddingWidth() << 1); 59 | 60 | if (contentionGroupCounter != null) 61 | size += contentionGroupCounter.count() * (MEMORY_LAYOUT.getContendedPaddingWidth() << 1); 62 | 63 | return size; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/org/github/jamm/strategies/UnsafeStrategy.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.strategies; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | import java.lang.reflect.Field; 5 | import java.lang.reflect.Modifier; 6 | 7 | import org.github.jamm.CannotMeasureObjectException; 8 | import org.github.jamm.VM; 9 | 10 | import static org.github.jamm.strategies.ContendedUtils.isClassAnnotatedWithContended; 11 | import static org.github.jamm.strategies.ContendedUtils.isContendedEnabled; 12 | import static org.github.jamm.strategies.ContendedUtils.isFieldAnnotatedWithContended; 13 | import static org.github.jamm.utils.MathUtils.roundTo; 14 | 15 | import sun.misc.Unsafe; 16 | 17 | /** 18 | * {@code MemoryMeterStrategy} relying on {@code Unsafe} to measure object sizes for Java versions ≥ 15. 19 | * 20 | *

In Java 15, the way the JVM layout fields across the hierarchy changed. Prior to Java 15 superclass field 21 | * always came first. Therefore that strategy could only look at the current class to find the greatest offsets. 22 | * From Java 15 onward it is not the case anymore. The JVM optimizes ensure minimal memory usage by packing the fields 23 | * in the best possible way across the hierarchy (https://bugs.openjdk.org/browse/JDK-8237767).

24 | * 25 | *

Another important change that came in Java 15 is the introduction of hidden classes (https://openjdk.org/jeps/371) 26 | * and the use of hidden class for lambda. Attempting to use {@code Unsafe.objectFieldOffset} on an hidden class field 27 | * will result in a {@code UnsupportedOperationException} preventing the {@code UnsafeStrategy} to evaluate correctly 28 | * the memory used by the class. To avoid that problem {@code UnsafeStrategy} will rely on the {@code SpecStrategy} to 29 | * measure hidden classes. This can lead to an overestimation of the object size as the {@code SpecStrategy} ignore some 30 | * optimizations performed by the JVM

31 | */ 32 | public final class UnsafeStrategy extends MemoryLayoutBasedStrategy { 33 | 34 | private final static Unsafe UNSAFE = VM.getUnsafe(); 35 | 36 | private final static int ARRAY_BASE_OFFSET = UNSAFE.arrayBaseOffset(Object[].class); 37 | 38 | /** 39 | * Method Handle for the {@code Class.isRecord} method introduced in Java 14. 40 | */ 41 | private final MethodHandle isRecordMH; 42 | 43 | /** 44 | * Method Handle for the {@code Class.isHidden} method introduced in Java 15. 45 | */ 46 | private final MethodHandle isHiddenMH; 47 | 48 | /** 49 | * The strategy used for hidden classes and records. 50 | */ 51 | private final MemoryLayoutBasedStrategy hiddenClassesOrRecordsStrategy; 52 | 53 | public UnsafeStrategy(MethodHandle isRecordMH, 54 | MethodHandle isHiddenMH, 55 | MemoryLayoutBasedStrategy strategy) { 56 | this.isRecordMH = isRecordMH; 57 | this.isHiddenMH = isHiddenMH; 58 | this.hiddenClassesOrRecordsStrategy = strategy; 59 | } 60 | 61 | @Override 62 | public long measureInstance(Object instance, Class type) { 63 | 64 | try { 65 | 66 | // If the class is a hidden class ore a record 'unsafe.objectFieldOffset(f)' will throw an UnsupportedOperationException 67 | // In those cases, rather than failing, we rely on the Spec strategy to provide the measurement. 68 | if ((Boolean) isRecordMH.invoke(type) || (Boolean) isHiddenMH.invoke(type)) 69 | return hiddenClassesOrRecordsStrategy.measureInstance(instance, type); 70 | 71 | long size = 0; 72 | boolean isLastFieldWithinContentionGroup = false; 73 | while (type != null) 74 | { 75 | for (Field f : type.getDeclaredFields()) { 76 | if (!Modifier.isStatic(f.getModifiers())) { 77 | long previousSize = size; 78 | size = Math.max(size, UNSAFE.objectFieldOffset(f) + measureField(f.getType())); 79 | // In Java 17, disabling Contended has no effect on field level annotations 80 | if (previousSize < size) 81 | isLastFieldWithinContentionGroup = isFieldAnnotatedWithContended(f); 82 | } 83 | } 84 | 85 | if (isClassAnnotatedWithContended(type) && isContendedEnabled(type)) 86 | size += MEMORY_LAYOUT.getContendedPaddingWidth(); 87 | 88 | type = type.getSuperclass(); 89 | } 90 | if (size == 0) { 91 | size = MEMORY_LAYOUT.getObjectHeaderSize(); 92 | } else { 93 | if (isLastFieldWithinContentionGroup) 94 | size += MEMORY_LAYOUT.getContendedPaddingWidth(); 95 | } 96 | size = size > 0 ? size : MEMORY_LAYOUT.getObjectHeaderSize(); 97 | return roundTo(size, MEMORY_LAYOUT.getObjectAlignment()); 98 | } 99 | catch (Throwable e) { 100 | throw new CannotMeasureObjectException("The object of type " + type + " cannot be measured by the unsafe strategy", e); 101 | } 102 | } 103 | 104 | @Override 105 | protected int arrayBaseOffset() { 106 | return ARRAY_BASE_OFFSET; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/org/github/jamm/string/PlainReflectionStringMeter.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.string; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | 5 | import org.github.jamm.CannotAccessFieldException; 6 | import org.github.jamm.MemoryMeterStrategy; 7 | 8 | /** 9 | * {@code StringMeter} used to measure Java 9+ strings where the {@code String} value is a {@code byte} array. 10 | * 11 | */ 12 | final class PlainReflectionStringMeter extends StringMeter { 13 | 14 | /** 15 | * The method handle used to access the value field. 16 | */ 17 | private final MethodHandle valueMH; 18 | 19 | public PlainReflectionStringMeter(MethodHandle valueMH) { 20 | this.valueMH = valueMH; 21 | } 22 | 23 | @Override 24 | public long measureStringValue(MemoryMeterStrategy strategy, String s) { 25 | try { 26 | return strategy.measureArray((byte[]) valueMH.invoke(s)); 27 | } catch (Throwable e) { 28 | throw new CannotAccessFieldException("The value of the value field from java.lang.String cannot be retrieved", e); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/org/github/jamm/string/PreJava9StringMeter.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.string; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | 5 | import org.github.jamm.CannotAccessFieldException; 6 | import org.github.jamm.MemoryMeterStrategy; 7 | 8 | /** 9 | * {@code StringMeter} used to measure pre-Java 9 strings where the {@code String} value is a {@code char} array. 10 | * 11 | */ 12 | final class PreJava9StringMeter extends StringMeter { 13 | 14 | /** 15 | * The method handle used to access the value field. 16 | */ 17 | private final MethodHandle valueMH; 18 | 19 | public PreJava9StringMeter(MethodHandle valueMH) { 20 | this.valueMH = valueMH; 21 | } 22 | 23 | @Override 24 | public long measureStringValue(MemoryMeterStrategy strategy, String s) { 25 | 26 | if (strategy.supportComputeArraySize()) 27 | return strategy.computeArraySize(s.length(), Character.BYTES); 28 | 29 | try { 30 | return strategy.measureArray((char[]) valueMH.invoke(s)); 31 | } catch (Throwable e) { 32 | throw new CannotAccessFieldException("The value of the 'value' field from java.lang.String cannot be retrieved", e); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/org/github/jamm/string/StringMeter.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.string; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | import java.lang.reflect.Field; 5 | import java.util.Optional; 6 | 7 | import sun.misc.Unsafe; 8 | 9 | import org.github.jamm.CannotAccessFieldException; 10 | import org.github.jamm.MemoryMeter; 11 | import org.github.jamm.MemoryMeterStrategy; 12 | import org.github.jamm.VM; 13 | import org.github.jamm.strategies.MemoryMeterStrategies; 14 | import org.github.jamm.utils.MethodHandleUtils; 15 | 16 | import static org.github.jamm.utils.MethodHandleUtils.methodHandle; 17 | 18 | /** 19 | * Utility to measure the value field of a {@code String} 20 | */ 21 | public abstract class StringMeter { 22 | 23 | /** 24 | * Enable or disable string optimization through the {@code org.github.jamm.string.Optimize} String system property. {@code true} by default. 25 | */ 26 | public static final boolean ENABLED = Boolean.parseBoolean(System.getProperty("org.github.jamm.string.Optimize", "true")); 27 | 28 | /** 29 | * The String shallow size stored as a constant to have it compiled directly into the measure method. 30 | */ 31 | public static final long STRING_SHALLOW_SIZE = MemoryMeterStrategies.getInstance() 32 | .getStrategy(MemoryMeter.BEST) 33 | .measure(""); 34 | 35 | /** 36 | * Measure the deep size of the specified String. 37 | * 38 | * @param strategy the strategy to perform the measurement 39 | * @param s the string 40 | * @return the size of the deep string 41 | */ 42 | public long measureDeep(MemoryMeterStrategy strategy, String s) { 43 | return STRING_SHALLOW_SIZE + measureStringValue(strategy, s); 44 | } 45 | 46 | /** 47 | * Measure the size of the value of the specified String. 48 | * 49 | * @param strategy the strategy to perform the measurement 50 | * @param s the string 51 | * @return the size of the string value field 52 | */ 53 | protected abstract long measureStringValue(MemoryMeterStrategy strategy, String s); 54 | 55 | /** 56 | * Creates a new {@code StringMeter} instance. 57 | * @return a new {@code StringMeter} instance. 58 | */ 59 | public static StringMeter newInstance() { 60 | 61 | try { 62 | Field field = String.class.getDeclaredField("value"); 63 | 64 | Optional mayBeTrySetAccessible = MethodHandleUtils.mayBeMethodHandle(Field.class, "trySetAccessible"); // Added in Java 9 65 | if (mayBeTrySetAccessible.isPresent()) { 66 | 67 | // Base on the JMH benchmarks, Unsafe is faster than using a MethodHandle so we try to use Unsafe first and default to reflection if it is unavailable. 68 | Unsafe unsafe = VM.getUnsafe(); 69 | 70 | if (unsafe == null) { 71 | 72 | if ((boolean) mayBeTrySetAccessible.get().invoke(field)) { 73 | return new PlainReflectionStringMeter(methodHandle(field)); 74 | } 75 | throw new CannotAccessFieldException("The value of the 'value' field from java.lang.String" 76 | + " cannot be retrieved as the field cannot be made accessible and Unsafe is unavailable"); 77 | } 78 | 79 | long valueFieldOffset = unsafe.objectFieldOffset(field); 80 | return new UnsafeStringMeter(unsafe, valueFieldOffset); 81 | } 82 | 83 | field.setAccessible(true); 84 | return new PreJava9StringMeter(methodHandle(field)); 85 | 86 | } catch (Throwable e) { 87 | throw new CannotAccessFieldException("The value of the 'value' field from java.lang.String cannot be retrieved", e); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/org/github/jamm/string/UnsafeStringMeter.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.string; 2 | 3 | import org.github.jamm.MemoryMeterStrategy; 4 | 5 | import sun.misc.Unsafe; 6 | 7 | /** 8 | * {@code StringMeter} using {@code Unsafe} to measure the String value on Java 9+ versions when the field is not 9 | * accessible through reflection. 10 | */ 11 | final class UnsafeStringMeter extends StringMeter { 12 | 13 | private final Unsafe unsafe; 14 | 15 | private final long valueFieldOffset; 16 | 17 | UnsafeStringMeter(Unsafe unsafe, long valueFieldOffset) { 18 | this.unsafe = unsafe; 19 | this.valueFieldOffset = valueFieldOffset; 20 | } 21 | 22 | @Override 23 | public long measureStringValue(MemoryMeterStrategy strategy, String s) { 24 | return strategy.measureArray((byte[]) unsafe.getObjectVolatile(s, valueFieldOffset)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/org/github/jamm/utils/ArrayMeasurementUtils.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.utils; 2 | 3 | import static org.github.jamm.utils.MathUtils.roundTo; 4 | 5 | /** 6 | * Utility methods to measure arrays. 7 | */ 8 | public final class ArrayMeasurementUtils { 9 | 10 | /** 11 | * Computes the size of an array from its base offset, length, elementSize and object alignment. 12 | * 13 | * @param arrayBaseOffset the array base offset 14 | * @param length the array length 15 | * @param elementSize the size of the array elements 16 | * @param objectAlignment the object alignment (padding) in bytes 17 | * @return the size of the array 18 | */ 19 | public static long computeArraySize(int arrayBaseOffset, int length, int elementSize, int objectAlignment) { 20 | return roundTo(arrayBaseOffset + length * (long) elementSize, objectAlignment); 21 | } 22 | 23 | private ArrayMeasurementUtils() { 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/org/github/jamm/utils/ByteBufferMeasurementUtils.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.utils; 2 | 3 | import java.lang.reflect.Field; 4 | import java.nio.ByteBuffer; 5 | 6 | import org.github.jamm.VM; 7 | import org.github.jamm.accessors.FieldAccessor; 8 | 9 | /** 10 | * Utility methods for {@code ByteBuffers} measurements. 11 | */ 12 | public final class ByteBufferMeasurementUtils { 13 | 14 | /** 15 | * The field used to store heap ByteBuffer underlying array. 16 | */ 17 | private static final Field HB_FIELD = getDeclaredField(ByteBuffer.class, "hb"); 18 | 19 | /** 20 | * The field used to store direct ByteBuffer attachment. 21 | */ 22 | private static final Field ATT_FIELD = getDeclaredField(ByteBuffer.allocateDirect(0).getClass(), "att"); 23 | 24 | /** 25 | * Retrieves the underlying capacity of the specified buffer. 26 | * 27 | * @param buffer the buffer 28 | * @param accessor the field accessor for this java version 29 | * @return the underlying capacity of the specified buffer. 30 | */ 31 | public static int underlyingCapacity(ByteBuffer buffer, FieldAccessor accessor) { 32 | 33 | if (buffer.isDirect()) { 34 | 35 | if (buffer.isReadOnly() && VM.isPreJava12JVM()) { 36 | // Pre-java 12, a DirectByteBuffer created from another DirectByteBuffer was using the source buffer as an attachment 37 | // for liveness rather than the source buffer's attachment (https://bugs.openjdk.org/browse/JDK-8208362) 38 | // Therefore for checking slices we need to go one level deeper. 39 | buffer = (ByteBuffer) accessor.getFieldValue(buffer, ATT_FIELD); 40 | } 41 | ByteBuffer att = (ByteBuffer) accessor.getFieldValue(buffer, ATT_FIELD); 42 | return att == null ? buffer.capacity() : att.capacity(); 43 | } 44 | 45 | if (buffer.isReadOnly()) { 46 | byte[] hb = (byte[]) accessor.getFieldValue(buffer, HB_FIELD); 47 | return hb.length; 48 | } 49 | 50 | return buffer.array().length; 51 | } 52 | 53 | /** 54 | * Returns the declared field with the specified name for the given class. 55 | * 56 | * @param cls the class 57 | * @param fieldName the field name 58 | * @return the declared field 59 | */ 60 | private static Field getDeclaredField(Class cls, String fieldName) { 61 | try { 62 | return cls.getDeclaredField(fieldName); 63 | } catch (Exception e) { 64 | throw new IllegalStateException(e); 65 | } 66 | } 67 | 68 | private ByteBufferMeasurementUtils() { 69 | 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/org/github/jamm/utils/MathUtils.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.utils; 2 | 3 | public final class MathUtils 4 | { 5 | /** 6 | * Rounds x up to the next multiple of m. 7 | * 8 | * @param x the number to round 9 | * @param m the multiple (must be a power of 2) 10 | * @return the rounded value of x up to the next multiple of m. 11 | */ 12 | public static long roundTo(long x, int m) { 13 | return (x + m - 1) & -m; 14 | } 15 | 16 | /** 17 | * Rounds x up to the next multiple of m. 18 | * 19 | * @param x the number to round 20 | * @param m the multiple (must be a power of 2) 21 | * @return the rounded value of x up to the next multiple of m. 22 | */ 23 | public static int roundTo(int x, int m) { 24 | return (x + m - 1) & -m; 25 | } 26 | 27 | private MathUtils() 28 | { 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/org/github/jamm/utils/MethodHandleUtils.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.utils; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | import java.lang.invoke.MethodHandles; 5 | import java.lang.reflect.Field; 6 | import java.lang.reflect.Method; 7 | import java.util.Optional; 8 | 9 | public final class MethodHandleUtils 10 | { 11 | /** 12 | * Returns the {@code MethodHandle} for the specified class and method if the method exists. 13 | * 14 | * @param klass the class 15 | * @param methodName the method name 16 | * @return an {@code Optional} for the {@code MethodHandle} 17 | */ 18 | public static Optional mayBeMethodHandle(Class klass, String methodName) { 19 | try { 20 | Method method = klass.getMethod(methodName, new Class[0]); 21 | return Optional.of(methodHandle(method)); 22 | } catch (Exception e) { 23 | return Optional.empty(); 24 | } 25 | } 26 | 27 | /** 28 | * Returns the {@code MethodHandle} for the specified method. 29 | * 30 | * @param method the method 31 | * @return the {@code MethodHandle} for the specified method 32 | * @throws IllegalAccessException if the method is not accessible 33 | */ 34 | public static MethodHandle methodHandle(Method method) throws IllegalAccessException { 35 | return MethodHandles.lookup().unreflect(method); 36 | } 37 | 38 | /** 39 | * Returns the {@code MethodHandle} for the specified field. 40 | * 41 | * @param field the field 42 | * @return the {@code MethodHandle} for the specified field 43 | * @throws IllegalAccessException if the method is not accessible 44 | */ 45 | public static MethodHandle methodHandle(Field field) throws IllegalAccessException { 46 | return MethodHandles.lookup().unreflectGetter(field); 47 | } 48 | 49 | private MethodHandleUtils() {} 50 | } 51 | -------------------------------------------------------------------------------- /test/org/github/jamm/IdentityHashSetTest.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.junit.Test; 7 | 8 | import static org.junit.Assert.*; 9 | 10 | public class IdentityHashSetTest { 11 | @Test 12 | public void addSame() { 13 | IdentityHashSet s = new IdentityHashSet(); 14 | Object o = new Object(); 15 | assertTrue(s.add(o)); 16 | assertFalse(s.add(o)); 17 | assertEquals(1, s.size); 18 | assertEquals(32, s.table.length); 19 | } 20 | 21 | @Test 22 | public void addMany() { 23 | List ref1 = new ArrayList<>(); 24 | List ref2 = new ArrayList<>(); 25 | for (int i = 0; i < 250; i++) 26 | assertTrue(ref1.add("o" + i)); 27 | for (int i = 0; i < 250; i++) 28 | assertTrue(ref2.add("x" + i)); 29 | 30 | IdentityHashSet s = new IdentityHashSet(); 31 | for (int i = 0; i < ref1.size(); i++) { 32 | Object o = ref1.get(i); 33 | assertEquals(i, s.size); 34 | assertEquals("" + i, expectedCapacity(i), s.table.length); 35 | assertTrue(s.add(o)); 36 | } 37 | assertEquals(ref1.size(), s.size); 38 | 39 | for (Object o : ref1) 40 | assertFalse(s.add(o)); 41 | 42 | for (int i = 0; i < ref2.size(); i++) { 43 | Object o = ref2.get(i); 44 | assertEquals(ref1.size() + i, s.size); 45 | assertTrue(s.add(o)); 46 | } 47 | assertEquals(ref1.size() + ref2.size(), s.size); 48 | 49 | for (Object o : ref1) 50 | assertFalse(s.add(o)); 51 | 52 | for (Object o : ref2) 53 | assertFalse(s.add(o)); 54 | } 55 | 56 | private static int expectedCapacity(int i) { 57 | // the backing array must be at most 2/3 full in order to have enough 'null's in the open-addressing-map 58 | i = (i * 3) / 2; 59 | 60 | // next power of 2 61 | i = 1 << (32 - Integer.numberOfLeadingZeros(i)); 62 | return Math.max(32, i); 63 | } 64 | } -------------------------------------------------------------------------------- /test/org/github/jamm/MathUtilsTest.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.github.jamm.utils.MathUtils.roundTo; 6 | import static org.junit.Assert.*; 7 | 8 | public class MathUtilsTest 9 | { 10 | @Test 11 | public void testRoundToInt() { 12 | 13 | assertEquals(0, roundTo(0, 8)); 14 | assertEquals(8, roundTo(1, 8)); 15 | assertEquals(8, roundTo(2, 8)); 16 | assertEquals(8, roundTo(3, 8)); 17 | assertEquals(8, roundTo(8, 8)); 18 | assertEquals(16, roundTo(9, 8)); 19 | assertEquals(16, roundTo(10, 8)); 20 | assertEquals(40, roundTo(36, 8)); 21 | 22 | assertEquals(0, roundTo(0, 16)); 23 | assertEquals(16, roundTo(1, 16)); 24 | assertEquals(16, roundTo(2, 16)); 25 | assertEquals(16, roundTo(3, 16)); 26 | assertEquals(16, roundTo(8, 16)); 27 | assertEquals(16, roundTo(9, 16)); 28 | assertEquals(16, roundTo(10, 16)); 29 | assertEquals(32, roundTo(20, 16)); 30 | assertEquals(48, roundTo(36, 16)); 31 | } 32 | 33 | @Test 34 | public void testRoundToLong() { 35 | 36 | assertEquals(0, roundTo(0L, 8)); 37 | assertEquals(8, roundTo(1L, 8)); 38 | assertEquals(8, roundTo(2L, 8)); 39 | assertEquals(8, roundTo(3L, 8)); 40 | assertEquals(8, roundTo(8L, 8)); 41 | assertEquals(16, roundTo(9L, 8)); 42 | assertEquals(16, roundTo(10L, 8)); 43 | assertEquals(40, roundTo(36L, 8)); 44 | 45 | assertEquals(0, roundTo(0L, 16)); 46 | assertEquals(16, roundTo(1L, 16)); 47 | assertEquals(16, roundTo(2L, 16)); 48 | assertEquals(16, roundTo(3L, 16)); 49 | assertEquals(16, roundTo(8L, 16)); 50 | assertEquals(16, roundTo(9L, 16)); 51 | assertEquals(16, roundTo(10L, 16)); 52 | assertEquals(32, roundTo(20L, 16)); 53 | assertEquals(48, roundTo(36L, 16)); 54 | } 55 | 56 | @Test 57 | public void testModulo() { 58 | 59 | assertEquals(0, 0 & 7); 60 | assertEquals(1, 1 & 7); 61 | assertEquals(2, 2 & 7); 62 | assertEquals(5, 5 & 7); 63 | assertEquals(0, 8 & 7); 64 | assertEquals(1, 9 & 7); 65 | assertEquals(4, 12 & 7); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/org/github/jamm/MemoryMeterTest.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm; 2 | 3 | import java.lang.ref.PhantomReference; 4 | import java.lang.ref.ReferenceQueue; 5 | import java.lang.ref.SoftReference; 6 | import java.util.Arrays; 7 | import java.util.Collection; 8 | import java.util.Date; 9 | import java.util.function.Predicate; 10 | 11 | import org.junit.Assert; 12 | import org.junit.BeforeClass; 13 | import org.junit.Test; 14 | import org.junit.runner.RunWith; 15 | import org.junit.runners.Parameterized; 16 | 17 | import org.github.jamm.testedclasses.PublicClassWithPackageProtectedClassField; 18 | 19 | import static org.junit.Assert.assertEquals; 20 | import static org.junit.Assert.assertNotEquals; 21 | 22 | @RunWith(Parameterized.class) 23 | public class MemoryMeterTest { 24 | 25 | @BeforeClass 26 | public static void logInfoAtStartup() { 27 | System.setProperty("org.github.jamm.strategies.LogInfoAtStartup", "true"); 28 | } 29 | 30 | @Parameterized.Parameters 31 | public static Collection guesses() { 32 | 33 | return Arrays.asList(MemoryMeter.Guess.INSTRUMENTATION, 34 | MemoryMeter.Guess.INSTRUMENTATION_AND_SPECIFICATION, 35 | MemoryMeter.Guess.UNSAFE, 36 | MemoryMeter.Guess.SPECIFICATION); 37 | } 38 | 39 | private final MemoryMeter.Guess guess; 40 | 41 | public MemoryMeterTest(MemoryMeter.Guess guess) 42 | { 43 | this.guess = guess; 44 | } 45 | 46 | @Test 47 | public void testWithNull() { 48 | MemoryMeter meter = MemoryMeter.builder().withGuessing(guess).build(); 49 | 50 | assertEquals(0L, meter.measure(null)); 51 | assertEquals(0L, meter.measureDeep(null)); 52 | } 53 | 54 | @Test 55 | public void testCycle() throws Exception { 56 | MemoryMeter meter = MemoryMeter.builder().withGuessing(guess).build(); 57 | 58 | Recursive dummy = new Recursive(); 59 | long shallowSize = meter.measure(dummy); 60 | assertEquals("Deep size of Recursive is shallow size when child==null", shallowSize, meter.measureDeep(dummy)); 61 | dummy.child = dummy; 62 | assertEquals("Deep size of Recursive is shallow size when child==this", shallowSize, meter.measureDeep(dummy)); 63 | } 64 | 65 | @Test 66 | public void testDeep() { 67 | MemoryMeter meter = MemoryMeter.builder().withGuessing(guess).build(); 68 | 69 | Recursive root = new Recursive(); 70 | Recursive recursive = root; 71 | for (int i = 0; i < 100000; i++) { 72 | recursive.child = new Recursive(); 73 | recursive = recursive.child; 74 | } 75 | long shallowSize = meter.measure(root); 76 | assertEquals(shallowSize * 100001, meter.measureDeep(root)); 77 | } 78 | 79 | @SuppressWarnings("unused") 80 | private static class Recursive { 81 | int i; 82 | Recursive child = null; 83 | } 84 | 85 | @SuppressWarnings("unused") 86 | public static class Outer { 87 | public int[] somethingHeavy = new int[100]; 88 | public Inner inner = new Inner(); 89 | 90 | private class Inner { 91 | public int integer = 1; 92 | } 93 | } 94 | 95 | @Test 96 | public void testWithInnerClass () { 97 | Outer outer = new Outer(); 98 | MemoryMeter meter = MemoryMeter.builder().withGuessing(guess).build(); 99 | 100 | long outerSize = meter.measure(outer); 101 | long innerSize = meter.measure(outer.inner); 102 | long somethingHeavySize = meter.measure(outer.somethingHeavy); 103 | 104 | long size = outerSize + innerSize + somethingHeavySize; 105 | 106 | assertEquals(size, meter.measureDeep(outer)); 107 | 108 | meter = MemoryMeter.builder().withGuessing(guess).ignoreOuterClassReference().build(); 109 | 110 | assertEquals(innerSize, meter.measureDeep(outer.inner)); 111 | } 112 | 113 | @Test 114 | public void testMeasureKnownSingletons() { 115 | MemoryMeter meter = MemoryMeter.builder().withGuessing(guess).measureKnownSingletons().build(); 116 | 117 | long classFieldSize = meter.measureDeep(new HasClassField()); 118 | long enumFieldSize = meter.measureDeep(new HasEnumField()); 119 | 120 | meter = MemoryMeter.builder().withGuessing(guess).build(); 121 | 122 | assertNotEquals(classFieldSize, meter.measureDeep(new HasClassField())); 123 | assertNotEquals(enumFieldSize, meter.measureDeep(new HasEnumField())); 124 | } 125 | 126 | @Test 127 | public void testClassFilteringWithObjectField() { 128 | // Measure excluding class and enum object 129 | MemoryMeter meter = MemoryMeter.builder().withGuessing(guess).build(); 130 | 131 | HasObjectField withClass = new HasObjectField(String.class); 132 | HasObjectField withString = new HasObjectField(1); 133 | 134 | assertEquals(meter.measure(withClass), meter.measureDeep(withClass)); 135 | assertEquals(meter.measure(withString) + meter.measure(1), meter.measureDeep(withString)); 136 | } 137 | 138 | @Test 139 | public void testIgnoreNonStrongReferences() throws InterruptedException { 140 | // The Reference fields 'next' and 'discovered' are used by 'ReferenceQueue' instances and are not part of what 141 | // we want to measure. The 'queue' field is either a singleton 'ReferenceQueue.NULL' or a provided queue that user hold a reference to. 142 | // Therefore, those fields should be ignored. 143 | MemoryMeter meterIgnoring = MemoryMeter.builder().withGuessing(guess).build(); 144 | MemoryMeter meterMeasuring = MemoryMeter.builder().withGuessing(guess).measureNonStrongReferences().build(); 145 | 146 | // test SoftReference 147 | SoftReference ref = new SoftReference(new Date()); 148 | long refShallowSize = meterIgnoring.measure(ref); 149 | assertEquals(refShallowSize, meterIgnoring.measureDeep(ref)); 150 | 151 | // SoftReference + deep queue + referent 152 | long deepSize = refShallowSize + meterMeasuring.measureDeep(new ReferenceQueue()) + meterMeasuring.measure(new Date()); 153 | assertEquals(deepSize, meterMeasuring.measureDeep(ref)); 154 | 155 | HasReferenceField hasReferenceField = new HasReferenceField(ref); 156 | long hasReferenceFieldShallowSize = meterIgnoring.measure(hasReferenceField); 157 | assertEquals(refShallowSize + hasReferenceFieldShallowSize, meterIgnoring.measureDeep(hasReferenceField)); 158 | 159 | // HasReferenceField + SoftReference + deep queue + referent 160 | deepSize = hasReferenceFieldShallowSize + refShallowSize + meterMeasuring.measureDeep(new ReferenceQueue()) + meterMeasuring.measure(new Date()); 161 | assertEquals(deepSize, meterMeasuring.measureDeep(hasReferenceField)); 162 | 163 | // Test ReferenceQueue measurement with one object 164 | ReferenceQueue queue = new ReferenceQueue<>(); 165 | MyPhantomReference p = new MyPhantomReference(new Object(), queue); 166 | 167 | refShallowSize = meterIgnoring.measure(p); 168 | assertEquals(refShallowSize, meterIgnoring.measureDeep(p)); 169 | 170 | // PhantomReference + deep queue + referent 171 | deepSize = refShallowSize + meterMeasuring.measureDeep(new ReferenceQueue()) + meterMeasuring.measure(new Object()); 172 | assertEquals(deepSize, meterMeasuring.measureDeep(p)); 173 | 174 | System.gc(); 175 | // wait for garbage collection 176 | long start = System.currentTimeMillis(); 177 | while (!p.isEnqueued() && (System.currentTimeMillis() - start) < 1000) 178 | Thread.yield(); 179 | 180 | Assert.assertTrue(p.isEnqueued()); 181 | 182 | long queueShallowSize = meterIgnoring.measure(queue); 183 | long lockSize = meterIgnoring.measure(new Object()); // The ReferenceQueue lock field has the same size as an empty object. 184 | assertEquals(queueShallowSize + lockSize, meterIgnoring.measureDeep(queue)); 185 | 186 | assertEquals(p, queue.poll()); 187 | } 188 | 189 | private static class MyPhantomReference extends PhantomReference { 190 | 191 | public MyPhantomReference(Object referent, ReferenceQueue q) { 192 | super(referent, q); 193 | } 194 | } 195 | 196 | @SuppressWarnings("unused") 197 | private static class HasObjectField { 198 | private Object obj = String.class; 199 | 200 | public HasObjectField(Object obj) { 201 | this.obj = obj; 202 | } 203 | } 204 | 205 | @SuppressWarnings("unused") 206 | private static class HasClassField { 207 | private Class cls = String.class; 208 | } 209 | 210 | @SuppressWarnings("unused") 211 | private static class HasEnumField { 212 | enum Giant {Fee, Fi, Fo, Fum} 213 | 214 | private Giant grunt = Giant.Fee; 215 | } 216 | 217 | @SuppressWarnings("unused") 218 | private static class HasReferenceField { 219 | private final SoftReference ref; 220 | 221 | public HasReferenceField(SoftReference ref) { 222 | this.ref = ref; 223 | } 224 | } 225 | 226 | @Test 227 | public void testUnmeteredAnnotationOnFields() { 228 | MemoryMeter meter = MemoryMeter.builder().withGuessing(guess).build(); 229 | 230 | String s = "test"; 231 | 232 | long stringDeepSize = meter.measureDeep(s); 233 | long shallowSize = meter.measure(new WithoutAnnotationField(null)); 234 | assertEquals(stringDeepSize + shallowSize, meter.measureDeep(new WithoutAnnotationField(s))); 235 | 236 | shallowSize = meter.measure(new WithAnnotationField(null)); 237 | assertEquals(shallowSize, meter.measureDeep(new WithAnnotationField(s))); 238 | } 239 | 240 | @Test 241 | public void testUnmeteredTypeAnnotation() { 242 | MemoryMeter meter = MemoryMeter.builder().withGuessing(guess).build(); 243 | 244 | String s = "test"; 245 | assertEquals(0, meter.measureDeep(new ClassWithAnnotation(s))); 246 | } 247 | 248 | @Test 249 | public void testUnmeteredAnnotationOnParent() { 250 | MemoryMeter meter = MemoryMeter.builder().withGuessing(guess).build(); 251 | 252 | assertEquals(0, meter.measureDeep(new WithParentWithAnnotation("test"))); 253 | } 254 | 255 | @Test 256 | public void testUnmeteredAnnotationOnImplementedInteface() { 257 | MemoryMeter meter = MemoryMeter.builder().withGuessing(guess).build(); 258 | 259 | assertEquals(0, meter.measureDeep(new WithAnnotatedInterface())); 260 | } 261 | 262 | @Test 263 | public void testUnmeteredAnnotationOnFieldParent() { 264 | MemoryMeter meter = MemoryMeter.builder().withGuessing(guess).build(); 265 | 266 | long withoutSize = meter.measureDeep(new WithFieldWithAnnotatedParent(null)); 267 | 268 | WithParentWithAnnotation field = new WithParentWithAnnotation("test"); 269 | long withSize = meter.measureDeep(new WithFieldWithAnnotatedParent(field)); 270 | assertEquals(withoutSize, withSize); 271 | } 272 | 273 | @Test 274 | public void testUnmeteredAnnotationOnFieldInterface() { 275 | MemoryMeter meter = MemoryMeter.builder().withGuessing(guess).build(); 276 | 277 | long withoutSize = meter.measureDeep(new WithFieldAnnotatedInterface(null)); 278 | 279 | AnnotatedInterface field = new AnnotatedInterface() { 280 | }; 281 | long withSize = meter.measureDeep(new WithFieldAnnotatedInterface(field)); 282 | assertEquals(withoutSize, withSize); 283 | } 284 | 285 | @Test 286 | public void testUnmeteredAnnotationOnFieldImplementedInterface() { 287 | MemoryMeter meter = MemoryMeter.builder().withGuessing(guess).build(); 288 | 289 | long withoutSize = meter.measureDeep(new WithFieldTypeWithAnnotatedInterface(null)); 290 | 291 | WithAnnotatedInterface field = new WithAnnotatedInterface(); 292 | long withSize = meter.measureDeep(new WithFieldTypeWithAnnotatedInterface(field)); 293 | assertEquals(withoutSize, withSize); 294 | } 295 | 296 | @SuppressWarnings("unused") 297 | private static class WithoutAnnotationField { 298 | private String s; 299 | 300 | public WithoutAnnotationField(String s) { 301 | this.s = s; 302 | } 303 | } 304 | 305 | private static class WithAnnotationField { 306 | 307 | @Unmetered 308 | private String s; 309 | 310 | public WithAnnotationField(String s) { 311 | this.s = s; 312 | } 313 | } 314 | 315 | @Unmetered 316 | @SuppressWarnings("unused") 317 | private static class ClassWithAnnotation { 318 | private String s; 319 | 320 | public ClassWithAnnotation(String s) { 321 | this.s = s; 322 | } 323 | } 324 | 325 | private static class WithParentWithAnnotation extends ClassWithAnnotation { 326 | 327 | public WithParentWithAnnotation(String s) { 328 | super(s); 329 | } 330 | } 331 | 332 | @SuppressWarnings("unused") 333 | private static class WithFieldWithAnnotatedParent { 334 | 335 | private final WithParentWithAnnotation field; 336 | 337 | public WithFieldWithAnnotatedParent(WithParentWithAnnotation field) { 338 | this.field = field; 339 | } 340 | } 341 | 342 | @Unmetered 343 | private interface AnnotatedInterface { 344 | 345 | } 346 | 347 | @SuppressWarnings("unused") 348 | private static class WithFieldAnnotatedInterface { 349 | 350 | private final AnnotatedInterface field; 351 | 352 | public WithFieldAnnotatedInterface(AnnotatedInterface field) { 353 | this.field = field; 354 | } 355 | } 356 | 357 | private static class WithAnnotatedInterface implements AnnotatedInterface { 358 | 359 | } 360 | 361 | @SuppressWarnings("unused") 362 | private static class WithFieldTypeWithAnnotatedInterface { 363 | 364 | private final WithAnnotatedInterface field; 365 | 366 | public WithFieldTypeWithAnnotatedInterface(WithAnnotatedInterface field) { 367 | this.field = field; 368 | } 369 | } 370 | 371 | 372 | @Test 373 | public void testMeasureWithLambdaField() { 374 | MemoryMeter meter = MemoryMeter.builder().withGuessing(guess).build(); 375 | 376 | meter.measureDeep(new WithLambdaField("test")); 377 | } 378 | 379 | private static class WithLambdaField { 380 | 381 | private final String field; 382 | 383 | @SuppressWarnings("unused") 384 | private final Predicate greater; 385 | 386 | public WithLambdaField(String field) 387 | { 388 | this.field = field; 389 | this.greater = (e) -> WithLambdaField.this.isGreater(e); 390 | } 391 | 392 | public boolean isGreater(String s) 393 | { 394 | return s.compareTo(field) > 0; 395 | } 396 | } 397 | 398 | @Test 399 | public void testMeasureDeepWithPublicFieldInPackagePrivate() { 400 | 401 | MemoryMeter meter = MemoryMeter.builder().withGuessing(guess).build(); 402 | PublicClassWithPackageProtectedClassField object = new PublicClassWithPackageProtectedClassField("publicField", "packageProtected", "protected", "private"); 403 | long expected = object.measureDeep(meter); 404 | assertEquals(expected, meter.measureDeep(object)); 405 | } 406 | 407 | @Test 408 | public void testMeasurableInstance() { 409 | MemoryMeter meter = MemoryMeter.builder().withGuessing(guess).build(); 410 | 411 | ChildMeasurable bart = new ChildMeasurable("Bart"); 412 | ChildMeasurable lisa = new ChildMeasurable("Lisa"); 413 | ChildMeasurable maggie = new ChildMeasurable("Maggie"); 414 | ParentMeasurable homer = new ParentMeasurable("Homer", bart, lisa, maggie); 415 | 416 | long expectedSize = meter.measure(homer) 417 | + meter.measureDeep("Homer") 418 | + meter.measureArray(new Object[3]) 419 | + meter.measure(bart) 420 | + meter.measureDeep("Bart") 421 | + meter.measure(lisa) 422 | + meter.measureDeep("Lisa") 423 | + meter.measure(maggie) 424 | + meter.measureDeep("Maggie"); 425 | 426 | assertEquals(expectedSize, meter.measureDeep(homer)); 427 | Assert.assertTrue("the addChildrenTo method has not been called", homer.checkUsage()); 428 | } 429 | 430 | private static class ParentMeasurable implements Measurable 431 | { 432 | private final String name; 433 | 434 | private final ChildMeasurable[] children; 435 | 436 | boolean hasAddChildrenBeenUsed; 437 | 438 | public ParentMeasurable(String name, ChildMeasurable... children) { 439 | this.name = name; 440 | this.children = children; 441 | } 442 | 443 | @Override 444 | public void addChildrenTo(MeasurementStack stack) { 445 | hasAddChildrenBeenUsed = true; 446 | 447 | stack.pushObject(this, "name", name); 448 | stack.pushObject(this, "children", children); 449 | } 450 | 451 | public boolean checkUsage() { 452 | 453 | boolean check = hasAddChildrenBeenUsed; 454 | for (ChildMeasurable child : children) 455 | check &= child.hasAddChildrenBeenUsed; 456 | return check; 457 | } 458 | } 459 | 460 | private static class ChildMeasurable implements Measurable 461 | { 462 | private String name; 463 | boolean hasAddChildrenBeenUsed; 464 | 465 | public ChildMeasurable(String name) { 466 | this.name = name; 467 | } 468 | 469 | @Override 470 | public void addChildrenTo(MeasurementStack stack) { 471 | hasAddChildrenBeenUsed = true; 472 | 473 | stack.pushObject(this, "name", name); 474 | } 475 | } 476 | } 477 | -------------------------------------------------------------------------------- /test/org/github/jamm/jmh/BenchmarkMeasureArray.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.jmh; 2 | 3 | import java.lang.reflect.Array; 4 | import java.util.Random; 5 | import java.util.concurrent.TimeUnit; 6 | 7 | import org.github.jamm.MemoryMeter; 8 | import org.openjdk.jmh.annotations.Benchmark; 9 | import org.openjdk.jmh.annotations.BenchmarkMode; 10 | import org.openjdk.jmh.annotations.Fork; 11 | import org.openjdk.jmh.annotations.Level; 12 | import org.openjdk.jmh.annotations.Measurement; 13 | import org.openjdk.jmh.annotations.Mode; 14 | import org.openjdk.jmh.annotations.OutputTimeUnit; 15 | import org.openjdk.jmh.annotations.Param; 16 | import org.openjdk.jmh.annotations.Scope; 17 | import org.openjdk.jmh.annotations.Setup; 18 | import org.openjdk.jmh.annotations.State; 19 | import org.openjdk.jmh.annotations.Threads; 20 | import org.openjdk.jmh.annotations.Warmup; 21 | import org.openjdk.jmh.infra.Blackhole; 22 | 23 | @Threads(3) 24 | @Fork(value = 1, jvmArgsPrepend = { 25 | "-javaagent:target/jamm-0.4.1-SNAPSHOT.jar", 26 | }) 27 | @Warmup(iterations=4, time=5) 28 | @Measurement(iterations=5, time=5) 29 | @BenchmarkMode(Mode.AverageTime) 30 | @State(Scope.Thread) 31 | @OutputTimeUnit(TimeUnit.MICROSECONDS) 32 | public class BenchmarkMeasureArray { 33 | 34 | @Param({"INSTRUMENTATION", "INSTRUMENTATION_AND_SPECIFICATION", "SPECIFICATION", "UNSAFE"}) 35 | private String guess; 36 | 37 | private MemoryMeter meter; 38 | 39 | private static Object[] arrays; 40 | private static Object[] byteArrays; 41 | 42 | static { 43 | try { 44 | 45 | Class[] choices = new Class[] {byte.class, int.class, double.class, Object.class}; 46 | 47 | Random random = new Random(); 48 | 49 | arrays = new Object[1000]; 50 | for (int i = 0; i < arrays.length; i++) { 51 | arrays[i] = Array.newInstance(choices[random.nextInt(choices.length)], random.nextInt(100)); 52 | } 53 | 54 | byteArrays = new Object[1000]; 55 | for (int i = 0; i < byteArrays.length; i++) { 56 | byteArrays[i] = new byte[random.nextInt(100)]; 57 | } 58 | 59 | } catch (Exception e) { 60 | e.printStackTrace(); 61 | } 62 | } 63 | 64 | @Setup(Level.Iteration) 65 | public void setup() throws Exception { 66 | MemoryMeter.Guess guess = MemoryMeter.Guess.valueOf(this.guess); 67 | this.meter = MemoryMeter.builder().withGuessing(guess).build(); 68 | } 69 | 70 | @Benchmark 71 | public void measure(Blackhole bh) { 72 | for (Object o : arrays) 73 | bh.consume(meter.measure(o)); 74 | } 75 | 76 | @Benchmark 77 | public void measureByteArray(Blackhole bh) { 78 | for (Object o : byteArrays) 79 | bh.consume(meter.measureArray((byte[]) o)); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/org/github/jamm/jmh/BenchmarkMeasureInstance.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.jmh; 2 | 3 | import java.util.Random; 4 | import java.util.concurrent.TimeUnit; 5 | 6 | import org.github.jamm.MemoryMeter; 7 | import org.openjdk.jmh.annotations.Benchmark; 8 | import org.openjdk.jmh.annotations.BenchmarkMode; 9 | import org.openjdk.jmh.annotations.Fork; 10 | import org.openjdk.jmh.annotations.Level; 11 | import org.openjdk.jmh.annotations.Measurement; 12 | import org.openjdk.jmh.annotations.Mode; 13 | import org.openjdk.jmh.annotations.OutputTimeUnit; 14 | import org.openjdk.jmh.annotations.Param; 15 | import org.openjdk.jmh.annotations.Scope; 16 | import org.openjdk.jmh.annotations.Setup; 17 | import org.openjdk.jmh.annotations.State; 18 | import org.openjdk.jmh.annotations.Threads; 19 | import org.openjdk.jmh.annotations.Warmup; 20 | import org.openjdk.jmh.infra.Blackhole; 21 | 22 | @Threads(3) 23 | @Fork(value = 1, jvmArgsPrepend = { 24 | "-javaagent:target/jamm-0.4.1-SNAPSHOT.jar", 25 | }) 26 | @Warmup(iterations=4, time=5) 27 | @Measurement(iterations=5, time=5) 28 | @BenchmarkMode(Mode.AverageTime) 29 | @State(Scope.Thread) 30 | @OutputTimeUnit(TimeUnit.MICROSECONDS) 31 | public class BenchmarkMeasureInstance { 32 | 33 | @Param({"INSTRUMENTATION", "INSTRUMENTATION_AND_SPECIFICATION", "SPECIFICATION", "UNSAFE"}) 34 | private String guess; 35 | 36 | private MemoryMeter meter; 37 | 38 | private static Object[] objects; 39 | 40 | static 41 | { 42 | try { 43 | Class[] choices = new Class[] {ClassWithoutFields.class, 44 | ClassWithOnePrimitiveFields.class, 45 | ClassWithOneObjectField.class, 46 | ClassWithTreeObjectFields.class, 47 | ClassWithOneObjectFieldAndTwoPrimitives.class, 48 | ClassWithFiveObjectFields.class}; 49 | 50 | Random random = new Random(); 51 | 52 | objects = new Object[1000]; 53 | for (int i = 0; i < objects.length; i++) { 54 | objects[i] = choices[random.nextInt(choices.length)].newInstance(); 55 | } 56 | } catch (Exception e) { 57 | e.printStackTrace(); 58 | } 59 | } 60 | 61 | @Setup(Level.Iteration) 62 | public void setup() throws Exception { 63 | MemoryMeter.Guess guess = MemoryMeter.Guess.valueOf(this.guess); 64 | this.meter = MemoryMeter.builder().withGuessing(guess).build(); 65 | } 66 | 67 | @Benchmark 68 | public void measure(Blackhole bh) { 69 | for (Object o : objects) 70 | bh.consume(meter.measure(o)); 71 | } 72 | 73 | public static class ClassWithoutFields { 74 | } 75 | 76 | public static class ClassWithOnePrimitiveFields { 77 | int intField; 78 | } 79 | 80 | public static class ClassWithOneObjectField { 81 | Object field = new Object(); 82 | } 83 | 84 | public static class ClassWithTreeObjectFields { 85 | Object first = new Object(); 86 | 87 | Object second = new Object(); 88 | 89 | String[] third = new String[] {"one", "two"}; 90 | } 91 | 92 | public static class ClassWithOneObjectFieldAndTwoPrimitives { 93 | byte first; 94 | 95 | Object second = new Object(); 96 | 97 | double third; 98 | } 99 | 100 | public static class ClassWithFiveObjectFields extends ClassWithTreeObjectFields { 101 | Object fourth = new Object(); 102 | 103 | int[] fifth = new int[12]; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /test/org/github/jamm/jmh/BenchmarkMeasureString.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.jmh; 2 | 3 | import java.util.Random; 4 | import java.util.concurrent.TimeUnit; 5 | 6 | import org.github.jamm.MemoryMeter; 7 | //import org.github.jamm.MemoryMeterStrategy; 8 | //import org.github.jamm.utils.ArrayMeasurementUtils; 9 | import org.openjdk.jmh.annotations.Benchmark; 10 | import org.openjdk.jmh.annotations.BenchmarkMode; 11 | import org.openjdk.jmh.annotations.Fork; 12 | import org.openjdk.jmh.annotations.Level; 13 | import org.openjdk.jmh.annotations.Measurement; 14 | import org.openjdk.jmh.annotations.Mode; 15 | import org.openjdk.jmh.annotations.OutputTimeUnit; 16 | import org.openjdk.jmh.annotations.Param; 17 | import org.openjdk.jmh.annotations.Scope; 18 | import org.openjdk.jmh.annotations.Setup; 19 | import org.openjdk.jmh.annotations.State; 20 | import org.openjdk.jmh.annotations.Threads; 21 | import org.openjdk.jmh.annotations.Warmup; 22 | import org.openjdk.jmh.infra.Blackhole; 23 | 24 | @Threads(3) 25 | @Fork(value = 1, jvmArgsPrepend = { 26 | "-javaagent:target/jamm-0.4.1-SNAPSHOT.jar", 27 | // "--add-opens=java.base/java.lang=ALL-UNNAMED" 28 | }) 29 | @Warmup(iterations=4, time=5) 30 | @Measurement(iterations=5, time=5) 31 | @BenchmarkMode(Mode.AverageTime) 32 | @State(Scope.Thread) 33 | @OutputTimeUnit(TimeUnit.MICROSECONDS) 34 | public class BenchmarkMeasureString 35 | { 36 | @Param({"INSTRUMENTATION", "INSTRUMENTATION_AND_SPECIFICATION", "UNSAFE", "SPECIFICATION"}) 37 | private String guess; 38 | 39 | private MemoryMeter meter; 40 | 41 | private static String[] strings; 42 | 43 | private long emptySize; 44 | 45 | static 46 | { 47 | try { 48 | Random random = new Random(); 49 | 50 | String[] array = new String[1000]; 51 | for (int i = 0; i < array.length; i++) { 52 | int length = random.nextInt(50); 53 | array[i] = random.ints('a', 'z' + 1) 54 | .limit(length) 55 | .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) 56 | .toString(); 57 | } 58 | strings = array; 59 | 60 | } catch (Exception e) { 61 | e.printStackTrace(); 62 | } 63 | } 64 | 65 | @Setup(Level.Iteration) 66 | public void setup() throws Exception { 67 | MemoryMeter.Guess guess = MemoryMeter.Guess.valueOf(this.guess); 68 | this.meter = MemoryMeter.builder().withGuessing(guess).build(); 69 | emptySize = this.meter.measure(""); 70 | } 71 | 72 | @Benchmark 73 | public void measureStringDeep(Blackhole bh) { 74 | for (String s : strings) 75 | bh.consume(meter.measureStringDeep(s)); 76 | } 77 | // 78 | // @Benchmark 79 | // public void measureReference(Blackhole bh) { 80 | // for (String s : strings) { 81 | // bh.consume(emptySize + ArrayMeasurementUtils.computeArraySize(MemoryMeterStrategy.MEMORY_LAYOUT.getArrayHeaderSize(), s.length(), Character.BYTES, MemoryMeterStrategy.MEMORY_LAYOUT.getObjectAlignment())); 82 | // } 83 | // } 84 | 85 | @Benchmark 86 | public void measureDeep(Blackhole bh) { 87 | for (String s : strings) 88 | bh.consume(meter.measureDeep(s)); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /test/org/github/jamm/jmh/BenchmarkObjectGraphTraversal.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.jmh; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | import org.github.jamm.FieldAndClassFilter; 6 | import org.github.jamm.FieldFilter; 7 | import org.github.jamm.Filters; 8 | import org.github.jamm.Measurable; 9 | import org.github.jamm.MeasurementStack; 10 | import org.github.jamm.MemoryMeter; 11 | import org.github.jamm.MemoryMeterStrategy; 12 | import org.github.jamm.listeners.NoopMemoryMeterListener; 13 | import org.openjdk.jmh.annotations.Benchmark; 14 | import org.openjdk.jmh.annotations.BenchmarkMode; 15 | import org.openjdk.jmh.annotations.Fork; 16 | import org.openjdk.jmh.annotations.Level; 17 | import org.openjdk.jmh.annotations.Measurement; 18 | import org.openjdk.jmh.annotations.Mode; 19 | import org.openjdk.jmh.annotations.OutputTimeUnit; 20 | import org.openjdk.jmh.annotations.Scope; 21 | import org.openjdk.jmh.annotations.Setup; 22 | import org.openjdk.jmh.annotations.State; 23 | import org.openjdk.jmh.annotations.Threads; 24 | import org.openjdk.jmh.annotations.Warmup; 25 | import org.openjdk.jmh.infra.Blackhole; 26 | 27 | @Threads(3) 28 | @Fork(value = 1, jvmArgsPrepend = { 29 | "-javaagent:target/jamm-0.4.1-SNAPSHOT.jar", 30 | // "--add-opens=java.base/java.lang=ALL-UNNAMED" 31 | }) 32 | @Warmup(iterations=4, time=5) 33 | @Measurement(iterations=5, time=5) 34 | @BenchmarkMode(Mode.AverageTime) 35 | @State(Scope.Thread) 36 | @OutputTimeUnit(TimeUnit.MICROSECONDS) 37 | public class BenchmarkObjectGraphTraversal 38 | { 39 | private MemoryMeter meter; 40 | 41 | /** 42 | * The object being measured through reflection 43 | */ 44 | private static Object OBJ; 45 | 46 | /** 47 | * The object being measured through the Measurable interface 48 | */ 49 | private static Object MEASURABLE; 50 | 51 | static { 52 | 53 | OBJ = new ClassWithFiveObjectFields(new byte[20], 54 | new ClassWithOneObjectFieldsAndTwoPrimitives((byte) 8, new ClassWithoutFields(), Double.MAX_VALUE), 55 | new ClassWithOneObjectField(new Object[] {"test", new ClassWithOneObjectField(new Object()), new int[3]}), 56 | new ClassWithTreeObjectFields(new ClassWithTreeObjectFields(Boolean.TRUE, new ClassWithOnePrimitiveFields(12), new ClassWithoutFields()), 57 | new ClassWithOneObjectField(new Object()), 58 | new ClassWithoutFields()), 59 | "end"); 60 | 61 | MEASURABLE = new MeasurableClassWithFiveObjectFields(new byte[20], 62 | new MeasurableClassWithOneObjectFieldsAndTwoPrimitives((byte) 8, new MeasurableClassWithoutFields(), Double.MAX_VALUE), 63 | new MeasurableClassWithOneObjectField(new Object[] {"test", new MeasurableClassWithOneObjectField(new Object()), new int[3]}), 64 | new MeasurableClassWithTreeObjectFields(new MeasurableClassWithTreeObjectFields(Boolean.TRUE, new MeasurableClassWithOnePrimitiveFields(12), new MeasurableClassWithoutFields()), 65 | new MeasurableClassWithOneObjectField(new Object()), 66 | new MeasurableClassWithoutFields()), 67 | "end"); 68 | } 69 | 70 | @Setup(Level.Iteration) 71 | public void setup() throws Exception { 72 | MemoryMeterStrategy strategy = o -> 1; 73 | FieldFilter fieldFilter = Filters.getFieldFilters(false, false, false); 74 | FieldAndClassFilter classFilter = Filters.getClassFilters(false); 75 | 76 | this.meter = new MemoryMeter(strategy, classFilter, fieldFilter, NoopMemoryMeterListener.FACTORY); 77 | } 78 | 79 | @Benchmark 80 | public void measureThroughReflection(Blackhole bh) { 81 | bh.consume(meter.measureDeep(OBJ)); 82 | } 83 | 84 | @Benchmark 85 | public void measureThroughMeasurable(Blackhole bh) { 86 | bh.consume(meter.measureDeep(MEASURABLE)); 87 | } 88 | 89 | public static class ClassWithoutFields { 90 | 91 | } 92 | 93 | public static class MeasurableClassWithoutFields implements Measurable { 94 | 95 | @Override 96 | public void addChildrenTo(MeasurementStack stack) 97 | { 98 | } 99 | } 100 | 101 | @SuppressWarnings("unused") 102 | public static class ClassWithOnePrimitiveFields { 103 | 104 | private int intField; 105 | 106 | public ClassWithOnePrimitiveFields(int intField) { 107 | this.intField = intField; 108 | } 109 | } 110 | 111 | @SuppressWarnings("unused") 112 | public static class MeasurableClassWithOnePrimitiveFields implements Measurable { 113 | 114 | private int intField; 115 | 116 | @Override 117 | public long shallowSize(MemoryMeterStrategy strategy) { 118 | return 1; 119 | } 120 | 121 | public MeasurableClassWithOnePrimitiveFields(int intField) { 122 | this.intField = intField; 123 | } 124 | 125 | @Override 126 | public void addChildrenTo(MeasurementStack stack) { 127 | } 128 | } 129 | 130 | @SuppressWarnings("unused") 131 | public static class ClassWithOneObjectField { 132 | 133 | public static String staticField = "static"; 134 | 135 | private Object field; 136 | 137 | public ClassWithOneObjectField(Object field) { 138 | this.field = field; 139 | } 140 | } 141 | 142 | @SuppressWarnings("unused") 143 | public static class MeasurableClassWithOneObjectField implements Measurable { 144 | 145 | public static Object staticField = "static"; 146 | 147 | private Object field; 148 | 149 | public MeasurableClassWithOneObjectField(Object field) { 150 | this.field = field; 151 | } 152 | 153 | @Override 154 | public long shallowSize(MemoryMeterStrategy strategy) { 155 | return 1; 156 | } 157 | 158 | @Override 159 | public void addChildrenTo(MeasurementStack stack) { 160 | stack.pushObject(this, "field", field); 161 | } 162 | } 163 | 164 | @SuppressWarnings("unused") 165 | public static class ClassWithTreeObjectFields { 166 | 167 | private Object first; 168 | 169 | private Object second; 170 | 171 | private Object third; 172 | 173 | public ClassWithTreeObjectFields(Object first, Object second, Object third) { 174 | this.first = first; 175 | this.second = second; 176 | this.third = third; 177 | } 178 | } 179 | 180 | @SuppressWarnings("unused") 181 | public static class MeasurableClassWithTreeObjectFields implements Measurable { 182 | 183 | private Object first; 184 | 185 | private Object second; 186 | 187 | private Object third; 188 | 189 | public MeasurableClassWithTreeObjectFields(Object first, Object second, Object third) { 190 | this.first = first; 191 | this.second = second; 192 | this.third = third; 193 | } 194 | 195 | @Override 196 | public long shallowSize(MemoryMeterStrategy strategy) { 197 | return 1; 198 | } 199 | 200 | @Override 201 | public void addChildrenTo(MeasurementStack stack) { 202 | stack.pushObject(this, "first", first); 203 | stack.pushObject(this, "second", second); 204 | stack.pushObject(this, "third", third); 205 | } 206 | } 207 | 208 | @SuppressWarnings("unused") 209 | public static class ClassWithOneObjectFieldsAndTwoPrimitives { 210 | 211 | private byte first; 212 | 213 | private Object second; 214 | 215 | private double third; 216 | 217 | public ClassWithOneObjectFieldsAndTwoPrimitives(byte first, Object second, double third) { 218 | this.first = first; 219 | this.second = second; 220 | this.third = third; 221 | } 222 | } 223 | 224 | @SuppressWarnings("unused") 225 | public static class MeasurableClassWithOneObjectFieldsAndTwoPrimitives implements Measurable { 226 | 227 | private byte first; 228 | 229 | private Object second; 230 | 231 | private double third; 232 | 233 | public MeasurableClassWithOneObjectFieldsAndTwoPrimitives(byte first, Object second, double third) { 234 | this.first = first; 235 | this.second = second; 236 | this.third = third; 237 | } 238 | 239 | @Override 240 | public long shallowSize(MemoryMeterStrategy strategy) { 241 | return 1; 242 | } 243 | 244 | @Override 245 | public void addChildrenTo(MeasurementStack stack) { 246 | stack.pushObject(this, "second", second); 247 | } 248 | } 249 | 250 | @SuppressWarnings("unused") 251 | public static class ClassWithFiveObjectFields extends ClassWithTreeObjectFields { 252 | 253 | private Object fourth; 254 | 255 | private Object fifth; 256 | 257 | public ClassWithFiveObjectFields(Object first, Object second, Object third, Object fourth, Object fifth) { 258 | super(first, second, third); 259 | this.fourth = fourth; 260 | this.fifth = fifth; 261 | } 262 | } 263 | 264 | @SuppressWarnings("unused") 265 | public static class MeasurableClassWithFiveObjectFields extends MeasurableClassWithTreeObjectFields { 266 | 267 | private Object fourth; 268 | 269 | private Object fifth; 270 | 271 | public MeasurableClassWithFiveObjectFields(Object first, Object second, Object third, Object fourth, Object fifth) { 272 | super(first, second, third); 273 | this.fourth = fourth; 274 | this.fifth = fifth; 275 | } 276 | 277 | @Override 278 | public long shallowSize(MemoryMeterStrategy strategy) { 279 | return 1; 280 | } 281 | 282 | @Override 283 | public void addChildrenTo(MeasurementStack stack) { 284 | super.addChildrenTo(stack); 285 | stack.pushObject(this, "fourth", fourth); 286 | stack.pushObject(this, "fifth", fifth); 287 | } 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /test/org/github/jamm/strategies/MemoryMeterStrategiesTest.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.strategies; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.List; 6 | 7 | import org.junit.BeforeClass; 8 | import org.junit.Test; 9 | 10 | import org.github.jamm.MemoryMeter.Guess; 11 | 12 | import static org.github.jamm.MemoryMeter.Guess.INSTRUMENTATION; 13 | import static org.github.jamm.MemoryMeter.Guess.INSTRUMENTATION_AND_SPECIFICATION; 14 | import static org.github.jamm.MemoryMeter.Guess.SPECIFICATION; 15 | import static org.github.jamm.MemoryMeter.Guess.UNSAFE; 16 | import static org.junit.Assert.*; 17 | 18 | public class MemoryMeterStrategiesTest { 19 | 20 | @BeforeClass 21 | public static void logInfoAtStartup() { 22 | System.setProperty("org.github.jamm.strategies.LogInfoAtStartup", "true"); 23 | } 24 | 25 | @Test 26 | public void testInvalidEmptyGuessList() { 27 | MemoryMeterStrategies strategies = MemoryMeterStrategies.getInstance(); 28 | assertTrue(strategies.hasInstrumentation()); 29 | assertTrue(strategies.hasUnsafe()); 30 | 31 | List guesses = new ArrayList<>(); 32 | try { 33 | strategies.getStrategy(guesses); 34 | fail(); 35 | } catch (IllegalArgumentException e) { 36 | assertEquals("The guessList argument is empty", e.getMessage()); 37 | } 38 | 39 | guesses = Arrays.asList(INSTRUMENTATION, UNSAFE, SPECIFICATION); 40 | assertTrue(strategies.getStrategy(guesses) instanceof InstrumentationStrategy); 41 | 42 | guesses = Arrays.asList(INSTRUMENTATION_AND_SPECIFICATION, UNSAFE, SPECIFICATION); 43 | assertTrue(strategies.getStrategy(guesses) instanceof InstrumentationAndSpecStrategy); 44 | 45 | guesses = Arrays.asList(UNSAFE, INSTRUMENTATION); 46 | assertInvalidOrder(strategies, guesses); 47 | 48 | guesses = Arrays.asList(UNSAFE, INSTRUMENTATION_AND_SPECIFICATION); 49 | assertInvalidOrder(strategies, guesses); 50 | 51 | guesses = Arrays.asList(SPECIFICATION, INSTRUMENTATION); 52 | assertInvalidOrder(strategies, guesses); 53 | 54 | guesses = Arrays.asList(SPECIFICATION, INSTRUMENTATION_AND_SPECIFICATION); 55 | assertInvalidOrder(strategies, guesses); 56 | 57 | guesses = Arrays.asList(SPECIFICATION, UNSAFE); 58 | assertInvalidOrder(strategies, guesses); 59 | 60 | guesses = Arrays.asList(INSTRUMENTATION_AND_SPECIFICATION, INSTRUMENTATION); 61 | assertInvalidOrder(strategies, guesses); 62 | } 63 | 64 | private void assertInvalidOrder(MemoryMeterStrategies strategies, List guesses) { 65 | try { 66 | strategies.getStrategy(guesses); 67 | fail(); 68 | } catch (IllegalArgumentException e) { 69 | assertTrue(true); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test/org/github/jamm/string/StringMeterTest.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.string; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.Collection; 6 | import java.util.List; 7 | 8 | import org.junit.BeforeClass; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.junit.runners.Parameterized; 12 | 13 | import org.github.jamm.MemoryMeter; 14 | import org.github.jamm.MemoryMeter.Guess; 15 | import org.github.jamm.MemoryMeterStrategy; 16 | import org.github.jamm.strategies.MemoryMeterStrategies; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | import static org.junit.Assert.assertFalse; 20 | 21 | @RunWith(Parameterized.class) 22 | public class StringMeterTest { 23 | 24 | @BeforeClass 25 | public static void logInfoAtStartup() { 26 | System.setProperty("org.github.jamm.strategies.LogInfoAtStartup", "true"); 27 | } 28 | 29 | @BeforeClass 30 | public static void turnOffStringOptimization() { 31 | System.setProperty("org.github.jamm.string.Optimize", "false"); 32 | } 33 | 34 | @Parameterized.Parameters 35 | public static Collection guesses() { 36 | 37 | return Arrays.asList(Guess.INSTRUMENTATION, 38 | Guess.INSTRUMENTATION_AND_SPECIFICATION, 39 | Guess.UNSAFE, 40 | Guess.SPECIFICATION); 41 | } 42 | 43 | private final Guess guess; 44 | 45 | public StringMeterTest(Guess guess) { 46 | this.guess = guess; 47 | } 48 | 49 | @Test 50 | public void testMeasureDeepString() { 51 | 52 | String[] strings = new String[] {"", 53 | "a", 54 | "a bit longuer", 55 | "significantly longuer", 56 | "...... really ...... really .... really ... really .... longuer", 57 | "with a chinese character: 我"}; 58 | 59 | // The optimization is disabled for the MemoryMeter but not for the StringMeter which does not take into account the ENABLED field. 60 | MemoryMeter reference = MemoryMeter.builder().withGuessing(MemoryMeter.Guess.INSTRUMENTATION).build(); 61 | assertFalse(MemoryMeter.useStringOptimization()); 62 | StringMeter stringMeter = StringMeter.newInstance(); 63 | List guesses = new ArrayList<>(); 64 | guesses.add(guess); 65 | MemoryMeterStrategy strategy = MemoryMeterStrategies.getInstance().getStrategy(guesses); 66 | 67 | for (String string : strings) { 68 | assertEquals(reference.measureDeep(string), stringMeter.measureDeep(strategy, string)); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /test/org/github/jamm/testedclasses/PackageProtectedClass.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.testedclasses; 2 | 3 | import org.github.jamm.MemoryMeter; 4 | 5 | /** 6 | * Package protected class used by tests 7 | * 8 | */ 9 | class PackageProtectedClass 10 | { 11 | public String publicField; 12 | 13 | String packageProtectedField; 14 | 15 | protected String protectedField; 16 | 17 | private String privateField; 18 | 19 | public PackageProtectedClass(String publicField, 20 | String packageProtectedField, 21 | String protectedField, 22 | String privateField) { 23 | 24 | this.publicField = publicField; 25 | this.packageProtectedField = packageProtectedField; 26 | this.protectedField = protectedField; 27 | this.privateField = privateField; 28 | } 29 | 30 | public long measureDeep(MemoryMeter meter) { 31 | return meter.measure(this) 32 | + meter.measureDeep(publicField) 33 | + meter.measureDeep(packageProtectedField) 34 | + meter.measureDeep(protectedField) 35 | + meter.measureDeep(privateField); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/org/github/jamm/testedclasses/PublicClassWithPackageProtectedClassField.java: -------------------------------------------------------------------------------- 1 | package org.github.jamm.testedclasses; 2 | 3 | import org.github.jamm.MemoryMeter; 4 | 5 | public class PublicClassWithPackageProtectedClassField 6 | { 7 | public static String publicStaticField = "publicStaticField"; 8 | 9 | @SuppressWarnings("unused") 10 | private static String privateStaticField = "privateStaticField"; 11 | 12 | public String publicField; 13 | 14 | String packageProtectedField; 15 | 16 | protected String protectedField; 17 | 18 | private String privateField; 19 | 20 | private PackageProtectedClass packageProtectedClass; 21 | 22 | public PublicClassWithPackageProtectedClassField(String publicField, 23 | String packageProtectedField, 24 | String protectedField, 25 | String privateField) { 26 | 27 | this.publicField = publicField; 28 | this.packageProtectedField = packageProtectedField; 29 | this.protectedField = protectedField; 30 | this.privateField = privateField; 31 | this.packageProtectedClass = new PackageProtectedClass("packageProtectedClassPublicField", 32 | "packageProtectedClassPackageProtectedField", 33 | "packageProtectedClassProtectedField", 34 | "packageProtectedClassPrivateField"); 35 | } 36 | 37 | public long measureDeep(MemoryMeter meter) { 38 | return meter.measure(this) 39 | + meter.measureDeep(publicField) 40 | + meter.measureDeep(packageProtectedField) 41 | + meter.measureDeep(protectedField) 42 | + meter.measureDeep(privateField) 43 | + packageProtectedClass.measureDeep(meter); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /toolchains.example.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | jdk 6 | 7 | 1.8 8 | Oracle Corporation 9 | 10 | 11 | /Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home 12 | 13 | 14 | 15 | jdk 16 | 17 | 11 18 | Oracle Corporation 19 | 20 | 21 | /Library/Java/JavaVirtualMachines/openjdk-11.0.2.jdk/Contents/Home 22 | 23 | 24 | 25 | jdk 26 | 27 | 17 28 | Oracle Corporation 29 | 30 | 31 | /Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home 32 | 33 | 34 | 35 | --------------------------------------------------------------------------------