关于jvm的内存模型以及java的类加载过程在面试时总是被问到,所以趁着最近正好有时间可以记录一下自己的学习历程。
首先给大家介绍一个在GitHub上star 24k以上的一个关于java学习的开源项目
hub地址:https://github.com/Snailclimb/JavaGuide
关于类加载的过程我也是初学,所以如果有说的错误的或者是不全的希望大家能在评论处指出。
程序启动的过程为:
类加载的过程为 :
jvm默认的三大加载器为:
本地应用类加载器(AppClassloader)-》扩展类加载器-》(ExtClassloader(主要加载jre中扩展目录的jar包))-》启动类加载器(BootstrapClassloader(主要加载bin目录下的jar包))
三者的关系如下:
加载器的执行机制为:
双亲委派。(
例如用户自定义了一个类,全限定名称为 java.lang.String类,当jvm加载该类时,首先会让AppClassloader类进行加载(因为是用户自定义的),AppClassloader会先检查是否已经加载过该类,如果没有,会委托给它的上级类ExtClaaLoader进行加载,ExtClaaLoader类也会进行检查是否已经加载过这个类,如果没有就会再委托给它的上级类BootstrapClassloader类进行加载BootstrapClassloader也会进行检查是否已加载该类,很明显在加载jre bin目录下的jar包时就已经加载了系统中的类,所以不会再加载用户自定义的string类。
但是在使用tomact的web应用中,webapp会首先使用appclassloader加载classes目录下的类,完了再进行委派。(不知道我说的对不对,还请指正)
注:上述三个加载器之间的关系并不是继承关系,而是组合关系,类变量中都有各自的引用。
)
整个字节码加载到jvm内存中的过程如下:
加载(上述已阐明)-》连接-》-》初始化
连接又包含三个部分(我理解的不是很深,还请指正):
验证-》准备-》解析。其中
(1)验证: 参考地址(https://www.cnblogs.com/xiaoxian1369/p/5498817.html)
验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等。
格式验证:验证是否符合class文件规范
语义验证:检查一个被标记为final的类型是否包含子类;检查一个类中的final方法视频被子类进行重写;确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同)
操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否通过符号引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)
(2)准备:正式为类变量(static变量)分配内存并设置类变量初始值(不是指定值)的阶段,这些内存都将在方法区中进行分配。 (3)解析:虚拟机常量池的符号引用(例如方法的全限定名称)替换为字节引用(真实的地址值)过程
初始化:
- 初始化阶段是执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化(jvm控制)
- 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步
下面说一个很有意思的问题:
代码示例:
public class Student {
private String stuName;
private Integer age;
private static Integer stuClassNum = 020;//班级信息
static {
System.out.println("hello word");
}
public Student(String stuName, Integer age) {
this.stuName = stuName;
this.age = age;
}
public Student() {
}
public String getStuName() {
return stuName;
}
public void setStuName(String stuName) {
this.stuName = stuName;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public static Integer getStuClassNum() {
return stuClassNum;
}
public static void setStuClassNum(Integer stuClassNum) {
Student.stuClassNum = stuClassNum;
}
}
public class ClassLoadingTest {
public static void main(String[] args) {
//创建一个学生类对象
/* Student student = new Student(); //空参构造
Student student1 = new Student("xiaoming", 25); //含参构造*/
// 比较两种获取类对象的方式是否都会打印 Student类中的静态代码块的内容
Class cl1 = Student.class; //获取类对象方式一
try {
Class cl2 = Class.forName("Student"); //方式二
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
猜猜,main方法中 方式一和方式二哪个可以输出helloword?答案是方式二,因为方式一在类加载过程中并没有进行初始化,而静态代码块的执行只会在初始化时进行。