cannot find symbol什么意思_Class.forName 发生了什么

fda2a1d78a7065750563f510c6425d86.png

众所周知. Java中获取Class有三种方式

  • 方式一 :通过对象的getClass()方法
Class<?> clazz1 =str.getClass();
  • 方式二:通过类的.class属性
Class<?> clazz2 =String.class;
  • 方式三:通过Class类的静态方法forName(String className)
Class<?> clazz3 =Class.forName("java.lang.String");

那么引入一个问题.

早期 JDBC连接数据库的使用,需要使用Class.forName("com.mysql.jdbc.Driver") 加载驱动.但是这个句话与其他两句有什么区别呢. 为什么只能使用 Class.forName而不能通过另外两种方法呢 ?

写一段源码验证一下

  • Parent.java
package com.huangxunyi;

public class Parent {
    private static String name = getName();

    static {
        System.out.println("父类静态代码块");
    }

    {
        System.out.println("父类代码块");
    }

    public Parent() {
        System.out.println("父类构造函数");
    }

    private static String getName() {
        System.out.println("父类静态变量");
        return null;
    }

}
  • Son.java
package com.huangxunyi;

public class Son extends Parent {

    {
        System.out.println("子类代码块");
    }

    static {
        System.out.println("子类静态代码块");
    }

    private static String name = getName1();

    public Son() {
        System.out.println("子类构造方法");
    }

    private static String getName1() {
        System.out.println("子类静态变量");
        return null;
    }

}
  • Test.java
package com.huangxunyi;

public class Test {
    public static void main(String[] args) throws ClassNotFoundException {
        Class.forName("com.huangxunyi.Son");
    }
}

运行结果

Class.forName |  Class.forName().newInstance() |    Son.class               |
-----------------------------------------------|-----------------
父类静态变量    |  父类静态变量                    |    <no output>
父类静态代码块  |  父类静态代码块                   | 
子类静态代码块  |  子类静态代码块                   |
子类静态变量    |  子类静态变量                    |
              |  父类代码块                      |
              |  父类构造函数                    |
              |  子类代码块                      |
              |  子类构造方法                    |

众所周知. IDE只是字符串拼接器. 因此运行上述代码可以通过

$ javac -d . Parent.java Son.java Test.java 

生成目录结构如下

.
├── com
│   └── huangxunyi
│       ├── Parent.class
│       ├── Son.class
│       └── Test.class
├── Parent.java
├── Son.java
└── Test.java

然后通过如下命令即可运行

$ java com.huangxunyi.Test

252b2dbcff01c6f29ca7c55f995271b7.png
vscode launch.js
ps: 一定要通过第三个类(Test.java) 去重写 main方法. 因为如果main方法写在Parent/Son中在执行main方法的时候 Parent/Son 就已经被加载了.

然后就可以愉快地开始调试源码了.

Class.java中 . forName(String name)最终会调用native的方法 forName0(String name, boolean initialize,ClassLoader loader,Class<?> caller) 那么就从这个native 方法入手

位于jdk/src/java.base/share/native/libjava/Class.c

7be886a3f8989d184d6a9b7b91024a57.png
forName0

然后通过JVM_FindClassFromCaller 函数从指定的classloader中查找指定的类 此时类名应该位 com/huangxunyi/Son这样 splash形式

b57c7de2ce3a951ca7096afdc26a1499.png

然后来到 src/hotspot/share/prims/jvm.cppJVM_FindClassFromCaller

a5cc6d92db5ec468b756235d1cd05328.png

首先从符号表里面去查找,如果其不存在. 就添加到符号表中

c45a5c80d9e35d2317efa98f661df018.png
符号表(SymbolTable) 是由一组符号地址和符号信息构成的表格,最简单的可以理解为哈希表的K-V值对的形式。

然后来到 find_class_from_class_loader函数 即: 从指定的Classloader中找Class

核心逻辑位于src/hotspot/share/classfile/systemDictionary.cpp

Klass* SystemDictionary::resolve_or_null(Symbol* class_name, Handle class_loader, Handle protection_domain, TRAPS) {
  if (Signature::is_array(class_name)) {
    return resolve_array_class_or_null(class_name, class_loader, protection_domain, THREAD);
  } else {
    return resolve_instance_class_or_null_helper(class_name, class_loader, protection_domain, THREAD);
  }
}

// name may be in the form of "java/lang/Object" or "Ljava/lang/Object;"
InstanceKlass* SystemDictionary::resolve_instance_class_or_null_helper(Symbol* class_name,
                                                                       Handle class_loader,
                                                                       Handle protection_domain,
                                                                       TRAPS) {
  assert(class_name != NULL && !Signature::is_array(class_name), "must be");
  if (Signature::has_envelope(class_name)) {
    ResourceMark rm(THREAD);
    // Ignore wrapping L and ;.
    TempNewSymbol name = SymbolTable::new_symbol(class_name->as_C_string() + 1,
                                                 class_name->utf8_length() - 2);
    return resolve_instance_class_or_null(name, class_loader, protection_domain, THREAD);
  } else {
    return resolve_instance_class_or_null(class_name, class_loader, protection_domain, THREAD);
  }
}

然后来到一个非常核心的代码非常长的函数

src/hotspot/share/classfile/systemDictionary.cpp#resolve_instance_class_or_null

他主要完成了如下行为

1.执行查找以查看类是否已存在以及保护域是否具有正确的访问权限

6b3c5bf137e76bc5d71de8be37ea32bc.png
检查是否已经加载该类

2.获取对象锁 并 将要加载的类名hash加入placeholders 防止多线程重复加载

82fcc471de404b3a2065a8543ce793e0.png

3. 再次确认该类未加载

1d3c0474017bd425417c8140abf6bee1.png

4. 从placeholder中判断是否该类正在被加载.

8d7d23c841ffa2dcef62e26095ad3212.png

5. 添加此class正在被加载的记录到placeholder . (复杂)

6. 一切验证都通过后 并确保该类未加载后 真正的加载类

k = load_instance_class(name, class_loader, THREAD);

43e2e3c4ff5e81526b51a9bc91285dc8.png

7. 清理placeholder , 并在LOAD_INSTANCE里标记该类加载成功或失败

6f7f67c3e7b5a39c5b5d5a34289dc19d.png

其中第6步 load_instance_class 主要逻辑为 .

if (class_loader.is_null()) {
  // 通过 boot classloader 加载 ..
}else{
  // 通过指定的classloader加载
  // Use user specified class loader to load class. 
  // Call loadClass operation on class_loader.
}

如果class_loader.is_null()true , 通过src/hotspot/share/classfile/classLoader.cppInstanceKlass* ClassLoader::load_class(Symbol* name, bool search_append_only, TRAPS)方法中 KlassFactory::create_from_stream 从字节流中加载

  InstanceKlass* result = KlassFactory::create_from_stream(stream,
                                                           name,
                                                           loader_data,
                                                           protection_domain,
                                                           NULL, // unsafe_anonymous_host
                                                           NULL, // cp_patches
                                                           THREAD);

如果class_loader.is_null()false(本文Demo为false) , 通过JavaCalls::call_virtual方法生成jobject.

****************************************************************************

此方法最终会调用掉ClassLoader.loadClass(双亲委派) , 因此会进入到class_loader.is_null()为true的情况. 最终完成加载 . 并且转换为我们需要类

6a621b5870ee0f83327dab8670982a78.png
call_virtual
****************************************************************************

其大致调用栈如下 (摘自类加载与Java主类加载机制解析 - 博文视点)

      jvm.cpp::find_class_from_class_loader():执行klassOop klass = SystemDictionary::resolve_or_fail(name, loader, protection_domain, throwError != 0, CHECK_NULL)
        SystemDictionary::resolve_or_fail()
          SystemDictionary::resolve_or_null()
            SystemDictionary::resolve_instance_class_or_null():执行k = load_instance_class(name, class_loader, THREAD)(Do actual loading)
              SystemDictionary::load_instance_class()
                JavaCalls::call_virtual();
                  java.lang.ClassLoader.loadClass(String)
                    sun.misc.AppClassLoader.loadClass(String, boolean)
                      java.lang.ClassLoader.loadClass(String, boolean)
                        java.net.URLClassLoader.findClass(final String)

find_class_from_class_loader中执行完resolve_or_fail函数后. 此时已经完成了JVM类加载机制的 连接 接下来变通过Java层传入的参数 initialize 判断是否需要 初始化.

public static Class<?> forName(String name, boolean initialize, ClassLoader loader)

64ca6c03272e1966c57a05d723c31ad6.png

跟进去后来到src/hotspot/share/oops/instanceKlass.cppvoid InstanceKlass::initialize_impl(TRAPS)函数

initialize_impl
  1. 加锁检查.防止重复初始化

975529b3cde71e20123572b1ef9ac019.png

2. 如果 处于 being_initialized状态 并且 正在被*其它线程*初始化,则执行waitUninterruptibly等待其他线程完成后通知 .

318e827b949aada004360699b4f5c33a.png

3. 如果 处于 being_initialized状态 并且 正在被*当前线程*初始化,则直接返回

2c1e18269361b0b02b7d41be71fad785.png

4. 如果类已经初始化过了(fully_initialized), 直接返回 ( 每个类只初始化一次)

e354c2edb2973753700506aa392564dd.png

5. 如果初始化失败, 处于initialization_error 状态则抛出异常

fbcb76ce5d186281b91a644d635cdc0f.png

6. 设置初始化状态为being_initialized 和初始化线程

49fe6b818425fbf464e87feb81c0bc41.png

7. 如果当前 类 不是接口类型,且父类不为空同时父类且还未初始化,则执行父类的初始化。

(这里发生了套娃)

此时父类的静态变量静态代码块已经打印

adbe2d353b101fe02b938b778e9f0caa.png

8. 通过 call_class_initializer 执行静态代码. 在此之前 寻找这个类的aot编译方法,包括类初始值设定项 , 此时子类静态代码块子类静态变量被打印

ed3ed3a3a743959fba5c641f63eeee5e.png

9. 如果未出现异常则表示初始化成功, 那么设置 当前类 状态为fully_initialized, 并通知其它线程初始化已经完成

c203d9898eabf1d8a34f26cb72b2deb4.png

10/11. 如果出现异常.则设置当前类的状态为 initialization_error,并通知其它线程初始化发生异常。并且对Java TI 进行一些处理

c4e51b4f9d90f3bc89932bcd1198d96e.png
类的状态
  enum ClassState {
    allocated,                          // allocated (but not yet linked)
    loaded,                             // loaded and inserted in class hierarchy (but not linked yet)
    linked,                             // successfully linked/verified (but not initialized yet)
    being_initialized,                  // currently running class initializer
    fully_initialized,                  // initialized (successfull final state)
    initialization_error                // error happened during initialization
  };

这就是native方法forName0的主要流程了

回到我们的Demo

如果我们在调用 Class.forName() 的指定其不初始化呢

33c181f688226ceb31f3a4436adcea78.png

什么也没输出.

因此可以得出结论. Class.forName() 默认会对第一次加载的类初始化. 而 .class不会, 至于getClass() 你都能拿到对象了...可肯定已经初始化过了..

所以在 早期 JDBC连接数据库的使用,需要使用Class.forName("com.mysql.jdbc.Driver") 加载驱动

因为其内部有一个静态代码块将JDBC驱动加载至DriverManager

81eeda3eae3effe69d93082499c3ec68.png

注意是 早期 现在的JDBC早就不需要了 Class.forName() 了...

原因就是 ServiceLoader. 即 SPI

9ff78bd144545ccce2f97bdc68157be9.png
services
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值