【JVM基础篇】打破双亲委派机制

文章目录

    • 打破双亲委派机制
      • 自定义类加载器
        • 双亲委派机制核心代码
        • 打破双亲委派机制
        • 自定义类加载器父类怎么是AppClassLoader呢?
        • 两个自定义类加载器加载相同限定名的类,不会冲突吗?
        • 拓展类加载器功能
      • 线程上下文类加载器
        • JDBC案例
          • 那么问题来了,DriverManager怎么知道jar包中要加载的驱动在哪儿?
          • Jar包中的驱动是哪个加载器加载的
          • DriverManager本身是通过启动类加载器加载的,那SPI是如何获取应用程序类加载器的呢?
          • 总结
        • JDBC案例中真的打破了双亲委派机制吗?
      • Osgi框架的类加载器(简单了解)
      • 使用阿里arthas不停机解决线上问题
    • 文章说明

打破双亲委派机制

双亲委派机制虽然很有用,但有时候需要打破双亲委派机制才可以实现一些功能

打破双亲委派机制历史上出现了三种方式,但本质上只有第一种算是真正的打破了双亲委派机制

  • ※ 自定义类加载器并且重写loadClass方法。如Tomcat通过这种方式实现应用之间类隔离,《面试篇》中分享它的做法。
  • ※ 线程上下文类加载器。使用SPI机制、利用上下文类加载器加载类,比如JDBC和JNDI等。
  • Osgi框架的类加载器。Osgi框架实现了一套新的类加载器机制,允许同级之间委托进行类的加载,目前很少使用。

自定义类加载器

  • 一个Tomcat程序中是可以运行多个Web应用的,如果这两个应用中出现了相同限定名的类,比如Servlet类,Tomcat要保证这两个类都能加载并且它们应该是不同的类。
  • 如果不打破双亲委派机制,当应用类加载器加载Web应用1中的MyServlet之后,Web应用2中相同限定名的MyServlet类就无法被加载了。

在这里插入图片描述

Tomcat使用了自定义类加载器来实现应用之间类的隔离。 每一个应用会有一个独立的类加载器加载对应的类。

在这里插入图片描述

双亲委派机制核心代码

那么自定义加载器是如何能做到的呢?首先我们需要先了解,双亲委派机制的代码到底在哪里,接下来只需要把这段代码消除即可。

ClassLoader中包含了4个核心方法,双亲委派机制的核心代码就位于loadClass方法中。

// 类加载的入口,提供了双亲委派机制。内部会调用findClass   重要
public Class<?> loadClass(String name)

// 由类加载器子类实现,获取二进制数据调用defineClass ,
// 比如URLClassLoader会根据文件路径去获取类文件中的二进制数据。重要
// 这个方法ClassLoader是没有实现的,由子类来实现
protected Class<?> findClass(String name)

// 做一些类名的校验,然后调用虚拟机底层的方法(C++实现)将字节码信息加载到虚拟机内存中
protected final Class<?> defineClass(String name, byte[] b, int off, int len)

// 执行类生命周期中的连接阶段
protected final void resolveClass(Class<?> c)

1、入口方法:

调用重载方法,传递参数不执行连接阶段

在这里插入图片描述

添加测试B类,该类会在连接阶段会打印信息

在这里插入图片描述

尝试加载,发现没有任何输出,说明连接阶段和初始化阶段都没有执行

在这里插入图片描述

如果使用Class.forName,还是会有连接阶段

在这里插入图片描述

2、看看ClassLoader类里面的实现

【ClassLoader】

/**
 * Loads the class with the specified <a href="#name">binary name</a>.  The
 * default implementation of this method searches for classes in the
 * following order:
 *
 * <ol>
 *
 *   <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
 *   has already been loaded.  </p></li>
 *
 *   <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
 *   on the parent class loader.  If the parent is <tt>null</tt> the class
 *   loader built-in to the virtual machine is used, instead.  </p></li>
 *
 *   <li><p> Invoke the {@link #findClass(String)} method to find the
 *   class.  </p></li>
 *
 * </ol>
 *
 * <p> If the class was found using the above steps, and the
 * <tt>resolve</tt> flag is true, this method will then invoke the {@link
 * #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
 *
 * <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
 * #findClass(String)}, rather than this method.  </p>
 *
 * <p> Unless overridden, this method synchronizes on the result of
 * {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
 * during the entire class loading process.
 *
 * @param  name
 *         The <a href="#name">binary name</a> of the class
 *
 * @param  resolve
 *         If <tt>true</tt> then resolve the class
 *
 * @return  The resulting <tt>Class</tt> object
 *
 * @throws  ClassNotFoundException
 *          If the class could not be found
 */
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    // 通过加锁,让多线程下只有一个线程加载类
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        // 判断当前加载器是否加载过这个类,如果加载过,返回这个类对象
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 向上委派
                    c = parent.loadClass(name, false);
                } else {
                    // parent为空,通过启动类加载器进行加载(调用C++代码)
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                // c还是空,说明父类加载器都没有加载成功,换成当前加载器进行加载
                long t1 = System.nanoTime();
                // ClassLoader并没有实现当前方法,交给子类进行实现
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

在这里插入图片描述

如果查找都失败,进入加载阶段,首先会由启动类加载器加载,这段代码在findBootstrapClassOrNull中。如果失败会抛出异常,接下来执行下面这段代码:

在这里插入图片描述

父类加载器加载失败就会抛出异常,回到子类加载器的这段代码,这样就实现了加载并向下传递。

可以参考URLClassLoader中的实现

【URLClassLoader】

/**
 * Finds and loads the class with the specified name from the URL search
 * path. Any URLs referring to JAR files are loaded and opened as needed
 * until the class is found.
 *
 * @param name the name of the class
 * @return the resulting class
 * @exception ClassNotFoundException if the class could not be found,
 *            or if the loader is closed.
 * @exception NullPointerException if {@code name} is {@code null}.
 */
protected Class<?> findClass(final String name)
    throws ClassNotFoundException
{
    final Class<?> result;
    try {
        result = AccessController.doPrivileged(
            new PrivilegedExceptionAction<Class<?>>() {
                public Class<?> run() throws ClassNotFoundException {
                    // 获取path目录下的字节码文件
                    String path = name.replace('.', '/').concat(".class");
                    // 获取文件对象,供下面获取二进制数据
                    Resource res = ucp.getResource(path, false);
                    if (res != null) {
                        try {
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        } catch (ClassFormatError e2) {
                            if (res.getDataError() != null) {
                                e2.addSuppressed(res.getDataError());
                            }
                            throw e2;
                        }
                    } else {
                        return null;
                    }
                }
            }, acc);
    } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
    }
    if (result == null) {
        throw new ClassNotFoundException(name);
    }
    return result;
}

/*
 * Defines a Class using the class bytes obtained from the specified
 * Resource. The resulting Class must be resolved before it can be
 * used.
 */
private Class<?> defineClass(String name, Resource res) throws IOException {
    long t0 = System.nanoTime();
    int i = name.lastIndexOf('.');
    URL url = res.getCodeSourceURL();
    if (i != -1) {
        String pkgname = name.substring(0, i);
        // Check if package already loaded.
        Manifest man = res.getManifest();
        definePackageInternal(pkgname, man, url);
    }
    // Now read the class bytes and define the class
    java.nio.ByteBuffer bb = res.getByteBuffer();
    if (bb != null) {
        // Use (direct) ByteBuffer:
        CodeSigner[] signers = res.getCodeSigners();
        CodeSource cs = new CodeSource(url, signers);
        sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
        return defineClass(name, bb, cs);
    } else {
        // 获取文件的二进制数组
        byte[] b = res.getBytes();
        // must read certificates AFTER reading bytes.
        CodeSigner[] signers = res.getCodeSigners();
        CodeSource cs = new CodeSource(url, signers);
        sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
        // 将二进制数据传给父类ClassLoader
        return defineClass(name, b, 0, b.length, cs);
    }
}

【ClassLoader】

/**
 * Converts an array of bytes into an instance of class <tt>Class</tt>,
 * with an optional <tt>ProtectionDomain</tt>.  If the domain is
 * <tt>null</tt>, then a default domain will be assigned to the class as
 * specified in the documentation for {@link #defineClass(String, byte[],
 * int, int)}.  Before the class can be used it must be resolved.
 *
 * <p> The first class defined in a package determines the exact set of
 * certificates that all subsequent classes defined in that package must
 * contain.  The set of certificates for a class is obtained from the
 * {@link java.security.CodeSource <tt>CodeSource</tt>} within the
 * <tt>ProtectionDomain</tt> of the class.  Any classes added to that
 * package must contain the same set of certificates or a
 * <tt>SecurityException</tt> will be thrown.  Note that if
 * <tt>name</tt> is <tt>null</tt>, this check is not performed.
 * You should always pass in the <a href="#name">binary name</a> of the
 * class you are defining as well as the bytes.  This ensures that the
 * class you are defining is indeed the class you think it is.
 *
 * <p> The specified <tt>name</tt> cannot begin with "<tt>java.</tt>", since
 * all classes in the "<tt>java.*</tt> packages can only be defined by the
 * bootstrap class loader.  If <tt>name</tt> is not <tt>null</tt>, it
 * must be equal to the <a href="#name">binary name</a> of the class
 * specified by the byte array "<tt>b</tt>", otherwise a {@link
 * NoClassDefFoundError <tt>NoClassDefFoundError</tt>} will be thrown. </p>
 *
 * @param  name
 *         The expected <a href="#name">binary name</a> of the class, or
 *         <tt>null</tt> if not known
 *
 * @param  b
 *         The bytes that make up the class data. The bytes in positions
 *         <tt>off</tt> through <tt>off+len-1</tt> should have the format
 *         of a valid class file as defined by
 *         <cite>The Java&trade; Virtual Machine Specification</cite>.
 *
 * @param  off
 *         The start offset in <tt>b</tt> of the class data
 *
 * @param  len
 *         The length of the class data
 *
 * @param  protectionDomain
 *         The ProtectionDomain of the class
 *
 * @return  The <tt>Class</tt> object created from the data,
 *          and optional <tt>ProtectionDomain</tt>.
 *
 * @throws  ClassFormatError
 *          If the data did not contain a valid class
 *
 * @throws  NoClassDefFoundError
 *          If <tt>name</tt> is not equal to the <a href="#name">binary
 *          name</a> of the class specified by <tt>b</tt>
 *
 * @throws  IndexOutOfBoundsException
 *          If either <tt>off</tt> or <tt>len</tt> is negative, or if
 *          <tt>off+len</tt> is greater than <tt>b.length</tt>.
 *
 * @throws  SecurityException
 *          If an attempt is made to add this class to a package that
 *          contains classes that were signed by a different set of
 *          certificates than this class, or if <tt>name</tt> begins with
 *          "<tt>java.</tt>".
 */
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                     ProtectionDomain protectionDomain)
    throws ClassFormatError
{
    protectionDomain = preDefineClass(name, protectionDomain);
    String source = defineClassSourceLocation(protectionDomain);
    Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
    postDefineClass(c, protectionDomain);
    return c;
}

在这里插入图片描述

3、最后根据传入的参数判断是否进入连接阶段:

在这里插入图片描述

打破双亲委派机制

接下来实现打破双亲委派机制,其实就是重写双亲委派机制的部分代码

package classloader.broken;//package com.itheima.jvm.chapter02.classloader.broken;

import org.apache.commons.io.IOUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.ProtectionDomain;
import java.util.regex.Matcher;

/**
 * 打破双亲委派机制 - 自定义类加载器
 */
public class BreakClassLoader1 extends ClassLoader {

    private String basePath;
    private final static String FILE_EXT = ".class";

    //设置加载目录
    public void setBasePath(String basePath) {
        this.basePath = basePath;
    }

    //使用commons io 从指定目录下加载文件
    private byte[] loadClassData(String name)  {
        try {
            String tempName = name.replaceAll("\\.", Matcher.quoteReplacement(File.separator));
            FileInputStream fis = new FileInputStream(basePath + tempName + FILE_EXT);
            try {
                return IOUtils.toByteArray(fis);
            } finally {
                IOUtils.closeQuietly(fis);
            }

        } catch (Exception e) {
            System.out.println("自定义类加载器加载失败,错误原因:" + e.getMessage());
            return null;
        }
    }

    //重写loadClass方法
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        // 如果是java包下,还是走双亲委派机制
        if(name.startsWith("java.")){
            // 交给父类来加载
            return super.loadClass(name);
        }
        // 通过类名从磁盘中指定目录下找到字节码文件并加载二进制数据到内存中
        byte[] data = loadClassData(name);
        // 调用虚拟机底层方法,根据二进制数据,在方法区和堆区创建对象
        return defineClass(name, data, 0, data.length);
    }

    // 使用自定义加载器来加载类
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException {
        //第一个自定义类加载器对象
        BreakClassLoader1 classLoader1 = new BreakClassLoader1();
        classLoader1.setBasePath("D:\\lib\\");
        Class<?> clazz1 = classLoader1.loadClass("com.itheima.my.A");
        
         //第二个自定义类加载器对象
        BreakClassLoader1 classLoader2 = new BreakClassLoader1();
        classLoader2.setBasePath("D:\\lib\\");
        Class<?> clazz2 = classLoader2.loadClass("com.itheima.my.A");

        System.out.println(clazz1 == clazz2);

        Thread.currentThread().setContextClassLoader(classLoader1);

        System.out.println(Thread.currentThread().getContextClassLoader());

        System.in.read();
     }
}
自定义类加载器父类怎么是AppClassLoader呢?

在这里插入图片描述

默认情况下自定义类加载器的父类加载器是应用程序类加载器,除非进行手动设置:

在这里插入图片描述

以Jdk8为例,ClassLoader类中提供了构造方法设置parent的内容:

在这里插入图片描述

这个构造方法由另外一个构造方法调用,其中父类加载器由getSystemClassLoader方法设置,该方法返回的是AppClassLoader。

在这里插入图片描述

两个自定义类加载器加载相同限定名的类,不会冲突吗?

不会冲突,在同一个Java虚拟机中,只有相同类加载器+相同的类限定名才会被认为是同一个类。不同的加载器对象就算加载同一个类,也会认为是不同的类。

在Arthas中使用sc –d 类名的方式查看具体的情况。

如下代码:

 public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException {
    // 第一个自定义类加载器对象
    BreakClassLoader1 classLoader1 = new BreakClassLoader1();
    classLoader1.setBasePath("D:\\lib\\");
    Class<?> clazz1 = classLoader1.loadClass("com.itheima.my.A");
    
    // 第二个自定义类加载器对象
    BreakClassLoader1 classLoader2 = new BreakClassLoader1();
    classLoader2.setBasePath("D:\\lib\\");
    Class<?> clazz2 = classLoader2.loadClass("com.itheima.my.A");

    System.out.println(clazz1 == clazz2);
 }

打印的应该是false,因为两个类加载器对象不同,尽管加载的是同一个类名,最终Class对象也不是相同的。可以通过Arthas看详细信息:

在这里插入图片描述

也会出现两个不同的A类。

拓展类加载器功能

有的时候并不希望打破双亲委派机制,只是想拓展一些功能,例如从数据库中加载字节数据,这时候应该重写findClass方法

线程上下文类加载器

利用上下文类加载器加载类,大量用于java自己的技术中,比如JDBC和JNDI等。

JDBC案例

我们来看下JDBC的案例:

  • JDBC不希望出现某个特定数据库的语法,希望可以提高代码的泛化性,这样对接任何数据库都比较容易,JDBC中使用了DriverManager来管理项目中引入的不同数据库的驱动,比如mysql驱动、oracle驱动,DriverManager会去加载制定数据库的驱动,这样可以连接使用相应数据库了。DriverManager在加载驱动的时候,是打破了双亲委派机制的
package classloader.broken;//package com.itheima.jvm.chapter02.classloader.broken;

import com.mysql.cj.jdbc.Driver;

import java.sql.*;

/**
 * 打破双亲委派机制 - JDBC案例
 */
public class JDBCExample {
    // JDBC driver name and database URL
    static final String JDBC_DRIVER = "com.mysql.cj.jdbc.Driver";
    static final String DB_URL = "jdbc:mysql:///bank1";

    //  Database credentials
    static final String USER = "root";
    static final String PASS = "123456";

    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
        try {
            conn = DriverManager.getConnection(DB_URL, USER, PASS);
            stmt = conn.createStatement();
            String sql;
            sql = "SELECT id, account_name FROM account_info";
            ResultSet rs = stmt.executeQuery(sql);

            //STEP 4: Extract data from result set
            while (rs.next()) {
                //Retrieve by column name
                int id = rs.getInt("id");
                String name = rs.getString("account_name");

                //Display values
                System.out.print("ID: " + id);
                System.out.print(", Name: " + name + "\n");
            }
            //STEP 5: Clean-up environment
            rs.close();
            stmt.close();
            conn.close();
        } catch (SQLException se) {
            //Handle errors for JDBC
            se.printStackTrace();
        } catch (Exception e) {
            //Handle errors for Class.forName
            e.printStackTrace();
        } finally {
            //finally block used to close resources
            try {
                if (stmt != null)
                    stmt.close();
            } catch (SQLException se2) {
            }// nothing we can do
            try {
                if (conn != null)
                    conn.close();
            } catch (SQLException se) {
                se.printStackTrace();
            }//end finally try
        }//end try
    }//end main
}//end FirstExample

2、DriverManager类位于rt.jar包中,由启动类加载器加载。

在这里插入图片描述

3、依赖中的mysql驱动对应的类,由应用程序类加载器来加载。

在这里插入图片描述

在类中有初始化代码:

在这里插入图片描述

DriverManager属于rt.jar是启动类加载器加载的。而用户jar包中的驱动需要由应用类加载器加载,这就违反了双亲委派机制(一般是低层将加载任务委派给高层,这里确实启动类加载器将加载任务委派给应用程序类加载器)。(这点存疑,一会儿再讨论)

在这里插入图片描述

那么问题来了,DriverManager怎么知道jar包中要加载的驱动在哪儿?

1、在类的初始化代码中有这么一个方法LoadInitialDrivers

在这里插入图片描述

2、这里使用了SPI机制,去加载所有jar包中实现了Driver接口的实现类。

在这里插入图片描述

3、SPI机制就是在这个位置下存放了一个文件,文件名是接口的名字,文件里存储了实现类的类名。这样SPI机制就可以通过扫描这个文件里面的内容找到实现类了。DriverManager拿到实现类的类名之后,就可以加载驱动了

在这里插入图片描述

在这里插入图片描述

‘’’在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

通过迭代器遍历所有实现类,即所有驱动

在这里插入图片描述

Jar包中的驱动是哪个加载器加载的

在这里插入图片描述

DriverManager本身是通过启动类加载器加载的,那SPI是如何获取应用程序类加载器的呢?

SPI中利用了线程上下文类加载器(应用程序类加载器)去加载类并创建对象。

在这里插入图片描述

在这里插入图片描述

执行一下看看

在这里插入图片描述

一个创建之后,虚拟机会将应用程序加载器保存的线程的上下文中

在这里插入图片描述

所以创建线程时,线程上下文的类加载器就是应用程序加载器,如果想获取应用程序加载器,可以直接使用Thread.currentThread().getContextClassLoader()方法来获取

当然也是可以修改线程上下文中的类加载器的

在这里插入图片描述

总结

在这里插入图片描述

JDBC案例中真的打破了双亲委派机制吗?
  • 说法一,打破了双亲委派机制:最早这个论点提出是在周志明《深入理解Java虚拟机》中,他认为打破了双亲委派机制,这种由启动类加载器加载的类,委派应用程序类加载器去加载类的方式,所以打破了双亲委派机制。
  • 说法二(正确),没有打破:我们分别从DriverManager以及驱动类的加载流程上分析,JDBC只是在DriverManager加载完之后,通过初始化阶段触发了驱动类的加载,类的加载依然遵循双亲委派机制。双亲委派机制描述的是一个类的加载过程,这里实际上是加载了两种类,一个是DriverManager,另外一种是Jar包的驱动类。DriverManager的加载是满足双亲委派机制的,Jar包驱动类的加载也是满足双亲委派机制的,因为都是调用虚拟机自动的类加载器,并没有重写loadClass方法。所以我认为这里没有打破双亲委派机制,只是用一种巧妙的方法让启动类加载器加载的类去引发的其他类的加载。

Osgi框架的类加载器(简单了解)

历史上,OSGi模块化框架。它存在同级之间的类加载器的委托加载(如下图的加载器1和2)。OSGi还使用类加载器实现了热部署的功能。热部署指的是在服务不停止的情况下,动态地更新字节码文件到内存中。JDK9之后不再使用OSGi,现在可以使用arthas解决热部署问题。

在这里插入图片描述

由于这种机制使用已经不多,所以不再过多讨论OSGi,着重来看下热部署在实际项目中的应用。

使用阿里arthas不停机解决线上问题

背景:

小李的团队将代码上线之后,发现存在一个小bug,但是用户急着使用,如果重新打包再发布需要一个多小时的时间,所以希望能使用arthas尽快的将这个问题修复。

思路:

只需要将修改的类的字节码上传,然后让类加载器重新加载,即可实现热部署

  1. 在出问题的服务器上部署并启动一个 arthas。
  2. 使用jad命令jad --source-only 类全限定名 > 目录/文件名.java 将源代码反编译并保存到指定目录中,然后可以用其它编译器,比如 vim 来修改源码
  3. 使用mc 命令mc –c 类加载器的hashcode 目录/文件名.java -d 输出目录用来编译修改过的代码
  4. 用 retransform 命令retransform class文件所在目录/xxx.class加载新的字节码,将其更新到内存中

详细流程演示:

1、这段代码编写有误,在枚举中的类型判断上使用了== 而不是equals

在这里插入图片描述

2、枚举中是这样定义的,1001是普通用户,1002是VIP用户:

在这里插入图片描述

3、由于代码有误,导致传递1001参数时,返回的是收费用户的内容。

在这里插入图片描述

4、jad --source-only 类全限定名 > 目录/文件名.java 使用 jad 命令反编译,然后可以用其它编译器,比如 vim 来修改源码
在这里插入图片描述

这里直接双击文件使用finalShell编辑:

在这里插入图片描述

5、mc –c 类加载器的hashcode 目录/文件名.java -d 输出目录 使用mc 命令用来编译修改过的代码
在这里插入图片描述

可以使用sc -d查看指定类的类加载器hashcode
在这里插入图片描述

在这里插入图片描述

6、retransform class文件所在目录/xxx.class 用 retransform 命令加载新的字节码

在这里插入图片描述

7、测试:

在这里插入图片描述

注意事项:

1、程序重启之后,字节码文件会恢复(因为retransform只是将字节码信息更新到内存中),除非将class文件放入jar包中进行更新。

2、使用retransform不能添加方法或者字段,也不能更新正在执行中的方法。

文章说明

该文章是本人学习 黑马程序员 的学习笔记,文章中大部分内容来源于 黑马程序员 的视频黑马程序员JVM虚拟机入门到实战全套视频教程,java大厂面试必会的jvm一套搞定(丰富的实战案例及最热面试题),也有部分内容来自于自己的思考,发布文章是想帮助其他学习的人更方便地整理自己的笔记或者直接通过文章学习相关知识,如有侵权请联系删除,最后对 黑马程序员 的优质课程表示感谢。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/621554.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

打造本地GPT专业领域知识库AnythingLLM+Ollama

如果你觉得openai的gpt没有隐私&#xff0c;或者需要离线使用gpt&#xff0c;还是打造专业领域知识&#xff0c;可以借用AnythingLLMOllama轻松实现本地GPT. AnythingLLMOllama 实现本地GPT步聚&#xff1a; 1 下载 AnythingLLM软件 AnythingLLM官网地址&#xff1a; Anythi…

C++17新特性 结构化绑定

一、Python中的相似功能 熟悉python的应该对下面的代码很熟悉 def return_multiple_values():return 11, 7x, y return_multiple_values()函数返回一个元组&#xff0c;元组自动分配给了x和y。 二、C11中的元组 c11中就存在类似python元组的概念了&#xff1a; std::tupl…

高速电流反馈运放总结

目录 前言 基础架构 CFB运算放大器拓扑结构的进步 前言 最近项目发现有震荡&#xff0c;发现是电流反馈型运放导致&#xff0c;所以对电流运放的知识做了全面的复习。 基础架构 现在&#xff0c;我们将详细考察高速运算放大器中非常流行的电流反馈(CFB)运算放大器拓扑结 构…

黑盒测试中的边界值分析

黑盒测试是一种基于需求和规格的测试方法&#xff0c;它主要关注软件系统输出的正确性和完整性&#xff0c;而不考虑内部代码的实现方式。在黑盒测试中&#xff0c;边界值分析是一种重要的测试技术&#xff0c;它可以帮助测试人员有效地发现输入和输出的问题。本文将从什么是边…

【数据结构】二叉排序树(查找+插入+删除+效率分析)完整代码+解析

3.1 二叉排序树 3.1.1 定义 二叉排序树的定义 又称二叉查找树&#xff08;BST&#xff0c;Binary Search Tree&#xff09; 二叉排序树是具有以下性质的二叉树&#xff1a; 左子树结点值<根结点值<右子树结点值 进行中序遍历&#xff0c;可以得到一个递增的有序序列。 3…

无需公网IP、无需云服务器,异地组网实现远程直连NAS、游戏联机

手机图片、视频太多&#xff0c;存储空间不够用怎么办?出门在外无法直连家中NAS&#xff0c;远程访问NAS速度慢&#xff1f;自建私有云、多媒体服务器&#xff0c;如何多人远程共享媒体资源&#xff1f;幻兽帕鲁、我的世界、泰拉瑞亚…局域网游戏&#xff0c;想远程多人联机&a…

Golang面向对象编程(二)

文章目录 封装基本介绍封装的实现工厂函数 继承基本介绍继承的实现字段和方法访问细节多继承 封装 基本介绍 基本介绍 封装&#xff08;Encapsulation&#xff09;是面向对象编程&#xff08;OOP&#xff09;中的一种重要概念&#xff0c;封装通过将数据和相关的方法组合在一起…

RobbitMQ基本消息队列的消息接收

1.先给工程引入依赖 父工程有了子工程就不用导了 <!--AMQP依赖&#xff0c;包含RabbitMQ--> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId> </dependency> 2.配置yml…

基于大数据+Hadoop的豆瓣电子图书推荐系统设计和实现

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和学生毕业项目实战,高校老师/讲师/同行交流合作✌ 主要内容&#xff1a;SpringBoot、Vue、SSM、HLM…

linux学习:多媒体开发库SDL+视频、音频、事件子系统+处理yuv视频源

目录 编译和移植 视频子系统 视频子系统产生图像的步骤 api 初始化 SDL 的相关子系统 使用指定的宽、高和色深来创建一个视窗 surface 使用 fmt 指定的格式创建一个像素点​编辑 将 dst 上的矩形 dstrect 填充为单色 color​编辑 将 src 快速叠加到 dst 上​编辑 更新…

sqli-labs 第十七关

目录 找注入点&#xff1a; 源码分析&#xff1a; 测试&#xff1a; 奇怪现象&#xff1a; &#xff08;1&#xff09;&#xff1a;当我们输入的密码为字符进行注入时。 &#xff08;2&#xff09;&#xff1a;当我们输入的密码为整数时。 产生原因&#xff1a; 解决方法…

Docker:docker在项目中常用的一些命令

简介   Docker 是一个开源的容器化平台&#xff0c;它允许开发者将应用程序及其依赖项打包到一个可移植的容器中&#xff0c;并发布到任何安装了 Docker 引擎的机器上。这些容器是轻量级的&#xff0c;包含了应用程序运行所需的所有东西&#xff0c;如代码、系统库、系统工具…

SpringBoot集成Redis环境搭建及配置详解

前言 Redis作为当前最火的NoSQL数据库&#xff0c;支持很多语言客户端操作Redis。 而SpringBoot作为java当前最火的开发框架&#xff0c;提供了Spring-data-redis框架实现对Redis的各种操作。 在springboot1.5.x版本的默认的Redis客户端都是Jedis实现的&#xff0c;springboot…

大模型时代下两种few shot高效文本分类方法

介绍近年(2022、2024)大语言模型盛行下的两篇文本分类相关的论文&#xff0c;适用场景为few shot。两种方法分别是setfit和fastfit&#xff0c;都提供了python的包使用方便。 论文1&#xff1a;Efficient Few-Shot Learning Without Prompts 题目&#xff1a;无需提示的高效少…

浪潮信息企业级存储逆势增长 市场份额位列中国前二

2023年&#xff0c;中国企业级存储市场竞争激烈&#xff0c;在挑战重重之下&#xff0c;浪潮信息仍然实现逆势增长&#xff0c;销售额增幅达4.7%&#xff0c;市场份额相比2022年扩大0.6%&#xff0c;位列中国前二。另外&#xff0c;在高端和全闪存阵列细分市场&#xff0c;浪潮…

Vue3实战Easy云盘(三):文件删除+文件移动+目录导航+上传优化/文件过滤/搜索

一、文件删除 &#xff08;1&#xff09;选中了之后才可以删除&#xff0c;没有选中时就显示暗调删除按钮 &#xff08;2&#xff09;实现选中高亮功能 &#xff08;3&#xff09;单个删除 &#xff08;4&#xff09;批量删除 Main.vue中 <!-- 按钮3 --><!-- 如果sel…

鸿蒙内核源码分析(用户态锁篇) | 如何使用快锁Futex(上)

快锁上下篇 鸿蒙内核实现了Futex&#xff0c;系列篇将用两篇来介绍快锁&#xff0c;主要两个原因: 网上介绍Futex的文章很少&#xff0c;全面深入内核介绍的就更少&#xff0c;所以来一次详细整理和挖透。涉及用户态和内核态打配合&#xff0c;共同作用&#xff0c;既要说用户…

【Linux】文件描述符和重定向

目录 一、回顾C文件 二、系统文件I/O 2.1 系统调用 open 2.2 标志位传参 2.3 系统调用 write 2.4 文件描述符fd 2.5 struct file 2.6 fd的分配规则 2.7 重定向 2.7.1 基本原理&#xff1a; 2.7.2 系统调用 dup2 2.8 标准错误 一、回顾C文件 文件 内容 属性 对…

3分钟,学会一个 Lambda 小知识之【流API】

之前给大家介绍的 Lambda 小知识还记得吗&#xff1f;今天再来给大家介绍&#xff0c; 流API 的相关知识要点。 流API Stream是Java8中处理集合的关键抽象概念&#xff0c;它可以指定你对集合的&#xff0c;可以执行查找、过滤和映射等数据操作。 Stream 使用一种类似用 SQ…

资料如何打印更省钱

在日常工作和学习中&#xff0c;我们经常需要打印各种资料。然而&#xff0c;随着打印成本的不断提高&#xff0c;如何更省钱地打印资料成为了大家关注的焦点。今天&#xff0c;就为大家分享一些资料打印的省钱技巧&#xff0c;并推荐一个省钱又省心的打印平台。 首先&#xff…