javalambdabytecodehotswap

Hotswap code with lambda fails, indicating that the method cannot be deleted


Problem

Hotswap lambda failed.

Process

I try to hotswap a Method body containing lambda.

1. Hotswap Code

public class Example {
    private int x;
    public void print (int y) {
       Consumer<Integer> consumer = (key) -> System.out.println(key);
    }
}

2. An example

 //Methods after sugar removal
First use code 1 to compile a class and put it in the directory, then run code 2 hot change
        //from static to instance method failed
        1、Consumer<Integer> consumer = (key) -> System.out.println(key);     
        2、Consumer<Integer> hot = (key) -> System.out.println(key + "" + this);
        //After desugar method
       private static synthetic lambda$print$0(Ljava/lang/Integer;)V
       private synthetic lambda$print$1(Ljava/lang/Integer;)V

        //change static method params count successful
        String str = "args";
        Consumer<Integer> methodCount1 = (key) -> System.out.println(key);
        Consumer<Integer> methodCount2 = (key) -> System.out.println(key + str);
        private static synthetic lambda$print$2(Ljava/lang/Integer;)V
        private static synthetic lambda$print$3(Ljava/lang/String;Ljava/lang/Integer;)V

        //change instance method params count failed
        Consumer<Integer> instanceCount2 = (key) -> System.out.println(key + "" + this);
        Consumer<Integer> instanceCount3 = (key) -> System.out.println(key + str + this);
        private synthetic lambda$print$4(Ljava/lang/Integer;)V
        private synthetic lambda$print$5(Ljava/lang/String;Ljava/lang/Integer;)V


        //from nothing add or delete lambd  successful (compile static method)

        //from nothing add or delete lambd  fail (compile instance method)
        Consumer<Integer> instaceMethod = (key) -> System.out.println(this);
        private synthetic lambda$print$6(Ljava/lang/Integer;)V
        hot.accept(1);
        consumer.accept(1);

3. Hotswap type code

The agent code
   public class JavaAgent {

        public static void agentmain(String agentArgs, Instrumentation inst) throws Exception {
            System.out.println("开始热更新");
            // 获得所有已被装载的class文件
            Class[] classes = inst.getAllLoadedClasses();
            for (Class clazz : classes) {
                if (clazz.getName().equalsIgnoreCase(agentArgs) || clazz.getName().equalsIgnoreCase("com.jason.Normal")) {
                    System.out.println(clazz.getName());
                    inst.addTransformer(new ClassFileTransformer() {

                        @Override
                        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

                                //System.out.println("hotswap class name :" + className);
                                String str = "";
                                if (className.contains("Example")){
                                    str = "Example.class";
                                }else if (className.contains("Norm")){
                                    str = "Normal.class";
                                }else {
                                    return null;
                                }
                                byte[] bytes = fileToBytes(new File("C:\\hot\\"+str));
                                return bytes;
                        }
                    }, true);
                    // 重转换
                    inst.retransformClasses(clazz);
                }
            }
            System.out.println("热更新结束");
        }

        public static byte[] fileToBytes(File file) {
            try {
                FileInputStream in = new FileInputStream(file);
                byte[] bytes = new byte[in.available()];
                in.read(bytes);
                in.close();
                return bytes;
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }
     }
The hotswap code
     public class HotSwap {
         public static void hot(){
                try {
                    List<VirtualMachineDescriptor> list = VirtualMachine.list();
                    for (VirtualMachineDescriptor vmd : list) {
                            VirtualMachine virtualMachine = null;
                            virtualMachine = VirtualMachine.attach(vmd.id());
                            // 获得代理类位置 + 传递参数
                            virtualMachine.loadAgent("C:\\Users\\DELL\\Downloads\\JavaHotSwap-master\\JavaAgent\\target\\agentmain.jar", "com.jason.Example");
                            virtualMachine.detach();
                    }
                } catch (AttachNotSupportedException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (AgentLoadException e) {
                    e.printStackTrace();
                } catch (AgentInitializationException e) {
                    e.printStackTrace();
                }
            }  
}
The main code
int i = new Random().nextInt();
        new Example().print(i);
       // Normal.show();
       // System.out.println("hot");
        HotSwap.hot();
        new Example().print(i);
       // Normal.show();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
Exception
java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:386)
    at sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain(InstrumentationImpl.java:411)
Caused by: java.lang.UnsupportedOperationException: class redefinition failed: attempted to add a method
    at sun.instrument.InstrumentationImpl.retransformClasses0(Native Method)
    at sun.instrument.InstrumentationImpl.retransformClasses(InstrumentationImpl.java:144)
    at com.jason.JavaAgent.agentmain(JavaAgent.java:60)
    ... 6 more
com.sun.tools.attach.AgentInitializationException: Agent JAR loaded but agent failed to initialize
    at sun.tools.attach.HotSpotVirtualMachine.loadAgent(HotSpotVirtualMachine.java:121)
    at com.jason.HotSwap.hot(HotSwap.java:23)
    at com.jason.Main.main(Main.java:33)
Agent failed to start!
Exception in thread "Attach Listener" 

3. Thoughts

I know when bytecode is compiled, the instrument cannot change (add, delete, modify) methods. Why can I modify static methods at will but can't modify an instance method?


Solution

  • You stumbled over JDK-8192936. According to the bug report, a change made around Java 6 made the implementation accept changes regarding the addition or removal of private static and private final methods, despite the specification continued to state that this is not supported.

    So when a lambda expression is desugared into a private static method, which applies to all lambda expressions not capturing this, changing them happens to work.

    For lambda expressions capturing this, the behavior is compiler-dependent. It has become common to desugar them into a private instance method without final modifier, hence, they are not affected by the reported behavior. But in principle, a compiler could add the redundant final modifier. It’s also possible to convert the body of such a lambda expression into a static method, accepting the this reference as a parameter. That happened with older compilers.

    The fix according to the report is not to consider the “These restrictions may be lifted in future versions” statement of the specification, but rather, to remove the feature to match the specification literally. There will be an option, -XX:{+|-}AllowRedefinitionToAddOrDeleteMethods to toggle the behavior starting with JDK 13, for some time.