其他分享
首页 > 其他分享> > JVM与JMM

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)

  1. GC的主要区域是堆,方法区(也叫非堆),更具体点:主要是年轻代和老年代。相对应的JVM调优的对象是堆和方法区,99%是堆。
    在这里插入图片描述
  2. GC算法
    1. 引用计数器算法 对每一个对象记录其引用数,回收引用数为0的对象。
        优点 简单,实时性,如果有计数器为0直接回收。
        缺点 计数器会有消耗,无法解决循环引用问题
    2. 复制算法
      首先将保留的对象复制到一个下一个区域,然后释放当前占用的整个区域。
        优点 不会产生内存碎片(内存碎片过多会导致新对象存储时无法找到足够连续内存进行存储。)
        缺点 需要一块多余的存储空间
    3. 标记清除
      将可以回收的对象做标记,然后一次性把这些对象全部回收掉。
        优点 不需要多余空间就可以实现,不移动对象的位置。
        缺点 会产生大量内存碎片。时间复杂度高,两个阶段,扫描全部对象标记,扫描全部对象回收。
    4. 标记清除压缩
      在标记清除的基础上,把散乱的内存碎片压缩在一起
        优点 不需要多余空间就可以实现,解决了标记清除的内存碎片问题。
        缺点 时间复杂度高
    5. 分代收集算法
      现代的JVM大多采用这种方式。将堆分为年轻代和老年代。在年轻代中,由于对象生存周期短,每次回收都会有大量对象死去,这时采用复制算法。而老年代中,对象生存周期长,采用标记清除压缩算法或标记清除算法。
      补充:调用System.gc会优先调用重GC(full GC),但是不一定立刻调用。

    1.7 堆调优

    1. -Xms10m 调整堆的初始化内存
    2. -Xmx10m 调整堆的最大扩展内存,一般和初始化内存设置为一样,避免程序运行过程中,由于内存扩展,导致内存震荡,影响程序性能。
    3. -XX:+MaxTenuringThreshold=20,设置经过多少次GC后存活的年轻代重对象进入老年代。

1.8 类加载器

类加载器是用来加载class文件的。

  1. 三种类加载器
    • 启动类加载器(BootstrapClassLoader) 主要加载javahome/lib/rt.jar charsets.jar等java核心类库
    • 扩展类加载器(ExtClassLoader) 主要加载javahome/ext/中的类库
    • 应用类加载器(AppClassLoader) 加载用户路径classpath路径下的类
    • 用户类加载器(UserClassLoader) 加载用户自定义路径下的类
  2. 双亲委派机制
    双亲委派机制是类加载过程中的一种安全保障机制,保护java的核心类库不被修改。当一个类加载器收到一个加载类的请求时,首先会委托他的父类加载器加载,直到根加载器,找不到则向下委托,找到该类就进行加载,如果到了最下层的加载器仍然找不到该类,就会报出ClassNotFoundException异常。这种机制还避免了类被重复加载。
  3. 沙箱安全机制
    通过双亲委派机制实现了沙箱安全机制,保证java的核心类库不被修改,避免植入恶意代码。

二. JMM

1.1 什么是JMM

JMM是Java Memory Model(Java内存模型的简称),是一种符合内存模型规范的,屏蔽了各种硬件和操作系
统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。它规定了线程
的工作内存和主存之间的交互关系,以及线程之间的可见性和程序的执行顺序。

1.2 JMM图

JMMjava内存模型是模仿计算机缓存架构来做的,首先数据存放在主存中,当cpu要执行的时候,数据会放在缓存中等待cpu调用,到了cpu中又有寄存器来存储数据供cpu来进行计算。JMM是规定,每一个线程在执行的时候,有自己的内存空间,每一个线程要操作的数据从主存中获取。并且还规定了线程之间的可见性,以及程序的执行顺序。
JMM定义了8个数据原子性操作:

在这里插入图片描述
正是由于这些原子操作,会导致线程之间的不可见性。

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