前言
本专栏系列文章仅用于个人学习总结,从零开始逐步到常见服务框架。觉得基础的大佬可以提前离开。欢迎各位大佬评论指教,如有不当之处请及时联系调整 ~
本专栏工作之余抽空更新…
上一篇文章地址:1.java基础篇
下一篇文章地址:3.线程、并发相关
java基础进阶
1.Java中的异常体系
Java中的所有异常都来自顶级父类Throwable。
- Throwable下有两个子类Exception和Error。
- Error是程序无法处理的错误,一旦出现这个错误,则程序将被迫停止运行。
- Exception不会导致程序停止,又分为两个部分RunTimeException运行时异常和CheckedException检查异常。
- RunTimeException常常发生在程序运行过程中,会导致程序当前线程执行失败。
- CheckedException常常发生在程序编译过程中,会导致程序编译不通过。
2.什么是字节码?采用字节码的好处是什么?
java中的编译器和解释器:
Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。
编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做 字节码(即扩展名为 .class的文件),它不面向任何特定的处理器,只面向虚拟机。
每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。这也就是解释了Java的编译与解释并存的特点。
Java源代码---->编译器---->jvm可执行的Java字节码(即虚拟指令)---->jvm---->jvm中解释器----->解释class为机器可执行的二进制机器码---->程序运行。
① 解释型语言: 例如 python/js/Shell ,程序在运行时才翻译成机器语言,每执行一次翻译一次(效率较低)。
② 而java是编译(未运行已生成class)和解释分成两步来做。
采用字节码的好处:
Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。
3.Java类加载器
JDK自带有三个类加载器: bootstrap ClassLoader、ExtClassLoader、AppClassLoader。
- BootStrapClassLoader是ExtClassLoader的父类加载器,默认负责加载%JAVA_HOME%lib下的核心jar包(jre/lib/rt.jar等)和class文件。(顶级,jvm内置的引导类加载器)
- ExtClassLoader是AppClassLoader的父类加载器,负责加载%JAVA_HOME%/lib/ext文件夹下的jar包和class类。(扩展类加载器)
- AppClassLoader是自定义类加载器的父类,负责加载classpath下的类文件(我们写的java文件,或者依赖的jar)。又称系统类加载器,线程上下文加载器(贯穿所有类加载器,大家都可以访问)。
① 想要实现自定义类加载器需要继承 ClassLoader。(ClassLoader的构造函数中,参数调用了getSystemClassLoader()方法,将AppClassLoader设置为自定义类加载器的父类加载器)
② 三者之间是父子构造器关系(通过一个变量parent去维护),并非java的‘继承’关系。
类加载过程:
当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到JVM。
类加载过程有如下几步:
加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载
- 加载: 在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
- 验证: 校验字节码文件的正确性。
- 准备: 给类的静态变量分配内存,并赋予默认值。(不是我们定义的值,而是 0 null之类的)
- 解析: 将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成)
ps:动态链接是在程序运行期间将非静态方法的符号引用变为 地址引用。 - 初始化: 对类的静态变量初始化为指定的值,执行静态代码块。
- 使用:当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。
- 卸载:当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。
① 该过程也说明了静态修饰的在程序启动加载时,会预先分配内存,所以静态方法能直接用 类名.方法 调用。
② 类被加载到方法区中后主要包含 运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息。
- 类加载器的引用: 这个类到类加载器实例的引用
- 对应class实例的引用: 类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的对象实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。
查看类加载示例内容:(了解即可)
/**
* @author wook
* @date 2021/4/28 16:08
*/
public class TestJDKClassLoader {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader());
System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());
System.out.println();
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
ClassLoader extClassloader = appClassLoader.getParent();
ClassLoader bootstrapLoader = extClassloader.getParent();
System.out.println("the bootstrapLoader : " + bootstrapLoader);
System.out.println("the extClassloader : " + extClassloader);
System.out.println("the appClassLoader : " + appClassLoader);
System.out.println();
System.out.println("bootstrapLoader加载以下文件:");
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
System.out.println(urls[i]);
}
System.out.println();
System.out.println("extClassloader加载以下文件:");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println();
System.out.println("appClassLoader加载以下文件:");
System.out.println(System.getProperty("java.class.path"));
}
}
4.双亲委派机制
防止未接触过的同学看不懂,此处用大白话说明一下流程图(懂的跳过,讲的太细了):
① 假设A类执行一个main()方法,AppClassLoader不会直接去加载A类,它会查看是否已经有缓存,没有则“向上委托”(委托父容ExtClassLoader)。
② 看看父容器是否已经加载过A类,有则直接返回,否则继续“向上委托”(顶级父容器BootStrapClassLoader)
③ 看看顶级容器是否已经加载A类,有则直接返回,如果连顶级容器都找不到,则开始从“加载路径”找,看是否能加载到A类,有则加载并返回。否则准备“向下查找”。
④ 首先会经过ExtClassLoader,从它的“加载路径”找,有则加载并返回,否则继续“向下查找”,此时AppClassLoader终于有机会从它的“加载路径(classpath)”找A类了,有则加载并返回,若仍然找不到,报错:Class Not Found。
① 向上委托只是找缓存,并不执行加载操作;向下委托会去各自类加载器对应的‘加载路径’查找是否能加载A类。
② 第3大点java类加载器代码案例已提过各级容器的加载路径,忘记的可以翻一下。
③ 图中未列自定义类加载器,但是原理是一模一样的,此处不再赘述。
双亲委派模型的好处:
- 主要是为了安全性,避免用户自己编写的类动态替换 Java的一些核心类,比如 String。
package java.lang;
/**
* @author wook
* @date 2021/4/29 10:56
*/
public class String {
public static void main(String[] args) {
System.out.println("**************My String Class**************");
}
}
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
为避免有些同学看到这还是不懂,我在说明一下:
前面已经说了,类加载是先向上查找‘缓存’,所以该demo在父容器中找到了String并加载返回,而JDK定义的java是没有main方法的,所以此处报错。
- 同时也避免了类的重复加载,因为 JVM中区分不同类,不仅仅是根据全限定名,相同的 class文件被不同的 ClassLoader加载就是不同的两个类。
5.如何自定义类加载器?
自定义类加载器只需要继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是loadClass(String, boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空方法,所以我们自定义类加载器主要是重写findClass方法。
(了解完第3和第4大点后,建议和我一起尝试自定义,对加深理解帮助很大)
package com.wook.jvm;
import java.io.FileInputStream;
import java.lang.reflect.Method;
/**
* @author wook
* @date 2021/4/30 11:17
*/
public class MyClassLoaderTest {
static class MyClassLoader extends ClassLoader {
/**
* 设置当前自定义容器的“加载路径”
*/
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
/**
* 根据文件名从磁盘路径查找该类,并返回其字节数组
* @param name 类的全限定名
* @return 类的字节数组
*/
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
/**
* 从磁盘路径找到要加载的class(如果“向上委托”和“向下查找”无法从缓存和加载路径找到该class,则从我们自定义的类加载器“加载路径”加载)
* @param name 类的全限定名
* @return
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
//将class的字节数组传给 defineClass加载并放入内存(类元信息等)。
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}
public static void main(String args[]) throws Exception {
//初始化自定义类加载器,会先初始化父类ClassLoader,其父类会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
MyClassLoader classLoader = new MyClassLoader("D:/test");
//D盘创建 test/com/wook/jvm 几级目录,将WookJvmTest类的字节码文件WookJvmTest.class丢入该目录
//要注意的是,该class文件的 package头,要和你创建的目录一致
Class clazz = classLoader.loadClass("com.wook.jvm.WookJvmTest");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("fun");
method.setAccessible(true);
method.invoke(obj);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
结果:自定义的类加载器加载完WookJvmTest,用其实例来调用我啦
父加载器:sun.misc.Launcher$AppClassLoader
package com.wook.jvm;
/**
* @author wook
* @date 2021/4/30 14:21
*/
public class WookJvmTest {
private void fun(){
System.out.println("自定义的类加载器加载完WookJvmTest,用其实例来调用我啦");
}
}
五一高铁上补充的demo,有学到的同学点个赞吧~
6.GC如何判断对象可以被回收
- 引用计数法: 每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。(java未采用,很难解决对象之间相互循环引用的问题)
public class ReferenceCountingGc {
Object instance = null;
public static void main(String[] args) {
ReferenceCountingGc objA = new ReferenceCountingGc();//A引用1次
ReferenceCountingGc objB = new ReferenceCountingGc();//B引用1次
objA.instance = objB;//B引用2次
objB.instance = objA;//A引用2次
objA = null;//A与ReferenceCountingGc断开引用,变为1次
objB = null;//B与ReferenceCountingGc断开引用,变为1次
}
结果:此时虽然A和B都为null,理应算是垃圾被回收,但是他们的成员变量相互引
用着计数器=1,而计数只有0时才可以回收,导致垃圾无法回收!!
}
- 可达性分析法: 从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GCRoots 没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就判断是可回收对象。(例A- ①->B- ②->C,①如果断开了,则B C都应回收)
GC Roots的对象包含:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。(比如main()方法中new User(),这个user就算一个根 )
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。(final修饰)
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会。
对象被系统宣告死亡至少要经历两次标记过程:
- 第一次是经过可达性分析发现没有与GC Roots相连接的引用链。
- 第二次是在由虚拟机自动建立的Finalizer队列中判断是否需要执行finalize()方法。
当对象变成(GC Roots)不可达时:(了解即可)
- ① GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。
- ② 否则,判断对象是否未执行过finalize方法,执行过直接回收。
- ③ 未执行过,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。
- ④ 执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。(需要在重写方法中:其重新引用gcRoot)
import lombok.AllArgsConstructor;
import java.util.ArrayList;
import java.util.List;
/**
* @author wook
* @date 2021/5/1 15:36
*/
public class GCTest {
public static List<Object> list = new ArrayList<>();
public static void main(String[] args) throws Exception {
List<Object> list = new ArrayList<>();
for (int i = 0, j = 0; i < 100000; i++) {
//与gcRoot形成引用链的,不回收
list.add(new User(i++));
//与gcRoot未形成引用链,回收
new User(j--);
}
}
}
@AllArgsConstructor
class User {
private Integer id;
/**
* 可达性分析判定为需要回收后,想自救需要重写finalize方法
*/
@Override
protected void finalize() throws Throwable {
//将该注释开放出来,令其重新链上gcRoot,完成自救
//GCTest.list.add(this);
System.out.println("关闭资源,userid=" + id + "即将被回收");
}
}
① 每个对象只能触发一次finalize()方法。
② 由于finalize()方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不推荐大家使用。
方法区主要回收的是无用的类,那么如何判断一个类是无用的类呢?(了解即可)
类需要同时满足下面3个条件才能算是 “无用的类” :
- 该类所有的对象实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
第2点非常苛刻 !jvm的类加载器加载了很多类,难道因为User没了,就要回收jvm的类加载器?? 一般只有自定义的,比如tomcat会出现该情况,每一个jsp它都对应一个Tomcat类加载器,如果热更新,其每次都会去新建类加载器,回收旧的。