1、类的加载参考
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在方法区的内存中,然后再堆区创建一个java.lang.class对象,用来封装类再方法区中的数据结构。类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它2、类的生命周期
image.png
- 加载
在加载阶段,虚拟机需要完成以下三件事情:
a.通过一个类的全限定名来获取其定义的二进制字节流。
b.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
c.在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。加载完成后,虚拟机外部的二进制字节流就按虚拟机要求的格式存在方法区中,同时在堆区创建了一个java.lang.class对象用来访问方法区中的数据。 - 链接
将java类的二进制代码合并到jvm的运行状态之中的过程。
验证:确保加载的类信息符合jvm规范,没有安全方面的问题。
准备:正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。如public static int a = 8080,a在准备的值为0,但如果是final修饰的则是8080。
解析:虚拟机常量池内的符号引用替换为直接引用的过程。(比如String s ="aaa",转化为 s的地址指向“aaa”的地址)。。符号引用就是class文件中的CONSTANT_Class_info、CONSTANT_Field_info、CONSTANT_Method_info 等类型的常量。 - 初始化
为类的静态变量赋予正确的初始值,只有当对类的主动使用的时候才会导致类的初始化
初始化步骤:
a、假如这个类还没有被加载和连接,则程序先加载并连接该类
b、假如该类的直接父类还没有被初始化,则先初始化其直接父类
c、假如类中有初始化语句,则系统依次执行这些初始化语句
2.1 几种JVM常量池 参考
主要分为class文件常量池、运行时常量池、全局字符串常量池、基本类型包装类常量池
文件常量池
class文件是一组以8位字节为单位的二进制字节流,class文件存在非运行时常量池即文件常量池,主要用来存储字面量和符号引用
字面量:指文本字符串,也就是我们经常声明的:public String s = "abc"中的"abc";用final修饰的成员变量,包括静态变量、实例变量和局部变量。而对于基本类型数据(甚至是方法中的局部变量),private int value = 1;常量池中只保留了他的的字段描述符I和字段的名称value,他们的字面量不会存在于常量池。
符号引用:指类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。运行时常量池
在加载的第二个阶段将字节流的静态存储结构转换为方法区的运行时数据结构,就包含了文件常量池进入运行时常量池的过程,运行时常量池存储了文件常量池的符号信息和符号引用,同时在类加载的“解析阶段”还会将这些符号引用所翻译出来的直接引用(直接指向实例对象的指针)存储在 运行时常量池中全局字符串常量池
不同于基本数据类型,String类型是一个final对象,他的字面量存在于class文件常量池中,但是运行期行为却与普通常量不同,JDK 1.7中,字符串常量池和类引用被移动到了Java堆中(与运行时常量池分离)。
3、对象的创建过程
image.png
- 类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。 - 分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有“指针碰撞”和“空闲列表”两种,选择那种分配方式由 Java 堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
image - 初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。 - 设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。这些信息存放在对象头中。另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。 - 执行 init 方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
4、类加载器
在类加载的第一个阶段“加载”过程中,需要通过一个类的全限定名来获取此类的二进制字节流,完成这个动作的代码块就是类加载器。
- 启动类加载器
Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。 - 扩展类加载器
Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。 - 应用程序类加载器
Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
JVM类加载机制
- 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
- 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
- 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
5、双亲委派模型
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
5.1 优点
一个可以避免类的重复加载,另外也避免了java的核心API被篡改。
5.2 实现
public abstract class ClassLoader {
/**
java.lang.ClassLoader的loadClass()方法中,
先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,
若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载失败,则抛出ClassNotFoundException异常后,
再调用自己的findClass()方法进行加载。*/
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查类是否已加载
Class> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果找不到类,则引发ClassNotFoundException
// 从非空父类加载器
}
if (c == null) {
// 如果仍然找不到,则按顺序调用findclass
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// 这是定义类加载器;记录统计信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
5.3 JDBC和双亲委派模型关系 参考
在JDBC4.0以后,开始支持使用spi的方式来注册这个Driver,具体做法就是在mysql的jar包中的META-INF/services/java.sql.Driver 文件中指明当前使用的Driver是哪个。Class.forName()加载用的是调用者的Classloader,这个调用者DriverManager是在rt.jar中的,ClassLoader是启动类加载器,而com.mysql.jdbc.Driver肯定不在
解决办法,mysql的驱动加载过程:
第一,获取线程上下文类加载器Thread.setContextClassLoaser(),线程上下文类加载器让父级类加载器能通过调用子级类加载器来加载类,从而也就获得了应用程序类加载器(也可能是自定义的类加载器)
第二,从META-INF/services/java.sql.Driver文件中获取具体的实现类名“com.mysql.jdbc.Driver”
第三,通过线程上下文类加载器去加载这个Driver类,从而避开了双亲委派模型的弊端
6、类的实例化顺序(原文地址)
先静态,先父后子;先静态:父静态>子静态;优先级:父类>子类静态代码块>非静态代码块>构造函数。
- 父类中的static代码块,当前类的static代码块(注意代码块并不指静态方法);
- 顺序执行父类的普通代码块;
- 父类的构造函数;
- 子类普通代码块;
- 子类的构造函数,按顺序执行;
- 子类方法的执行。静态代码块只执行一次无论你创建几次对象。
7、String对象的两种创建方式
public class StringDemo4 {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "world";
String s3 = "helloworld";
System.out.println(s3 == s1 + s2);// false,s1与s2相加是先在字符串常量池中开一个空间,
//然后拼接,这个空间的地址就是s1与s2拼接后的地址。与s3的地址不同,所以输出为false。
System.out.println(s3.equals((s1 + s2)));// true
System.out.println(s3 == "hello" + "world");//true,s3与”hello”+”world”作比较,”hello”+”world”先拼接成”helloworld”,
//然后再去字符串常量池中找是否有”helloworld”,有,所以和s3共用一个字符串对象,则为true。
System.out.println(s3.equals("hello" + "world"));// true
}
}
字符串拼接会重新创建对象。
java基本数据类型Byte、Short、Integer、Long、Character、Boolean实现了常量池,这5类数据创建了[128,127]的相应类型的缓存数据,超过此范围仍然需要去创建新的对象,Float、Double没有实现常量池。
8、类加载过程
a. 在初始化类的时候,首先加载父类的静态变量和静态语句块(按照代码上下顺序执行),然后加载子类的,然后加载父类的非静态变量和非静态语句块(按照代码上下顺序执行),最后加载父类的构造函数,然后加载子类的非静态变量和非静态语句块(按照代码上下顺序执行),最后加载子类的构造函数。类的静态方法和非静态方法只有在主动调用的时候才执行。
b. 如果父类存在,子类可以不存在;如果子类存在,父类必须存在。
c. 类加载机制首先是分配内存空间(堆空间,物理存储地址,每个属性都需要分配物理空间,方法是不需要的,且这个时候物理空间指向的是空null);
当空间分配好后,进行属性初始化,把值放在栈空间中,前面的第一步过程中物理空间存储地址指向这个栈空间,这样就完成了属性值的初始化;当属性值完成了初始化的时候,就开始调用构造函数了,执行构造函数里面的代码块。
d. 在子类初始化的时候必须先去初始化父类,给子类和所有的父类都分配了内存空间后,先搞定堆内存指向null,才会去进行属性值的初始化,也就是在栈空间里面是属性的内容,前面分配的内存空间地址这个时候就指向栈内存的值;同名属性不会被子类给覆盖掉,只是把父类的隐藏掉;同名方法是多态,只会去调用子类的重载方法(也就是说父类引用指向子类对象时,父类引用调用的时子类重载的方法。)。
9、java反射
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
8.1获取class对象的方式
- 通过调用forName(String className),注意className的格式为:包名.类名获得Class对象。
- 通过类获取Class对象,String.class、int.class这些字节码是Class类的对象。
- 通过类的对象获取Class对象。
public static void getClass1() throws ClassNotFoundException {
//获取Class的方法
//正射
Student stu=new Student();
//第一种(优先选择):传的是包名.类名,通过包名和类名获得反射Class
Class cl1=Class.forName("day20.Student");
//第二种:通过类来获取Class对象
Class cl2=Student.class;
//第三种:通过对象获取Class对象
Student s=new Student();
Class cl3=s.getClass();
//判断三种方法是否是同一个Class对象
System.out.println(cl1==cl2);
System.out.println(cl2==cl3);
}