Java JVM 相关基础知识

Posted Jaine

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java JVM 相关基础知识相关的知识,希望对你有一定的参考价值。

1.JMM Java内存模型

   1)Java的并发采用“共享内存”模型,线程之间通过读写内存的公共状态进行通讯。多个线程之间是不能通过直接传递数据交互的,它们之间交互只能通过共享变量实现;
   2)主要目的是定义程序中各个变量的访问规则;
   3)Java内存模型规定所有变量都存储在主内存中,每个线程还有自己的工作内存:
  •  线程的工作内存中保存了被该线程使用到的变量的拷贝(从主内存中拷贝过来),线程对变量的所有操作都必须在工作内存中执行,而不能直接访问主内存中的变量;
  •  不同线程之间无法直接访问对方工作内存的变量,线程间变量值的传递都要通过主内存来完成
  •  主内存主要对应Java堆中实例数据部分;
  •  工作内存对应于虚拟机栈中部分区域
     4)Java线程之间的通信由内存模型JMM(Java Memory Model)控制:
  •  JMM决定一个线程对变量的写入何时对另一个线程可见。
  • 线程之间共享变量存储在主内存中
  • 每个线程有一个私有的本地内存,里面存储了读/写共享变量的副本。
  • JMM通过控制每个线程的本地内存之间的交互,来为程序员提供内存可见性保证。
    5)可见性、有序性:
  • 当一个共享变量在多个本地内存中有副本时,如果一个本地内存修改了该变量的副本,其他变量应该能够看到修改后的值,此为可见性。
  • 保证线程的有序执行,这个为有序性。(保证线程安全
    6)内存间交互操作:
  • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
  • unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存变量,把主内存的一个变量读取到工作内存中。
  • load(载入):作用于工作内存,把read操作读取到工作内存的变量载入到工作内存的变量副本中
  • use(使用):作用于工作内存的变量,把工作内存中的变量值传递给一个执行引擎。
  • assign(赋值):作用于工作内存的变量。把执行引擎接收到的值赋值给工作内存的变量。
  • store(存储):把工作内存的变量的值传递给主内存
  • write(写入):把store操作的值入到主内存的变量中
    7)注意:
  • 不允许read、load、store、write操作之一单独出现
  • 不允许一个线程丢弃assgin操作
  • 不允许一个线程不经过assgin操作,就把工作内存中的值同步到主内存中
  • 一个新的变量只能在主内存中生成
  • 一个变量同一时刻只允许一条线程对其进行lock操作。但lock操作可以被同一条线程执行多次,只有执行相同次数的unlock操作,变量才会解锁
  • 如果对一个变量进行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或者assgin操作初始化变量的值。
  • 如果一个变量没有被锁定,不允许对其执行unlock操作,也不允许unlock一个被其他线程锁定的变量
  • 对一个变量执行unlock操作之前,需要将该变量同步回主内存中

 2.java的堆和栈

1)堆 heap:可动态申请的内存空间(其记录空闲内存空间的链表由操作系统维护);       

    其中的内存在不需要时可以回收,以分配给新的内存请求,其内存中的数据是无序的
    一般由使用者自由分配,malloc 分配的就是堆,需要手动释放;
    被所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例

2)栈 stack:先进后出的数据结构,通常用于保存方法(函数)中的参数,局部变量.;

    在 java 中,所有基本类型和引用类型都在栈中存储.栈中数据的生存空间一般在当前 scopes 内(就是由{...}括起来的区域);

    先分配的内存必定后释放
    一般由系统自动分配,存放函数的参数值,局部变量等,自动清除
3.方法区 本地方法栈
 1)方法区 Method Area:和Java 堆一样,别名叫做 Non-Heap(非堆),是各个线程共享的内存区域;
          它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
  运行时常量池 Runtime Constant Pool:方法区的一部分;         
          Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
2)本地方法栈 Native Method Stacks:是为虚拟机使用到的 Native 方法服务
    注意:JVM 组成

 4.JVM 运行时数据区

  程序计数器:是一小块内存,记录着当前程序运行到哪了;

      字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程回复等都需要依赖这个计数器来完成;

       如果一个线程执行一个主要方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;

      如果正在执行的是一个本地方法,这个计数器的值则为空;

      唯一一个在Java的虚拟机规范中没有规定任何OutOfMemoryError异常情况的区域

 其余参考 2和3

 4.GC

  GC Garbage Collection:能可以自动监测对象是否超过作用域从而达到自动回收内存的目的

5.如何判断回收此对象

 1) 根索算法:每个对象看做根节点,GC roots ,也就是根对象,如果从一个对象没有到达根对象的路径,或者说从根对象开始无法引用到该对象,该对象就是不可达的

      GC Roots 对象 :

    虚拟机栈(栈帧中的本地变量表)中引用的对象,每个方法执行的时候,jvm都会创建一个相应的栈帧(栈帧中包括操作数栈、局部变量表、运行时常量池的引用);

        方法区中类静态属性引用的对象(使用static关键字),此类不属于任何实例;

     方法区常量引用的对象(使用static final关键字);

            本地方法栈(Native Stack)引用的对象,JNI技术  使用Native方法

 2)引用计数法(目前未使用)

   java在运行时,当有一个地方引用该对象实例,会将这个对象实例加1,引用失效时就减1,jvm在扫描内存时,发现引用计数值为0的则是垃圾对象,计数值大于0的则为活跃对象

6.Java 内存泄漏

   实际开发中,可能会存在无用但可达的对象,这些对象不能被 GC 回收,因此也会导致内存泄露的发生(OutOfMemoryError)
   常见的内存泄漏现象:
     1)长生命周期的对象持有短生命周期的引用,就很可能会出现内存泄露
public class Test {
    Object object;
    public void test(){
        object = new Object();
        //...其他代码
    }
}

 2)静态集合类像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放,因为他们也将一直被Vector等应用着

   3)当集合里面的对象属性被修改后,再调用remove()方法时不起作用;

   4)各种连接,数据库连接,网络连接,IO连接等没有显示调用close关闭,(Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL);

   注意:尽量减少使用静态变量,类的静态变量的生命周期和类同步的;

    明确内存对象的有效作用域;

    各种连接(数据库连接,网络连接,IO连接)操作,务必显示调用close关闭;

    对于不需要使用的对象手动设置null值,不管GC何时会开始清理,我们都应及时的将无用的对象标记为可被清理的对象

7.JVM 垃圾回收算法

   Java堆中的对象是分为新生代和老年代

 1)标记-清除算法  Mark-Sweep:首先标记出所有需要回收的对象,在标记完成之后统一回收所有标记的对象,但标记和清除过程的效率都不高,标记清除之后会产生大量不连续的内存碎片

        

  2)标记-整理算法 Mark-Compact:先标记所有可回收对象,让存活的对象向一端移动,然后直接清理掉端边界以外的内存

        

  3)复制 Copying:jvm扫描所有对象,通过根搜索算法标记被引用的对象,之后会申请新的内存空间,将标记的对象复制到新的内存空间里,存活的对象复制完,会清空原来的内存空间,将新的内存最为jvm的对象存储空间,但代价是将内存缩小为原来的一半 

    

 4)分代回收

      jvm常用回收算法就是分代回收,年轻代以复制算法为主,老年代以标记整理算法为主

      年轻代对象比较多,每次垃圾回收都有很多的垃圾对象回收,而且要尽可能快的减少生命周期短的对象,存活的对象较少,因此只要将有标记的对象复制到另一个内存区域,其余全部清除,并且复制的数量较少,效率较高;

     老年代是年轻代筛选出来的对象,需要删除的对象比较少,显然采用标记整理效率较高

8.JVM 垃圾回收器

新生代收集器:Serial、ParNew、Parallel Scavenge

老年代收集器:CMS、Serial Old、Parallel Old

整堆收集器: G1

并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。

并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上。

吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 ))。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%

 1)Serial 收集器

     串行;

     用于新生代的单线程收集器,采用复制算法进行垃圾收集;

  Serial 进行垃圾收集时,不仅只用一条线程执行垃圾收集工作,它在收集的同时,所有的用户线程必须暂停(Stop The World)

    适用于 Client 模式下的虚拟机,单CPU
   

 2)ParNew 收集器

    是一个 Serial 的多线程版本,其它与Serial并无区别;

 默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数;

    存在Stop The World;

  许多运行在Server模式下的虚拟机中首选的新生代收集器,除了Serial收集器外,目前只有它能与CMS收集器配合工作

  

 3)Parallel Scavenge 收集器

   使用复制算法的收集器,又是并行的多线程收集器;

  与 ParNew 的不同之处是ParNew 的目标是尽可能缩短垃圾收集时用户线程的停顿时间,Parallel Scavenge 的目标是达到一个可控制的吞吐量

  GC 自适应调节:-XX:+UseAdptiveSizePolicy 开启

      提供两个参数用于精确控制吞吐量:XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间 

                  XX:GCRatio 直接设置吞吐量的大小  计算方法是: 1 / (1 + n)

 4)Serial Old

       Serial 的老年代版本,同样是一个单线程收集器,采用标记-整理算法;

  用途:在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用;

     作为CMS收集器的后备方案

 5)Parallel Old

     Parallel Scavenge 的老年代版本,是一个多线程收集器,采用标记-整理算法;

  JDK1.6有Parallel Old收集器可搭配Parallel Scavenge收集器;

 JDK1.6及之后用来代替老年代的Serial Old收集器

   

 6)CMS(Concurrent Mark Sweep)

     以获取最短回收停顿时间为目标的收集器,基于标记-清除算法实现,并发收集、低停顿;

  JDK1.5推出;

  CMS的内存回收是与用户线程一起“并发”执行的

  运行过程:

          1)初始标记:Stop The World,初始标记仅仅标记GC Roots能直接关联到的对象,速度很快;

          2)并发标记:进行GC Roots Tracing的过程,同时开启GC和用户线程,用一个闭包结构去记录可达对象,找出存活对象且用户线程可并发执行;

          3)重新标记:修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录(采用多线程并行执行来提升效率);需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;

          4)并发清除:开启用户线程,同时GC线程开始对为标记的区域做清扫,回收所有的垃圾对象

      

    缺点:对CPU资源非常敏感;无法处理浮动垃圾,可能出现Concurrent Model Failure失败而导致另一次Full GC的产生;因为采用标记-清除算法所以会存在空间碎片的问题,导致大对象无法分配空间,不得不提前触发一次Full GC

 7)G1(Garbage-First)

     jdk1.7 才正式引用的商用收集器,在JDK1.9已经成为默认的收集器;

     以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征;

  特点:

        1)并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行;

   2)分代收集:

    能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;
    能够采用不同方式处理不同时期的对象;
    虽然保留分代概念,但Java堆的内存布局有很大差别;
    将整个堆划分为多个大小相等的独立区域(Region);
    新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合;
  3)空间整合:

             从整体看,是基于标记-整理算法;

  •   从局部(两个Region间)看,是基于复制算法:不会产生内存碎片,有利于长时间运行;

       4)可预测停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型;可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒

                                有计划的避免在整个Java堆中进行全区域的垃圾收集;G1跟踪各个Region里面的垃圾堆积的大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region;这样就保证了在有限的时间内可以获取尽可能高的收集效率

    G1如何避免整堆扫描:G1中每个Region都有一个与之对应的Remembered Set,,虚拟机发现程序在对Reference类型进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用对象是否处于多个Region中(即检查老年代中是否引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆进行扫描也不会有遗漏。

     运行大致过程:    

           初始标记:仅标记GC Roots能直接到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。(需要线程停顿,但耗时很短。)

          并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象。(耗时较长,但可与用户程序并发执行)

          最终标记:为了修正在并发标记期间因用户程序执行而导致标记产生变化的那一部分标记记录。且对象的变化记录在线程Remembered Set  Logs里面,把Remembered Set  Logs里面的数据合并到Remembered Set中。(需要线程停顿,但可并行执行。)

          筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。(可并发执行)

9.堆 内存划分
   不同的对象,生命周期是不一样的,因此不同生命周期的对象采用不同的收集方式,可以提高垃圾回收的效率 
   串行serial, 并行parallel, 以及CMS,把堆内存划分为固定大小的三个部分: 年轻代(young generation), 年老代(old generation), 以及持久代(permanent generation)

 

    1)年轻代 Young Generation

            新生成的对象          

           分为三个区,一个Eden区,大部分对象在Eden区中生成,当Eden区满时,还存活的对象将被复制到 Survivor区(两个中的一个), 当这个 Survivor区满时,此区的存活对象将被复制到另外一个 Survivor区,当这个 Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制到年老区(Tenured)         

          注 意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来的对象,和从前一个 Survivor复制过来的对象,而复制到年老区的只有从第一个 Survivor 去过来的对象。而且,Survivor 区总有一个是空的

    同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能

   2)老年代 Old Generation

        生命周期较长的对象

        在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中

  3)持久代 Permanent Generation

      用于存放静态文件,如今Java类、方法等;

     持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等

10.简述分代垃圾回收工作

.

 1)Eden 区,新创建的对象;

 2)当 Eden 满了(达到一定比例)不能创建新对象,则触发垃圾回收(minor GC),将无用对象清理掉,然后剩余对象复制到某个 Survivor 中,如 S1,同时清空 Eden 区。

 3)当 Eden 区再次满了,会将 S1 中的不能清空的对象存到另外一个 Survivor 中,如 S2,同时将 Eden 区中的不能清空的对象,也复制到 S1 中,保证 Eden 和 S1,均被清空。
 4)重复多次(默认 15 次)Survivor 中没有被清理的对象,则会复制到老年代 Old(Tenured)区中,
 5)当 Old 区满了,则会触发一个一次完整地垃圾回收(FullGC),之前新生代的垃圾回收称为(minor GC) 

  

以上是关于Java JVM 相关基础知识的主要内容,如果未能解决你的问题,请参考以下文章

详解Jvm内存结构

详解Jvm内存结构

详解Jvm内存结构

Java JVM 相关基础知识

JVM 相关知识

JVM基础知识