8

我对类加载的理解是,一个类在第一次需要时被加载(用一种非常简单的方式说)。使用 -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 等的东西?
4

2 回答 2

9

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

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

  • 加载
  • 链接
    • 确认
    • 准备
    • 解析度
  • 初始化
  • 使用
  • 卸下

现在我们关注加载的引用类的解析初始化阶段发生在解析阶段,而 <clint> 发生在初始化阶段。加载验证准备初始化卸载的顺序是固定的,但是调用解析阶段的时间不固定,它可能发生在初始化之前(对应你的前一种情况)阶段,也可能发生在初始化之后在某些场景下(对应你的后者案子)。

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

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

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

参考

于 2017-01-15T03:22:24.123 回答
7

这里是那些不想阅读所有评论的人的解决方案摘要;)

  1. 执行顺序的差异是由其中一个启动器-noverify指定的。验证器可能会导致加载其他类,如JVM Spec中的此处所指定。是否加载类似乎取决于分配对象的字段的类型。更多细节在这里。另一方面,当以 开头时-noverify,没有验证,因此类的加载仅发生在代码中首次使用它的确切位置,<clinit>在我的例子中,它在内部。
  2. 有一些方法可以在<clinit>无需修改字节码的情况下跟踪调用。一种方法是使用-XX:+TraceClassInitializationJDK8 上的。然而,这需要 JVM 的调试版本(注意:这不是您在调试模式下启动的程序,而是在启用调试的情况下编译的虚拟机。可以在此处找到如何构建它的指南)。另一种方式——尽管只有 JDK9 附带——是使用新的JEP 158:统一 JVM 日志记录功能,并在启动程序时提供类似以下内容:(
    -Xlog:class+load=info,class+init=info:file=trace.log请参阅此处了解如何获取标签和参数的完整列表)
于 2017-01-16T17:27:53.020 回答