它的重要 field 有 :
_java_mirror
即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴 露给 java 使用_super
即父类_fields
即成员变量_methods
即方法_constants
即常量池_class_loader
即类加载器_vtable
虚方法表如果这个类还有父类没有加载,则先触发父类的加载。
加载和链接可能是交替运行的。
_java_mirror
是存储在堆中类加载就是我们的字节码文件被方法区中的instanceKlass(C++的数据结构)解析,但是我们的java无法操作直接操作这个数据结构中的属性,所以引入一个java的mirror镜像,指向这个数据结构,且数据结构中存在这个镜像的地址,因此我们就可以通过这个镜像(可看作桥梁)间接的去访问我们保存在instaceklass的信息,但是需要注意的是我们的镜像保存在堆内存当中!
例如:我们再堆内存new 一个实例,这个实例的对象头保存的是镜像的地址,当我们的调用方法的时候,是先通过对象头找到镜像的地址,然后通过镜像地址找到instanceklass的地址,拿的需要的信息!
验证类是否符合 JVM规范,安全性检查,阻止不合法的类继续运行。用 UE 等支持二进制的编辑器修改 HelloWorld.class
的魔数,在控制台运行:
E:\git\jvm\out\production\jvm>java cn.itcast.jvm.t5.HelloWorld
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value //魔数不匹配
3405691578 in class file cn/itcast/jvm/t5/HelloWorld
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at
java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
为 static 变量分配空间,设置默认值:
将常量池中的符号引用解析为直接引用:
未被使用的类不会被解析,在常量池当中以符号的形式存在,只有new了才解析
/** * 解析的含义 */
public class Load2 {
public static void main(String[] args) throws ClassNotFoundException,IOException {
ClassLoader classloader = Load2.class.getClassLoader();
// loadClass 方法不会导致类的解析和初始化
Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C");
// new C(); 会导致类的加载、验证、准备、解析这些步骤
System.in.read();
}
}
class C {
D d = new D();
}
class D {
}
< init() > V 方法
初始化即调用 < cinit>()V ,虚拟机会保证这个类的『构造方法』的线程安全。
发生的时机
概括得说,类初始化是【懒惰的】
不会导致类初始化的情况:
测试代码:
class A {
static int a = 0;
static {
System.out.println("a init");
}
}
class B extends A {
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b init");
}
}
验证(测试时请先全部注释,每次只执行其中一个)
public class Load3 {
// main方法的所在类总会被先初始化
static {
System.out.println("main init");
}
public static void main(String[] args) throws ClassNotFoundException {
// 1. 静态常量(基本类型和字符串)不会触发初始化
System.out.println(B.b);
// 2. 类对象.class 不会触发初始化
System.out.println(B.class);
// 3. 创建该类的数组不会触发初始化
System.out.println(new B[0]);
// 4. 不会初始化类 B,但会加载 B、A
ClassLoader cl = Thread.currentThread().getContextClassLoader();
cl.loadClass("cn.itcast.jvm.t3.B");
// 5. 不会初始化类 B,但会加载 B、A
ClassLoader c2 = Thread.currentThread().getContextClassLoader();
Class.forName("cn.itcast.jvm.t3.B", false, c2);
// 1. 首次访问这个类的静态变量或静态方法时
System.out.println(A.a);
// 2. 子类初始化,如果父类还没初始化,会引发
System.out.println(B.c);
// 3. 子类访问父类静态变量,只触发父类初始化
System.out.println(B.a);
// 4. 会初始化类 B,并先初始化类 A
Class.forName("cn.itcast.jvm.t3.B");
}
}
从字节码分析,使用 a,b,c 这三个常量是否会导致 E 初始化:
public class Load4 {
public static void main(String[] args) {
System.out.println(E.a); //NO
System.out.println(E.b); //NO
System.out.println(E.c); //Yes
}
}
class E {
public static final int a = 10;
public static final String b = "hello";
public static final Integer c = 20;
}
以上的实现特点是:
类加载的过程包括:加载、验证、准备、解析、初始化。其中验证、准备、解析统称为连接。
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个 Java 虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等
类加载器的优先级(由高到低):启动类加载器 -> 扩展类加载器 -> 应用程序类加载器 -> 自定义类加载器
public class ClassLoaderDemo01 {
public static void main(String[] args) {
ClassLoaderDemo01 demo01 = new ClassLoaderDemo01();
System.out.println(demo01.getClass().getClassLoader().getParent().getParent()); //Bootstrap根加载器
System.out.println(demo01.getClass().getClassLoader().getParent()); //扩展类加载器
System.out.println(demo01.getClass().getClassLoader()); //应用类加载器
}
}
//null 由于跟加载器是c++写的所以,我们的java访问不到!
//sun.misc.Launcher$ExtClassLoader@1b6d3586
//sun.misc.Launcher$AppClassLoader@18b4aac2
java_home/jre/lib
下的jar包,比如rt.jar
(含有全部java api的类),根加载器用C/C++实现,用null表示,在java代码中无法获取到根加载器。package cn.itcast.jvm.t3.load;
public class G {
static {
System.out.println("classpath G init");
}
}
程序执行:
public class Load5_2 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G");
System.out.println(aClass.getClassLoader());
}
}
输出结果:
classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2 // 这个类是由应用程序加载器加载
写一个同名的类:
package cn.itcast.jvm.t3.load;
public class G {
static {
System.out.println("ext G init");
}
}
打个 jar 包:
E:\git\jvm\out\production\jvm>jar -cvf my.jar cn/itcast/jvm/t3/load/G.class // 将G.class打jar包
已添加清单
正在添加: cn/itcast/jvm/t3/load/G.class(输入 = 481) (输出 = 322)(压缩了 33%)
将 jar 包拷贝到JAVA_HOME/jre/lib/ext
(扩展类加载器加载的类必须是以jar包方式存在),重新执行 Load5_2
输出:
ext G init
sun.misc.Launcher$ExtClassLoader@29453f44 // 这个类是由扩展类加载器加载
用来加载System.getproperty(“java.class.path”)也就是我们常说的classpath下的类,此路径下都是应用程序的类,所以也可称为应用程序类加载器,它的父加载器是扩展类加载器,classLoader.getSystemClassLoader()返回的就是系统类加载器
以上就是java的核心类 + 对核心类补丁 + 用户自定的class,构成了我们java的基本盘!
所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查该类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 2. 有上级的话,委派上级 loadClass
c = parent.loadClass(name, false);
} else {
// 3. 如果没有上级了(ExtClassLoader),则委派
BootstrapClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long t1 = System.nanoTime();
// 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
c = findClass(name);
// 5. 记录耗时
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
例如:
public class Load5_3 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Load5_3.class.getClassLoader()
.loadClass("cn.itcast.jvm.t3.load.H");
System.out.println(aClass.getClassLoader());
}
}
执行流程为:
我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写
Class.forName("com.mysql.jdbc.Driver")
也是可以让 com.mysql.jdbc.Driver 正确加载的,你知道是怎么做的吗? 让我们追踪一下源码:
public class DriverManager {
// 注册驱动的集合
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers
= new CopyOnWriteArrayList<>();
// 初始化驱动
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
先不看别的,看看 DriverManager 的类加载器:
System.out.println(DriverManager.class.getClassLoader());
打印 null,表示它的类加载器是 Bootstrap ClassLoader,会到 JAVA_HOME/jre/lib 下搜索类,但 JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包,这样问题来了,在 DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?
继续看 loadInitialDrivers()
方法:
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// 1)使用 ServiceLoader 机制加载驱动,即 SPI
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
// 2)使用 jdbc.drivers 定义的驱动名加载驱动
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
// 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器
Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
先看 2)发现它最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此 可以顺利完成类加载
再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)
定如下,在 jar 包的 META-INF/services
包下,以接口全限定名名为文件,文件内容是实现类名称
这样就可以使用:
ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allImpls.iterator();
while(iter.hasNext()) {
iter.next();
}
来得到实现类,体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:
接着看 ServiceLoader.load 方法:
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取线程上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由 Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader 的内部类 LazyIterator 中:
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service, "Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service, "Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service, "Provider " + cn + " could not be instantiated", x);
}
throw new Error(); // This cannot happen
}
问问自己,什么时候需要自定义类加载器:
步骤:
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/m0_46571920/article/details/121622105
内容来源于网络,如有侵权,请联系作者删除!