重学Java系列-2. JVM内存模型 & 类加载机制

Java内存模型 & JVM内存分区

线程之间的通信

  • 在命令式编程中,线程之间的通信机制有两种共享内存和消息传递。
  1. 共享内存:线程之间通过写-读内存中的公共状态来隐式进行通信,典型的共享内存通信方式就是通过共享对象进行通信。
  2. 消息传递:线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信,在java中典型的消息传递方式就是wait()和notify(),notifyAll()。

Java内存模型

  • Java的并发采用的是共享内存模型,JMM决定一个线程对共享变量的写入何时对另一个线程可见。
  • JMM(Java Memory Model)是Java虚拟机规范定义的,用来屏蔽掉Java程序在各种不同的硬件和操作系统对内存的访问的差异。
Java虚拟机规范中试图定义一种Java内存模型来屏蔽掉各种硬件和操作系统的内存访问差异。
---《深入理解Java虚拟机》
复制代码
  • 这组规则是控制程序中各个变量在共享数据区域和私有数据区域的访问方式,围绕原子性,有序性、可见性展开;Java内存模型中规定所有变量都存储在主内存, 主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行;

  • 线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

  • 如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成,而且这8个操作必须是原子性的。

数据同步八大原子操作
  • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
  • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
硬件内存架构
  • 硬件内存架构包括:多CPU, CPU寄存器, 高速缓存cache, 内存

  • 缓存一致性问题: CPU和内存是不直接通讯的,因为两者的运行效率是不一样的,为了提高效率,计算机引入高速缓存来充当介质。在多核CPU中,每个CPU都拥有自己的缓存,那同一个数据,在CPU各自的高速缓存中,以及内存中,可能就不一致了,为了解决这一问题又引出了缓存一致性协议(MESI)。在读写时要根据协议进行操作,来维护缓存的一致性。MESI中的字母表示可以标记高速缓存行的四种独占状态:修改(M),独占(E),共享(S),无效(I)

  • 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内存结构的对应关系

  • 主内存,共享数据区域,对应堆和方法区
  • 工作内存,线程私有数据区域,对应程序计数器、虚拟机栈以及本地方法栈

开线程影响哪块内存?

  • 每当有线程被创建的时候,JVM就需要为其在内存中分配虚拟机栈和本地方法栈来记录调用方法的内容,分配程序计数器记录指令执行的位置,这样的内存消耗就是创建线程的内存代价。

Java内存模型解决的问题

1. 多线程读同步问题与共享对象可见性(多线程缓存与指令重排序)
  • 可见性(共享对象可见性):线程对共享变量修改的可见性。当一个线程修改了共享变量的值,其他线程能够立刻得知这个修改;
线程缓存导致的可见性问题
  • 一个线程将共享对象读到cpu缓存并修改,如果没有刷新回共享内存,其他线程访问到的就会是修改前的共享对象;
  • 解决上面问题可以用volatile关键字,synchronized关键字
  1. volatile关键字保证可见性:可以保证直接从主存中读取一个变量,如果这个变量被修改后,总是会被写回到主存中去。
  2. synchronized和Lock也可以保证可见性:“如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值”、“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)”
synchronized和Lock的区别
  • Lock底层实现主要是Volatile + CAS(乐观锁),而Synchronized是一种悲观锁,比较耗性能;但是在JDK1.6以后对Synchronized的锁机制进行了优化,加入了偏向锁、轻量级锁、自旋锁、重量级锁,在并发量不大的情况下,性能可能优于Lock机制。所以建议一般请求并发量不大的情况下使用synchronized关键字。
指令序列的重排序:
  1. 编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序:现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
  • 重排序保证在单线程下不会改变执行结果,但在多线程下可能会改变执行结果。
重排序导致的可见性问题
  • 如果在本地线程内观察,所有操作都是有序的(“线程内表现为串行”(Within-Thread As-If-Serial Semantics));如果在一个线程中观察另一个线程,所有操作都是无序的(“指令重排序”现象和“线程工作内存与主内存同步延迟”现象)。
  • 解决上面问题可以用volatile关键字,synchronized关键字
  1. volatile:通过内存屏障(Memory Barrier )可以禁止特定类型处理器的重排序,从而让程序按我们预想的流程去执行。内存屏障,又称内存栅栏,是一个CPU指令; volatile是基于Memory Barrier实现的。如果一个变量是volatile修饰的,JMM会在写入这个字段之后插进一个Write-Barrier指令,并在读这个字段之前插入一个Read-Barrier指令。
  2. synchronized和Lock来保证有序性:“一个变量在同一个时刻只允许一条线程对其进行lock操作”
2. 多线程写同步问题与原子性(多线程竞争race condition)
多线程竞争(Race Conditions)问题
  • 多个线程在这个共享对象上更新变量,就有可能发生race conditions。
  • 使用原子性保证多线程写同步问题:原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不 会被其他线程影响。除了JVM自身提供的对基本数据类型读写操作的原子性外,可以通过 synchronized和 Lock实现原子性。因为synchronized和Lock能够保证任一时刻只有一个线程访问该代码块。
  • 通过 CAS保证原子性
  • 使用原子数值类型,如AtomicInteger
  • 使用原子属性更新器 AtomicReferenceFieldUpdater
  • volatile无法保证原子性
什么是CAS?
  • Compare and Swap
  • CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。
  • 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。
  • 无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。Java中通过Unsafe来实现了CAS。
  • java.util.concurrent包全完建立在CAS之上,没有CAS也就没有此包,可见CAS的重要性。
  • 漏洞:CAS操作的"ABA"问题,java.util.concurrent包为了解决这个问题,提供了一个带有标记的原子引用类"AtomicStampedReference",它可以通过控制变量值的版本来保证CAS的正确性;

Java为什么能跨平台?

  • 因为Java程序编译之后的代码不是能被硬件系统直接运行的代码,而是一种“中间码”——字节码。然后不同的硬件平台上安装有不同的Java虚拟机(JVM),由JVM来把字节码再“翻译”成所对应的硬件平台能够执行的代码。因此对于Java编程者来说,不需要考虑硬件平台是什么。所以Java可以跨平台。

Java的二进制兼容性:

定义:

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

优势:
  1. java将二进制兼容性的粒度从整个库(如unix的.so库文件,windows的.dll库文件),细化到了单个的类(.class)
  2. java的二进制兼容性不需要有意识的去规划,而是一种与生具来的天性(.java-->.class)
  3. 传统的共享对象只针对函数名称,而java二进制兼容性考虑到类重载,函数签名(方法名+形参类型列表),返回值类型;
  4. java提供了更完善的错误控制机制,版本不兼容会触发异常,但可以方便的捕获和处理
几个关键点:
  • 延迟绑定(Late Binding),指java直到运行时才检查类,域,方法的名称,这意味着只要域,方法的名称(及类型)一样,类的主题可以任意替换(其实还与public,private,static,abstract等修饰符有关)
  • 方法的兼容性:要注意重写对父类方法的覆盖;(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

  • 类的加载就是虚拟机通过一个类的全限定名来获取描述此类的二进制字节流,而完成这个加载动作的就是类加载器。
  • Java程序是由若干个.class文件组成的,当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能,而这些功能都被封装在不同的class文件当中,所以经常要从这个class文件中要调用另外一个class文件中的方法,如果另外一个文件不存在的话,则会引发系统异常。而程序在启动的时候,并不会一次性加载程序所要用到的class文件,而是根据程序的需要,通过Java的类加载器(ClassLoader)来动态加载某个class文件到内存中的,只有class文件被载入到了内存之后,才能被其它class文件引用。

类加载有三种方式:

  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类的方式来实现自己的类加载器;
继承关系
  • ClassLoader是一个抽象类,其中定义了ClassLoader的主要功能;
  • SecureClassLoader继承了抽象类ClassLoader,但SecureClassLoader并不是ClassLoader的实现类,而是拓展了ClassLoader类加入了权限方面的功能,加强了ClassLoader的安全性;
  • URLClassLoader继承自SecureClassLoader,用来通过URl路径从jar文件和文件夹中加载类和资源;
  • ExtClassLoader和AppClassLoader都继承自URLClassLoader,它们都是Launcher 的内部类,Launcher 是Java虚拟机的入口应用,ExtClassLoader和AppClassLoader都是在Launcher中进行初始化的。

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中;
  • PathClassLoader 和 DexClassLoader 都能加载外部的 dex/apk,只不过区别是 DexClassLoader 可以指定 optimizedDirectory,也就是 dex2oat 的产物 .odex 存放的位置,而 PathClassLoader 只能使用系统默认位置/data/dalvik-cache。
  • 但是这个 optimizedDirectory 在 Android 8.0 以后也被舍弃了,只能使用系统默认的位置了,也就是说,在 8.0 上,PathClassLoader 和 DexClassLoader 其实已经没有什么区别了。
继承关系
  • ClassLoader是一个抽象类,其中定义了ClassLoader的主要功能。BootClassLoader是它的内部类;
  • SecureClassLoader类和JDK8中的SecureClassLoader类的代码是一样的,它继承了抽象类ClassLoader。SecureClassLoader并不是ClassLoader的实现类,而是拓展了ClassLoader类加入了权限方面的功能,加强了ClassLoader的安全性。
  • URLClassLoader类和JDK8中的URLClassLoader类的代码是一样的,它继承自SecureClassLoader,用来通过URl路径从jar文件和文件夹中加载类和资源。
  • InMemoryDexClassLoader是Android8.0新增的类加载器,继承自BaseDexClassLoader,用于加载内存中的dex文件。
  • BaseDexClassLoader继承自ClassLoader,是抽象类ClassLoader的具体实现类,PathClassLoader和DexClassLoader都继承它。

双亲委派机制:

  • 判定两个类是否相等,只有在这两个类被同一个类加载器加载的情况下才有意义,否则即便是两个类来自同一个Class文件,被不同类加载器加载,它们也是不相等的。
  • ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(BootstrapClassLoader)本身没有父类加载器,但可以用作其它lassLoader实例的的父类加载器。
  • 当一个ClassLoader实例需要加载某个类时,它会在试图搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器BootstrapClassLoader试图加载,如果没加载到,则把任务转交给ExtensionClassLoader试图加载,如果也没加载到,则转交给AppClassLoader进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等待URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,将它加载到内存当中,最后返回这个类在内存中的Class实例对象。
优点
  1. 避免重复加载,当父ClassLoader已经加载了该类的时候,就没有必要让子ClassLoader再加载一次,而是先从缓存中直接读取。
  2. 更加安全,如果不使用双亲委托模式,就可以自定义一个String类来替代系统的String类,这显然会造成安全隐患,采用双亲委托模式会使得系统的String类在Java虚拟机启动时就被加载,也就无法自定义String类来替代系统的String类,除非我们修改类加载器搜索类的默认算法。还有一点,只有两个类名一致并且被同一个类加载器加载的类,Java虚拟机才会认为它们是同一个类,想要骗过Java虚拟机显然不会那么容易。
ClassLoader创建单例类的多个实例
  • 可以通过不同的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.Test001@1af7130
复制代码

参考

我是今阳,如果想要进阶和了解更多的干货,欢迎关注微信公众号 “今阳说” 接收我的最新文章

Guess you like

Origin juejin.im/post/7049998517015674893