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

参考

  • 深入了解Java虚拟机:JVM高级特性与最佳实践(第二版)(重视大众号今阳说,回复“深入了解JVM”即可领取)

  • 全面了解Java内存模型

  • 图解Java内存模型

  • Android解析ClassLoader(一)Java中的ClassLoader

  • Android解析ClassLoader(二)Android中的ClassLoader

  • Java二进制兼容性原理

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