既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上物联网嵌入式知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新
推荐一个更好的可视化工具,查看字节码的 IDEA 插件:jclasslib
可以很清晰的看到,常量池、接口、字段、方法、属性里面的字节码信息,比如:在对比循环体外实例化对象和循环体内实例化对象差异化时,可以通过该工具来查看两者的区别
class 文件到底是什么样的呢?
当一个 class 文件躺在硬盘上,该内容被 load 进内存以后,会创建两块内容
- 比如:String,将 String,class 二进制内容扔到了内存中
- 该内容于是乎生成了一个 class 对象(指令)该指令指向了生成好的二进制内容
class 存放在内存分区中,内存分区就是存常量、存 class 各种各样的信息,实际上它这块内容逻辑上叫 Method Area 方法区
JDK 8 之前该方法区实现落地在 PermGen 永久代中
JDK 8 之后该方法区实现落地在 Metaspace 元空间中
永久代、元空间两者之间的区别?
- 永久代:逻辑上属于堆,物理上不属于堆,存在于虚拟机中
- 元空间:逻辑上、物理上都属于堆,但其不存在于虚拟机中,它使用的是物理内存
Class 加载、链接、初始化
Loading 加载过程:将 class 文件加载进内存中,也就是对 class 文件中的一个个二进制字节码读取后进行装载
Linking 链接过程分为以下三部分:
- Verification:校验装载进来的 class 文件是不是符合 class 文件的标准、规范,假设 > 读取进来的 class 文件开头并非以 CA FE BA BE,那么在这个步骤就会被拒绝了
- Preparation:将 class 文件中静态成员变量赋予默认值,并非赋予初始值,假设 > static int i = 1,在这个步骤并不会把变量 i 赋值为 1,而是先赋值为 0
- Resolution:将类、方法、属性等符号引用解析为直接引用,要给它转换为直接内存地址,直接可以访问到的
Initializing 初始化过程:调用类初始化代码,静态变量在此时会赋予初始化
当该 class 对应所有对象都没有引用指向以后,当发生 GC 时,这些对象资源都会被回收掉
加载、类加载器
类加载器:class 文件从虚拟机加载到内存里都是通过 ClassLoader 类加载器加载进内存的,ClassLoader 是顶级父类,它是一个 abstract 类,它一定会有对应的子类实现,若想查看某个 class 是被哪个加载器加载进内存的,可以调用 类名.class.getClassLoader() 方法进行查看,示例代码如下:
/\*\*
\* @author vnjohn
\* @since 2023/6/24
\*/
public class ClassLoaderLevel {
public static void main(String[] args) {
System.out.println("String:"+ String.class.getClassLoader());
System.out.println("HKSCS:"+ HKSCS.class.getClassLoader());
System.out.println("DNSNameService:"+ DNSNameService.class.getClassLoader());
System.out.println("ClassLoaderLevel:"+ ClassLoaderLevel.class.getClassLoader());
System.out.println("DNSNameService#parent#classLoader:"+ DNSNameService.class.getClassLoader().getClass().getClassLoader());
System.out.println("ClassLoaderLevel#parent#classLoader:"+ ClassLoaderLevel.class.getClassLoader().getClass().getClassLoader());
}
}
执行结果及描述如下:
// 输出结果为 null,因为其是顶级加载器 Bootstrap 所加载的
String:null
// 输出结果为 null,因为其也是顶级加载器 Bootstrap 所加载的
HKSCS:null
// 该类位于 ext 目录某个 jar 包下,所以它由扩展类加载器 ExtClassLoader 所加载
DNSNameService:sun.misc.Launcher$ExtClassLoader@2ef1e4fa
// 该类是我们自己写的类,所以它由应用类加载器 AppClassLoader 所加载
ClassLoaderLevel:sun.misc.Launcher$AppClassLoader@18b4aac2
// 输出结果为 null,父加载器并不是类的加载器的加载器
DNSNameService#parent#classLoader:null
ClassLoaderLevel#parent#classLoader:null
通过示例代码演示过后,下面来介绍不同层次的类加载器,它们各自所负责的事情是什么
- Bootstrap:启动类加载器,又称之为引导类加载器,它用于加载 lib 里 JDK 最核心的内容,比如 > rt.jar、charset.jar 等包下的核心类,当在什么时候调用 getClassLoader 方法拿到的加载器结果是 null 值时,那么就代表是从最顶层的类加载器中进行加载的
- Extension:扩展类加载器,加载扩展包各种各样的文件,扩展包一般放在 jdk 安装目录下的 jre/lib/ext/*.jar 包
- App:应用加载器,平时经常会用到的类加载器,用它来指定加载 class path 指定的内容
- Custom:自定义类加载器
Custom ClassLoader 父类加载器 > application 父类加载器 > Extension 父类加载器 > Bootstrap
它们只是语义上的继承
双亲委派
如上图,描述类加载其实在内部是遵循双亲委派机制去进行加载的,那么下面来仔细描述当一个 class 文件要被 load 进内存,是怎样的一个加载过程?
- 任何一个 class,当你有自定义 ClassLoader 类加载器时,这时候就先尝试去自定义类加载器里面找,它内部维护着缓存,说你有没有已经帮我加载进来了,如果已经加载进来一遍就不需要加载第二遍,它如果没有在自己的自定义缓存找到的话,它并不是直接加载这块内存,它会先去它的父加载器 App 应用加载器中,问父亲你有没有把我这个类加载进来呢
- App 应用加载器,这时候就会去它的缓存里面找有没有这个类,如果有就返回,没有就继续委托给它的父加载器 Extension 扩展类加载器
- Extension 扩展类加载器,这时候就会去它的缓存里面找有没有这个类,如果有就返回,没有就继续委托给它的父加载器 Bootstrap 启动类加载器
- Bootstrap 启动类加载器,这时候就会去它的缓存里面找有没有这个类,如果有就返回,没有就往回委托给它的子加载器 Bootstrap Extension 扩展类加载器
- Extension 扩展类加载器,说我只负责加载扩展 jar 包里面的类,其他的我一概不知道也找不到,然后一直向下往回委托到 App 应用加载器、Custom ClassLoader 自定义类加载器中去找
整个加载过程,经过了一圈又一圈,才会真正把类加载进来,当我们能够把该类加载进来时叫做成功,若加载不进来,抛出异常 ClassNotFound 类找不到,这整个过程就叫做双亲委派
为什么要搞双亲委派机制?
主要是为了安全,若任何一个 Class 文件都可以把它加载进内存的话,那我就可以将 java.lang.String 交由给 ClassLoader,将密码存储成 String 类型对象,把 String load 进内存后,打包给客户,然后就可以偷偷摸摸把密码随便传递,这就造成了密码隐私泄露,极其不安全
当出现了双亲委派机制后,就不会这样了,自定义类加载器加载一次,java.lang.String 就产生了警惕性,它会先去上面查有没有加载过,若上面有加载过就不会返回给你,不给你进行重新加载
Launcher 核心类
Launcher 类是 ClassLoader 中的一个包装启动类,Bootstrap、Extension、App 类加载器它们所加载的路径都来自于 Launcher 核心类的源码
Bootstrap ClassLoader 加载路径:System.getProperty(“sun.boot.class.path”)
Extension ClassLoader 加载路径:System.getProperty(“java.ext.dirs”)
App ClassLoader 加载路径:System.getProperty(“java.class.path”)
如上面,示例代码执行的结果来看,可以得知
// 该类位于 ext 目录某个 jar 包下,所以它由扩展类加载器 ExtClassLoader 所加载
DNSNameService:sun.misc.Launcher$ExtClassLoader@2ef1e4fa
// 该类是我们自己写的类,所以它由应用类加载器 AppClassLoader 所加载
ClassLoaderLevel:sun.misc.Launcher$AppClassLoader@18b4aac2
Launcher 类来自于 sun,misc 包,ExtClassLoader、AppClassLoader 来自于 Launcher 源码,它默认显示为类名字后面 + 哈希 code 码,$ 符号 > 代表的意思就是 ExtClassLoader、AppClassLoader 都属于 Launcher 类的内部类!
下面通过一段小程序来看看这三个类加载器里到底加载了哪些文件,先通过指定的路径拿到属性值,然后再将指定符号替换为换行符
Windows 通过 ; 符号替换,Mac 通过 : 符号替换
/\*\*
\* @author vnjohn
\* @since 2023/6/24
\*/
public class ClassLoaderScope {
public static void main(String[] args) {
String bootstrapProperty = System.getProperty("sun.boot.class.path");
System.out.println("Bootstrap ClassLoader:");
System.out.println(bootstrapProperty.replaceAll(":", System.lineSeparator()));
System.out.println();
String extProperty = System.getProperty("java.ext.dirs");
System.out.println("Ext ClassLoader:");
System.out.println(extProperty.replaceAll(":", System.lineSeparator()));
System.out.println();
String appProperty = System.getProperty("java.class.path");
System.out.println("App ClassLoader:");
System.out.println(appProperty.replaceAll(":", System.lineSeparator()));
}
}
执行结果如下:
Bootstrap ClassLoader:
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/resources.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/rt.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/sunrsasign.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/jsse.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/jce.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/charsets.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/jfr.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/classes
Ext ClassLoader:
/Users/vnjohn/Library/Java/Extensions
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/ext
/Library/Java/Extensions
/Network/Library/Java/Extensions
/System/Library/Java/Extensions
/usr/lib/java
App ClassLoader:
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/charsets.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/deploy.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/ext/cldrdata.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/ext/dnsns.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/ext/jaccess.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/ext/jfxrt.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/ext/localedata.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0\_333.jdk/Contents/Home/jre/lib/ext/nashorn.jar
// ..... 省略其他
ClassLoader 相关源码
实现自定义类加载器之前,先阅读一下 ClassLoader 相关的源码部分
// 当前类加载器的父加载器
private final ClassLoader parent;
/\*\*
\* name:当前要加载的全限定类名
\* resolve:是否将符号引用转换为可以直接访问的地址
\*/
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// getClassLoadingLock(name):为加载类时都获取一把锁
synchronized (getClassLoadingLock(name)) {
// 首先,先检查该类是否被加载过了,若加载过了直接返回
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 若父加载器不为空
if (parent != null) {
// 父类先进行加载
c = parent.loadClass(name, false);
} else {
// 父加载器为空时,说明当前加载器是启动类加载器:Bootstrap
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 若类找不到就抛出 ClassNotFoundException 异常
}
if (c == null) {
// 仍然未找到,调用 findClass 方法查找该类
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;
}
}
loadClass 方法执行过程说明了,当你要加载一个类时只需要调用 ClassLoader#loadClass 方法就能够把该类加载进内存,加载到内存以后它会返回一个 Class 类的对象
经过上面的源码分析,若在自己缓存中没有找到,父加载器中也没有加载成功,最后只能回来自己再去调用 ClassLoader#findClass 方法去加载,它由 protected 修饰受保护的,只能在子类里面去进行访问,如下:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
该方法实现只有一句话,只能在它的子类里面去进行访问,很简单,我们只需要实现这个方法,就可以自定义类加载器加载我们所需要的类,这个就是钩子函数 > 模版方法设计模式
ClassLoader 相关问题
1、什么时候需要自定义加载器去实现?
Tomcat 在加载自定义的那部分类时(WEB-INF/classes 目录、WEB-INF/lib 目录中的 JAR文件),肯定是需要自定义类加载器(WebAppClassLoader)去加载这些 class 文件的,热部署的实现也是基于此加载器去实现的
2、如何指定类加载器的 parent
通过 super(parent) 指定
如下是示例代码:
/\*\*
\* @author vnjohn
\* @since 2023/6/24
\*/
public class ClassLoaderParent extends ClassLoader{
private static ClassLoaderParent PARENT = new ClassLoaderParent();
private static class MyClassLoader extends ClassLoader {
public MyClassLoader() {
super(PARENT);
}
}
}
3、如何打破双亲委派机制?
1、JDK 1.2 之前,自定义类加载器都必须重写 ClassLoader#loadClass 方法
2、ThreadContextClassLoader 提供了一种机制来绕过双亲委派机制,以实现在特定的线程上下文中加载类,通过 Thread#setContextClassLoader(ClassLoader cl) 方法可以设置线程上下文类加载器
3、在 OSGi 模块化开发框架中,各自应用程序存在自己的模块化机制和类加载器,可以加载同一个类库的不同版本,并且可以加载同名的类。在这种情况下,打破双亲委派机制的主要方式是使用双亲委派模型的变种,即双亲委派模型的扩展
自定义简单 ClassLoader
1、定义一个测试类:HelloWorld 后,通过 javac 编译成 class 文件
/\*\*
\* @author vnjohn
\* @since 2023/6/24
\*/
public class HelloWorld {
public void sayHello() {
System.out.println("Hello Vnjohn");
}
}
2、自定义类加载器:MyLoader,实现 URLClassLoader 类,重写 findClass 方法,在 findClass 方法块中会用到辅助方法 defineClass > 将字节数组转换为指定类名的 class 类对象
/\*\*
\* @author vnjohn
\* @since 2023/6/24
\*/
public class MyClassLoader extends URLClassLoader {
private static final String FILE_PATH = "/Users/vnjohn/Desktop/";
public MyClassLoader() {
super(new URL[0]);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// File 创建一个位于 /Users/vnjohn/Desktop 目录,传入的名字是 org.vnjohn.HelloWorld
// 然后将中间的 . 替换成 / 最后就可以通过全路径找到 class 文件所在位置
File f = new File(FILE_PATH, name.replace(".", "/").concat(".class"));
FileInputStream fis = null;
try {
fis = new FileInputStream(f);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b;
while ((b = fis.read()) != 0) {
baos.write(b);
}
// 转换为二进制字节数组
byte[] bytes = baos.toByteArray();
baos.close();
// 通过 defineClass 将字节数组转换为指定类名 name 的 class 类对象
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
assert fis != null;
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return super.findClass(name); //throws ClassNotFoundException
}
public static void main(String[] args) throws Exception {
ClassLoader l = new MyClassLoader();
Class<?> clazz = l.loadClass("org.vnjohn.HelloWorld");
Class<?> clazz1 = l.loadClass("org.vnjohn.HelloWorld");
// 返回 true,class 对象只会加载一次,第二次不会再加载
System.out.println(clazz == clazz1);
// 通过反射获取实例对象再访问其方法
HelloWorld h = (HelloWorld) clazz.newInstance();
h.sayHello();
// AppClassLoader 加载器
System.out.println(l.getClass().getClassLoader());
// AppClassLoader 加载器
System.out.println(l.getParent());
// ClassLoader 默认的加载器就是 AppClassLoader
System.out.println(getSystemClassLoader());
}
}
3、执行 main 主方法,结果如下:
true
Hello Vnjohn
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
自定义加密 ClassLoader
自定义类加载器时还可以自己加密 class 文件,防止反编译、篡改,可以进行手动解密、加密,在这里采用简单一点的办法 > 异或处理;异或一次就是加密、再异或一次就是解密,先把 Class 文件读出来进行异或加密,然后再行写回去;还是以测试类:HelloWorld 为例
/\*\*
\* @author vnjohn
\* @since 2023/6/24
\*/
public class MyClassLoaderWithEncription extends URLClassLoader {
public MyClassLoaderWithEncription() {
super(new URL[0]);
}
private static final int SEED = 0B10110110;
private static final String FILE_PATH = "/Users/vnjohn/Desktop/";
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// File 创建一个位于 /Users/vnjohn/Desktop 目录,传入的名字是 org.vnjohn.HelloWorld
// 然后将中间的 . 替换成 / 最后就可以通过全路径找到 class 文件所在位置
File f = new File(FILE_PATH, name.replace(".", "/").concat(".class"));
FileInputStream fis = null;
try {
fis = new FileInputStream(f);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b;
while ((b = fis.read()) != 0) {
baos.write(b ^ SEED);
}
// 转换为二进制字节数组
byte[] bytes = baos.toByteArray();
baos.close();
// 通过 defineClass 将字节数组转换为指定类名 name 的 class 类对象
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
assert fis != null;
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return super.findClass(name); //throws ClassNotFoundException
}
// 读文件操作
private static void encFile(String name) throws Exception {
File f = new File(FILE_PATH, name.replace('.', '/').concat(".class"));
FileInputStream fis = new FileInputStream(f);
// 写入到 /Users/vnjohn/Desktop 目录下的 .class 文件
FileOutputStream fos = new FileOutputStream(new File(FILE_PATH, name.replaceAll("\\.", "/").concat(".class")));
int b;
while((b = fis.read()) != -1) {
// 对每一个字节进行异或加密
fos.write(b ^ SEED);
}
fis.close();
fos.close();
}
public static void main(String[] args) throws Exception {
// 先将文件读出来进行异或加密
encFile("HelloWorld");
ClassLoader l = new MyClassLoaderWithEncription();
Class<?> clazz = l.loadClass("org.vnjohn.HelloWorld");
Class<?> clazz1 = l.loadClass("org.vnjohn.HelloWorld");
// 返回 true,class 对象只会加载一次,第二次不会再加载
System.out.println(clazz == clazz1);
// 通过反射获取实例对象再访问其方法
HelloWorld h = (HelloWorld) clazz.newInstance();
h.sayHello();
// AppClassLoader 加载器
System.out.println(l.getClass().getClassLoader());
// AppClassLoader 加载器
System.out.println(l.getParent());
// ClassLoader 默认的加载器就是 AppClassLoader
System.out.println(getSystemClassLoader());
}
}
执行结果跟 自定义简单 ClassLoader 执行的结果是一样的!!
打破双亲委派机制伪代码
只是重写 ClassLoader#findClass 方法是无法打破双亲委派机制的,若要打破它只能重写 ClassLoader#loadClass 方法,如下:
/\*\*
\* @author vnjohn
\* @since 2023/6/24
\*/
public class MyClassLoaderBreak extends URLClassLoader {
private static final String FILE_PATH = "/Users/vnjohn/Desktop/";
public MyClassLoaderBreak() {
super(new URL[0]);
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
File f = new File(FILE_PATH + name.replaceAll("\\.", "/").concat(".class"));
if(!f.exists()) {
return super.loadClass(name);
}
try {
InputStream is = new FileInputStream(f);
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
e.printStackTrace();
}
return super.loadClass(name);
}
public static void main(String[] args) throws Exception {
ClassLoader classLoader = new MyClassLoaderBreak();
Class<?> clazz = classLoader.loadClass("HelloWorld");
// 若在这中间 HelloWorld 改变了代码,下面的类加载器是可以加载到最新代码的.
classLoader = new MyClassLoaderBreak();
Class<?> clazzNew = classLoader.loadClass("HelloWorld");
// false
System.out.println(clazz == clazzNew);
}
}
注意:看如下这段代码,改变了这段代码的逻辑:
if(!f.exists()) {
return super.loadClass(name);
}
首先调用 loadClass 方法去找你要加载的 class 文件,若没找到就会让父加载器去 load,若找到了就自己 load;之前在这里会调用 findLoadedClass 方法去判断在缓存中是否加载过了,若要加载同一个 class 是覆盖不了的,但是这里把整体 ClassLoader 双亲委派机制干掉就行了
Tomcat 热部署就是这么干的,自定义类加载器重新实例化,然后再去加载所需要调用的类,所以说热部署会相当来说比较慢!!
类懒加载顺序
懒加载,规范来说应该是lazy initializing,JVM 规范并没有规定何时去加载,JVM 虚拟机内部实现都是用的懒加载,当什么时候需要用到这个类时才去进行加载,并不是一个 jar 包文件里面有几千个多类,但我只用到了一个类,还要去将它全部加载进来
JVM 严格规定几种情况下会必须初始化,如下:
- new、getstatic、putstatic、invokestatic 指令,只单独访问 final 变量不会初始化
- java.lang.reflect 对类进行反射调用时
- 初始化子类时,其父类会先进行初始化
- 虚拟机启动时,被执行的主类 main 方法必须初始化,比如:WebApplication
- 动态语言支持:java.lang.invoke.MethodHandle 解析的结果为 REF_getstatic、REF_putstatic、REF_invokestatic 方法句柄时,该类必须初始化
收集整理了一份《2024年最新物联网嵌入式全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升的朋友。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人
都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!