什么是内存模型
-
写入动作可见
内存模型定义了一个充分必要条件,保证其它CPU的写入动作对该CPU是可见的,而且该CPU的写入动作对其它CPU也是可见的
- 强内存模型,所有CPU在任何时候都能看到内存中任意位置相同的值,这种完全是硬件提供的支持。
- 弱内存模型,需要执行一些特殊指令(就是经常看到或者听到的,memory barriers内存屏障),刷新CPU缓存的数据到内存中,保证这个写操作能够被其它CPU可见,或者将CPU缓存的数据设置为无效状态,保证其它CPU的写操作对本CPU可见。通常这些内存屏障的行为由底层实现,对于上层语言的程序员来说是透明的(不需要太关心具体的内存屏障如何实现)。
- 重排序可以发生在好几个地方:编译器、运行时、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文件等各类路径、文件的类
image.java文件 -> 通过你的JDK环境相关指令编译 -> .class文件 -> JVM初始化之后,如果有类的执行、调用等相关操作,JVM就会将.class文件加载到内存中,并开始下面的一系列处理:(链接->初始化)
/* 类加载方式:都是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;
}
//方式二
类的初始化之前
-
加载
- 通过一个类全限定名(java.lang.String)来获取定义此类的二进制字节流
- 将其所代表的静态存储结构 》 方法区运行时数据结构
- 在---堆---中生成一个代表这个类的java.lang.Class
网友评论