`
RednaxelaFX
  • 浏览: 3047762 次
  • 性别: Icon_minigender_1
  • 来自: 海外
社区版块
存档分类
最新评论

动态加载的时候一个小细节

    博客分类:
  • Java
阅读更多
昨天在处理一处动态加载逻辑的时候遇到了bug……做个最小repro出来记着。

假设有以下目录结构:
├─path1_not_on_classpath
│      Alpha.class
│      Beta.class
│      IAlpha.class
│
├─path2_not_on_classpath
│      Charlie.class
│
└─test
       TestInterfaceClassLoad.class
       TestInterfaceClassLoad.java

其中IAlpha、Alpha、Beta、Charlie几个.class文件是先前由以下源码编译得到的:
public interface IAlpha {
  Alpha foo(Beta b);
  Charlie bar();
}

class Alpha { }
class Beta { }
class Charlie { }

注意我特意把Charlie.class放到了不同的目录下来再现遇到的问题。

然后写以下代码来测试动态加载:
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

public class TestInterfaceClassLoad {
  public static void main(String[] args) throws Exception {
    URLClassLoader loader = new URLClassLoader(new URL[] {
      new File("../path1_not_on_classpath").toURI().toURL()
    }, Thread.currentThread().getContextClassLoader());
    Class<?> clzAlpha = Class.forName("IAlpha", true, loader);
    System.out.println(clzAlpha);
  }
}

编译,在上述目录结构中的test目录不加任何特别参数运行,一切正常。
在上面测试的最后遍历一下动态加载进来的接口上的所有public方法:
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

public class TestInterfaceClassLoad {
  public static void main(String[] args) throws Exception {
    URLClassLoader loader = new URLClassLoader(new URL[] {
      new File("../path1_not_on_classpath").toURI().toURL()
    }, Thread.currentThread().getContextClassLoader());
    Class<?> clzAlpha = Class.forName("IAlpha", true, loader);
    System.out.println(clzAlpha);
    for (Method m : clzAlpha.getMethods()) System.out.println(m);
  }
}

这就挂了:
Exception in thread "main" java.lang.NoClassDefFoundError: Charlie
        at java.lang.Class.getDeclaredMethods0(Native Method)
        at java.lang.Class.privateGetDeclaredMethods(Unknown Source)
        at java.lang.Class.privateGetPublicMethods(Unknown Source)
        at java.lang.Class.getMethods(Unknown Source)
        at TestInterfaceClassLoad.main(TestInterfaceClassLoad.java:13)
Caused by: java.lang.ClassNotFoundException: Charlie
        at java.net.URLClassLoader$1.run(Unknown Source)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(Unknown Source)
        at java.lang.ClassLoader.loadClass(Unknown Source)
        at java.lang.ClassLoader.loadClass(Unknown Source)
        at java.lang.ClassLoader.loadClassInternal(Unknown Source)
        ... 5 more

再挖一挖可以看到真正出错的stack trace:
java.lang.Class.getDeclaredMethods0(Native Method)
java.lang.Class.privateGetDeclaredMethods(Unknown Source)
java.lang.Class.privateGetPublicMethods(Unknown Source)
java.lang.Class.getMethods(Unknown Source)
TestInterfaceClassLoad.main(TestInterfaceClassLoad.java:13)


把需要的路径加上,使classloader能找到Charlie.class,问题自然消失:
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

public class TestInterfaceClassLoad {
  public static void main(String[] args) throws Exception {
    URLClassLoader loader = new URLClassLoader(new URL[] {
      new File("../path1_not_on_classpath").toURI().toURL(),
      new File("../path2_not_on_classpath").toURI().toURL()
    }, Thread.currentThread().getContextClassLoader());
    Class<?> clzAlpha = Class.forName("IAlpha", true, loader);
    System.out.println(clzAlpha);
    for (Method m : clzAlpha.getMethods()) System.out.println(m);
  }
}


其实原本遇到的问题跟这个repro有细微不同,但都是在加载接口时漏了一些依赖的类使得加载失败,抛出NoClassDefFoundError。

根据JVM规范第二版,class要进入可用状态需要经过loading、linking、initialization三个阶段,其中linking阶段又包括verification、preparation、resolution三步。

通过传入JVM启动参数-XX:+TraceClassLoading,可以看到第一版测试里与我写的代码直接相关的类的输出是:
[Loading TestInterfaceClassLoad from file:/D:/test/jdk1.6.0_14/test_classload/test/]
[Loaded TestInterfaceClassLoad from file:/D:/test/jdk1.6.0_14/test_classload/test/]
[Loading IAlpha from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
[Loaded IAlpha from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
interface IAlpha

很明显,在IAlpha达到"loaded"状态的时候,它依赖的其它class还没进入加载范围。

第二版的相关输出是:
[Loading TestInterfaceClassLoad from file:/D:/test/jdk1.6.0_14/test_classload/test/]
[Loaded TestInterfaceClassLoad from file:/D:/test/jdk1.6.0_14/test_classload/test/]
[Loading IAlpha from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
[Loaded IAlpha from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
interface IAlpha
[Loading java.lang.Class$MethodArray from D:\test\jdk1.6.0_14\fastdebug\jre\lib\rt.jar]
[Loaded java.lang.Class$MethodArray from D:\test\jdk1.6.0_14\fastdebug\jre\lib\rt.jar]
[Loading Beta from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
[Loaded Beta from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
[Loading Alpha from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
[Loaded Alpha from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]

可以看到IAlpha.foo()的参数与返回值的类型对应的class也被加载进来了。IAlpha.bar()参数列表为空,返回值类型为Charlie,如果正常的话应该在这里看到Charlie也被加载进来才对。然而实际看到的是抛异常的stack trace。

第三版的相关输出是:
[Loading TestInterfaceClassLoad from file:/D:/test/jdk1.6.0_14/test_classload/test/]
[Loaded TestInterfaceClassLoad from file:/D:/test/jdk1.6.0_14/test_classload/test/]
[Loading IAlpha from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
[Loaded IAlpha from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
interface IAlpha
[Loading java.lang.Class$MethodArray from D:\test\jdk1.6.0_14\fastdebug\jre\lib\rt.jar]
[Loaded java.lang.Class$MethodArray from D:\test\jdk1.6.0_14\fastdebug\jre\lib\rt.jar]
[Loading Beta from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
[Loaded Beta from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
[Loading Alpha from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
[Loaded Alpha from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
[Loading Charlie from file:/D:/test/jdk1.6.0_14/test_classload/test/../path2_not_on_classpath/]
[Loaded Charlie from file:/D:/test/jdk1.6.0_14/test_classload/test/../path2_not_on_classpath/]
public abstract Alpha IAlpha.foo(Beta)
public abstract Charlie IAlpha.bar()

这里就看到一切正常了,在main()里调用System.out.println()要输出的内容都能看到了。

OK,那到底异常是从哪里抛出来的呢?
测试是在Sun的JDK 1.6系上跑的。下面以这个实现为前提来讨论。

先看点小细节。注意在第二和第三版的输出中都可以看到HotSpot加载了java.lang.Class$MethodArray这个嵌套类。它是哪儿来的呢?在出错的stack trace上可以看到有Class.privateGetPublicMethods()方法,看看它的实现:
// Returns an array of "root" methods. These Method objects must NOT
// be propagated to the outside world, but must instead be copied
// via ReflectionFactory.copyMethod.
private Method[] privateGetPublicMethods() {
    checkInitted();
    Method[] res = null;
    if (useCaches) {
        clearCachesOnClassRedefinition();
        if (publicMethods != null) {
            res = (Method[]) publicMethods.get();
        }
        if (res != null) return res;
    }

    // No cached value available; compute value recursively.
    // Start by fetching public declared methods
    MethodArray methods = new MethodArray();
    {
        Method[] tmp = privateGetDeclaredMethods(true);
        methods.addAll(tmp);
    }
    // Now recur over superclass and direct superinterfaces.
    // Go over superinterfaces first so we can more easily filter
    // out concrete implementations inherited from superclasses at
    // the end.
    MethodArray inheritedMethods = new MethodArray();
    Class[] interfaces = getInterfaces();
    for (int i = 0; i < interfaces.length; i++) {
        inheritedMethods.addAll(interfaces[i].privateGetPublicMethods());
    }
    if (!isInterface()) {
        Class c = getSuperclass();
        if (c != null) {
            MethodArray supers = new MethodArray();
            supers.addAll(c.privateGetPublicMethods());
            // Filter out concrete implementations of any
            // interface methods
            for (int i = 0; i < supers.length(); i++) {
                Method m = supers.get(i);
                if (m != null && !Modifier.isAbstract(m.getModifiers())) {
                    inheritedMethods.removeByNameAndSignature(m);
                }
            }
            // Insert superclass's inherited methods before
            // superinterfaces' to satisfy getMethod's search
            // order
            supers.addAll(inheritedMethods);
            inheritedMethods = supers;
        }
    }
    // Filter out all local methods from inherited ones
    for (int i = 0; i < methods.length(); i++) {
        Method m = methods.get(i);
        inheritedMethods.removeByNameAndSignature(m);
    }
    methods.addAllIfNotPresent(inheritedMethods);
    methods.compactAndTrim();
    res = methods.getArray();
    if (useCaches) {
        publicMethods = new SoftReference(res);
    }
    return res;
}

这个方法创建了MethodArray的实例,顺带就让HotSpot把该类给加载了进来。

现在来看看上面例子里对抛出异常有直接贡献的方法调用树:
(同名重载相互调用的只写一个,缩进在同一层的说明是被同一个方法调用的。
相关方法上面的文件路径是实际代码在JDK源码中的位置。)
j2se/src/share/classes/java/lang/Class.java
java.lang.Class.getMethods()
  java.lang.Class.privateGetPublicMethods()
    java.lang.Class.privateGetDeclaredMethods()
      java.lang.Class.getDeclaredMethods0()

        hotspot/src/share/vm/prims/jvm.cpp
        JVM_GetClassDeclaredMethods()

          hotspot/src/share/vm/runtime/reflection.cpp
          Reflection::new_method()
            get_parameter_types() <= 参数与返回值类型都包含在内
              get_mirror_from_signature()

                hotspot/src/share/vm/classfile/systemDictionary.cpp
                SystemDictionary::resolve_or_fail()
                  SystemDictionary::resolve_or_null()
                    SystemDictionary::resolve_instance_class_or_null()
                      SystemDictionary::load_instance_class()
                        
                        hotspot/src/share/vm/runtime/javaCalls.cpp
                        JavaCalls::call_virtual() <= 对ClassLoader.loadClass()的虚方法调用;
                                                     由于URLClassLoader没有覆写loadClass(),
                                                     这里实际调用的是ClassLoader的版本
                        
                          j2se/src/share/classes/java/lang/ClassLoader.java
                          java.lang.ClassLoader.loadClass()
                          
                            j2se/src/share/classes/java/net/URLClassLoader.java
                            java.net.URLClassLoader.findClass()  <= 找不到,抛ClassNotFoundException
                            
                  SystemDictionary::handle_resolution_exception() <= 换成NoClassDefFoundError再抛出来


看到这个流程,可以发现HotSpot VM中加载类是lazy的,一个class被加载后并不马上解析(resolve)所有依赖项。在调用Class.getMethods()之前,IAlpha.class已经被正确加载并初始化了。等到实际要去获取IAlpha上的方法信息时,方法上依赖的类型才被解析,解析的过程中又会尝试加载依赖的类型。
回头看JVM规范里的一段描述:
Java Virtual Machine Specification, 2nd, 2.17.3 写道
The Java programming language allows an implementation flexibility as to when linking activities (and, because of recursion, loading) take place, provided that the semantics of the language are respected, that a class or interface is completely verified and prepared before it is initialized, and that errors detected during linkage are thrown at a point in the program where some action is taken by the program that might require linkage to the class or interface involved in the error.

For example, an implementation may choose to resolve each symbolic reference in a class or interface individually, only when it is used (lazy or late resolution), or to resolve them all at once, for example, while the class is being verified (static resolution). This means that the resolution process may continue, in some implementations, after a class or interface has been initialized.

第二段里举个这么明确的例子,自然不是没有道理的……

记完。留着文件名和方法名以后回头再找的时候方便。
话说在寻找相关方法的时候,发现instanceKlass::link_class_impl()有调用初始化vtable(虚方法表)和itable(接口方法表)的逻辑。再看JVM规范里的一段话:
Java Virtual Machine Specification, 2nd, 2.17.3 写道
Implementations of the Java virtual machine may precompute additional data structures at preparation time in order to make later operations on a class or interface more efficient. One particularly useful data structure is a "method table" or other data structure that allows any method to be invoked on instances of a class without requiring a search of superclasses at invocation time.

不由得一笑 ^_^
1
0
分享到:
评论

相关推荐

    Android 免安装动态加载APK

    例如,DynamicAPK是阿里巴巴开源的一个强大的动态加载框架,提供了完整的解决方案。 4. **资源管理**:动态加载的APK也需要获取到自己的资源,这需要处理资源ID映射。Android的资源ID是全局唯一的,因此需要在主...

    动态加载EXE和DLL

    在动态加载的这个DLL中定义了一个函数MRun,该函数可以动态执行一 个EXE,即:调用资源中的EXE文件或TMemoryStream中被载入的EXE流。 此技术的好处:直接把资源中的EXE加载到内存中执行,使用程序自 身嵌入的EXE...

    wpf加载图片到datagrid动态控件

    在项目中添加一个新的C#类,如`ImageItem.cs`: ```csharp public class ImageItem { public string ImagePath { get; set; } } ``` 这个简单的类包含了图片的路径信息。 为了动态加载图片,我们需要填充...

    图片细节放大查看

    这个视图通常是一个具有透明背景的`&lt;div&gt;`,里面嵌套一个更小的`&lt;img&gt;`元素。`&lt;div&gt;`的大小可以根据需求设置,比如200x200像素。 4. **更新放大视图的内容**:每次鼠标移动时,根据计算出的放大区域,调整小`&lt;img&gt;`...

    6.0动态加载权限自己封装

    5. **封装库的实现**:自己封装的权限管理库可能会包含一个类或一系列辅助方法,这些方法会隐藏上述细节,提供更简洁的API供开发者调用。例如,可能有一个`checkAndRequestPermission()`方法,它自动检查权限状态并...

    MFC加载动态图片

    本文将深入探讨如何使用MFC加载动态图片,这是一个常见的需求,尤其是在创建用户界面时添加动画效果。 首先,我们需要理解动态图片通常指的是GIF格式的图像,因为GIF支持帧间的延迟时间,可以连续播放,形成动画...

    仿QQ动态背景-SWF动态背景

    本篇将深入探讨如何利用SWF(ShockWave Flash)文件来创建仿QQ动态背景,并介绍一个封装好的类库,使其实现变得更加简便。 SWF是一种由Adobe Flash技术生成的文件格式,主要用于在网页上展示动画、交互式内容以及...

    easyui iframe 页面重复加载的问题

    首先在页面加载完成事件中,遍历所有的iframe,将src设置为空字符串,然后绑定一个事件监听器到easyui的tab组件上,监听tab的select事件。在该事件中,获取被选中的标签对应的iframe,并为其动态设置正确的src值。 ...

    网站加载图标

    网站加载图标是网页设计中的一个重要元素,它在用户等待页面完全加载时提供视觉反馈,以显示网站正在运行并处理请求。这些图标通常是一系列动态图形,如旋转的圆圈、循环箭头或填充进度条,旨在减少用户的感知等待...

    swing之滚动条下拉加载数据源码

    `swing之滚动条下拉加载数据源码`这个标题暗示了我们将在 Swing 应用程序中实现一个功能,即当用户滚动到底部时自动加载更多数据。这种机制常见于许多现代应用程序,如社交媒体应用、新闻阅读器等,以提高用户体验。...

    加载SD卡中的so

    在Android开发中,NDK(Native Development Kit)允许...总之,动态加载SD卡上的.so文件可以为大型应用带来诸多优势,但同时也需要处理更多细节问题。通过合理的规划和实现,这种技术能够有效地优化应用的大小和性能。

    RecyclerView下拉刷新上拉加载

    本资料主要涉及RecyclerView的下拉刷新和上拉加载功能,这两种功能在移动应用中非常常见,用于实现数据的动态加载,提升用户体验。 下拉刷新(Pull-to-Refresh)是指用户在顶部向下滑动列表时,触发加载新数据的...

    flash加载外部位图

    1. **创建Loader对象**:首先,我们需要创建一个Loader对象,它是加载所有类型内容的基础。在AS3.0中,你可以这样创建它: ```actionscript var loader:Loader = new Loader(); ``` 2. **监听事件**:然后,我们...

    VC++实现动态创建对话框

    2. **设计对话框资源**:尽管动态创建对话框,但通常仍需要一个对话框模板资源,用于在设计时布局控件的初始位置和大小。在资源文件中,可能会有一个名为`IDD_DYNAMIC_DIALOG`的对话框资源。 3. **实现对话框的...

    datagridview单元格添加树控件.

    它是一个强大的组件,可以用来显示和编辑表格数据。默认情况下,每个单元格只能显示单一的文本或数值。但是,通过自定义绘制或者使用其他控件,我们可以扩展其功能,使其能够承载更复杂的元素,如树控件。 树控件...

    winform中实现动态的添加内容到指定的word文档中

    在Windows Forms(Winform)应用程序开发中,有时候我们需要与Microsoft Office接口进行交互,例如向已存在的Word文档中动态添加内容。这个过程涉及到自动化技术,它允许我们使用C#或VB.NET等.NET语言来控制Word应用...

    delphi 编写动态库并实现回调函数

    在动态库的上下文中,这意味着我们的动态库可以接受一个函数指针,然后在适当的时候调用这个函数。这样,动态库就可以通知或与调用它的应用程序进行交互,而无需知道具体的实现细节。 在 Delphi 中创建动态库,我们...

    Flutter实现动态高斯模糊AppBar

    在本文中,我们将深入探讨如何使用Flutter框架来实现一个动态高斯模糊的AppBar。Flutter,由Google开发,是一个用于构建高性能、多平台的移动应用程序的开源UI工具包。它的核心特性之一是Dart编程语言,这使得开发者...

    c# DataGridView中添加下拉列表

    首先,我们需要创建一个`DataGridView`控件并添加到窗体中。这可以通过Visual Studio的拖放功能完成,或者通过代码动态创建。一旦`DataGridView`创建完毕,我们可以在设计时或运行时添加列。对于包含下拉列表的列,...

    在MFC工具栏ToolBar上面添加Edit控件

    在资源编辑器中,你需要为工具栏添加一个新的按钮。这个按钮将作为Edit控件的占位符。设置其ID,例如ID_EDIT_TOOLBAR,并给它一个合适的图标或文字描述。 步骤2:自定义消息映射 在你的对话框类(通常是CDialog派生...

Global site tag (gtag.js) - Google Analytics