JVM相关总结

作者: ChaosCoffee | 来源:发表于2017-08-28 14:53 被阅读0次

    [JVM]

    虽然大部分情况下不需要我们直接对Java内存方面进行直接操作,但是了解其中的原理和调优还是比较重要的,JVM中的总结如下:

    [运行数据区]

    1. 程序计数器
      每个线程都有一个独立的程序计数器,是线程私有的。
      若是正在运行一个方法,计数器记录正在执行虚拟机字节码指令地址;
      执行的是native的方法,计数器为空(Undefined),不存在OutOfMemoryError

    2. 虚拟机栈
      线程私有,java方法执行的内存模型。
      每个方法运行的时候会创建一个栈帧,
      栈帧存储着 ->
      (a)局部变量
      用于存放方法参数和方法内部定义的局部变量
      包括基本数据类型boolean,byte,char,int,short,double,long.float,对象引用类型reference->不等于对象本身,returnAddress
      (b)操作数栈
      后入先出
      (c)动态链接
      (d)方法返回地址
      返回方法被调用的位置
      (e)一些额外的附加信息
      规范里没有提到的信息,例如与调试相关的信息

    3. 本地方法栈
      线程私有;
      虚拟机使用的native的方法服务。

    4. java堆
      java堆是被所以线程共享的一块内存区域,启动时候创建。
      所有对象实例和数据在堆上分配内存。

    5. 方法区
      各个线程共享的内存区域;
      存储java虚拟机加载的类信息,访问修饰符,方法描述,常量,静态变量,编译后的代码等数据。
      (a)运行的常量池
      class文件->动态性
      (b)直接内存
      例如NIO基于通道和缓冲区的I/O方式调用native

    [垃圾回收]

    1. 引用计数算法
      对象添加一个引用计数器,每当有一个地方引用时候,计数器加1;引用失效时,计数器值减1;任何时候计数器的值为0,对象不再使用。
    2. 可达性分析算法
      以GC Roots对象为起始点,当一个对象到达GCRoots没有任何引用链的时候,证明对象不可达。
      作为GCRoots对象的包括:
    (a) 虚拟机栈(栈帧中的本地变量表)中引用的对象  
    (b) 方法区中类静态属性引用的对象  
    (c) 方法区中常量引用的对象  
    (d) 本地方法栈中JNI(即一般说的native方法)引用的对象
    
    1. 标记-清除算法
    2. 复制算法
      内存分为一块较大的Eden和两块较小的Survivor空间,每次使用Eden和一块Survivor空间;
      回收时,将Eden和Survivor存活的对象一次性复制到另外一块Survivor空间,清理用过的Eden和Survivor空间,默认Eden和Survivor大小比例8:1
    3. 标记-整理算法

    [垃圾收集器]

    1. Serial收集器
      新生代采取复制算法,暂停所有用户线程;
      老年代采取标记整理算法,暂停用户所以线程
    2. ParNew收集器
      Serial收集器的多线程版本
      GC多线程,新生代采取复制算法,暂停所有用户线程;
      老年代采取标记整理算法,暂停用户所以线程
    3. Parallel Scavenge收集器
      CMS收集器尽可能缩短垃圾收集时用户线程的停顿时间
      Parallel Scavenge收集器目的达到一个可控制的吞吐量(吞吐量=运行代码时间/(运行代码时间+垃圾收集时间))
    4. Serial Old收集器
    5. Parallel Old收集器
    6. CMS收集器
      以获取最短回收停顿时间为目标的收集器。
      采取标记-清除算法 ->
    (1)初始标记
    (2)并发标记
    (3)重新标记
    (4)并发清除
    
    1. G1收集器
      面向服务端应用的垃圾收集器
      步骤 ->
    (a)初始标记
    (b)并发标记
    (c)最终标记
    (d)筛选回收
    

    [内存分配]

    -Xms20M,-Xmx20M,-Xmn10M -XX:SurvivorRatio=8,-XX:+PrintGCDetails
    java堆大小为20M,不可扩展,10M分配新生代,10M分配老年代
    新生代的Eden区与一个Survivor区的空间比例为8:1
    新生代的总可用空间(Eden区+1个Survivor区的总容量)
    -XX:PermSize永久代
    -XX:MaxPermSize永久代最大容量

    存储的是java的类信息,包括解析得到的方法、属性、字段等等。永久带基本不参与垃圾回收。
    在jdk1.8之前,通过-XX:PermSize=64m -XX:MaxPermSize=128m来调整永久代大小,
    在jdk1.8之后,永久代被移除,原本存储在永久代的数据将存放在一个叫做元空间的本地内存区域,
    通过 -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=128m来调整元空间大小

    [类加载的时机]

    生命周期
    -> 加载

    (a)通过一个类的全限定名来获取定义此类的二进制字节流   
    (b)将这个字节流所代表的静态存储结构转化为方法区运行时的数据结构  
    (c)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
    

    -> 验证

    (a)文件格式验证  
    (b)元数据验证  
    (c)字节码验证  
    (d)符号引用验证
    

    -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载

    [类加载器]

    1. 启动类加载器(Bootstrap ClassLoader)
      负责将存放<JAVA_HOME>\lib目录中;
      被-Xbootclasspath参数指定的路径中(仅按照文件名识别,如rt.jar)
    2. 扩展类加载器(Extension ClassLoader)
      负责加载<JAVA_HOME>\lib\ext目录中
      或者被java.ext.dirs系统变量所指定的路径中所有的类库
    3. 应用类加载器(Application ClassLoader)
      负责加载用户类路径ClassPath上所指定的类库
    4. 自定义类加载器 UserClassLoader

    如果一个类加载器收到了类加载的请求,请求委派给父类加载器,只有当父类反馈无法完成,子类尝试加载。

    [Java内存模型]

    Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。
    主内存主要对应与java堆里的对象实例数据部分,而工作内存则对应虚拟机栈中的部分区域。
    内存间的交互

    1. lock锁定
      作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
    2. unlock(解锁)
      作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后才能被其他线程锁定。
    3. read(读取)
      作用于主内存的变量,将一个变量的值从主内存传输到线程的工作内存,以便随后的load动作使用。
    4. load(载入)
      作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
    5. use(使用)
      作用于工作内存的变量,它把一个变量的值传递给执行引擎,每当虚拟机遇到需要使用变量的值的字节码指令时将会执行这个操作。
    6. assign(赋值)
      作用于工作内存的变量,它把执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
    7. store
      作用于工作内存的变量,它把工作内存内存中一个变量的值传送给主内存中,以便随后的write操作使用。
    8. write(写入)
      作用于主内存的变量,它把从store操作从工作内存中得到的变量的值放入主内存的变量中。

    volatile

    1. 此变量对所以线程可见(等线程A写进主内存结束,线程B才开始读取)
    2. 主内存一致,工作内存不能保持一致
    3. 禁止指令重排序优化

    内存模型特征

    1. 原子性
    2. 可见性
      变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性。
      volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。
    3. 有序性

    [线程的实现]

    1. 使用内核线程实现
      内核通过操纵调度器对线程进行调度,并将线程的任务映射到各个处理器上。
      内核线程一种高级接口--轻量级进程LWP,每个轻量级进程由一个内核线程支持。
      用户态和内核态中来回切换。
      1:1
    2. 使用用户线程实现
      1:N
    3. 使用用户线程实现和使用用户线程加轻量级进程混合实现
      N:M

    [线程安全的同步方法]

    1. 互斥同步
      互斥是实现同步的一种手段,临界区,互斥量,信号量
      阻塞同步
    2. 非阻塞同步
    (a)测试并设置
    (b)获取并增加
    (c)交换
    (d)比较并交换
    (e)加载链接/条件存储
    
    1. 无同步方案
      不涉及共享数据,不依赖堆的数据和公共系统资源

    [锁优化]

    1. 自旋锁和自适应自旋
      自旋--线程执行一个忙循环-XX:+UseSpinning参数来开启
      自旋超过一定次数没有成功获得锁,便使用传统方式挂起线程,自旋次数默认是10次,用户可以使用参数 -XX:PreBlockSpin更改
    2. 自适应的自旋
      由前一次在同一个锁上的自旋时间时间及锁的拥有者的状态来决定,
      如果同一个锁对象,自旋刚刚成功过,允许自旋更长时间,很少成功,则省略自旋这个过程。
    3. 锁消除
      虚拟机及时编译器运行的时候,对一些代码要求同步,但是不可能存在数据竞争的进行消除。
    4. 锁粗化
      虚拟机探测到一串零碎的操作对同一个对象枷锁,会把加锁同步到整个操作序列外部。
    5. 轻量级锁
      无竞争的情况下使用CAS操作去消除同步使用的互斥量
    6. 偏向锁
      无竞争的情况下把整个同步都消除掉,CAS操作不进行。
      -XX:+UseBiasedLocking
      锁对象第一次被线程获取,会把对象头标志设置为“01”,即偏向模式,同时用CAS操作记录在对象的Mark Word之中,
      如果CAS成功,以后每次持有偏向锁的线程都不再进行任何同步操作。
      如果有另一个线程来获取,偏向锁结束,根据锁对象是否处于被锁定的状态,撤销偏向,后续操作和轻量级锁执行类似。
      缺点:多个线程下访问,并不适合。

    本文参考于《深入理解Java虚拟机》推荐大家看一看,会对自己收获很大。

    相关文章

      网友评论

        本文标题:JVM相关总结

        本文链接:https://www.haomeiwen.com/subject/gmpidxtx.html