文章目录整体架构JVM = 类加载器(classloader) 执行引擎
(executionengine) 运行时数据区域(runtime dataarea)
java每个线程都有一个虚拟机栈
类加载器 ClassLoader1.加载过程 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,引用失效时就减1。当为0的时候就判断对象不会再被引用。 优点:实现简单效率高,被广泛使用与如python何游戏脚本语言上。 缺点:难以解决循环引用的问题,就是假如两个对象互相引用已经不会再被其它其它引用,导致一直不会为0就无法进行回收。 可达性分析算法 目前主流的商用语言[如java、c#]采用的是可达性分析算法判断对象是否存活。这个算法有效解决了循环利用的弊端。 它的基本思路是通过一个称为“GC Roots”的对象为起始点,搜 索所经过的路径称为引用链,当一个对象到GC Roots没有任何引用跟它连接则证明对象是不可用的。
回收算法
1.标记/清除算法【最基础】 (老年代 标记清楚 、整理 新生代 是复制算法) 标记 也是红灰白 对灰色进行清除 标记整理是红色进行整
理 ,清楚会有很多碎片,效率高
2.复制算法 复制内存区域,标记 红蓝灰白色 红色不可回收 灰色可回
收 白色没有分配 蓝色 预留 , 效率高,内存复制没有碎
片,缺点 利用率只有一半()
3.标记/整理算法 jvm采用`分代收集算法`对不同区域采用不同的回收算法。
其中新生代使用的是复制算法,老年代使用的是标记清除、标记整理算法。·
jvm 三大特性 可见性 原子性 有序性volatile 具有可见性和禁止命令重排序,不保证原子性。所以能够达到一次修改其他线程可见 原子性即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
例如 i=0 只有一步操作 把0赋给变量i i 则不是,先取出i 再执行i 1 最后把结果赋给i 进行了三部,不符合原子性 在单线程环境下我们可以认为以上都是原子性操作,但是在多线程环境下则不同,Java只保证了基本数据类型的变量和赋值操作才是原子性的(注:在32位的JDK环境下,对64位数据的读取不是原子性操作*,如long、double)。要想在多线程环境下保证原子性,则可以通过锁、synchronized来确保。
可见性可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性有序性:即程序执行的顺序按照代码的先后顺序执行。
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
|