java内存分析(1)——类加载的内存分析

java文件如何被运行

先通过编译,将.java文件编译成.class文件,然后让类加载器加载到方法区中去

image-20210819111135267

类加载的内存分析

JVM内存结构

方法区:

  • 用于存放类似于元数据信息方面的,比如类信息,常量静态变量,编译后代码……
  • 类加载器将**.class文件**搬过来就是先丢到这一块上

堆:

  • 主要存放了一些存储的数据,比如对象实例(包括class对象实例)数组
  • 它和方法区都属于线程共享区域,也就是说它们的线程不安全

栈:

  • 这是我们的代码运行空间。我们编写的每一个方法都会放到栈里面运行
  • 线程独享

类加载器的流程

加载

  1. 将.class文件加载到内存
  2. 将静态数据结构转换为方法区中运行时的数据结构
  3. 在堆中生成一个代表这个类的java.lang.Class对象作为数据访问的入口

链接

  1. 验证:确保加载的类符合JVM规范和安全,保证被校验类的方法在运行时不会做出危害虚拟机的事件,其实是一个安全检查
  2. 准备:为static变量方法区中分配内存空间,设置变量的初始值
  3. 解析:虚拟机将常量吃内的符号引用替换为直接引用的过程

初始化

  1. 执行类构造器()方法。类构造器方法是由编译期自动收集类中所有的类变量的赋值动作和静态代码块中的语句合并产生的。

分析类的初始化

什么时候会发生类初始化?

  1. 类的主动引用(一定会发生类的初始化)
    • 当虚拟机启动,先初始化main方法所在的类
    • new一个类的对象
    • 调用类的静态成员(除了final常量)和静态方法
    • 使用java.lang.reflect包的方法对类进行反射调用
    • 当初始化一个类,如果其父类没有被初始化,则会先初始化他的父类
  2. 类的被动引用(不会发生类的初始化)
    • 当访问一个静态域时,只有真正声明这个域的类才会被初始化。如当通过子类引用父类的静态变量,不会导至子类初始化
    • 通过数组定义类引用,不会触发此类的初始化
    • 引用常量不会出发此类的初始化(常量在链接阶段就存入调用类的常量池中了)

类加载器

类加载的作用:

将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口(注意:入口在堆中)

类加载器的加载顺序

加载一个Class类的顺序也是有优先级的,类加载器从最底层开始往上的顺序是这样的

这里写图片描述

  1. BootstrapClassLoader(启动类加载器)

    负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,加载System.getProperty(“sun.boot.class.path”)所指定的路径或jar

  2. ExtensionClassLoader(标准扩展类加载器)

    负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar-Djava.ext.dirs指定目录下的jar包。载System.getProperty(“java.ext.dirs”)所指定的路径或jar。

  3. AppClassLoader(系统类加载器)

    负责记载classpath中指定的jar包及目录中class

  4. CustomClassLoader(自定义加载器)

    属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现。

双亲委派机制

当一个类收到了加载请求时,它是不会先自己去尝试加载的,而是委派给父类去完成。

只有当父类加载器范阔自己无法完成这个请求(也就是父类加载器都没找到加载所需的Class)时,子类加载器才会自行尝试加载

好处:加载位于rt.jar包中的类时不管是那个加载器加载,最终都会委托到BootStrap ClassLoader进行加载,这样保证了使用不同的类加载器得到的都是同一个结果。

其实这是一个隔离的作用,避免了我们的代码影响了JDK的代码。

缺点:

  • 灵活性降低:由于类加载的过程需要不断地委托给父类加载器,这种机制可能导致实际应用中类加载的灵活性降低。
  • 增加了类加载时间:在类加载的过程中,需要不断地查询并委托父类加载器,这意味着类加载所需要的时间可能会增加。在类数量庞大或类加载器层次比较深的情况下,这种时间延迟可能会变得更加明显。

打破双亲委派机制

在一些实际应用场景中,需要打破这个机制,实现更加灵活的类加载

  • OSGi(Open Service Gateway Initiative):OSGi 是一个模块化系统和服务平台,提供了一个强大的类加载器模型。在 OSGi 中,每个模块都有一个独立的类加载器,可以按需加载来自不同模块的类。这有助于解决 JAR 地狱问题,提高模块化和动态更新能力。

  • Tomcat Web容器:Tomcat 的 Web 应用类加载器可以加载 Web 应用程序中的本地类库,从而使得每个 Web 应用程序可以使用各自的版本的类库。这些 Web 应用的类加载器都是${tomcat-home}/lib 中类库的子类加载器。

  • Java Agent: Java Agent 是一种基于 Java Instrumentation API 的技术,它可以在运行时修改已加载的类的字节码,从而实现类的热替换、AOP(面向切面编程)等功能。这种技术在诸如热部署、性能监控和分布式追踪等场景中有广泛应用。

  • JDK 中的 URLClassLoader:JDK 自带的 URLClassLoader 可以用来加载指定 URL 路径下的类。实际上,它实现了一种子类优先的策略,先尝试加载自身路径下的类,再委托给父类加载器,从而打破了双亲委派机制。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;

public class CustomClassLoader extends ClassLoader {

// 自定义类加载器必须提供一个加载类文件的位置
private String classesPath;

public CustomClassLoader(String classesPath, ClassLoader parent) {
super(parent);
this.classesPath = classesPath;
}

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//首先,检查已加载的类
Class<?> loadedClass = findLoadedClass(name);

if (loadedClass == null) {
// 如果已加载类中没有该类, 尝试用自定义的方法加载
try {
loadedClass = findClassInPath(name);
} catch (ClassNotFoundException e) {
// 如果自定义加载方法找不到类,则委托给父类加载器
loadedClass = super.loadClass(name, resolve);
}
}

if (resolve) {
resolveClass(loadedClass);
}

return loadedClass;
}

private Class<?> findClassInPath(String className) throws ClassNotFoundException {
try {
String filePath = className.replace('.', '/') + ".class";
byte[] classBytes = Files.readAllBytes(Paths.get(classesPath, filePath));

return defineClass(className, classBytes, 0, classBytes.length);
} catch (Exception e) {
throw new ClassNotFoundException("Class not found in classes path: " + className, e);
}
}

public static void main(String[] args) throws Exception {
String pathToClasses = "/path/to/your/classes";
String className = "com.example.SampleClass";
String methodName = "sampleMethod";

// 创建自定义类加载器实例,将类的加载权交给它
CustomClassLoader customClassLoader = new CustomClassLoader(pathToClasses, CustomClassLoader.class.getClassLoader());

// 使用自定义类加载器加载类
Class<?> customClass = customClassLoader.loadClass(className);

// 创建类的实例并调用方法
Object obj = customClass.newInstance();
Method method = customClass.getDeclaredMethod(methodName);
method.setAccessible(true);
method.invoke(obj);
}
}

类缓存

某一个类被类加载器加载到内存中,维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象

参考文章:大白话带你认识JVM(转) - 壹袋米 - 博客园 (cnblogs.com)

参考文章:深入了解双亲委派机制(常说的类加载机制)-CSDN博客

Contents
  1. 1. java文件如何被运行
  2. 2. 类加载的内存分析
    1. 2.1. JVM内存结构
  3. 3. 类加载器的流程
  4. 4. 分析类的初始化
    1. 4.1. 什么时候会发生类初始化?
  5. 5. 类加载器
    1. 5.1. 类加载的作用:
    2. 5.2. 类加载器的加载顺序
    3. 5.3. 双亲委派机制
    4. 5.4. 打破双亲委派机制
    5. 5.5. 类缓存
|