Slide 1

Slide 1 text

Instrument to Remove Using Java agents for fun and profit Johannes Bechberger

Slide 2

Slide 2 text

org.springframework.boot spring-boot-starter-validation org.springframework.boot spring-boot-starter-thymeleaf org.springframework.boot spring-boot-starter-test test com.h2database h2 runtime com.mysql mysql-connector-j runtime

Slide 3

Slide 3 text

Do you need all?

Slide 4

Slide 4 text

Probably not

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

Software systems have a natural tendency to grow in size and complexity over time. A part of this growth comes with new features or bug fixes, while another part is due to useless code that accumulates over time. The problem of safely debloating real-world applications remains a long- standing software engineering endeavor today. — Soto-Valero et. al “ Source: https://arxiv.org/pdf/2008.08401.pdf

Slide 7

Slide 7 text

How do we debloat?

Slide 8

Slide 8 text

Debloating tools Dynamic Static Coverage Profiling Code Analysis Dependency Analysis ./gradlew lintGradle

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

You know the JVM? Could you help us debloat programs using agents or profilers?

Slide 14

Slide 14 text

Existing tools have problems...

Slide 15

Slide 15 text

So let’s write our own * as a proof of concept for a blog post *

Slide 16

Slide 16 text

Reduce the Problem

Slide 17

Slide 17 text

Find unused classes Source: https://arxiv.org/pdf/2008.08401.pdf

Slide 18

Slide 18 text

Find unused classes class A { static { Store.getInstance().processClassUsage("A"); } private int field; public void method() {...} }

Slide 19

Slide 19 text

When are static initializers called “ • T is a class and an instance of A is created. • A static method declared by A is invoked. • A static field declared by A is assigned. • A static field declared by A is used and the field is not a constant variable – JLS SE17 §12.4.2. Detailed Initialization Procedure

Slide 20

Slide 20 text

Static initializers in interfaces interface I { static { Store.getInstance().processClassUsage("I"); } public void method(); } Forbidden in Java Allowed in Byte code

Slide 21

Slide 21 text

Classes found in byte code Recorded classes Bloat classes Dynamically generated classes (e.g. for lambdas)

Slide 22

Slide 22 text

What to do with this information?

Slide 23

Slide 23 text

Classes found in byte code Recorded classes Bloat classes Dynamically generated classes (e.g. for lambdas) Remove

Slide 24

Slide 24 text

class UnusedClass { static { LOG.error("Class UnusedClass is used " + "which is not allowed"); } private int field; public void method() {...} }

Slide 25

Slide 25 text

And now for something complete different Source: Monty Python

Slide 26

Slide 26 text

Implementation

Slide 27

Slide 27 text

Structure JVM with Agent Instrumenter Used Classes Instrumenter JVM Instr. JAR Used Classes Reduced JAR JAR with Error Messages JAR Due to Spring classloader magic

Slide 28

Slide 28 text

Agent Structure Main premain(args) ClassTF transform Store Class State start store

Slide 29

Slide 29 text

package my.app; class A { static { Store.getInstance().processClassUsage("my.app.A"); } private int field; public void method() {...} } Only one problem...

Slide 30

Slide 30 text

package java.lang; public final class String implements ... { static { Store.getInstance().processClassUsage("java.lang.String"); } @Stable private final byte[] value; ... public String() {...} ... }

Slide 31

Slide 31 text

Class Loader Hierarchies platform app bootstrap X Y class reference delegates to

Slide 32

Slide 32 text

Agent Structure Main premain(args) ClassTF transform Store Class State start store Runtime JAR

Slide 33

Slide 33 text

Class Loader Hierarchies platform app bootstrap X Y class reference delegates to searches in runtime.jar https://mostlynerdless.de/blog/2023/06/02/class-loader-hierarchies/

Slide 34

Slide 34 text

Implemented via inst.appendToBootstrapClassLoaderSearch(new JarFile( getExtractedJARPath().toFile())); instrumentation Instrumentation { void appendToBootstrapClassLoaderSearch(JarFile jarfile); }

Slide 35

Slide 35 text

Normally public static void main(String[] args) { ... } public static void main(String args[]) { ... } void main() { ... }

Slide 36

Slide 36 text

Main Class public static void agentmain(String agentArgs) { ... } JVM start later attach attach public static void premain(String agentArgs) { ... }

Slide 37

Slide 37 text

Main Class public static void agentmain(String agentArgs, Instrumentation inst) { ... } public static void premain(String agentArgs, Instrumentation inst) { ... }

Slide 38

Slide 38 text

Main Class public class Main { public static void premain(String agentArgs, Instrumentation inst) { AgentOptions options = new AgentOptions(agentArgs); ... inst.appendToBootstrapClassLoaderSearch( new JarFile(getExtractedJARPath().toFile())); ... inst.addTransformer(new ClassTransformer(options), true); } ... } Instrumentation.retransformClasses

Slide 39

Slide 39 text

ClassTransformer extends ClassFileTransformer “ An agent registers an implementation of this interface using the addTransformer method so that the transformer’s transform method is invoked when classes are loaded, redefined, or retransformed – ClassFileTransformer Documentation

Slide 40

Slide 40 text

ClassTransformer#transform byte[] transform( ClassLoader loader, String className, Class> klass, ProtectionDomain domain, byte[] classfileBuffer) throws IllegalClassFormatException { ... }

Slide 41

Slide 41 text

ClassTransformer#transform if (className.startsWith("me/bechberger/runtime/Store") || className.startsWith("me/bechberger/ClassTransformer") || className.startsWith("java/") || className.startsWith("jdk/internal") || className.startsWith("sun/")) { return classfileBuffer; } How to actually transform the bytecode?

Slide 42

Slide 42 text

https://www.javassist.org/

Slide 43

Slide 43 text

ClassTransformer#transform private void transform(String className, CtClass cc) { String cn = formatClassName(className); Store.getInstance() .processClassLoad(cn,cc.getClassFile().getInterfaces()); cc.makeClassInitializer() .insertBefore( String.format("me.bechberger.runtime.Store.getInstance()" + ".processClassUsage(\"%s\");", cn)); } ?

Slide 44

Slide 44 text

With Spring-PetClinic Class org.apache.tomcat.util.net.SocketBufferHandler is used which is not allowed Class org.apache.tomcat.util.net.SocketBufferHandler$1 is used which is not allowed Class org.apache.tomcat.util.net.NioChannel is used which is not allowed Class org.apache.tomcat.util.net.NioChannel$1 is used which is not allowed ... java -javaagent:./target/dead-code.jar=output=classes.txt \ -jar petclinic.jar u ch.qos.logback.classic.encoder.PatternLayoutEncoder l ch.qos.logback.classic.joran.JoranConfigurator u ch.qos.logback.classic.jul.JULHelper u ch.qos.logback.classic.jul.LevelChangePropagator ...

Slide 45

Slide 45 text

Other applications of agents

Slide 46

Slide 46 text

Main agentmain(args) premain(args) Profiler sample sleep Store start store Write your own profiler See https://mostlynerdless.de

Slide 47

Slide 47 text

@parttimen3rd on Twitter parttimenerd on GitHub mostlynerdless.de @SweetSapMachine sapmachine.io