JVM内存模型

2019-01-04  本文已影响0人  Jacquie葭葵

什么是内存模型

  1. 写入动作可见

    内存模型定义了一个充分必要条件,保证其它CPU的写入动作对该CPU是可见的,而且该CPU的写入动作对其它CPU也是可见的

    1. 强内存模型,所有CPU在任何时候都能看到内存中任意位置相同的值,这种完全是硬件提供的支持。
    2. 弱内存模型,需要执行一些特殊指令(就是经常看到或者听到的,memory barriers内存屏障),刷新CPU缓存的数据到内存中,保证这个写操作能够被其它CPU可见,或者将CPU缓存的数据设置为无效状态,保证其它CPU的写操作对本CPU可见。通常这些内存屏障的行为由底层实现,对于上层语言的程序员来说是透明的(不需要太关心具体的内存屏障如何实现)。
    3. 重排序可以发生在好几个地方:编译器、运行时、JIT等,比如编译器会觉得把一个变量的写操作放在最后会更有效率,编译后,这个指令就在最后了(前提是只要不改变程序的语义,编译器、执行器就可以这样自由的随意优化),一旦编译器对某个变量的写操作进行优化(放到最后),那么在执行之前,另一个线程将不会看到这个执行结果。
Class Reordering {
    int x = 0, y = 0;
    public void writer() {
        x = 1;
        y = 2;
        }
    public void reader() {
        int r1 = y;
        int r2 = x; 
        } 
    /*在writer方法中,可能发生了重排序,
    *y的写入动作可能发在x写入之前,这种情况下,
    *线程B就有可能看到 x的值还是0。
    */
}

在Java内存模型中,描述了在多线程代码中,哪些行为是正确的、合法的,以及多线程之间如何进行通信,代码中变量的读写行为如何反应到内存、CPU缓存的底层细节。

在Java中包含了几个关键字:volatile、final和synchronized,帮助程序员把代码中的并发需求描述给编译器。Java内存模型中定义了它们的行为,确保正确同步的Java代码在所有的处理器架构上都能正确执行。

synchronization 可以实现什么

Synchronization有多种语义,其中最容易理解的是互斥,对于一个monitor对象,只能够被一个线程持有,意味着一旦有线程进入了同步代码块,那么其它线程就不能进入直到第一个进入的线程退出代码块(这因为都能理解)

对于两个线程来说,在相同的monitor对象上同步是很重要的,以便正确的设置happens-before关系。

final 可以影响什么

如果一个类包含final字段,且在构造函数中初始化,那么正确的构造一个对象后,final字段被设置后对于其它线程是可见的。

这里所说的正确构造对象,意思是在对象的构造过程中,不允许对该对象进行引用,不然的话,可能存在其它线程在对象还没构造完成时就对该对象进行访问,造成不必要的麻烦。

JVM的内存分区

每个线程都有独立的栈并且是相互隔离的,

栈的大小

一个是jvm参数 -XSS,默认值随着虚拟机版本以及操作系统影响。我们可以认为64位linux默认是1m的样子。 除了JVM设置,我们还可以在创建Thread的时候手工指定大小

栈的大小影响到了线程的最大数量,尤其在大流量的server中,我们很多时候的并发数受到的是线程数的限制,这时候需要了解限制在哪里

可看作:

线程数 = (系统空闲内存-堆内存(-Xms, -Xmx)- perm区内存(-XX:MaxPermSize)) / 线程栈大小(-Xss)

堆的结构

对于大多数应用来说,Java 堆(Java Heap)是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

新对象会首先分配在Eden中(如果对象过大,比如大数组,将会直接放到老年代)。在GC中,Eden中的对象会被移动到survivor中,直至对象满足一定的年纪(定义为熬过minor GC的次数),会被移动到老年代。

方法区

又称为“静态区”,和堆一样,被所有线程共享。 其中包含所有的 class 和 static 变量

GC 垃圾收集

思考一下复制和标记清除/整理的区别,为什么新生代要用复制?因为对新生代来讲,一次垃圾收集要回收掉绝大部分对象,我们通过冗余空间的办法来加速整理过程(不冗余空间的整理操作要做swap,而冗余只需要做move)。同时可以记录下每个对象的『年龄』从而优化『晋升』操作使得中年对象不被错误放到老年代。而反过来老年代偏稳定,我们哪怕是用清除,也不会产生太多的碎片,并且整理的代价也并不会太大。

引用与内存实例

首先,字面值及其引用
int a = 1;
int b = 1;
int b = 2;
//先创建名为a的引用,再去找是否已存在字面值为1的地址,没找到开辟新地址放1.
//再创建b的引用变量,---栈---中已有“1”,直接指向它
//改变b时,a不会改变,因为改变的是b的引用,而不是指向的字面值
new String()
String str1 = "Hello";
String str2 = "Hello";
System.out.println(str1 == str2);//true
String str3 = new String("Hello");
String str4 = new String("Hello");
System.out.println(str3 == str4);//false
//String 是特殊的包装类
//用new()来新建,会存放在---堆---中,每次调用都是新的。
//而创建引用常量还是和字面值一样,不会新建,而去---栈---中的常量池中寻找
String str5 = str1+"World";
String str6 = "HelloWorld";
System.out.println(s5 == s6);//false
//equals判断的是字面值是否相等,而==判断的是引用是否指向同一个对象

new()出一个实例,JVM首先在堆中为其分配内存,拥有着指向方法区

类加载过程

参考链接:https://www.jianshu.com/p/ace2aa692f96

从.java文件到实际加载到内存中

JVM调用指定的ClassLoader去加载.class文件等各类路径、文件的类

.java文件 -> 通过你的JDK环境相关指令编译 -> .class文件 -> JVM初始化之后,如果有类的执行、调用等相关操作,JVM就会将.class文件加载到内存中,并开始下面的一系列处理:(链接->初始化)

image
/* 类加载方式:都是JVM调用ClassLoader去加载类 */
//方式一  Class.forName(String name);
public static Class<?> forName(String className) throws ClassNotFoundException { 
    return forName(className, true, VMStack.getCallingClassLoader()); 
}
public static Class<?> forName(String className, boolean shouldInitialize,
                               ClassLoader classLoader) throws ClassNotFoundException {
    if (classLoader == null) { 
        classLoader = BootClassLoader.getInstance(); 
    } 
    Class<?> result; 
    try { 
        result = classForName(className, shouldInitialize, classLoader); 
    } catch (ClassNotFoundException e) { 
        Throwable cause = e.getCause(); 
        if (cause instanceof LinkageError) { 
            throw (LinkageError) cause; 
        } 
        throw e; 
    } 
    return result; 
} 

//方式二

类的初始化之前
  1. 加载
    • 通过一个类全限定名(java.lang.String)来获取定义此类的二进制字节流
    • 将其所代表的静态存储结构 》 方法区运行时数据结构
    • 在---堆---中生成一个代表这个类的java.lang.Class
上一篇下一篇

猜你喜欢

热点阅读