In ThreadLocalBufferAllocator.getBufferAllocator() there is no guarantee that the SoftReference doesn't get cleared between its construction and its later use of bAllocatorRef.get(). Therefore the method can return null. Calling code (e.g. UTF8Reader.<init>() ) does expects to get non-null though and can crash with NPE.
   public static BufferAllocator getBufferAllocator() {
        SoftReference<BufferAllocator> bAllocatorRef = tlba.get();
        if (bAllocatorRef == null || bAllocatorRef.get() == null) {
            bAllocatorRef = new SoftReference<>(new BufferAllocator());
            tlba.set(bAllocatorRef);
        }
 
        return bAllocatorRef.get(); // <--- returns null, because soft-ref was already cleared
   }
This semi-reliably crashes SPECjvm2008 XML tests with Shenandoah and aggressive mode that does back-to-back cycles.
Example fix that keeps return value reachable:
   public static BufferAllocator getBufferAllocator() {
        BufferAllocator ba = null;
        SoftReference<BufferAllocator> bAllocatorRef = tlba.get();
        if (bAllocatorRef != null) {
            ba = bAllocatorRef.get();
        }
        if (ba == null) {
            ba = new BufferAllocator();
            bAllocatorRef = new SoftReference<>(ba);
            tlba.set(bAllocatorRef);
        }
        return ba;
   }