JVM = 类加载器(classloader) + 执行引擎
(executionengine) + 运行时数据区域(runtime dataarea)
java每个线程都有一个虚拟机栈
1.加载过程 7 个步骤 加载–>验证–>准备–>解析–>初始化–>使用–>卸载
2.ClassLoader–>BaseDexClassLoader–>PathClassLoder ,DexClassLoader
3.双亲委托模式 先走父类的加载器加载类,若果没有找打父类才会轮到自己,好处 避免重复加载 安全
1.程序计数器 :
我们知道对于一个处理器(如果是多核cpu那就是一核),在一个
确定的时刻都只会执行一条线程中的指令,一条线程中有多个指
令,为了线程切换可以恢复到正确执行位置,每个线程都需有独
立的一个程序计数器,不同线程之间的程序计数器互不影响,独立存储。
2.本地方法栈 : 简单的理解为 C 代码的执行去
3.堆 : 简单的说就是对象的存储区,它是被所有线程共享的一块区域
堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的
对象是线程共享的,所以多线程的时候也需要同步机制。
堆 回收算法使用的复制算法 效率高 没有碎片 利用率低
分为三个区 eden from to (survivor) 按照 8:1:1因为大多数的对象都是朝生夕死的。
4.栈 :
栈描述的是Java方法执行的内存模型。
每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,
操作栈,动态链接,方法出口等信息。每一个方法被调用的过程
就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。
5.方法区
方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又
被称为非堆。用于存储已被虚拟机加载的类信息、常量、静态变
量,如static修饰的变量加载类的时候就被加载到方法区中。
6.GC
堆的回收为了高效的回收,jvm将堆分为三个区域
1.新生代(Young Generation)NewSize和MaxNewSize分别可以控制年轻代的初始大小和最大的大小
2.老年代(Old Generation)
3.永久代(Permanent Generation)【1.8以后采用元空间,就不在堆中了】
对象是否存活
回收算法
1.标记/清除算法【最基础】 (老年代 标记清楚 、整理 新生代 是复制算法)
标记 也是红灰白 对灰色进行清除 标记整理是红色进行整
理 ,清楚会有很多碎片,效率高
2.复制算法
复制内存区域,标记 红蓝灰白色 红色不可回收 灰色可回
收 白色没有分配 蓝色 预留 , 效率高,内存复制没有碎
片,缺点 利用率只有一半()
3.标记/整理算法
jvm采用`分代收集算法`对不同区域采用不同的回收算法。
其中新生代使用的是复制算法,老年代使用的是标记清除、标记整理算法。·
volatile 具有可见性和禁止命令重排序,不保证原子性。所以能够达到一次修改其他线程可见
即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性:即程序执行的顺序按照代码的先后顺序执行。
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性,即 volatile 保证了 可见性和有序性 没有原子性 。在JVM底层volatile是采用“内存屏障”来实现的。
有了CPU高速缓存虽然解决了效率问题,但是它会带来一个新的问题:数据一致性。在程序运行中,会将运行所需要的数据复制一份到CPU高速缓存中,在进行运算时CPU不再也主存打交道,而是直接从高速缓存中读写数据,只有当运行结束后才会将数据刷新到主存中。举一个简单的例子:
i++i++
当线程运行这段代码时,
首先会从主存中读取i( i = 1),
然后复制一份到CPU高速缓存中,
然后CPU执行 + 1 (2)的操作,
然后将数据(2)写入到告诉缓存中,
最后刷新到主存中。
其实这样做在单线程中是没有问题的,有问题的是在多线程中。如下:
假如有两个线程A、B都执行这个操作(i++),按照我们正常的逻辑思维主存中的i值应该=3,但事实是这样么?分析如下:
两个线程从主存中读取i的值(1)到各自的高速缓存中,然后线程A执行+1操作并将结果写入高速缓存中,最后写入主存中,此时主存i==2,线程B做同样的操作,主存中的i仍然=2。所以最终结果为2并不是3。这种现象就是缓存一致性问题。
通过在总线加LOCK#锁的方式
通过缓存一致性协议
但是方案1存在一个问题,它是采用一种独占的方式来实现的,即总线加LOCK#锁的话,只能有一个CPU能够运行,其他CPU都得阻塞,效率较为低下。
第二种方案,缓存一致性协议(MESI协议)它确保每个缓存中使用的共享变量的副本是一致的。其核心思想如下:当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的,因此其他CPU在读取该变量时,发现其无效会重新从主存中加载数据。
一个int变量,用volatile修饰,多线程去操作++,线程安全吗?
不安全。volatile只能保证可见性,并不能保证原子性。
i++实际上会被分成多步完成:
1)获取i的值;
2)执行i+1;
3)将结果赋值给i。
volatile只能保证这3步不被重排序,多线程情况下,可能两个线程同时获取i,执行i+1,然后都赋值结果2,实际上应该进行两次+1操作。
那如何才能保证i++线程安全?
可以使用java.util.concurrent.atomic包下的原子类,如AtomicInteger。
其实现原理是采用CAS自旋操作更新值。CAS即compare and swap的缩写,中文翻译成比较并交换。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。自旋就是不断尝试CAS操作直到成功为止。
CAS实现原子操作会出现什么问题?
ABA问题。因为CAS需要在操作之的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成,有变成A,那么使用CAS进行检查时会发现它的值没有发生变化,但实际上发生了变化。ABA问题可以通过添加版本号来解决。Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。
循环时间长开销大。pause指令优化。
只能保证一个共享变量的原子操作。可以合并成一个对象进行CAS操作。
参考 https://www.jianshu.com/p/76959115d486 //这个文章比较好,讲解了volatile 同时还讲解了jvm内存模型
https://www.cnblogs.com/chenssy/p/6379280.html