JVM

类加载

类加载过程

  1. 加载:

    1. 根据类的全限定名查找类的字节码文件,通过二进制流读入字节码

    2. 将字节流所代表的静态存储结构转化为方法区的运行时数据结构(可以理解为将磁盘数据读入内存中)

    3. 生成class对象,作为方法区这个对象的入口

  2. 连接:把类的二进制数据合并到JRE中;

    1. 校验:检查载入Class文件数据的正确性;包含四种:文件格式验证,元数据验证,字节码验证,符号引用验证。

      • 文件格式验证:验证字节流是否符合Class文件规范,能否被当前版本虚拟机处理,如是否以魔数0xCAFEBABE开头,主次版本号是否受当前JVM支持等

        只有通过了文件格式验证,字节流才会存入方法区,后面三个验证都是基于方法区的存储结构的,不会再操作字节流

      • 元数据验证:对字节码描述的信息进行语义分析 如:这个类是否有父类(除了java.lang.Object之外,所有的类都应该父类,没有显式继承父类的类,都默认继承java.lang.Object类),这个类是否继承了不允许被继承的类,非抽象类是否实现了父类中或接口中的所有抽象方法,类中的字段、方法是否与父类产生矛盾等

      • 字节码验证:这是整个验证阶段过程中最复杂的一个阶段,主要是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段的基础上,此阶段对类的方法体进行校验分析,保证被校验类不会在运行时作出危害虚拟机的行为,如:保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,保证任何跳转指令都不会跳转到方法体以外的字节码指令上等

      • 符号引用验证:这个验证发生在虚拟机将符号引用化为直接引用的时候,这个转化发生在解析阶段。符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配校验。如:符号引用种通过字符串描述的全限定名是否能找到对应的类,符号引用中的类、字段、方法、的可访问性是否可被当前类访问。

        符号引用验证主要是为了解析行为能正常进行,如果不能通过符号引用验证,虚拟机将抛出Java.lang.IncompatibleClassChangeError的子类异常。

    2. 准备:给类的静态变量分配存储空间和初始值;

    3. 解析:将符号引用转成直接引用;

      1. 符号引用:一组符号标识引用的目标,字面量。例如A类引用B类,加载的时候,就是用的字面量

      2. 直接引用:指针、相对偏移量、指向目标的句柄

  3. 初始化:主要是根据程序中的赋值语句主动为类变量斌值。当有继承关系时,先初始化父类再初始化子类,所以创建一个子类时其实内存中存在两个对象实例

  4. 使用:使用程序之间的相互调用

  5. 卸载:卸载即销毁一个对象,一般情況下中由JVM垃圾回收器完成。代码层面的销毁只是将引用置为null

类加载器(装载阶段)

  • 启动(Bootstrap)类加载器

    启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中

  • 扩展(Extension)类加载器

    扩展类加载器ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库

  • 系统(System)类加载器

    也称应用程序加载器是指 Sun公司实现的AppClassLoader。它负责加载系统类路径java -classpath-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。

双亲委派模型

双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器

优点:具有优先的层级关系,可以避免类的重复加载

注意:加载器之间的关系不是继承,而是通过持有变量

tomcat如何实现隔离

  • CommonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;

  • CatalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;

  • SharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;

  • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;

jdbc的讨论

我们用JDBC的时候,是使用 Drivermanager进而获取 Connection, Drivermanager在java. sql包下,显然是由 Bootstrap类加载器进行装载。当我们使用 Drivermanager. getconnection时,得到的一定是厂商实现的类。但 Bootstrap classloader不会能加载到各个厂商实现的类,DrⅳerManager的解决方案就是,在Drⅳermanager初始化的时候,得到线程上下文加载器,去获取 Connection的时候,是使用线程上下文加载器去加载 Connection的,而这里的线程上下文加载器实际上还是 App classloader。所以在获取 Connection的时候,还是先找 Ext classloader和 Bootstrap Classloader,只不过这俩加载器肯定是加载不到的,最终会由 App classloader进行加载

类初始化过程

一个类初始化就是执行 Clinit()方法,过程如下

  • 父类初始化

  • static变量初始化/ static块(按照文本顺序执行)

Java Language Specification中,类初始化详细过程如下(最重要的是类初始化是线程安全的)

  1. 每个类都有一个初始化锁LC,进程获取LC(如果没有获取到,就一直等待)

  2. 如果C正在被其他线程初始化,释放LC并等待C初始化完成

  3. 如果C正在被本线程初始化,即递归初始化,释放LC

  4. 如果C已经被初始化了,释放LC

  5. 如果C处于 erroneous状态,释放LC并抛出异常 Noclassdeffound error

  6. 否则,将C标记为正在被本线程初始化,释放LC;然后,初始化那些fina且为基础类型的类成员变量

  7. 初始化C的父类SC和各个接口Sn(按照 Implements子句中的顺序来);如果SC或SN初始化过程中抛出异常,则获取LC,将C标记为 erroneous,并通知所有线程,然后释放LC,然后再抛出同样的异常

  8. 从 classloader处获取 assertion是否被打开

  9. 接下来,按照文本顺序执行类变量初始化和静态代码块,或接口的字段初始化,把它们当作是一个个单独的代码块。

  10. 如果执行正常,获取LC,标记C为已初始化,并通知所有线程,然后释放LC

内存区域 **

java执行引擎:执行优化字节码,并进行垃圾回收

  • 线程共享

      • 组成

        • new 对象

      • 大小

        • -Xms:堆的最小值;

        • -Xmx:堆的最大值;

        • -Xmn:新生代的大小;

        • -XX:NewSize;新生代最小值;

        • -XX:MaxNewSize:新生代最大值;

    • 方法区(1.7永久带,1.8元空间),Fs:常量;Ss静态变量参考地址

      • 全局字符串池:存储字符串的值

      • class文件常量池:每个class都有,存放编译期生成的字面量和符号引用

        • 字面量:文本字符串、基本类型、final常量

        • 符号引用:类的名称和全限定名、字段的名称和描述符、方法的名称和描述符

      • 运行时常量池:类加载完成后,将class文件常量池的符号引用转存(每个类都有);类在解析后,将符号引用替换为直接引用。

  • 线程私有

    • 虚拟机栈(由栈针组成):执行java方法

      • 组成

        • 局部变量表

        • 操作数站

        • 动态链接

        • 返回地址

      • 大小:-Xss调整

      • 异常:java.lang.StackOverflowError,栈溢出,一般是无限递归导致的。算法中递归和循环方法的实现都有意义,递归简单,循环速度快

    • 本地方法栈:执行本地方法

    • 程序计数器:存储执行指令的地址,确实多线程指令可以正确的执行

堆 **

  • 经过验证大部分存活时间不会太长,所以大部分对象都在新生代中

元空间

原因:

  1. 字符串存在永久代中,容易出现性能问题和内存溢出。

  2. 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

  3. 永久代会为GC带来不必要的复杂度,并且回收效率偏低。

元空间的特点

  1. 每个加载器有专门的存储空间

  2. 不会单独回收某个类。

  3. 元空间里的对象的位置是固定的

  4. 如果发现某个加载器不再存货了,会把相关的空间整个回收。

实际加载过程*

过程:

  1. 类的编译

    • 将java文件生成class文件

  2. 类的加载

    1. 类加载使用了双亲委派模型

    2. 装载:根据类的全限定名查找类的字节码文件,并生成class对象,采用双亲委派模型

    3. 链接:把类的二进制数据合并到JRE中;

    4. 初始化:对类的静态变量,静态代码块执行初始化操作(优先执行超类的)这里MyA的a/b变量就会被初始化

  3. 类的执行

    1. 方法的执行

      1. 线程创建成功后,都会有程序计数器(存放下一个要执行指令的地址),栈(保存方法的每次调用)

      2. 栈会存储局部变量表(存放使用的局部变量)、操作数栈(中继结果)

    2. 需要创建对象

      1. 分配内存: 为新生对象分配内存。方式有 “指针碰撞” 和 “空闲列表” 两种。

        1. 通过逃逸分析,判断对象是在栈上分配还是在堆上分配。

        2. 尝试TLAB(本地线程分配缓存)分配

        3. 判断对象是否可以直接分配到老年代

        4. 在eden区分配。

      2. 初始化零值

      3. 设置对象头

      4. 执行init一些方法

  1. 类加载检查:检查MyA这个类是否已经被加载,如果没有被加载

    1. 装载:根据类的全限定名查找类的字节码文件,并生成class对象,采用双亲委派模型

    2. 链接:把类的二进制数据合并到JRE中;

    3. 初始化:对类的静态变量,静态代码块执行初始化操作(优先执行超类的)这里MyA的a/b变量就会被初始化

  2. 分配内存: 为新生对象分配内存。方式有 “指针碰撞” 和 “空闲列表” 两种。

    1. 通过逃逸分析,判断对象是在栈上分配还是在堆上分配。

    2. 尝试TLAB(本地线程分配缓存)分配

    3. 判断对象是否可以直接分配到老年代

    4. 在eden区分配。

  3. 初始化零值。

  4. 设置对象头。

  5. 执行 init 方法。

jvm调优

  • GC的时间足够的小

  • GC的次数足够的少

  • 发生Full GC的周期足够的长

  • 防止内存溢出

参数

  1. Xms\xmx 指定堆的最大和最小,一般是3G,一般会设置一样,防止动态调整消耗内存

  2. NewRatio新生代和老年代的比例

  3. GC相关的,HeapDumpOnOutOfMemoryError、PrintGCDetails打印GC日志

  4. -Xss的大小,帧的大小,防止栈内存溢出的,调整了栈的大小

问题排查

对于还在正常运行的系统

  1. 可以使用jmap来查看JVM中各个区域的使用情况

  2. 可以通过 jstack来査看线程的运行情况,比如哪些线程阻塞、是否岀现了死锁

  3. 可以通过 jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc比较频繁,那么就得进行调优了

  4. 通过各个命令的结果,或者 jvisualvn等工具来进行分析

  5. 首先,初步猜测频繁发送 fullgc的原因,如果频繁发生fullgc但是又一直没有岀现内存溢岀,那么表示fullgc实际上是回收了很多对象了,所以这些对象最好能在 youngc过程中就直接回收掉,避免这些对象进入到老年代,对于这种情况,就要考虑这些存活时间不长的对象是不是比较大,导致年轻代放不下,直接进入到了老年代,尝试加大年轻代的大小,如果改完之后,fullgc減少,则证明修改有效

  6. 同时,还可以找到占用CPU最多的线程,定位到具体的方法,优化这个方法的执行,看是否能避免某些对象的创建,从而节省内存

对于已经发生了OOM的系统

  1. 一般生产系统中都会设置当系统发生了OOM时,生成当时的dump文件XX: +heapdumponoutofmemoryerror-xx Heapdumppath=/usr /local/base)

  2. 我们可以利用 jsisualvm等工具来分析dump文件

  3. 根据dump文件找到异常的实例对象,和异常的线程(占用CPU高),定位到具体的代码

  4. 然后再进行详细的分析和调试

总之,调优不是一蹴而就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题

频繁GC

  1. 査看监控,以了解出现问题的时间点以及当前FGC的频率

  2. 了解该时间点之前有没有程序上线、基础组件升级等情况。【多数是bug,看刚上线的功能】

  3. 了解JVM的参数设置,包括:堆空间各个区域的大小设置,新生代和老年代分别采用了哪些垃圾收集器,然后分析JVM参数设置是否合理。

  4. 再对步骤1中列出的可能原因做排除法,其中元空间被打满、内存泄漏、代码显式调用gc方法比较容易排查

  5. 针对大对象或者长生命周期对象导致的FGC,可通过jmap- histo命令并结合dump堆内存文件作进一步分析,需要先定位到可疑对象

  6. 通过可疑对象定位到具体代码再次分析,这时候要结合GC原理和JVM参数设置,弄清楚可疑对象是否满足了进入到老年代的条件才能下结论。

内存溢出

  • java.lang.OutOfMemory Error: java heap space堆栈溢出,代码可题的可能性极大

  • java.lang.OutOfMemory Error:GC over head limit exceeded系统处于高频的GC状态,而且回收的效果依然不佳的情况,就会开始报这个错误,这种情况一般是产生了很多不可以被释放的对象,有可能是引用使用不当导致,或申请大对象导致,但是 java heap space的内存溢出有可能提前不会报这个错误,也就是可能内存就直接不够导致,而不是高频GC.

  • java.lang.OutOfMemory Error: Perm Gen space jdk1.7之前才会出现的问题,原因是系统的代码非常多或引用的第三方包非常多、或代码中使用了大量的常量、或通过 inter注入常量或者通过动态代码加载等方法,导致常量池的膨胀

  • java.lang.OutOfMemory Error: Direct buffer memory直接内存不足,因为wm垃圾回收不回收掉直接内存这部分的内存,所以可能原因是直接或间接使用了 ByteBuffer中的allocateDirect方法的时候,而没有做 clear

  • java. lang. Stackoverflowerror -Xss设置的太小了

  • java.lang.OutOfMemory Error: unable to create new native thread 堆外内存不足,无法为线程分配内存区域

  • java.lang.OutOfMemory Error: request{} byte for tout of swap地址空间不够

GC *

  • 并行( Parallel):多个垃圾收集线程并行工作,此时用户线程处于等待状态

  • 并发( Concurrent):用户线程和垃圾收集线程同时执行

  • 吞吐量:运行用户代码时间/(运行用户代码时间+垃圾回收时间)

对象存活

  • 引用计数法:对象持有一个引用计数器,当有引用时,计数器+1,没有计数器-1,任意时刻计数器为0,则回收

    • 优点:效率高,实现简单

    • 缺点:存在相互引用的问题

  • 可达性分法:以GC Root作为起点,向下搜索,搜索的路径称为引用链,当对象到GC Root没有引用链时,说明对象不可用

    • GC Root

      • 虚拟机栈(局部变量表)中引用的对象

      • 方法区中类静态属性引用的变量

      • 方法区中常量引用的对象

      • 本地方法栈中JNI引用的对象

垃圾回收算法

  1. 标记清除

    • 过程:标记需要回收的对象、标记完成后统一回收

    • 缺点:

      • 效率不高

      • 会产生大小的空间碎片

  2. 复制

    • 过程:将内存分为两个大小相同的两块,当一块用完后,把活着的对象复制到另一块,然后将这块内存空间一次性清理调

    • 优点:实现简单,高效

    • 缺点:牺牲了一半的空间内存

  3. 标记整理

    • 过程:标记需要回收的对象,然后将存活的对象向另一端移动,清理掉以外的内存

垃圾收集器

  • 新生代收集器(全部的都是复制算法): Serial、 Parnew、 Parallel Scavenge

  • 老年代收集器:CMS(标记清理)、 Serial old(标记整理)、 Parallel old(标记整理)整堆收集器

  • G1(—个 Region中是标记清除算法,2个 Region之间是复制算法)

Serial(新生代)

最古老的收集器,单线程,独占式,成熟,适合单CPU 服务器

采用了复制算法

特点:单线程、简单髙效(与其他收集器的单线程相比),对于限定单个CP∪的环境来说,Serⅰa收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最髙的单线程手机效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束( Stop The World)。

应用场景:适用于 Client模式下的虚拟机

Serial/ Serial old收集器运行示意图

ParNew (新生代)

和Serial基本没区别,唯一的区别:多线程,多CPU的,停顿时间比Serial少

采用了复制算法

特点:多线程、 Pardew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX: Parallelgcthreads参数来限制垃圾收集的线程数。和 Serial收集器一样存在 Stop The World

可题应用场景: Pardew收集器是许多运行在 Server模式下的虚拟机中首选的新生代收集器,因为它是除了Serial收集器外,唯一个能与CMS收集器配合工作的。

Pardew/Seria/ Old组台收集器运行六意如下.

Serial Old (新生代)

特点:同样是单线程收集器,采用标记整理算法

应用场景:主要也是使用在 Client模式下的虚拟机中。也可在 Server模式下使用Server模式下主要的两大用途(在后续中详细讲解…)

1.在DK1.5以及以前的版本中与 Parallel Scavenge收集器搭配使用。

2.作为CMS收集器的后备方案,在并发收集 Concurent mode failure时使用。Seria/ Serial old收集器工作过程图( Serial收集器图示相同)

Parallel Scavenge(新生代)

关注吞吐量的垃圾收集器,高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。【所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。】

采用了复制算法

特点:属于新生代收集器也是采用复制算法的收集器,又是并行的多线程收集器(与 Pardew收集器类似)该收集器的目标是达到—一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与Pardew收集器最重要的一个区别)

GC自适应调节策略: Parallel Scavenge收集器可设置-X:+ Useadptive Size Policy参数。当开关打开时不需要手动指定新生代的大小(Xmn)、Eden与 Survivor区的比例(-XX: Survivor Ration)、晋升老年代的对象年龄(-XX: Pretenuresize Threshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。

Parallel Scavenge收集器使用两个参数控制吞吐量

  • XX: Maxgcpausemillis控制最大的垃圾收集停顿时间

  • XX: Gcratio直接设置吞吐量的大小。

Parallel old(老年代)

特点:多线程,采用标记整理算法。

应用场景:注重高吞吐量以及CPU资源敏感的场合,都可以优先考虑 Parallel Scavenge+ Parallel old收集器。

Concurrent Mark Sweep (老年代)

CMS收集器是一种以获取最短回收停顿时间为目标的收集器。

特点:基于标记清除算法实现。并发收集、低停顿

应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如Web程序、b/s服务

标记清除

获取最短的停顿时间,步骤:

  1. 初始标记-短暂,仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。STW问题

  2. 并发标记-和用户的应用程序同时进行,进行GC RootsTracing的过程

  3. 重新标记-短暂,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。STW问题

  4. 并发清除,不需要停顿。

  • 优点

    • 由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

  • 缺点

    • 浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。

    • CPU 资源敏感:因为并发阶段多线程占据CPU 资源,如果CPU 资源不足,效率会明显降低。

    • 会产生空间碎片:标记- 清除算法会导致产生不连续的空间碎片

G1(综合)

G1收集器款面向服务端应用的垃圾收集器

过程:

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

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

最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs 里面,最终标记阶段需要把Remembered Set Logs 的数据合并到Remembered Set 中。这阶段需要停顿线程(STW),但是可并行执行。

筛选回收:首先对各个Region 中的回收价值和成本进行排序,根据用户所期望的GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。

特点:

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

分代收集: 与其他收集器一样,分代概念在 G1 中依然得以保留。虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但它能够采用不同的方式 去处理新创建的对象和已经存活了一段时间、熬过多次 GC 的旧对象以获取更好的收集效果。

空间整合: 与 CMS 的“标记—清理”算法不同,G1 从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复 制”算法实现的,但无论如何,这两种算法都意味着 G1 运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运 行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。

空间布局:在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

可预测的停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型。能让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N亳秒

-XX:MaxGCPauseMillis 指定目标的最大停顿时间,G1 尝试调整新生代和老年代的比例,堆大小,晋升年龄来达到这个目标时间。

G1收集可以分为两大部分:

  • 全局并发标记(global concurrent marking)

  • 拷贝存活对象(evacuation),或者叫迁移。

而这两部分可以相对独立执行。

G1 Evacuation

Evacuation阶段是全暂停的。它负责把一部分region里的活对象拷贝到空region里去,然后回收原本的region的空间。 Evacuation阶段可以自由选择任意多个region来独立收集构成收集集合(collection set,简称CSet),靠per-region remembered set(简称RSet)实现。这是regional garbage collector的特征。

触发fullGC

  1. Evacuation的时候没有足够的to- space:来存放晋升的对象;

  2. 并发处理过程完成之前空间耗尽。

使用场景

  • 面向服务端应用,针对具有大内存、多处理的机器(在普通大小的堆里表现并不惊喜)

  • 最主要的应用时需要低GC延迟,并具有大堆的应用程序提供解决方案

    • 在堆大小约6G或更大时,可预测的暂停时间可以低于0.5秒(G1通过每次只清理一部分而不是全部的Region的增量式清理来保证每次GC停顿时间不会太长)

  • 用来替换掉1.5中的CMS收集器,在下面的情况时,使用G1可能比CMS好

    • 超过50%的Java堆被活动数据占用

    • 对象分配频率或年代提升频率变化很大

    • GC停顿时间过程(长于0.5至1s)

  • HotSpot垃圾收集器里,除了G1以外,其他的垃圾收集器使用内置的JVM线程执行GC的多线程操作,而G1 GC可以采用应用线程承担后台运行的GC工作,即当JVM的GC线程处理速度慢时,系统会调用用用程序线程帮助加速垃圾回收过程

GC的选择

关于gc的选择除非应用程序有非常严格的暂停时间要求,否则请先运行应用程序并允许ⅥM选择收集器(如果没有特别要求。使用VM提供给的默认GC就好)如有必要,调整堆大小以提高性能。如果性能仍然不能满足目标,请使用以下准则作为选择收集器的起点

  • 如果应用程序的数据集较小(最大约100MB),则选择带有选项-XX:+ Useseriaigc的串行收集器。

  • 如果应用程序将在单个处理器上运行,并且没有暂停时间要求,则选择带有选项×XUse Serialgci的串行收集器。

  • 如果(a)峰值应用程序性能是第一要务,并且(b)没有暂停时间要求或可接受秒或更长时间的暂停,则让ⅥM选择收集器或使用XX:+ Useparallelgc选择并行收集器。

  • 如果响应时间比整体吞吐量更重要,并且垃圾收集暂停时间必须保持在大约—秒钟以内,则选择具有XX:+UseG1GC。(值得注意的是JDK9中CMS已经被 Deprecated,不可使用!移除该选项)

  • 如果使用的是jdk8,并且堆内存达到了16G,那么推荐使用G1收集器,来控制每次垃圾收集的时间。

  • 如果响应时间是高优先级,或使用的堆非常大,请使用-ⅩX: UsezGC选择完全并发的收集器。(值得注意的是JDK11开始可以启动zGC,但是此时ZGC具有实验性质,在JDK15中[202009]发布才取消实验性质的标签,可以直接显示启用,但是JDK15默认GC仍然是G1)

这些准则仅提供选择收集器的起点,因为性能取决于堆的大小,应用程序维护的实时数据量以及可用处理器的数量和速度。如果推荐的收集器没有达到所需的性能,则首先尝试调整堆和新生代大小以达到所需的目标。如果性能仍然不足,尝试使用其他收集器

总体原则:减少 STOP THE WORD时间,使用并发收集器(比如CMS+ Pardew,G1)来减少暂停时间,加快响应时间,并使用并行收集器来增加多处理器硬件上的总体吞吐量。

  • 吞吐量优先: Parallel Scavenge+ Parallel old(多线程并行)

  • 响应时间优先:cms+ par new(并发回收垃圾)

jstack的使用(排查cpu占用过高的问题)

  1. top指令确认问题的java进程

  2. top -Hp pid 确认出现问题的线程

  3. jstack pid 查看堆栈信息

  4. printf "%x" pid 十进制的pid转十六进制方便查找

  5. 问题排查: