3

We have an application with custom classloader and I need to access classLoader field on given classes. However this field is not accessible via reflection :-(

The JavaDoc for java.lang.Class is clear:

// This field is filtered from reflection access, i.e. getDeclaredField
// will throw NoSuchFieldException

So this is what I get when calling getDeclaredField("classLoader") Can this be obtained somehow (I see IntelliJ debugging does that somehow; how?)

Maybe some byteBuddy trickery?

kosta5
  • 1,129
  • 3
  • 14
  • 36
  • Note - I need reflection access. The actual problem is much more complex. I need to de-set classLoader (remove the reference to it) because java keeps custom Classloaders alive in heap too long and they eat up our non-heap memory. – kosta5 Dec 27 '19 at 11:46
  • 1
    When you have a `Class` instance whose classLoader field you could set to `null`, you shouldn’t be surprised that the loader can’t get collected—you are holding a reference to a `Class` instance defined by this loader. – Holger Jan 06 '20 at 17:13
  • I m not surprised :) to actually elaborate on root cause. I have many (~7) weak references to object of `Class` loaded by custom Classloader. And since there is enough space in heap, GC keeps those objects alive for a long time. What GC does not realize is that keeping these weak-referenced objects alive causes their Classloaders not to be GCed (which results in those Classloaders eating a lot of metaspace) – kosta5 Jan 07 '20 at 18:22
  • 1
    That's not how garbage collection works. A weakly reachable object is not kept longer than an unreachable object (that's the difference to soft references). Of course, the gc may not clear them for a long time because it never runs when there is enough memory, but then, these weak reference are a red herring, as, whether they exist or not, a garbage collector that doesn't run doesn't clean up classes or class loaders in either case. Overwriting the reference from the `Class` to the `ClassLoader` still wouldn't change that, a gc that doesn't run doesn't traverse that reference anyway. – Holger Jan 07 '20 at 21:35
  • yep... I am not sure I agree if "a wekly reachable object is not kept longer than unreachable object" (my obervations prove otherwise; do you have any good source on Full GC vs. moves across young/old gens?). But point taken. Removing the reference between classloader and class is a B*A*D idea – kosta5 Jan 10 '20 at 18:32
  • 1
    It’s the primary goal to clear a weak reference immediately when the referent is unreachable. In practice, the referent will be skipped during the marking phase and the reference object linked to a chain of discovered references. When the marking phase is over, the gc only has to traverse the discovered references and clear all weak references whose referent has not been marked through a different path. I don’t know whether concurrent collectors have some additional obstacles, but in most cases, it’s the application itself, which prevents the collection by calling `get()` from time to time. – Holger Jan 13 '20 at 17:03
  • 2
    I just tested with a tight loop creating class loaders, loading a class, creating an instance and a weak reference to the class and abandoning it. With Serial and Parallel GC, all of them are collected in the next cycle, with G1GC, a significant gap between created and collected loaders existed, though an explicit `System.gc(); System.runFinalization();` made it catch up. So this isn’t about loaders and weak references, but the fact that collectors like G1GC allow uncollected objects *in general*. They are designed to focus on the biggest chunk of garbage (hence “garbage first”) and low pauses – Holger Jan 13 '20 at 17:54

5 Answers5

6

To answer your question: You can still hack your way to the field using Byte Buddy by creating a mirror of the class which will have a smiliar class layout such that you can access and modify fields using Unsafe by first creating a mirror of the class that hides fields from reflection:

Class<?> mirror = new ByteBuddy()
    .with(TypeValidation.DISABLED)
    .redefine(Class.class)
    .name("mirror.Class")
    .noNestMate()
    .make()
    .load(null)
    .getLoaded();

Class<?> unsafeType = Class.forName("sun.misc.Unsafe");
Field theUnsafe = unsafeType.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Object unsafe = theUnsafe.get(null);

long offset = (Long) unsafeType
    .getMethod("objectFieldOffset", Field.class)
    .invoke(unsafe, mirror.getDeclaredField("classLoader"));
ClassLoader loader = (ClassLoader) unsafeType
    .getMethod("getObject", Object.class, long.class)
    .invoke(unsafe, Foo.class, offset - 4);

The mirror has a smiliar field layout as the original class such that you can retain that layout and access fields as you demand it. You can in the same way use putObject to override the field value.

Would I recommend this approach however? Absolutely not. This will stop working in any future version of Java, too. If you need some extra time to work on a proper solution, this might be a way to go but long term, you should refactor your code to make this work-around uneccessary.

Rafael Winterhalter
  • 42,759
  • 13
  • 108
  • 192
  • 2
    While I really like the trick with a mirror class, it does not seem to work in this case. The mirror class will *not* have the same layout. HotSpot uses a [special layout](http://hg.openjdk.java.net/jdk/jdk/file/7c2236ea739e/src/hotspot/share/classfile/classFileParser.cpp#l4139) for several bootstrap classes, including `java.lang.Class`, in order to access their fields directly inside JVM. But a mirror class generated with ByteBuddy won't be treated specially. That's where `- 4` in your example comes from. – apangin Dec 29 '19 at 16:32
  • That's true, I should have mentioned that hack. I think it's eight if compressed oops are disabled, too. – Rafael Winterhalter Dec 29 '19 at 19:15
  • 1
    Thanks budy ;) in the end I went the more painful but much more correct way --> manually cleaning up those weak references in problematic objects holding weakreferences to my CL (as e.g. internal map/reverseMap attributes of com.sun.jmx.mbeanserver.StandardMBeanIntrospector); will try to post some final results later on – kosta5 Dec 30 '19 at 09:31
  • There is much simpler way, you can just class private `getDeclaredFields0` on class and skip all that filtering. – GotoFinal Jan 10 '20 at 15:05
3

Java debuggers see this field, because they rely on JDWP, which works on top of native APIs: JNI and JVM TI.

You technically can access/modify classLoader field with JNI or Unsafe, but please don't do this!

After all, why do you think this field is filtered from reflection access? Exactly to prevent people from shooting themselves in the foot by modifying the field.

The key point is that a class should never be separated from its class loader. HotSpot JVM cannot unload classes one by one; instead, it unloads the whole class loader, when no live references to the ClassLoader object remain.

When a ClassLoader object is garbage collected, the corresponding part of the Metaspace can be reclaimed, along with the metadata for all classes loaded by this ClassLoader.

Now, what happens if you null out classLoader field? If there are no more references to the corresponding ClassLoader, it becomes eligible for garbage collection (seems like exactly what you want). But this may trigger class unloading, that will kill all the metadata for classes of this loader. However, classes (and their instances) are completely broken without the metadata. After that, any operation on your class or one of its instances may randomly crash JVM or make the application otherwise unstable.

apangin
  • 92,924
  • 10
  • 193
  • 247
  • thanks for enlightening me regarding the JDWP :) That makes sense. Yeah... I was honestly afraid of some of what you are saying and suspected that. Thanks for confirming. have taken a route of manually cleaning up those weak references in problematic objects holding weakreferences to my CL (as e.g. internal map/reverseMap attributes of com.sun.jmx.mbeanserver.StandardMBeanIntrospector) – kosta5 Dec 30 '19 at 09:27
1

It is good that you explained the reason of this question: reducing memory usage. If you manage to detch classes from their class loaders, other problems can occur. For instance, equals() and instanceof would work differently (if at all), deserialization of objects can work differently, etc.

1) I'd suggest you to check what is the real reason of memory consumption: is it the class loaded instance itself or is it one of classes loaded by this class loader? For instance, a class can have some static field that consumes much memory.

2) If the class loader instance consumes much memory, consider using weak reference or a cache for the field that consumes much memory.

3) If you want to try a "nicer way around": Consider Java Agent, transform() or redefineClasses. May be in this way you can add needed behaviour to the classes loaded by your class loaders and simplify your task of eliminating unneeded references and freeing some memory.

mentallurg
  • 4,967
  • 5
  • 28
  • 36
  • yeah... I was worried a little about that. The issue is that there are internal JVM/or Spring caches using weakreferences. But there are just too many and since our heap is "doing fine" classloaders remain not GCed due to enough space in heap. This leads to increasing non-heap while heap is doing just fine (classloaders are not GCed even if they only hang on a couple of weakreferences. The more painful way is to manually remove those weakreferences from individual objects at classloader close() – kosta5 Dec 27 '19 at 12:24
  • PS - once i dont care if equals() or instanceof on those objects would not work correctly as they are from a classloader that has been closed already – kosta5 Dec 27 '19 at 12:25
  • *classloaders remain not GCed due to enough space in heap* - since there is enough space in heap, why do you worry about heap and why do you want to remove your class loaders from heap? – mentallurg Dec 27 '19 at 12:32
  • because those classloaders hang on to a lot of loaded classes in metaspace. I m not worried about heap but non-heap (metaspace in particular). By keeping the classloaders alive in heap their non-heap footprint is very large. – kosta5 Dec 27 '19 at 13:01
  • By removing them from heap (having them GCed) they would free up the much wanted non-heap space as a side effect – kosta5 Dec 27 '19 at 13:02
  • 1
    OK. Then I'd suggest to identify which field or fields in class loaders cost much memory and to free it up, as mentioned, e.g. per kind of cache. Then class loaders will still remain, but will be *dehydrated*. – mentallurg Dec 27 '19 at 13:05
  • Thanks for the discussion. Appreciated! I just hoped there is a nicer way around (usually those caches are quite internal and a lot of reflection is necessary). OK - on a second thought setting a classLoader field to null is not nice. It just seemed easier. Thanks anyway – kosta5 Dec 27 '19 at 15:27
  • A nicer way around? How about implementing a Java Agent? :) I have added one more option to the answer. – mentallurg Dec 27 '19 at 20:23
  • JavaAgent is not applicable in my case. Some classes (e.g. com.sun.jmx.mbeanserver.StandardMBeanIntrospector) create weakreference to my CL and I want that. Then at the same time I want those references gone at classloader.close()) I have taken a route of manually cleaning up those weak references in such objects (as e.g. internal map/reverseMap attributes of com.sun.jmx.mbeanserver.StandardMBeanIntrospector) – kosta5 Dec 30 '19 at 09:26
  • There is much simpler way, you can just class private `getDeclaredFields0` on class and skip all that filtering. – GotoFinal Jan 10 '20 at 15:05
1

There is much simpler way than any provided answer here, but might not work on all JVM vendors: you can skip filtering by just invoking raw native method:

    Method getDeclaredFields0 = Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class);
    getDeclaredFields0.setAccessible(true);
    Field[] unfilteredFields = (Field[]) getDeclaredFields0.invoke(Class.class, false);

And then you can just iterate that array to find your field.

You can use this trick to also completely remove filtering, as its stored in map inside jdk.internal.reflect.Reflection class, but fields are also filtered by default.

private static volatile Map<Class<?>,String[]> fieldFilterMap;
private static volatile Map<Class<?>,String[]> methodFilterMap;

static {
    Map<Class<?>,String[]> map = new HashMap<Class<?>,String[]>();
    map.put(Reflection.class,
        new String[] {"fieldFilterMap", "methodFilterMap"});
    map.put(System.class, new String[] {"security"});
    map.put(Class.class, new String[] {"classLoader"});
    fieldFilterMap = map;

    methodFilterMap = new HashMap<>();
}
GotoFinal
  • 3,585
  • 2
  • 18
  • 33
0

This is not an answer to the actual question but addresses the root cause why this question was asked. Our symptoms (motivations why I asked this)

  • We use custom classloading extensively (tens of Classloaders created every minute that "come and go")

  • GC was not collecting those classloaders "fast enough" even if they had no direct references based on heapdump analysis. Only quite a few WeakReferences

  • Classloaders not being GCed over long period of time (we had closed classloaders that were still present in heap hours after they got closed) caused drastic metaspace expansion (3G+)

Silver bullet that fixed this was setting a JVM flag that forces full GC more often than it naturally occurs.

===> -XX:MaxMetaspaceExpansion <=== (in our case -XX:MaxMetaspaceExpansion=8M

Some more reading can be found e.g. here: What is the Metadata GC Threshold and how do I tune it?

kosta5
  • 1,129
  • 3
  • 14
  • 36