JVM与JMM
作者:互联网
一.JVM
1.1 什么是JVM?
JVM是Java virtual Machine(Java虚拟机),他是用来提供Java程序运行环境的。
1.2 JVM在系统中的位置?
JVM在操作系统之上,应用程序之间。
1.3 JVM的体系结构?
JVM的内存模型从上到下分别是:类加载器-JVM运行时数据区-执行引擎-本地方法接口(JNI)-本地方法库。
JVM运行时数据区包括:虚拟机栈,本地方法栈,程序计数器,堆,方法区。
1.4 JVM内存模型
JVM体系结构中除去类加载器,后面的部分就是JVM内存模型。
1.5 JVM内存模型中各个区域的作用
程序计数器:每个线程都会有自己的PC,用于记录程序执行位置,便于系统执行其他完其他程序后返回当前程序的正确位置继续执行。
虚拟机栈:每个线程都会有自己的栈,栈中存放的是许多栈帧,栈帧就是方法调用占用的内存空间,每一个方法调用都会生成一个栈帧。当栈帧过多,或栈帧过大时,会出现StackOverFlowError错误。可以通过VM options设置虚拟机参数为:-Xss10k/10m设置栈内存大小。栈中存放的是:8大基本类型变量,引用变量的引用。
栈帧中包括:局部变量表,操作数栈,动态链接和方法返回地址。
本地方法栈:和虚拟机栈的作用相同,当方法使用native修饰时,代表是本地方法,就会通过本地方法栈执行该方法。
什么是本地方法?native关键字修饰的方法,与本地方法库和本地方法接口(JNI)相关,他要操作的方法超出了Java的操作范围,一般是C,C++的方法。
方法区:也叫非堆(Non-heap),是一个规范,具体的实现可以由JVM自己来做。它存放的是静态变量,常量,类模板(方法,构造方法代码,接口定义等)运行时常量池,字符串常量池。
堆: 用来存放实例对象。jdk1.8之后,字符串常量池也放在堆中。
堆中详细分了:
年轻代(YoungGen):包括Eden区,Survivor0(From) 区,Survivor1(To)区。
老年代(OldGen):存储多次无法GC的对象,或占用空间较大的对象。
永久代(PermGen):jdk1.8之后,使用元空间MetaSpace来实现,它存放在本机内存上,并将字符串常量池放在堆中。运行时常量池放在本机内存。无论是PermGen还是MetaSpace都是对方法区的实现。
执行引擎:解析java字节码指令,即执行代码,得到运行结果。
本地方法接口:用于调用本地方法库中的方法。
1.6 垃圾回收机制GC(Gabage Collection)
- GC的主要区域是堆,方法区(也叫非堆),更具体点:主要是年轻代和老年代。相对应的JVM调优的对象是堆和方法区,99%是堆。
- GC算法
- 引用计数器算法 对每一个对象记录其引用数,回收引用数为0的对象。
优点 简单,实时性,如果有计数器为0直接回收。
缺点 计数器会有消耗,无法解决循环引用问题 - 复制算法
首先将保留的对象复制到一个下一个区域,然后释放当前占用的整个区域。
优点 不会产生内存碎片(内存碎片过多会导致新对象存储时无法找到足够连续内存进行存储。)
缺点 需要一块多余的存储空间 - 标记清除
将可以回收的对象做标记,然后一次性把这些对象全部回收掉。
优点 不需要多余空间就可以实现,不移动对象的位置。
缺点 会产生大量内存碎片。时间复杂度高,两个阶段,扫描全部对象标记,扫描全部对象回收。 - 标记清除压缩
在标记清除的基础上,把散乱的内存碎片压缩在一起
优点 不需要多余空间就可以实现,解决了标记清除的内存碎片问题。
缺点 时间复杂度高 - 分代收集算法
现代的JVM大多采用这种方式。将堆分为年轻代和老年代。在年轻代中,由于对象生存周期短,每次回收都会有大量对象死去,这时采用复制算法。而老年代中,对象生存周期长,采用标记清除压缩算法或标记清除算法。
补充:调用System.gc会优先调用重GC(full GC),但是不一定立刻调用。
1.7 堆调优
- -Xms10m 调整堆的初始化内存
- -Xmx10m 调整堆的最大扩展内存,一般和初始化内存设置为一样,避免程序运行过程中,由于内存扩展,导致内存震荡,影响程序性能。
- -XX:+MaxTenuringThreshold=20,设置经过多少次GC后存活的年轻代重对象进入老年代。
- 引用计数器算法 对每一个对象记录其引用数,回收引用数为0的对象。
1.8 类加载器
类加载器是用来加载class文件的。
- 三种类加载器
- 启动类加载器(BootstrapClassLoader) 主要加载javahome/lib/rt.jar charsets.jar等java核心类库
- 扩展类加载器(ExtClassLoader) 主要加载javahome/ext/中的类库
- 应用类加载器(AppClassLoader) 加载用户路径classpath路径下的类
- 用户类加载器(UserClassLoader) 加载用户自定义路径下的类
- 双亲委派机制
双亲委派机制是类加载过程中的一种安全保障机制,保护java的核心类库不被修改。当一个类加载器收到一个加载类的请求时,首先会委托他的父类加载器加载,直到根加载器,找不到则向下委托,找到该类就进行加载,如果到了最下层的加载器仍然找不到该类,就会报出ClassNotFoundException异常。这种机制还避免了类被重复加载。 - 沙箱安全机制
通过双亲委派机制实现了沙箱安全机制,保证java的核心类库不被修改,避免植入恶意代码。
二. JMM
1.1 什么是JMM
JMM是Java Memory Model(Java内存模型的简称),是一种符合内存模型规范的,屏蔽了各种硬件和操作系
统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。它规定了线程
的工作内存和主存之间的交互关系,以及线程之间的可见性和程序的执行顺序。
1.2 JMM图
JMMjava内存模型是模仿计算机缓存架构来做的,首先数据存放在主存中,当cpu要执行的时候,数据会放在缓存中等待cpu调用,到了cpu中又有寄存器来存储数据供cpu来进行计算。JMM是规定,每一个线程在执行的时候,有自己的内存空间,每一个线程要操作的数据从主存中获取。并且还规定了线程之间的可见性,以及程序的执行顺序。
JMM定义了8个数据原子性操作:
- read 从主存中读取数据
- load 将读取的数据写入工作内存
- use 从工作内存读数据来计算
- assign 将计算后的数据重新赋值到工作内存
- store 将工作内存中数据写入主内存
- write 将写入主内存的数据赋值给变量
- lock 将主内存变量加锁,标识为线程独占状态
- unlock 将主内存解锁 解锁后其他线程可以锁定该变量
正是由于这些原子操作,会导致线程之间的不可见性。
1.3如何实现线程可见
原理:多个线程从主存中读取共享数据到各自的内存空间,一旦某个线程修改了该数据的值,会立刻同步到主存中。其他线程通过总线嗅探机制可以感知数据的变化,从而将自己存储空间的值失效。
1.4 缓存一致性协议
这类协议有:MSI,MESI,MOSI...
很多主流CPU实现MESI,要想实现线程可见,就要打开缓存一致协议,java中通过volatile关键字打开该协议。
1.5 volatile关键字
通过volatile关键字打开缓存一致性协议。保证线程可见性。并且可以提供内存读写屏障功能,避免指令重排。
原理:
看了很多资料,博客,我的理解就是,对于一个添加volatile关键字的变量,在进行操作的时候,汇编码会对相应的操作语句添加lock前缀指令,在操作时候锁定主线(也就是无法与读写内存)以及缓存,让其他线程无法进行操作,待当前线程的操作完毕后,其他线程的工作内存中的该数据会失效。这个是可见性原理。
1.6 什么是指令重排?
Java程序在运行的时候,指令是会进行重排序的。CPU根据指令运行时间,对指令运行顺序进行重排,以提高运行效率。前提是:不影响语义。
指令重排序在多线程中可能会导致,对象的半初始化。
1.7 volatile关键字避免指令重排?
通过添加volatile关键字,实际上是使用了lock指令前缀在操作该数据的时候,lock指令可以提供内存读写屏障功能,让两边的指令不发生重排序。
lock前缀提供的内存读写屏障原理?
我的理解是:对于一个添加volatile关键字的变量,在进行操作的时候,汇编码会对相应的操作语句添加lock前缀指令,lock指令可以提供类似内存屏障的功能,让两边的指令不进行重排序。
1.8 DLC(双重检查锁定)创建单例对象的时候需不需要加volatile?
什么是DCL?
DCL是一种创建单例对象的方式。使用一次判空(为空则进行创建对象) + 加锁(避免多线程创建不同对象) + 再一次判空(在多线程下,在线程B等待锁的时候,一旦线程A完成对象创建释放了锁,那么线程B会继续进行创建对象,就不是单例对象了)的方式,实现需要某一对象的时候在进行创建。
需要volatile。volatile避免指令重排,保证线程可见性。
文章知识点可能有不准确或者不清楚的地方,有大佬发现希望可以评论指正。
标签:对象,指令,线程,内存,JVM,JMM,加载 来源: https://www.cnblogs.com/sqzr316/p/16246874.html