从本篇文章开始进入JVM的学习,前面我们介绍了JAVA、JVM等等
这篇文章我们从类加载子系统开始进入学习
请先看以下的简图,class Files我们称为字节码,从字节码开始后续操作都需要JVM负责
第一步:我们要将Class文件加载到内存当中,而类加载需要用到类加载子系统Class Loader来进行加载
同时对应到我们的内存当中,生成一个大的Class对象并且将必要的静态属性进行初始化等等(方法区提现)
第二步:当我们真正去执行字节码指令的时候,就需要执行引擎去发挥作用,按照我们程序的字节码指令去依次执行(涉及到虚拟机栈里去局部变量表取数据,以及操作入栈),若需要创建对象的话还需要用到堆空间
第三步:当程序继续往下走的时候,还会用到程序计数器,若用到本地的C类库,还需要用到本地方法栈
根据详细图,我们可以看到类加载子系统分三个部分
如图所知加载阶段分三个环节:引导类、扩展类、系统类等加载器
紧接着就是静态变量的一个显示初始化,接下来就将每个字节码文件要用到的,在对应的在内存中把类或者接口加载进来
在内存层面运行时数据区有:PC寄存器(程序计数器)、栈(虚拟机栈)、本地方法栈、堆区、方法区
PC寄存器:每一个线程一份
虚拟机栈:每一个线程一份,每一个线程用的栈里面一个一个结构称为栈桢,栈桢又分为局部变量表、操作数栈、动态链接、方法返回地址等
本地方法栈:涉及到本地方法接口API调用叫本地方法栈
堆区:主要应对Java对象等都放在堆空间中,也是GC重点考虑的一个空间因为堆区会被线程共享的
方法区:主要存放类的信息(常量、方法信息等等)都放在方法区
注意:方法区只有HotSpot虚拟机有,J9,JRockit都没有
执行引擎又分解释器、即时编译器、垃圾回收器,将我们的指令变成机器指令供CPU去执行,要想和操作系统打交道需要关注执行引擎打交道
若想真正了解一个虚拟机,可以手写一个虚拟机
如果自己想手写一个Java虚拟机的话,主要考虑类加载器、执行引擎结构
我们刚刚提到类加载子系统呢分三个阶段:加载、链接、初始化等阶段
JAVA虚拟机提到说明:任何语言可以考虑用直接的编辑器生成符合Java虚拟机规范的Class文件来在Java虚拟机进行解释运行
1.class file(在上图中就是Car.class文件)存在于本地硬盘上,通过类加载器把它加载到内存运行时数据区
2.class file加载到JVM中后会被称为DNA元数据模板放在方法区
3.car.class文件可以调用getClassLoader()方法获取加载此类的加载器,同时可以根据car.class的构造器创在堆空间中创建多个对象
4.对应的对象可以通过getClass()获取到类的本身,知道由那个类创建的对象
而本地磁盘的Class文件是由二进制流的方式加载到内存中,类加载器起到快递员的身份
接下来我们使用一段代码来体会一下加载过程
public class HelloLoader {
public static void main(String[] args) {
System.out.println("谢谢ClassLoader加载我....");
System.out.println("你的大恩大德,我下辈子再报!");
}
}
//运行结果如下:
谢谢ClassLoader加载我....
你的大恩大德,我下辈子再报!
那么我们的这个HelloLoader类,它的加载过程是怎么样的呢?
接下来我们对类加载器进行加载、链接、初始化不同阶段进行展开,看看做了哪些事情
我们一起看看加载阶段的一些加载说明:
在内存中生成一个代表这个类的java.lang.Class对象
,作为方法区这个类的各种数据的访问入口那么对于一些加载.class文件的方式我们可以进行一些举例说明
前面我们提到过链接阶段分三个环节:验证、准备、解析
对于前面的加载阶段完成后
,我们就已经生成了一个比较大的Class对象
,第一个验证环节主要做以下几件事情
确保Class文件的字节流中包含信息符合当前虚拟机要求
,保证被加载类的正确性,不会危害虚拟机自身安全文件格式验证,元数据验证,字节码验证,符号引用验证
。我们可以将上面举例的HelloLoader类查看它的字节码
使用 BinaryViewer软件查看字节码文件,能被java虚拟机识别的其开头均为 CAFE BABE
class文件在文件开头有特定的文件标识说的就是这
如果发现你不是一个合法的字节码文件,那么将会验证不通过
刚刚介绍的是验证环节,接下来是链接阶段的准备环节介绍主要以下事情
注意:这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中
我们可以举个例子一起来看看static静态变量在准备阶段的初始值
public class HelloApp {
//prepare:a = 0 ---> initial : a = 1
private static int a = 1;
public static void main(String[] args) {
System.out.println(a);
}
}
刚刚介绍的是准备环节,接下来是链接阶段的解析环节介绍主要以下事情
我们可以反编译 class 文件后可以查看符号引用,下面带# 的就是符号引用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CUY2C2ct-1618752754833)(https://segmentfault.com/img/bVcPcSW)]
当执行完加载阶段、链接阶段到达初始化阶段时,就会执行类构造器方法()的过程。
此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来
。
我们可以举例一个示例代码一起来看看具体的()
public class ClassInitTest {
private static int num = 1;
public static void main(String[] args) {
System.out.println(a);
}
}
当然我们运行后会输出:1,这时我们使用 BinaryViewer软件查看字节码文件的()方法里有什么
我们刚刚()是对所有类变量的赋值动作和静态代码块中的语句合并而来
若我们没有类变量它会出现吗?
public class ClinitTest {
private int a = 1;
public static void main(String[] args) {
System.out.println(a);
}
}
这时我们再使用使用BinaryViewer软件查看字节码文件的()方法里有什么
你就会发现没有,这就说明没有类变量的赋值动作和静态代码块中的语句它就不会有
我们之前说任何一个类声明后,至少存在一个类的构造器,使用看看并且观察一下
public class ClinitTest {
private int a = 1;
public ClinitTest(){
a =10;
int d =20;
}
public static void main(String[] args) {
System.out.println(a);
}
}
那么我们使用BinaryViewer软件查看字节码文件init方法里有什么
构造器方法中指令按语句在源文件中出现的顺序执行。
而()不同于类的构造器。(关联:构造器是虚拟机视角下的())
并且当若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕。
我们举例一个示例代码进行说明这种情况并通过字节码观察看看是否这样
public class ClinitTestl {
static class Father {
public static int A = 1 ;
statict{
A=2;
}
}
static class Son extends Father {
public static int B= A;
}
public static void main (String[] args){
//加载Father类,其次加载Son类。
System.out.println(Son.B);//2
}
}
当我们执行执行 main() 方法需要加载 ClinitTest1 类,再调用另一个类Son的静态变量所以此时需要加载 Son 类(此时执行()方法)但是在此之前需要执行父类的加载,一起来看看字节码是怎么样的
以及虚拟机必须保证一个类的()方法在多线程下被同步加锁。
也就是说保证我们的类只加载一次
我们可以使用示例代码来体会一下这个说法
class DeadThread{
static{
if(true){
System.out.println(Thread.currentThread().getName() + "初始化当前类");
while(true){
}
}
}
}
public class DeadThreadTest {
public static void main(String[] args) {
Runnable r = () -> {
System.out.println(Thread.currentThread().getName() + "开始");
DeadThread dead = new DeadThread();
System.out.println(Thread.currentThread().getName() + "结束");
};
Thread t1 = new Thread(r,"线程1");
Thread t2 = new Thread(r,"线程2");
t1.start();
t2.start();
}
}
//运行结果如下:
线程2开始
线程1开始
线程2初始化当前类
//程序卡死了...
当我们的两个线程同时去加载 DeadThread 类,先加载 DeadThread 类的线程抢到了同步锁,然后在类的静态代码块中执行死循环,而另一个线程在等待同步锁的释放
所以无论哪个线程先执行 DeadThread 类的加载,另外一个类也不会继续执行。(一个类只会被加载一次)
使用死循环是模拟虚拟机在加载的时候只执行一次,而其他线程进入阻塞状态
一般JVM支持支持两种类型的类加载器分别为
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器
但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器
所以将扩展类加载器、系统类加载器也认为是自定义类加载器
我们使用代码来体会一些这几种提到加载器
public class ClassLoaderTest {
public static void main(String[] args) {
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//获取系统类加载器其上层:扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d
//获取扩展类加载器其上层:获取不到引导类加载器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader);//null
}
}
那么对于这些加载器分别能加载哪些路径下的文件呢?
public class ClassLoaderTest1 {
public static void main(String[] args) {
System.out.println("**********启动类加载器**************");
//获取BootstrapClassLoader能够加载的api的路径
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL element : urLs) {
System.out.println(element.toExternalForm());
}
}
}
//运行结果如下:
**********启动类加载器**************
file: /D:/developer_tools/Java/jdk1.8.0_131/jre/lib/resources.jar
file: /D:/developer_tools/Java/jdk1.8.0_131/jre/lib/rt.jar
file: /D:/developer_tools/Java/jdk1.8.0_131/jre/lib/sunrsasign.jar
file: /D:/developer.tools/Java/jdk1.8.0_131/jre/lib/jsse.jar
file: /D:/developer.tools/Java/jdk1.8.0_131/jre/lib/jce.jar
file: /D:/developer.tools/Java/jdk1.8.0_131/jre/lib/charsets.jar
file: /D:/developer.tools/Java/jdk1.8.0_131/jre/lib/jfr.jar
file: /D:/developer.tools/Java/jdk1.8.0_131/jre/classes
我们可以打开路径下的jsee.jar包里的Class文件反查看加载器是什么
public class ClassLoaderTest1 {
public static void main(String[] args) {
//file: /D:/developer.tools/Java/jdk1.8.0_131/jre/lib/jsse.jar
//从路径中随意选择一个类,来看看他的类加载器是什么:引导类加载器
ClassLoader classLoader = Provider.class.getClassLoader();
System.out.println(classLoader);//运行结果:null
}
}
接下来我们接着看看扩展类的加载器有哪一些
public class ClassLoaderTest1 {
public static void main(String[] args) {
System.out.println("***********扩展类加载器*************");
String extDirs = System.getProperty("java.ext.dirs");
for (String path : extDirs.split(";")) {
System.out.println(path);
}
}
}
//运行结果如下:
***********扩展类加载器**** *** ******
D: \developer_tools\Java\jdk1.8.0_131\jre\lib\ext
C: \Windows\Sun\Java\lib\ext
同理我们打开文件路径通过Class文件反查一下加载器是什么
public class ClassLoaderTest1 {
public static void main(String[] args) {
//file: D:\developer_tools\Java\jdk1.8.0_131\jre\lib\ext
ClassLoader classLoader1 = CurveDB.class.getClassLoader();
System.out.println(classLoader1);
}
}
//运行结果如下:
sun.misc.Launcher$ExtClassLoader@1540e19d
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时我们还可以自定义类加载器,来定制类的加载方式。
public class CustomClassLoader extends ClassLoader {
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
try {
//将路径下的文件以流的形式存入到内存中
byte[] result = getClassFromCustomPath(name);
if (result == null) {
throw new FileNotFoundException();
} else {
//defineClass和findClass搭配使用
return defineClass(name, result, 0, result.length);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
throw new ClassNotFoundException(name);
}
//自定义流的获取方式
private byte[] getClassFromCustomPath(String name) {
//从自定义路径中加载指定类:细节略
//如果指定路径的字节码文件进行了加密,则需要在此方法中进行解密操作。
return null;
}
}
ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)
以下这些方法都不是抽象方法,可以具体的实现
我们可以根据代码示例体会看看一下
public class ClassLoaderTest2 {
public static void main(String[] args) {
try {
//1.
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
System.out.println(classLoader);
//2.
ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
System.out.println(classLoader1);
//3.
ClassLoader classLoader2 = ClassLoader.getSystemClassLoader().getParent();
System.out.println(classLoader2);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
//运行结果如下:
null
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1540e19d
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。
而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式
我们使用一个案例引入这个双亲委派机制,我们在自己的src路径下创建自己的java.lang.String类
public class String {
//
static{
System.out.println("我是自定义的String类的静态代码块");
}
}
这时我们在创建一个新的Test类来引用它,并且看看他的加载器是什么
public class StringTest {
public static void main(String[] args) {
java.lang.String str = new java.lang.String();
System.out.println("hello,atguigu.com");
StringTest test = new StringTest();
System.out.println(test.getClass().getClassLoader());
}
}
//运行结果如下:
hello,atguigu.com
sun.misc.Launcher$AppClassLoader@18b4aac2
我们发现程序并没有输出我们静态代码块中的内容,可见仍然加载的是 JDK 自带的 String 类。
这时我们将代码进行修改一下,再来运行起来看看是怎么样的输出结果
package java.lang;
public class String {
//
static{
System.out.println("我是自定义的String类的静态代码块");
}
//错误: 在类 java.lang.String 中找不到 main 方法
public static void main(String[] args) {
System.out.println("hello,String");
}
}
//运行结果如下:
错误:在类java.lang.String中找不到main方法,请将main方法定义为:
public static void main (String[] args)
否则JavaFX 应用程序类必须扩展javafx.application.Application
由于双亲委派机制一直找父类,所以最后找到了Bootstrap ClassLoader,Bootstrap ClassLoader找到的是 JDK 自带的 String 类,在那个String类中并没有 main() 方法,所以就报了上面的错误
接下来我们在创建一个示例来java.lang包下看看是否能运行起来
package java.lang;
public class ShkStart {
public static void main(String[] args) {
System.out.println("hello!");
}
}
//运行结果如下:
java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
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:335)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main"
即使类名没有重复,也禁止使用java.lang这种包名。这是一种保护机制
在比如我们使用加载jdbc.jar 用于实现数据库连接的时候需要用到SPI接口,而SPI接口属于rt.jar包中Java核心api
这个时候我们就要使用双清委派机制,引导类加载器把rt.jar包加载进来针对具体的第三方实现jar包时使用系统类加载器来加载
从这里面就可以看到SPI核心接口由引导类加载器来加载,SPI具体实现类由系统类加载器来加载
通过上面的例子,我们可以知道,双亲机制可以
当我们运行自定义String类main方法的时候出现了报错,这种其实就是沙箱安全机制,不允许你在程序中破坏核心的源代码程序
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
换句话说,在JVM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的
尚硅谷:JVM虚拟机(宋红康老师)
我是小白弟弟,一个在互联网行业的小白,立志成为一名架构师
https://blog.csdn.net/zhouhengzhe?t=1