jvm总结

1.jvm内存结构:程序计数器、虚拟机栈、本地方法栈、java堆、方法区

  • 程序计数器:线程私有的,一块很小的内存空间,作为当前线程的行号指示器,用于记录当前虚拟机正在执行的线程指令地址。
  • 虚拟机栈:线程私有的,每个方法执行的时候都会创建一个栈帧,用于存储局部变量表,操作数,动态链接和方法返回等信息,当线程请求的栈深度超过了虚拟机允许的最大深度时,就会抛出StackOverFlowError
  • 本地方法栈:线程私有的,保存的是native方法的信息,当一个jvm创建的线程调用native方法后,jvm不会在虚拟机栈中为该线程创建栈帧,而是简单的动态链接并直接调用该方法。
  • java堆:是所有线程共享的一块内存,几乎所有对象的实例和数组都要在堆上分配内存,因此该区域经常发生垃圾回收的操作;
  • 方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据,即永久代

2.jvm内存模型(JMM)

  • 在底层处理器内存模型的基础上,定义自己的多线程语义,明确指定了一组排序规则,来保证线程间的可见性。(happens-before:要想保证b操作能够看到a操作的结果,a和b之间必须满足happens-before)
    • 单线程规则:一个线程中的每个动作都happens-before该线程中后续的每个动作
    • 监视器锁定规则:监听器的解锁动作happens-before后续对这个监听器的锁定动作
    • volatile变量规则:对volatile字段的写入动作happens-before后续对这个字段的每个读取动作
    • 线程start规则:线程start()方法的执行happens-before一个启动线程内的任意动作
    • 线程join规则:一个线程内的所有动作happens-before任意其他线程在该线程join()成功返回之前
    • 传递性:如果A happens-before B ,且B happens-before C 那么A happens-before C
  • 关键字:volatile,final和synchronized
    • volatile:保证可见性和有序性
    • synchronized:保证可见性和有序性,通过管程,保证一组动作的原子性
    • final:通过禁止在构造函数初始化和给final字段赋值这两个动作的重排序,保证可见性(如果this引用逃逸就不好说可见性了)
    • 编译器遇到这些关键字时,会插入相应的内存屏障,保证语义的正确性
    • 注:synchronized不保证同步块内的代码禁止重排序,因为它通过锁保证同一时刻只有一个线程访问同步块(临界区),即同步块的代码只满足as-if-serial语义-只要单线程的执行结果不改变,可以进行重排序

java内存模型描述的是多线程对共享内存修改后彼此之间的可见性,这确保正确同步的java代码可以在不同体系结构的处理器上正确运行。

3.heap和stack

  • 申请方式
    • stack:由系统自动分配,例如,声明在函数中一个局部变量int b;系统自动在栈中为b开辟空间
    • heap:需要程序员自己申请,并指明大小,在c中malloc函数,对于java要手动new Object()的形式开辟
  • 申请后系统的响应
    • stack:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出
    • heap:操作系统有一个记录空闲内存地址的链表,当系统受到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆节点,然后将该节点从空闲节点链表中删除,并将该结点的空间分配给程序,另外,由于找到的堆节点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
  • 申请大小的限制
    • stack:栈是向低地址扩展的数据结构,是一块连续的内存的区域
    • heap:堆是向高地址扩展的数据结构,是不连续的内存区域,
  • 申请效率的比较
    • stack:由系统自动分配,速度较快,但程序员无法控制
    • heap:由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便
  • heap和stack中存储内容
    • stack:函数调用时,第一个进栈的是主函数中后的下一条指令的地址,然后还是函数的各个参数
    • heap:一般是在堆的头部用一个字节存放堆的大小,

4.栈内存溢出

  • 栈是线程私有的,栈的生命周期和线程一样,每个方法在执行的时候创建一个栈帧,包含局部变量表,操作数栈,动态链接,方法出口等信息,局部变量表又包括基本数据类型和对象的引用
  • 当线程请求的栈深度超过了虚拟机允许的最大深度时,会抛出StackOverFlowError异常,方法递归调用可能会出现该问题
  • 调整参数-xss去调整jvm栈的大小

5.OOM

除了程序计数器,其他内存区域都有oom的风险

  • 栈一般经常发生StackOverflowError,eg:32位windows系统单进程限制2G内存,无限创建线程就会发生栈的OOM
  • java8常量池移到堆中,溢出会出java.lang.OutOfMemoryError:Java heap space,设置最大元空间参数无效
  • 堆内存溢出,GC之后无法在堆中申请内存创建对象就会报错
  • 方法区oom,动态生成大量的类,jsp等
  • 直接内存oom,涉及到-xx:MaxDirectMemorySize参数和Unsafe对象对内存的申请

排查oom的方法:

  • 增加两个参数:
    • -xx:+HeapDumpOnOutOfMemoryError
    • -xx:HeapDumpPath=/tmp/heapdump.hprof
    • 当oom发生时自动dump堆内存信息到指定目录
  • 同时jstat查看监控jvm的内存和gc情况,先观察问题大概出现在什么区域
  • 使用MAT工具载入到dump文件,分析大对象的占用情况,比如hashmap做缓存未清理,时间长了就会内存溢出可以把他改为弱引用。

6.JVM的常量池

  • class文件常量池:class文件是一组以字节为单位的二进制数据流,在java代码编译期间,编写的java文件就被编译为.class文件格式的二进制数据存放在磁盘中,其中就包括class文件常量池。
  • 运行时常量池:运行时常量池相对于class常量池一大特征就是具有动态性,java规范并不要求常量只能在运行时才产生,运行时常量池的内容并不全部来自class常量池,在运行时可以通过代码生成常量并将其放入运行时常量池中,这种特性被用的最多的就是String.intern()。
  • 全局字符串常量池:JVM所维护的一个字符串实例的引用表,在HotSpot VM中,是一个叫做StringTable的全局表。在字符串常量池中维护的是字符串实例的引用,底层C++实现就是一个Hashtable。这些被维护的引用所指的字符串实例,被称作”被驻留的字符串”或”interned string”或通常所说的”进入了字符串常量池的字符串”。
  • 基本类型包装类对象常量池:java中基本类型的包装类的大部分都实现了常量池技术,这些类是
    Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。这5种整型的包装类也只是在对应值小于等于127时才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象。

7.判断一个对象是否存活

  • 引用计数法:引用计数器为0,说明这个对象没有引用,无法解决循环引用问题

  • 可达性分析法:一个对象在GCroots没有任何引用链相连接时,说明对象不可用

    • 虚拟机栈中引用的对象
    • 方法区类静态属性引用的变量
    • 方法区常量池引用的对象
    • 本地方法栈JNI引用的对象

    一个对象满足上述条件时,不会马上被回收,还需两次标记

    ①:判断当前对象是否有finalize()方法并且该方法没有被执行过,若不存在则标记为垃圾对象,等待回收,若有的话,第二次标记。

    ②:将当前对象放入F-Queue队列,并生成一个finalize线程去执行该方法,虚拟机不保证该方法一定会被执行,因为如果线程执行缓慢或进入了死锁,会导致回收系统的崩溃,如果执行了finalize方法后仍然没有与GC roots有直接或间接的引用,则该对象会被回收。

8.引用

  • 强引用:普通的对象引用关系,String s = new String(“xxx”);
  • 软引用:用于维护一些可有可无的对象,只有内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常
  • 弱引用:拥有更短的生命周期,当jvm垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象
  • 虚引用:主要用来跟踪对象被垃圾回收的活动

9.垃圾回收算法:标记清除法,标记整理法,复制算法,分代收集算法

  • 标记清除法:
    • 利用可达性去遍历内存,把存活对象和垃圾对象进行标记
    • 再遍历一遍,将所有标记对象回收掉
    • 效率不行,标记和清除的效率都不高;标记和清除后会产生大量的不连续空间分片,可能会导致之后程序运行的时候需分配大对象而找不到连续分片而不得不触发一次GC
  • 标记整理法:
    • 利用可达性去遍历内存,把存活对象和垃圾对象进行标记
    • 将所有的存活的对象向一端移动,将端边界以外的对象都回收掉
    • 使用于存活对象多,垃圾少的情况,需要整理的过程,无空间碎片产生
  • 复制算法
    • 将内存按照容量大小分成大小相等的两块,每次只使用一块,当一块使用完了,就将还存活的对象移到另一块上,然后把使用过的内存空间移除
    • 不会产生空间碎片,内存使用率极低;
  • 分代收集算法
    • 根据内存对象的存活周期不同,java虚拟机一般将内存分成新生代和老年代,
      • 在新生代中,有大量对象死去和少量对象存活,所以采用复制算法,只需要付出少量存活对象的复制成本就可与完成收集
      • 老年代中因为对象的存活率极高,没有额外的空间对他进行分配担保,所以采用标记清理或标记整理算法回收。

10.垃圾回收器

  • Serial:单线程收集器,收集垃圾时,必须stop the world,使用复制算法,最大特点是进行垃圾回收时,需要对所有正在执行的线程暂停,对于有些应用是难以接受的。多数可以接受,是client级别的默认gc方式。
  • ParNew:Serial收集器的多线程版本,也需要stop the world
  • Parallel Scavenge:新生代收集器,复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量,和ParNew最大的区别是GC自动调节策略,虚拟机会根据系统的运行状态收集性能监控信息,动态设置做这些参数,以提供最优停顿时间和最高吞吐量。
  • Serial Old:serial收集器的老年代版本,单线程收集器,使用标记整理算法。
  • Parallel Old:是parallel scavenge收集器的老年代版本,使用多线程,标记-整理算法
  • CMS:是一种以获得最短回收停顿时间为目标收集器,标记清除算法,过程:
    • 初始标记,并发标记,重新标记,并发清除
    • 收集结束会产生大量空间碎片
  • G1:标记整理算法:流程:
    • 初始标记,并发标记,最终标记,筛选回收
    • 不会产生空间碎片,可以精确地控制停顿。
    • G1将整个堆分为大小相等的多个region,G1跟踪每个区域的垃圾大小,在后台维护一个优先级列表,每次根据允许的收集时间,优先回收价值最大的区域,已达到在有限时间内获取尽可能高的回收效率

11.垃圾回收器-CMS

  • CMS并发标记清除:以获取最短回收停顿时间为目标的收集器,在垃圾收集时使得用户线程和GC线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。
  • 回收过程
    • 初始标记:标记GCRoot开始的下级对象,过程中会STW,但是跟GC Root直接关联的下级对象不会很多,过程很快。
    • 并发标记:根据上一步结果,继续向下标识所有关联的对象,直到这条链上的最尽头,多线程,不阻塞,没有STW
    • 重新标记:再标记一次,第二部并没有阻塞其他工作线程,其他的线程在标识过程中,很有可能会产生新的垃圾
    • 并发清除:清理删除掉标记阶段的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发进行的。
  • 问题:
    • 并发回收导致cpu资源紧张
    • 无法清理浮动垃圾
    • 并发失败
    • 内存碎片问题

12.垃圾回收器-G1

  • 过程
    • 初始标记:仅仅标记一下GC Roots能直接关联到的对象,并修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象,这个阶段需停顿线程,但耗时很短,借用进行Minor GC时候同步完成,所以G1收集器在这个阶段实际并没有额外的停顿
    • 并发标记:从GC Roots开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,阶段耗时较长,但可与用户程序并发执行,当对象图扫描完成以后,还要重新在并发时有引用变动的对象
    • 最终标记:对用户线程做短暂的暂停,处理并发阶段结束后仍有引用变动的对象
    • 清理标记:更新region统计数据,对各个region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个region构成回收集,然后把决定回收的那一部分region的存活对象复制到空的region中,再清理掉整个旧region的全部空间,这里的操作涉及存活对象的移动,必须暂停用户线程,由多条回收器线程并行完成的

13.JVM一次完整的GC

Java堆内存划分

  • 新生代 ( Young ):占总空间的1/3, 3 个分区:Eden、To Survivor、From Survivor,默认占比是 8:1:1。
    • 新生代的垃圾回收(Minor GC)后只有少量对象存活,选用复制算法,只需要少量的复制成本就可以完成回收。
  • 老年代 ( Old ):老年代占 2/3。
    • 老年代的垃圾回收(Major GC)使用“标记-清理”或“标记-整理”算法。

转化流程:

  • 对象优先在Eden分配。当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。
    • 在 Eden 区执行了第一次 GC 之后,存活的对象会被移动到其中一个 Survivor 分区;
    • Eden 区再次 GC,这时会采用复制算法,将 Eden 和 from 区一起清理,存活的对象会被复制到 to 区;
    • 移动一次,对象年龄加 1,对象年龄大于一定阀值会直接移动到老年代。GC年龄的阀值可以通过参数 -XX:MaxTenuringThreshold 设置,默认为 15;
    • 动态对象年龄判定:Survivor 区相同年龄所有对象大小的总和 > (Survivor 区内存大小 * 这个目标使用率)时,大于或等于该年龄的对象直接进入老年代。其中这个使用率通过 -XX:TargetSurvivorRatio 指定,默认为 50%;
    • Survivor 区内存不足会发生担保分配,超过指定大小的对象可以直接进入老年代。
  • 大对象直接进入老年代,大对象就是需要大量连续内存空间的对象(比如:字符串、数组),为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
  • 老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行Full GC,Full GC 清理整个内存堆 – 包括年轻代和老年代。

14.Minor GC 和Full GC有什么不同

  • Minor GC :只收集新生代的GC
    • 触发条件:当Eden区满时,触发Minor GC。
  • Full GC:收集整个堆,包括 新生代,老年代,永久代(在 JDK 1.8及以后,永久代被移除,换为metaspace 元空间)等所有部分的模式
    • 触发条件:
      • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存。如果发现统计数据说之前Minor GC的平均晋升大小比目前old gen剩余的空间大,则不会触发Minor GC而是转为触发full GC。
      • 老年代空间不够分配新的内存(或永久代空间不足,但只是JDK1.7有的,这也是用元空间来取代永久代的原因,可以减少Full GC的频率,减少GC负担,提升其效率)
      • 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
      • 调用System.gc时,系统建议执行Full GC,但是不必然执行。

15.空间分配担保原则

年代空间分配担保机制来保证对象能够进入老年代:YougGC时新生代有大量对象存活下来,survivor 区放不下了,这时老年代也放不下这些对象了,使用空间分配担保原则。

执行YoungGC 前,JVM会先检查老年代最大可用连续空间是否大于新生代所有对象的总大小。极端情况下,新生代 YoungGC 后,所有对象都存活下来了,而 survivor 区又放不下,所有对象都要进入老年代了。

  • 如果老年代的可用连续空间是大于新生代所有对象的总大小的,那就可以放心进行 YoungGC。
  • 如果老年代的内存大小是小于新生代对象总大小的,那就有可能老年代空间不够放入新生代所有存活对象
    • JVM就会先检查 -XX:HandlePromotionFailure 参数是否允许担保失败,如果允许,就会判断老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次YoungGC,尽管这次YoungGC是有风险的。
    • 如果小于,或者 -XX:HandlePromotionFailure 参数不允许担保失败,就会进行一次 Full GC。
      在允许担保失败并尝试进行YoungGC后,可能会出现三种情况:
      • ① YoungGC后,存活对象小于survivor大小,此时存活对象进入survivor区中
      • ② YoungGC后,存活对象大于survivor大小,但是小于老年大可用空间大小,此时直接进入老年代。
      • ③ YoungGC后,存活对象大于survivor大小,也大于老年大可用空间大小,老年代也放不下这些对象了,此时就会发生“Handle Promotion Failure”,就触发了 Full GC。如果 Full GC后,老年代还是没有足够的空间,此时就会发生OOM内存溢出了。

16.类加载

  • 类的生命周期

    • 加载:
      • 通过类的全限定性类名获取该类的二进制流
      • 将该二进制流的静态存储结构转为方法区的运行时数据结构
      • 在堆中为该类生成一个class对象
    • 连接
      • 验证:验证该class文件中的字节流信息复合虚拟机的要求,不会威胁到jvm的安全
      • 准备:为class对象的静态变量分配内存,初始化其初始值
      • 解析:该阶段主要完成符号引用转化为直接引用
    • 初始化:到了初始化阶段,才开始执行类中定义的java代码,初始化阶段是调用类构造器的过程
    • 使用
    • 卸载
  • 类加载器

    • 启动类加载器:加载java核心类库,无法被java程序直接引用
    • 扩展类加载器:加载java的扩展库,java虚拟机实现会提供一个扩展库目录,该类加载器在扩展库目录里面查找并加载java类
    • 系统类加载器:它根据java的类路径来加载类,一般,java应用的类都是通过它来加载的
    • 自定义类加载器:由java语言实现,继承自ClassLoader
  • 双亲委派模型

    • 收到类加载的请求,首先不会尝试自己去加载,将请求委派给父类加载器去加载,只有父类加载器在自己的搜索范围类查找不到该类的时候,子加载器才会尝试自己去加载该类。
    • 防止内存中出现多个相同的字节码,如果没有双亲委派,用户就可以自定义一个java.lang.String类,无法保证类的唯一性
    • 打破双亲委派模型:自定义类加载器,继承ClassLoader类,重写loadClass方法和findClass方法
    • 例子:
      • JNDI引入线程上下文类加载器,默认应用程序类加载器加载SPI代码
      • Tomcat,应用的类加载器优先自行加载应用目录下的class,并不是先委派给父加载器,加载不了才委派给父加载器
        • tomcat造了一堆自己的classloader,目的
          • 对于各个webapp中的class和lib,需要相互隔离,不能出现一个应用中加载的类库会影响另一个应用的情况,对于许多应用需要有共享的lib以便不浪费资源
          • 与jvm一样的安全性问题,使用单独的classLoader去装载tomcat自身的类库,以免其他恶意或无意的破坏
          • 热部署
      • OSGI:实现模块化热部署,为每个模块都自定义了类加载器,需要更换模块时,模块与类加载器一起更换
  • JVM调优命令

    • jps:显示指定系统内所有的HotSpot虚拟机进程
    • jstat:用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载,内存,垃圾收集,JIT编译等运行数据
    • jmap:用于生成heap dump文件,如果不使用这个命令,还可以使用-xx:+HeapDumpOnOutOfMemoryError 参数来让虚拟机出现OOM的时候,自动生成dump问价
    • jhat:jhat命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以再浏览器看,在此要注意,一般不会直接在服务器上进行分析,因此jhat是一个耗时并且耗费硬件资源的过程,一般把服务器生成的dump文件复制到本地或其他机器上进行分析
    • jstack:用于生成java虚拟机当前时刻的线程快照,jstack来查看各个线程的调用堆栈,就可以知道响应的线程到底在后台做什么事情,或等待什么资源,如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack 和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序处发生问题。

你可能感兴趣的:(jvm,jvm)