系统相关
首页 > 系统相关> > 重学Java. JVM内存模型 & 类加载机制

重学Java. JVM内存模型 & 类加载机制

作者:互联网

Java内存模型 & JVM内存分区

线程之间的通信

  1. 共享内存:线程之间通过写-读内存中的公共状态来隐式进行通信,典型的共享内存通信方式就是通过共享对象进行通信。

  2. 消息传递:线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信,在java中典型的消息传递方式就是wait()和notify(),notifyAll()。

Java内存模型

Java虚拟机规范中试图定义一种Java内存模型来屏蔽掉各种硬件和操作系统的内存访问差异。
---《深入理解Java虚拟机》

数据同步八大原子操作

硬件内存架构

JVM主要包括四个部分

  1. 类加载器(ClassLoader):在JVM启动时或者在类运行将需要的class加载到JVM中;

  2. 执行引擎:负责执行class文件中包含的字节码指令;

  3. 内存区(也叫运行时数据区):是在JVM运行的时候操作所分配的内存区。运行时内存区主要可以划分为5个区域: 方法区,java堆,java栈,程序计数器,本地方法栈;

  4. 本地方法接口:主要是调用C或C++实现的本地方法及回调结果;

JVM内存分区

  1. 方法区(MethodArea):用于存储已被虚拟机加载的类信息,常量、静态变量、即时编译器编译后的代码等数据,别名Non-Heap(非堆),这个区域的内存回收主要是常量池的回收和类型的卸载;

  2. java堆(Heap):唯一目的就是存放对象实例,是GC管理的主要区域(因此也被称作GC堆);方法区和堆是被所有java线程共享的。

  3. java虚拟机栈:和线程生命周期相同,每当创一个线程时,JVM就会为这个线程创建一个对应的java栈,在这个java栈中又会包含多个栈帧,每运行一个方法就建一个栈帧,用于存储局部变量表(基本类型和对象引用)、操作数栈、动态链接,方法返回等,也就是我们常说的调用栈。

  4. 本地方法栈(Native MethodStack):和java栈的作用差不多,只不过是为JVM使用到native方法服务的,有的虚拟机会把它和虚拟机栈合二为一。

  5. 程序计数器(PCRegister):用于保存当前线程执行的内存地址。由于JVM程序是多线程执行的,所以为了保证程切换回来后,还能恢复到原先状态,就需要一个独立计数器,记录之前中断的地方,可见程序计数器也是线程私有的。唯一一个再java虚拟机规范中没有规定任何OOMError情况的区域;

Java内存模型和JVM内存结构的对应关系

开线程影响哪块内存?

Java内存模型解决的问题

1. 多线程读同步问题与共享对象可见性(多线程缓存与指令重排序)

线程缓存导致的可见性问题

  1. volatile关键字保证可见性:可以保证直接从主存中读取一个变量,如果这个变量被修改后,总是会被写回到主存中去。

  2. synchronized和Lock也可以保证可见性:“如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值”、“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)”

synchronized和Lock的区别

指令序列的重排序:

  1. 编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

  2. 指令级并行的重排序:现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

  3. 内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

重排序导致的可见性问题

  1. volatile:通过内存屏障(Memory Barrier )可以禁止特定类型处理器的重排序,从而让程序按我们预想的流程去执行。内存屏障,又称内存栅栏,是一个CPU指令; volatile是基于Memory Barrier实现的。如果一个变量是volatile修饰的,JMM会在写入这个字段之后插进一个Write-Barrier指令,并在读这个字段之前插入一个Read-Barrier指令。

  2. synchronized和Lock来保证有序性:“一个变量在同一个时刻只允许一条线程对其进行lock操作”

2. 多线程写同步问题与原子性(多线程竞争race condition)

多线程竞争(Race Conditions)问题

什么是CAS?

Java为什么能跨平台?

Java的二进制兼容性:

定义:

一个类改变时,新版的类是否可以直接替换原来的类,却不至于损坏其他由不同厂商,作者开发的依赖于该类的组件

优势:

  1. java将二进制兼容性的粒度从整个库(如unix的.so库文件,windows的.dll库文件),细化到了单个的类(.class)

  2. java的二进制兼容性不需要有意识的去规划,而是一种与生具来的天性(.java-->.class)

  3. 传统的共享对象只针对函数名称,而java二进制兼容性考虑到类重载,函数签名(方法名+形参类型列表),返回值类型;

  4. java提供了更完善的错误控制机制,版本不兼容会触发异常,但可以方便的捕获和处理

几个关键点:

private static void testBinaryCompatibility() {
        class Language {
            String greeting = "你好";
            void perform() {
                System.out.println("白日依山尽");
            }
        }

        class French extends Language {
            String greeting = "Bon jour";
            void perform() {
                System.out.println("To be or not to be.");
            }
        }

        French french=new French();
        Language language=french;
        french.perform();
        language.perform();//调用实际实例的方法体
        System.out.println(french.greeting);
        System.out.println(language.greeting);//依赖于实例的类型

    }

    //输出结果如下:
    请输入要执行的方法名:testBinaryCompatibility
    To be or not to be.
    To be or not to be.
    Bon jour
    你好

类加载器 ClassLoader

类加载有三种方式:

  1. 命令行启动应用时候由JVM初始化加载

  2. 通过Class.forName()方法动态加载

  3. 通过ClassLoader.loadClass()方法动态加载

Java中的ClassLoader:

  1. Bootstrap ClassLoader(启动):C/C++代码实现的加载器(所以不能被Java代码访问到,并不继承java.lang.ClassLoader),负责加载Java虚拟机运行时所需要的系统类,默认在$JAVA_HOME/jre/lib目录中,也可以通过启动Java虚拟机时指定-Xbootclasspath选项,来改变Bootstrap ClassLoader的加载目录。

  2. Extension ClassLoader(扩展):用于加载 Java 的拓展类 ,拓展类的jar包一般会放在$JAVA_HOME/jre/lib/ext目录下,用来提供除了系统类之外的额外功能。也可以通过-Djava.ext.dirs选项添加和修改Extensions ClassLoader加载的路径。

  3. App ClassLoader(应用):负责加载当前应用程序Classpath目录下的所有jar和Class文件。也可以加载通过-Djava.class.path选项所指定的目录下的jar和Class文件,如果应用程序中没有实现自己的类加载器,一般就是这个类加载器去加载应用程序中的类库。

  4. Custom ClassLoader: 除了系统提供的类加载器,还可以自定义类加载器,自定义类加载器通过继承java.lang.ClassLoader类的方式来实现自己的类加载器;

继承关系

Android中的ClassLoader:

  1. BootClassLoader:Android系统启动时会使用BootClassLoader来预加载常用类,与Java中的BootClassLoader不同,它并不是由C/C++代码实现,而是由Java实现的;是ClassLoader的内部类,并继承自ClassLoader。BootClassLoader是一个单例类,需要注意的是BootClassLoader的访问修饰符是默认的,只有在同一个包中才可以访问,因此我们在应用程序中是无法直接调用的。

  2. DexClassLoader:DexClassLoader可以加载dex文件以及包含dex的压缩文件(apk和jar文件),继承自BaseDexClassLoader ,方法实现都在BaseDexClassLoader中

DexClassLoader构造方法有四个参数:
1. dexPath:dex相关文件路径集合,多个路径用文件分隔符分隔,默认文件分隔符为‘:’
2. optimizedDirectory:解压的dex文件存储路径,这个路径必须是一个内部存储路径,一般情况下使用当前应用程序的私有路径:/data/data/<Package Name>/...。
3. librarySearchPath:包含 C/C++ 库的路径集合,多个路径用文件分隔符分隔分割,可以为null。
4. parent:父加载器。
  1. PathClassLoader:Android系统使用PathClassLoader来加载系统类和应用程序的类,继承自BaseDexClassLoader,实现都在BaseDexClassLoader中;

继承关系

双亲委派机制:

优点

  1. 避免重复加载,当父ClassLoader已经加载了该类的时候,就没有必要让子ClassLoader再加载一次,而是先从缓存中直接读取。

  2. 更加安全,如果不使用双亲委托模式,就可以自定义一个String类来替代系统的String类,这显然会造成安全隐患,采用双亲委托模式会使得系统的String类在Java虚拟机启动时就被加载,也就无法自定义String类来替代系统的String类,除非我们修改类加载器搜索类的默认算法。还有一点,只有两个类名一致并且被同一个类加载器加载的类,Java虚拟机才会认为它们是同一个类,想要骗过Java虚拟机显然不会那么容易。

ClassLoader创建单例类的多个实例

  1. 一个单例类

class Test001 implements Serializable {
    private Test001() {
    }
    private static class Test001Holder{
       private static Test001 instance=new Test001();
    }
    public static Test001 getInstance(){
        return Test001Holder.instance;
    }

    private Object readResolve() {
        return Test001Holder.instance;
    }
}
  1. 下面通过classLoader获取上面单例类的class对象,并通过反射调用其getInstance方法

val testClassName="com.jinyang.plugin001.Test001"
var pluginClassLoader111 = DexClassLoader(plugin001Path, dexOutPath, nativeLibDir, this::class.java.classLoader)
val pluginClassLoader222 = DexClassLoader(plugin001Path, dexOutPath, nativeLibDir, this::class.java.classLoader)
var pluginClassLoader333 = PathClassLoader(plugin001Path, nativeLibDir, this::class.java.classLoader)
val class111=pluginClassLoader111.loadClass(testClassName)
val class111_2=pluginClassLoader111.loadClass(testClassName)
val class222=pluginClassLoader222.loadClass(testClassName)
val class333=pluginClassLoader333.loadClass(testClassName)
log("class111 调用 getInstance: "+class111.getDeclaredMethod("getInstance").invoke(null))
log("class111 再次调用 getInstance : "+class111.getDeclaredMethod("getInstance").invoke(null))
log("class111的同一个classloader对象创建的class111_2 调用 getInstance: "+class111_2.getDeclaredMethod("getInstance").invoke(null))
log("class111的同一个classLoader类的不同对象创建的class222 调用 getInstance: "+class222.getDeclaredMethod("getInstance").invoke(null))
log("class111的不同classLoader类的对象创建的class333 调用 getInstance:: "+class333.getDeclaredMethod("getInstance").invoke(null))
  1. 输出结果

class111 调用 getInstance: com.jinyang.plugin001.Test001@7097ae2
class111 再次调用 getInstance : com.jinyang.plugin001.Test001@7097ae2
class111的同一个classloader对象创建的class111_2 调用 getInstance: com.jinyang.plugin001.Test001@7097ae2
class111的同一个classLoader类的不同对象创建的class222 调用 getInstance: com.jinyang.plugin001.Test001@f465e73
class111的不同classLoader类的对象创建的class333 调用 getInstance:: com.jinyang.plugin001.

标签:重学,Java,ClassLoader,线程,内存,JVM,java,加载
来源: https://blog.csdn.net/slw20010213/article/details/122371362