【问题标题】:Loading, Linking, and Initializing - When does a class get loaded?加载、链接和初始化 - 什么时候加载一个类?
【发布时间】:2017-05-30 02:49:48
【问题描述】:

我对类加载的理解是,一个类在第一次需要时被加载(用一种非常简单的方式说)。使用 -verbose:class 和 Iterators 类的修改版本运行以下示例,该版本在调用其 clinit 时会打印一条消息,但我观察到了一些我无法真正解释的事情:

public class IteratorsTest
{
    public static void main(String[] args)
    {
        com.google.common.collect.Iterators.forArray(1, 2, 3);
    }
}

(清理后的)输出如下:

[Loaded com.google.common.collect.Iterators from file:...]
[Loaded com.google.common.collect.Iterators$1 from file:...]
---------> Iterators <clinit>

为什么在调用 clinit 之前加载 Iterators$1?它只在 clinit 中定义,不是吗?

  static final UnmodifiableListIterator<Object> EMPTY_LIST_ITERATOR =
      new UnmodifiableListIterator<Object>() {
  ...
  }

这会产生以下字节码:

static <clinit>()V
   L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "---------> Iterators clinit --------------"**
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    NEW com/google/common/collect/Iterators$1
    DUP
    INVOKESPECIAL com/google/common/collect/Iterators$1.<init> ()V
   L2
    PUTSTATIC com/google/common/collect/Iterators.EMPTY_LIST_ITERATOR : Lcom/google/common/collect/UnmodifiableListIterator;

更让我困惑的是,我还有一个示例(太复杂,无法在此处发布),其中与上面主要代码中的代码行相同的代码导致以下输出:

[Loaded com.google.common.collect.Iterators from file:...]
---------> Iterators <clinit>
[Loaded com.google.common.collect.Iterators$1 from file:...]

这实际上也是我对简单测试程序的期望。

我试图在这里找到答案 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html ,但这并没有真正帮助。

  • 有时先执行 clinit 而有时先加载匿名类的原因是什么?
  • 当 JVM 调用类的 clinit 时,有没有办法跟踪?类似于 -verbose:class 或 -XX:+TraceClassLoading 等的东西?

【问题讨论】:

  • openjdk9 的新日志基础设施允许对许多内部结构进行相当细粒度的分解,包括类加载和初始化。也许这会提供你想要的信息。
  • 好主意,我了解了新的日志记录功能,但还没有尝试过。也许值得进行试运行。谢谢!
  • 虽然我找到了它,但我想知道 JDK9 是否会有所帮助,是的,确实可以完成这项工作。我从 -Xlog:class+init=info,class+load=info:file=trace.log 开始(顺便说一句:某处是否有可用选择器的完整列表?这里提供的 openjdk.java.net/jeps/158 还不够)。 PS:实际上是你在这里的评论stackoverflow.com/questions/39321345/… 我第一次读到这个。

标签: java jvm classloader bytecode static-initializer


【解决方案1】:

有时先执行 clinit 而有时先加载匿名类的原因是什么?

类加载过程包含以下过程。

  • 加载中
  • 链接
    • 验证
    • 准备工作
    • 分辨率
  • 初始化
  • 使用
  • 卸载

现在我们关注的是解析初始化阶段 reference class 加载发生在 resolution 阶段 发生在 initialization 阶段。 加载验证准备初始化卸载的顺序是固定的,但是调用解析阶段的时间不固定,它可能发生在初始化之前(对应你的前一种情况)阶段,它也可能在某些情况下发生在初始化之后(对应你的后一种情况)。

出于性能考虑,HotSpot VM 通常会等到类初始化后再加载和链接一个类。所以如果A类引用B类,加载A类不一定会导致B类的加载(除非需要验证)。执行引用 B 的第一条指令将导致 B 的初始化,这需要加载和链接 B 类。

当 JVM 调用类的 clinit 时,有没有办法跟踪?类似于 -verbose:class 或 -XX:+TraceClassLoading 等的东西?

我不知道是否存在一些 jvm 参数,您可以直接获取 jvm 调用 方法的时间,但是还有另一种方法可以实现这一点,使用 jvm_ti 。您可以侦听诸如 methodEntry 之类的事件,然后获取调用 方法的时间。欲了解更多信息谷歌jvm_ti

参考

【讨论】:

  • 我也使用 -XX:+TraceClassResolution 运行,但在加载类之前 Iterators$1 没有输出。是什么让您认为决议的时间不固定?据我了解,是 JVM 规范第 5.4.3 章中的说明导致了符号引用的解析。但是,这些指令仅在 clinit 期间调用(因此我希望此时的分辨率和负载)。在调用迭代器的 clinit 之前,我真的需要了解是什么触发了 Iterators$1 的加载。
  • @Haasip Satang the jvm specification 5.4 linking 说:a Java Virtual Machine implementation may choose to resolve each symbolic reference in a class or interface individually when it is used ("lazy" or "late" resolution), or to resolve them all at once when the class is being verified ("eager" or "static" resolution).
  • 是的,但是由于我们讨论的是同一个虚拟机(不是不同的实现),我会假设相同的行为。但即使 JVM 决定为同一行代码选择不同的方法,我也想了解它的原因。这肯定不是随机的。它总是首先在程序 A 中加载类,而在 B 中首先调用 clinit。一行代码的方法在两个程序中完全相同。问题是 JVM 为什么选择不同的策略。注意:我正在对 JVM 中的非确定性进行一些研究,这就是为什么我需要了解这个低级细节。
  • @Haasip Satang 我同意你的观点,这不是随机的。并且您似乎可以使用 jvm 参数 -XX:+TraceClassLoading -XX:+TraceClassResolution 它会记录一些 RESOLVE 信息,这些信息告诉使用加载类的原因。关于你之前的例子,它记录RESOLVE com.google.common.collect.Iterators com.google.common.collect.Iterators$1 (verification),它响应the HotSpot VM generally waits until class initialization to load and link a class(unless required for verification)。我已经更新了我的答案,希望对你有点帮助。
  • 感谢您的反馈,但我不得不承认我仍然不明白这一点。我们似乎对加载和分辨率有相同的理解。我还使用了您提到的 jvm 参数(如上所述在我之前的 cmets 中)。然而,这些都不能解释为什么 JVM 会在 cliinit 中引用匿名类 Iterators$1 之前加载它。在我得到的日志输出中,在实际加载 Iterators$1 之前,我也没有得到任何引用 Iterators$1 的 RESOLVE 语句。我仍然很困惑。 PS:我更新了 Q 以添加一点字节码来显示第一个引用。
【解决方案2】:

这里是为那些不想阅读所有 cmets 的人总结的解决方案;)

  1. 执行顺序的差异是由指定了-noverify 的启动器之一引起的。验证器可能会导致加载其他类,如JVM Spec 中所述。是否加载类似乎取决于分配对象的字段的类型。更多详情here。另一方面,当以-noverify 开始时,没有验证,因此类的加载仅发生在代码中首次使用它的确切位置,在我的例子中,它位于&lt;clinit&gt; 内。
  2. 有一些方法可以跟踪调用&lt;clinit&gt;,而无需修改字节码。一种方法是在 JDK8 上使用-XX:+TraceClassInitialization。然而,这需要 JVM 的调试版本(注意:这不是您的程序在调试模式下启动,而是真正启用调试编译的 VM。可以找到如何构建它的指南 here)。另一种方法——尽管只有 JDK9 自带——是使用新的 JEP 158: Unified JVM Logging feature 并在启动程序时提供类似以下内容:
    -Xlog:class+load=info,class+init=info:file=trace.log(有关如何获取完整列表的信息,请参阅 here标签和参数)

【讨论】:

    猜你喜欢
    • 2011-11-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-27
    • 2012-03-10
    • 1970-01-01
    • 2021-01-19
    • 1970-01-01
    相关资源
    最近更新 更多