优秀jvm原理和实战链接:https://developer.51cto.com/art/201201/312639.htm

1.JVM8内存模型:

  1.各区域介绍:

    1. 程序计数器:

      1.PC 寄存器,也叫程序计数器。

        1.当前线程所执行的字节码的行号指示器;

        2.当前线程私有;

        3.不会出现OutOfMemoryError情况

      2.JVM支持多个线程同时运行,每个线程都有自己的程序计数器。

      3.倘若当前执行的是 JVM 的方法,则该寄存器中保存当前执行指令的地址;

      4.倘若执行的是native 方法,则PC寄存器中为空(undefined)。

    2. Java虚拟机栈:

    

 

     

      1.每个线程有一个私有的栈,随着线程的创建而创建,其生命周期与线程同进同退。

      2.栈里面存着的是一种叫“栈帧”的东西,每个Java方法在被调用的时候都会创建一个栈帧,一旦完成调用,则出栈。

      3.所有的的栈帧都出栈后,线程也就完成了使命。

      4.栈帧中存放了局部变量表(基本数据类型和对象引用)、操作数栈、动态链接(指向当前方法所属的类的运行时常量池的引用等)、方法出口(方法返回地址)、和一些额外的附加信息。

      5.栈的大小可以固定也可以动态扩展。当栈调用深度大于JVM所允许的范围,会抛出StackOverflowError的错误,不过这个深度范围不是一个恒定的值。

        1.线程私有,生命周期与线程相同;

        2.java方法执行的内存模型,每个方法执行的同时都会创建一个栈帧,存储局部变量表(基本类型、对象引用)、操作数栈、动态链接、方法出口等信息;

        3.StackOverflowError异常:当线程请求的栈深度大于虚拟机所允许的深度;

        4.OutOfMemoryError异常:如果栈的扩展时无法申请到足够的内存。

    3. 本地方法栈:

      1.本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的.

    4. 堆:

    

 

     5. 元数据区
      1.元数据区取代了1.7版本及以前的永久代。元数据区和永久代本质上都是方法区的实现。方法区存放虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

      2.JDK 8 中永久代向元空间的转换的几点原因

        1、字符串存在永久代中,容易出现性能问题和内存溢出。
        2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
        3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

 

 

 2.JVM垃圾收集算法优化:

原文链接:https://www.cnblogs.com/csniper/p/5592593.html

  1.根据Java虚拟机规范,JVM将内存划分为:

    1.New(年轻代)

      1.年轻代用来存放JVM刚分配的Java对象;

      2.New又分为几个部分

        1.Eden:Eden用来存放JVM刚分配的对象

        2.Survivor1和Survivor2:两个Survivor空间一样大,当Eden中的对象经过垃圾回收没有被回收掉时,会在两个Survivor之间来回Copy,当满足某个条件,比如Copy次数,就会被Copy到Tenured。

    2.Tenured(年老代)

      1.年轻代中经过垃圾回收没有回收掉的对象将被Copy到年老代;

    3.永久代(Perm)

      1,永久代存放Class、Method元信息,其大小跟项目的规模、类、方法的量有关,一般设置为128M就足够,设置原则是预留30%的空间;

    4.其中New和Tenured属于堆内存,堆内存会从JVM启动参数(-Xmx:3G)指定的内存中分配;

    5.Perm不属于堆内存,有虚拟机直接分配,但可以通过-XX:PermSize -XX:MaxPermSize 等参数调整其大小。

  2.垃圾回收算法:

    1.垃圾回收算法可以分为三类,都基于标记-清除(复制)算法:

      1.Serial算法(单线程)

      2.并行算法:并行算法是用多线程进行垃圾回收,回收期间会暂停程序的执行;

      3.并发算法:并发算法,也是多线程回收,但期间不停止应用执行;

    2.什么时候会发生GC?

      1.当年轻代内存满时,会引发一次普通GC,该GC仅回收年轻代。需要强调的时,年轻代满是指Eden代满,Survivor满不会引发GC;

      2.当年老代满时会引发Full GC,Full GC将会同时回收年轻代、年老代;

      3.当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载;

    3.OutOfMemoryException如何发生?

      1.并不是内存被耗空的时候才抛出

      2. 满足这两个条件将触发OutOfMemoryException

        1.JVM98%的时间都花费在内存回收

        2.每次回收的内存小于2%

  3.内存泄漏及解决方法:

    1.系统崩溃前的一些现象:导致系统会无法响应新的请求,逐渐到达OutOfMemoryError的临界值。

      1.每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也有之前的0.5s延长到4、5s;

      2.FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC;

      3.年老代的内存越来越大并且每次FullGC后年老代没有内存被释放;

    2.生成堆的dump文件

       1.通过JMX的MBean生成当前的Heap信息,大小为一个3G(整个堆的大小)的hprof文件,如果没有启动JMX可以通过Java的jmap命令来生成该文件。

     3.分析dump文件

       1.下面要考虑的是如何打开这个3G的堆信息文件,显然一般的Window系统没有这么大的内存,必须借助高配置的Linux。当然我们可以借助X-Window把Linux上的图形导入到Window。我们考虑用下面几种工具打开该文件:

      1. Visual VM
      2. IBM HeapAnalyzer
      3. JDK 自带的Hprof工具

       2.使用这些工具时为了确保加载速度,建议设置最大内存为6G。

          1.使用后发现,这些工具都无法直观地观察到内存泄漏,Visual VM虽能观察到对象大小,但看不到调用堆栈;

          2.HeapAnalyzer虽然能看到调用堆栈,却无法正确打开一个3G的文件。

          3.因此,我们又选用了Eclipse专门的静态内存分析工具:Mat。

     4.分析内存泄漏

       1.通过Mat我们能清楚地看到,哪些对象被怀疑为内存泄漏,哪些对象占的空间最大及对象的调用关系。

       2.针对本案,在ThreadLocal中有很多的JbpmContext实例,经过调查是JBPM的Context没有关闭所致。

       3.通过Mat或JMX我们还可以分析线程状态,可以观察到线程被阻塞在哪个对象上,从而判断系统的瓶颈。

     5.回归问题

         1.Q:为什么崩溃前垃圾回收的时间越来越长?

         A:根据内存模型和垃圾回收算法,垃圾回收分两部分:内存标记、清除(复制),标记部分只要内存大小固定时间是不变的,变的是复制部分,因为每次垃圾回收都有一些回收不掉的内存,所以增加了复制量,导致时间延长。所以,垃圾回收的时间也可以作为判断内存泄漏的依据

        2. Q:为什么Full GC的次数越来越多?

         A:因此内存的积累,逐渐耗尽了年老代的内存,导致新对象分配没有更多的空间,从而导致频繁的垃圾回收

         3.Q:为什么年老代占用的内存越来越大?

         A:因为年轻代的内存无法被回收,越来越多地被Copy到年老代

  4.性能调优:

    1.在CPU负载不足的同时,偶尔会有用户反映请求的时间过长,我们意识到必须对程序及JVM进行调优,以下几个方面:

      1.Java线程池(java.util.concurrent.ThreadPoolExecutor):解决用户响应时间长的问题

        1.Java线程池有几个重要的配置参数:

          1.corePoolSize:核心线程数(最新线程数)

          2.maximumPoolSize:最大线程数,超过这个数量的任务会被拒绝,用户可以通过RejectedExecutionHandler接口自定义处理方式

          3.keepAliveTime:线程保持活动的时间

          4.workQueue:工作队列,存放执行的任务。Queue的不同选择,线程池有完全不同的行为:

            1.SynchronousQueue: 一个无容量的等待队列,一个线程的insert操作必须等待另一线程的remove操作,采用这个Queue线程池将会为每个任务分配一个新线程  

            2.LinkedBlockingQueue : 无界队列,采用该Queue,线程池将忽略 maximumPoolSize参数,仅用corePoolSize的线程处理所有的任务,未处理的任务便在LinkedBlockingQueue中排队

            3.ArrayBlockingQueue: 有界队列,在有界队列和 maximumPoolSize的作用下,程序将很难被调优:更大的Queue和小的maximumPoolSize将导致CPU的低负载;小的Queue和大的池,Queue就没起动应有的作用。

      2.连接池(org.apache.commons.dbcp.BasicDataSource)

      3.JVM启动参数:

        1.jvm调优原则(目标):

          1.GC的时间足够的小

          2.GC的次数足够的少

          3.发生Full GC的周期足够的长

          4.如果满足下面的指标,则一般不需要进行GC:

              1.Minor GC执行时间不到50ms;Minor GC执行约10秒一次;

              2.Full GC执行时间不到1s; Full GC执行不低于10分钟1次;

        2.前两个目前是相悖的,要想GC时间小必须要一个更小的堆,要保证GC次数足够少,必须保证一个更大的堆,我们只能取其平衡:

          (1)针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,我们通常把最大、最小设置为相同的值;

          (2)年轻代和年老代将根据:  

              1.默认的比例(1:2)分配堆内存;

              2.可以通过调整二者之间的比率NewRadio来调整二者之间的大小;

              3.也可以针对回收代,比如年轻代,通过 -XX:newSize -XX:MaxNewSize来设置其绝对大小。为了防止年轻代的堆收缩,我们通常会把-XX:newSize -XX:MaxNewSize设置为同样大小。

            (3)年轻代和年老代设置多大才算合理?这个我问题毫无疑问是没有答案的,否则也就不会有调优。我们观察一下二者大小变化有哪些影响

              1.年轻代越大,必然导致年老代越小:

                1.大的年轻代会减少普通GC的次数,但会增加每次GC的时间;

                2.小的年老代会增加的Full GC频率;

              2.年轻代越小,必然导致年老代越大:

                1.小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;

                2.大的年老代会减少Full GC的频率

              3.如何选择应该依赖应用程序对象生命周期的分布情况

                1.如果应用存在大量的临时对象,应该选择更大的年轻代;

                2.如果存在相对较多的持久对象,年老代应该适当增大。

                3.但很多应用都没有这样明显的特性,在抉择时应该根据以下两点:

                  (1)本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:2也是这个道理

                  (2)通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。但应该给年老代至少预留1/3的增长空间

        4.在配置较好的机器上(比如多核、大内存),可以为年老代选择并行收集算法: -XX:+UseParallelOldGC ,默认为Serial收集

        5.线程堆栈的设置:每个线程默认开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言默认值,一般256K就足用。理论上,在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程,但实际上还受限于操作系统。

      4.程序算法:改进程序逻辑,使用合理算法提高性能

      5.调优工具:

        1. dump:format=b,file=文件名.hprof pid 来dump内存,生成dump文件,并使用Eclipse下的mat差距进行分析

        2.Eclipse MAT 安装及使用https://blog.csdn.net/kas_uo/article/details/80179856

3.垃圾收集器:

  垃圾收集器总结文章:https://blog.csdn.net/SilenceOO/article/details/77869485?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EOPENSEARCH%7Edefault-1.base&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EOPENSEARCH%7Edefault-1.base

  1.收集器搭配:

  

 

  2.年轻代收集器如下:

    1.Serial收集器: 

      1.Serial收集器是在client模式下默认的新生代收集器,其收集效率大约是100M左右的内存需要几十到100多毫秒;

      2.在client模式下,收集桌面应用的内存垃圾,基本上不影响用户体验。所以,一般的Java桌面应用中,直接使用Serial收集器(不需要配置参数,用默认即可)。

 

    2.ParNew收集器:

      1.Serial收集器的多线程版本,这种收集器默认开通的线程数与CPU数量相同,-XX:ParallelGCThreads可以用来设置开通的线程数。

      2.可以与CMS收集器配合使用,事实上用-XX:+UseConcMarkSweepGC选择使用CMS收集器时,默认使用的就是ParNew收集器,所以不需要额外设置-XX:+UseParNewGC,设置了也不会冲突,因为会将ParNew+Serial Old作为一个备选方案;

      3.如果单独使用-XX:+UseParNewGC参数,则选择的是ParNew+Serial Old收集器组合收集器。
      4.一般情况下,在server模式下,如果选择CMS收集器,则优先选择ParNew收集器。
    3.Parallel Scavenge收集器:

      1.关注的是吞吐量(关于吞吐量的含义见上一篇博客),可以这么理解,关注吞吐量,意味着强调任务更快的完成,而如CMS等关注停顿时间短的收集器,强调的是用户交互体验。
      2.在需要关注吞吐量的场合,比如数据运算服务器等,就可以使用Parallel Scavenge收集器。
  2.老年轻收集器如下:
    1.Serial Old收集器:

      1.在1.5版本及以前可以与 Parallel Scavenge结合使用(事实上,也是当时Parallel Scavenge唯一能用的版本),另外就是在使用CMS收集器时的备用方案,发生 Concurrent Mode Failure时使用。
      2.如果是单独使用,Serial Old一般用在client模式中。
    2.Parallel Old收集器:

      1.在1.6版本之后,与 Parallel Scavenge结合使用,以更好的贯彻吞吐量优先的思想,如果是关注吞吐量的服务器,建议使用Parallel Scavenge + Parallel Old 收集器。
    3.CMS收集器:

      1.这是当前阶段使用很广的一种收集器,国内很多大的互联网公司线上服务器都使用这种垃圾收集器(http://blog.csdn.net/wisgood/article/details/17067203),CMS收集器以获取最短回收停顿时间为目标,非常适合对用户响应比较高的B/S架构服务器。
     4.CMSIncrementalMode: 

      1.CMS收集器变种,属增量式垃圾收集器,在并发标记和并发清理时交替运行垃圾收集器和用户线程。
   3.G1 收集器:

    1.面向服务器端应用的垃圾收集器,计划未来替代CMS收集器。G1的设计原则就是简单可行的性能调优。

    2.G1收集器原理:

    

 

      1.G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。

      2.不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。

      3.老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。

      4.这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。  

      5.在G1中,还有一种特殊的区域,叫Humongous区域:

        1.如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。

        2.为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。

    3.对象分配策略,它分为3个阶段:

    1. TLAB(Thread Local Allocation Buffer)线程本地分配缓冲区
      1. TLAB为线程本地分配缓冲区,它的目的为了使对象尽可能快的分配出来。如果对象在一个共享的空间中分配,我们需要采用一些同步机制来管理这些空间内的空闲空间指针。
    2. Eden区中分配
      1. 在Eden空间中,每一个线程都有一个固定的分区用于分配对象,即一个TLAB。分配对象时,线程之间不再需要进行任何的同步。
    3. Humongous区分配
    4. 对TLAB空间中无法分配的对象,JVM会尝试在Eden空间中进行分配。如果Eden空间无法容纳该对象,就只能在老年代中进行分配空间

     4.G1提供了两种GC模式,Young GC和Mixed GC:

      1.G1的Young GC:

        1.Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。

        2.在这种情况下,Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。

        3.Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC停止工作,应用线程继续执行。

        4.G1引进了RSet的概念。它的全称是Remembered Set,作用是跟踪指向某个heap区内的对象引用。

      2.G1的 Mix GC:

      Mix GC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。

      它的GC步骤分2步:

      1. 全局并发标记(global concurrent marking),5个步骤:
        1. 初始标记(initial mark,STW)
          在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。
        2. 根区域扫描(root region scan)
          G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。
        3. 并发标记(Concurrent Marking)
          G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断
        4. 最终标记(Remark,STW)
          该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。
        5. 清除垃圾(Cleanup,STW)
          在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。
      2. 拷贝存活对象(evacuation)

    5.三色标记算法

      1.提到并发标记,我们不得不了解并发标记的三色标记算法。它是描述追踪式回收器的一种有用的方法,利用它可以推演回收器的正确性。 首先,我们将对象分成三种类型的。

        1.黑色:根对象,或者该对象与它的子对象都被扫描

        2.灰色:对象本身被扫描,但还没扫描完该对象中的子对象

        3.白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象