1. JVM内存结构概述
2. 类加载子系统
2.1 类加载过程
Class文件需要加载到虚拟机中之后才能运行,那JVM是如何加载这些Class文件的呢 ?
2.1.1 加载
2.1.1.1 步骤
- 通过一个类的全限定名获取定义此类的二进制字节流。
将这个字节流所代表的 静态存储结构 转化为方法区的运行时数据结构。
(JDK<=1.7,方法区 的实现方式为:永久代。JDK>=1.8, 方法区的实现方式为:元空间)
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法去这个类的各种数据的访问入口。(此步骤生成大的Class实例,在内存中)
2.1.1.2 加载.class文件的方式
2.1.2 链接
2.1.2.1 验证
2.1.2.1.1 文件格式验证
2.1.2.1.2 元数据验证
2.1.2.1.3 字节码验证
2.1.2.1.4 符号引用验证
2.1.2.2 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。这些内存都将在方法区中分配。
- 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
这里所设置的初始值”通常情况”下是数据类型默认的零值(如0、0L、null、false等),比如我们定义了public static int value=111 ,那么 value 变量在准备阶段的初始值就是 0 而不是111(初始化阶段才会赋值)。
特殊情况:比如给 value 变量加上了 fianl 关键字public static final int value=111 ,那么准备阶段 value 的值就被赋值为 111。(因为final修饰的变量在编译器即被赋值)
2.1.2.3 解析
2.1.3 初始化
初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码),初始化阶段是执行初始化方法 <clinit> ()
方法的过程 ;
此方法不需要定义,是javac编译器自动收集 类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
- 也就是说,当我们的代码中包含 static 变量的时候,就会有
()方法。
重点 : 只有当类中包含 类变量 和静态代码块 时 才会有这个
静态代码块在此步骤执行。类变量在此环节被显示赋值。
2.1.3.1 触发初始化的情景
对于初始化阶段,虚拟机严格规范了有且只有5种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):
- 当遇到 new 、 getstatic、putstatic或invokestatic 这4条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
- 当jvm执行new指令时会初始化类。即当程序创建一个类的实例对象。
- 当jvm执行getstatic指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
- 当jvm执行putstatic指令时会初始化类。即程序给类的静态变量赋值。
- 当jvm执行invokestatic指令时会初始化类。即程序调用类的静态方法。
- 使用
java.lang.reflect
包的方法对类进行反射调用时如Class.forname(“…”),newInstance()等等。 ,如果类没初始化,需要触发其初始化。 - 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
- 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
- MethodHandle和VarHandle可以看作是轻量级的反射调用机制,而要想使用这2个调用, 就必须先使用findStaticVarHandle来初始化要调用的类。
2.2 类加载器分类
2.2.1 启动类加载器
- 这个类加载使用C/C++语言实现的,嵌套在JVM内部
- 它用来加载Java的核心类库 (JAVA_HOME/jre/lib/rt.jar、resource.jar或sum.boot.class.path路径下的内容),用于提供JVM自身需要的类(JDK最核心的源代码都在rt.jar (runtime) 中)
- 并不继承自java.lang.ClassLoader,没有父加载器
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器
(这里的父类,不是继承的关系。加载的路径明显表示 是相互独立的);
- 由于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
2.2.1.1 加载路径
``` @Test public void test() {
}URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL urL : urLs) {
System.out.println(urL.toExternalForm());
}
file:/D:/dev/JDK/jre/lib/resources.jar file:/D:/dev/JDK/jre/lib/rt.jar file:/D:/dev/JDK/jre/lib/sunrsasign.jar file:/D:/dev/JDK/jre/lib/jsse.jar file:/D:/dev/JDK/jre/lib/jce.jar file:/D:/dev/JDK/jre/lib/charsets.jar file:/D:/dev/JDK/jre/lib/jfr.jar file:/D:/dev/JDK/jre/classes
<a name="HF4Id"></a>
### 2.2.2 扩展类加载器
- [x] Java语言编写,由sum.music.Launcher$ExtClassLoader实现
- [x] 派生于ClassLoader类
- [x] 父类加载器为启动类加载器
- [x] 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的**jre/lib/ext子目录(扩展目录)下加载类库**。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载.
<a name="tlyJV"></a>
#### 2.2.2.1 加载路径
@Test public void test() { String property = System.getProperty(“java.ext.dirs”); for(String path:property.split(“;”)){ System.out.println(path); } }
D:\dev\JDK\jre\lib\ext
C:\Windows\Sun\Java\lib\ext
```
2.2.3 应用程序类加载器
- java语言编写,由sum.misc.Launcher$AppClassLoader实现
- 派生于ClassLoader类
- 父类加载器为扩展类加载器
- 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
-
**该类加载是程序中默认的类加载器**
,一 般来说,Java应用的类都是由它来完成加载 - 通过ClassLoader.getSystemClassLoader()方法可以获取到该类加载器
2.3 双亲委派机制
2.3.1 工作原理
2.3.2 双亲委派机制的优势
- 避免类的重复加载
- 保护程序安全,防止核心API被恶意篡改。(沙箱安全机制)
在自定义的java.lang下新建类时,也是不被允许的。
2.3.3 破坏双亲委派机制
2.3.3.1 为何要破坏双亲委派机制
首先要明确的是 双亲委派机制存在缺陷,2.3.3.2 如何破坏双亲委派机制
https://blog.csdn.net/awake_lqh/article/details/1061712192.4 沙箱安全机制
举例说明:
我们自定义一个String类,包名设置为:java.lang.String
运行main方法发现直接报错。
原因:自定义String类加载时,根据双亲委派机制,最后时候启动类加载器加载rt.jar下的String类,并非我们自定义的类,所以直接报错提示无main方法。
2.5 如何判断两个Class对象是否相同
在JVM中表示2个Class对象是否为同一个类存在2个必要条件:
类的完整类名必须一致,包括包名。
加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
2.6 对类加载器的引用(具体需要在动态链接中理解)
2.7 类的主动引用和被动引用
引用
https://blog.csdn.net/sj15814963053/article/details/109851454 | 类加载器子系统写的很全面 |
---|---|
推荐IDEA插件:jclasslib ByteCode Viewer 可以查看类的字节码 |