JVM调优

JVM调优

JVM基础

Java从编码到执行流程图:

在这里插入图片描述

类加载的过程:双亲委派

在这里插入图片描述
双亲委派的原因:主要是为了安全,次要是为了减小性能损耗

双亲委派机制过程? 即向上检查,向下委派

1.当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
2.当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
3.如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载。
4.若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

自定义类加载器:继承ClassLoader类,重写findClass方法

1.继承ClassLoader类
2.重写findClass方法,把文件转成二进制文件
3.利用defineClass方法把二进制文件转成Class对象

打破双亲委派机制:

  • 怎么打破: 重写loadClass()方法
    • 1.JDK1.2之前,还没有定义loadClass方法。自定义的ClassLoader必须重新loadClass()方法,就会打破双亲委派机制
    • 2.ThreadContextClassLoader可以实现基础类调用实现类代码,通过thread.setContextClassLoader指定
    • 3.热启动,热部署 Tomcat等有自己的模块指定classLoader(可以加载同一类库的不同版本)
      将原本的classloader全部干掉,然后加载新的

自定义类加载器:可以加密

类加载器的层次?

  • 启动类加载器 Bootstrap ClassLoader ,负责加载存放在JDK\jre\lib下的一些基本类库(rt.jar,所有的java.* 的类都是启动类加载器加载)。启动类加载器无法被Java程序直接引用
  • 拓展类加载器 Extension ClassLoader,负责加载lib\ext目录中,javax .* 开头的类,开发者可以直接使用拓展类加载器
  • 应用程序类加载器 Application ClassLoader,负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果没有自定义加载器,程序一般为默认加载器。
  • 自定义类加载器 User Loader 可以执行自定义的一些功能
    • 可以做数字签名验证
    • 动态创建复合用户特定需要的定制化构建类
    • 从特定的场所取得java class(网络,数据库中)

JVM类加载机制有哪些?

1.全盘负责
当类的加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
2.父类委托
先让父类的加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
3.缓存机制
缓存机制将会保证所有加载过的Class都会被缓存,当程序需要某个Class,先从缓存区寻找,只有缓存区不存在,才读取该类的二进制文件转换为Class对象,存入缓存。所有修改了Class后,必须重启JVM,程序修改才生效
4.双亲委派机制
如果一个类加载器收到了类加载的请求,它首先不自己尝试加载,而是把请求委托给父类加载器,依次向上。因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器,只有当父加载器的范围没有找到所需的类,即无法完成加载,子加载器才会尝试自己去加载该类

Class.forName()和ClassLoader.loadClass()区别?

  • Class.forName():将类的.class文件加载到jvm中,还会对类进行解释,执行static块。而且还可以通过参数控制是否执行static块 Class.forName(name, initialize, loader)
  • ClassLoader.loadClass():只会将.class文件加载到jvm中,不执行static内容,只有newInstance才会执行static块

编译器(JIT)-- Java默认混合模式:解释器+热点代码编译

为了提升效率,对于热点的代码编译成本地代码

  • 热点代码检测:
    • 多次被调用的方法 方法计数器
    • 多次被调用的循环 循环计数器

JVM的懒加载(按需加载)

伪共享 缓存行

使用填充对齐

一个空object占多少个字节?

在开启压缩指针的情况下,一个空object会占12个字节,但是为了避免伪共享问题,JVM会按照8个字节的倍数就行填充,所以会填充4个字节变成16个字节长度。
在没有开启压缩指针的情况下,object默认会占用16个字节,16个字节正好是8的倍数,所以不会填充。
里面主要存分为三部分
对象头 (数组长度,类元指针)
实例数据
对其填充

类加载的生命周期?

  • 加载
    查找并加载类的二进制数据
  • 连接
    • 验证 确保被加载类的正确性
    • 准备 为类的静态变量分配内存,并将其初始化为默认值
    • 解析 把类中的符号引用转换为直接引用
  • 初始化
    为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。
  • 使用
    类访问方法区内的数据结构的接口, 对象是Heap区的数据
  • 卸载
    结束生命周期
    加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。
    这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

总结:
1.类的加载:默认值->初始值
2.对象加载:申请内存-默认值-初始值

对象创建过程?

在这里插入图片描述
对象创建过程可以分为下面几步:
首先判断当前对象是否以及被初始化加载过,如果说没有加载,那么就会调用类加载器进行加载,调用目标类的构造器完成初始化。这个时候主要是对目标类里的静态变量、成员变量、静态代码块进行初始化。
然后因为类加载时其实初始大小就定了,这个时候就在堆上面给目标对象分别空间。
再然后对目标对象的成员变量进行初始化,像int初始化为0,对象初始化为null。为了保证后续这些值可以使用。同时再对对象的方法头,hashcode,锁标记进行设置。
最后,由java执行加载器生成的init()方法它是一个实例构造器。把我们类的成员变量的值,构造函数,父类构造函数,代码块等方法集合。所以执行init()方法就可以最终完成对象创建。

Spring Bean生命周期

Spring 生命周期全过程大致分为五个阶段:创建前准备阶段、创建实例阶段、依赖注入阶段、容器缓存阶段和销毁实例阶段。

创建前准备阶段:
从上下文和相关配置中解析并查找bean有关的扩展实现

创建实例阶段:
通过反射来创建bean实例对象,并且扫描和解析bean声明的一些属性

依赖注入阶段:
如果被实例化的 Bean 存在依赖其他 Bean 对象的情况,则需要对这些依赖 bean 进行对象注入。比如常见的@Autowired、setter 注入等依赖注入的配置形式。
中间有涉及代理的步骤:通过BeanPostProcessor的前置通知和后置通知对bean对象进行拓展。

容器缓存阶段:
容器缓存阶段主要是把 bean 保存到容器以及 Spring 的缓存中,到了这个阶段,Bean就可以被开发者使用了

销毁实例阶段:
当 Spring 应用上下文关闭时,该上下文中的所有 bean 都会被销毁。
如果存在 Bean 实现了 DisposableBean 接口,或者配置了destory-method属性,会在这个阶段被调用

内存结构

在这里插入图片描述总的来说分为两个子系统、两个组件
子系统:
classloader:用来装载class文件到运行时数据区间
Execution engine:执行引擎,执行class中的指令 JIT&GC
组件:
Runtime data area:运行时数据区,就是我们常说的JVM内存区
Native Interface:本地接口

JVM的组成部分

JVM其中内存既有私有又有公有部份

线程私有:

1.程序计数器
每个线程都有自己独立的程序计数器来贮存自己线程的下一条指令的地址。因为基本都是多线程运行,CPU不停切换线程,所以需要每个线程都分配了一个PC寄存器,每个线程都独立计算,不会互相影响。
2.虚拟栈堆
用来存贮线程的一些局部变量,方法返回结果等。堆的操作就只有入栈和出栈。它不存在垃圾回收的问题,可以通过参数xss设置栈深度。可能出现OOM 栈空间不足等异常
3.本地方法区
用来储存本地方法接口,主要是调用一些非java代码

线程公有:

1.方法区
用来贮存类的信息,常量池,静态变量等数据,是JVM规范中定义的一个概念区,并未明确规定怎么实现她。Java8之前是永久代的概念,Java8后就被元空间取代了
2.堆
主要存贮类初始化的对象,我们平常所说的jvm gc回收就是针对堆空间的。其中分为两个部分。

  • 年轻代 约占堆大小的1/3
    分为eden区 from survivor区 to survivor区,占比约8:1:1
    年轻代是所有新对象创建的地方,当占满年轻代时,则会执行垃圾收集,称为mimor GC
    1.大多数新创建的对象放在Eden内存空间中,此时初始化年龄计数为1
    2.Eden空间占满后执行minor gc,将幸存的对象移动到一个幸存者空间
    3.minor gc对survivor区进行管理,把他们都移动到另外一个幸存者空间(每次都有一个空间是空的)
    4.幸存者空间每存活一次GC,计数+1。多次GC存活下来的对象(默认阈值为15)就移动到老年代
  • 老年代 约占堆大小的2/3
    主要存贮从年轻代多次minor gc幸存的对象,以及一些大的对象,数组也会直接放在老年代,避免在年轻代中发生大量拷贝。当老年区被占满时会执行major gc,通常时间更长。

堆和栈的区别:

物理地址:
堆的物理地址不连续,性能慢些
栈就是数据结构中的栈,物理地址连续,先进后出
内存分别:
堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。
栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。
存放内容:
堆存放的是对象的实例和数组。因此该区更关注的是数据的存储
栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。
屏障区别:
栈是不会启用屏障保护机制,堆上面才有屏障机制

永久代和元空间的区别?

1.永久代除了保存元数据还会保存一些无关杂项,分离得不彻底;元空间只会保留元数据到Metaspace,符号引用在native heap中
2.永久代是使用的虚拟机固定内容,不可扩容;元空间是使用的系统内存,永远不会OOM。元空间内存理论上可以达到系统最大,但是建议还是设置内存限制元空间大小,避免导致系统内存使用率过高,而且还可能导致一些如类加载器泄露的方法过晚暴露。

什么是 TLAB (Thread Local Allocation Buffer)?

首先从内存模型的角度,对年轻代Eden区域继续划分一小块区域用来为每个线程分配私有缓存区。多线程同时分配内存时使用TLAB可以避免一系列非线程安全问题,提高内存分配的吞吐量,一般把它叫做快速分配策略。

为什么要有 TLAB ?

1.堆区本来就是多线程共享的,任何线程都可以访问堆中的共享数据
2.对象实例在jvm堆中创建十分频繁,因此并发环境从堆中分配内存就会线程不安全
3.为避免多线程操作同一地址,需要加锁等,影响分配速度
4.所以,可以通过-xx:useTLAB来开启TLAB,在Eden中分出一个缓存区,默认1%大小,-xxTLABWasteTargetPercent来修改。先在TLAB中分配内存,一旦失败,再通过加锁赖在Eden中分配内存

GC垃圾回收

发生JVM内存泄露怎么看?

如果发生内存泄露,一般根据现象去定位问题
第一步确认是否是内存泄露导致的,比如频繁fullGC,full GC卡顿、年轻代内存一直在高位无法释放,老年代逐步增长。我们可以通过下载GC日志,查看这些信息。
也可以使用dump工具,把内存dump下来,使用MAT工具分析,一般都能定位到有问题的类,然后针对这部分代码进行优化。
一般可能循环引用、资源未释放导致的。

GC日志怎么看?重点看什么?

像一般idea里面启动,可以在VM options里面加上-xx:+PrintGCDetails可以看到GC日志
或者启动java进程时,执行-xx:+PrintGCDetails -xloggc:<文件路径>可以把日志拉下来
GC日志重点看:发生GC是哪个垃圾收集器,发生的是minor gc还是full gc,看gc的年龄阈值,看GC前后容量变化的大小等等

GC判断是否可被回收的依据

GC判断某个对象是否可被回收的依据是,是否有有效的引用指向该对象。如果没有有效引用指向该对象(基本意味着不存在访问该对象的方式),那么该对象就是可回收的。这里的有效引用 并不包括弱引用。也就是说,虽然弱引用可以用来访问对象,但进行垃圾回收时弱引用并不会被考虑在内,仅有弱引用指向的对象仍然会被GC回收。

如何判断一个对象是否可以回收?

其实判断一个对象是否可以回收主要就是看这个对象是否还在被引用,只要一个对象确定没有被引用,那么就被认为是可以被回收的。
目前针对这个回收有两种算法:
1.引用计数法
给对象加上一个引用计数器,每增加一个引用,则计数+1,没减少一个引用则计数-1。当计数为0时,证明该对象没有被引用了,那么就视为可以被回收。但是目前主流JVM都不采用该算法,主要是它在面对复杂的引用,比如循环引用,互相依赖都会让计数永远不为0,将导致对象无法被回收,发生内存泄露。
2.可达性分析
将JVM中一定不会被回收的作为GC ROOTs,比如虚拟机栈的引用对象,本地方法对象等,作为GC ROOTs当做搜索起点,去遍历它们的直接或者间接引用。如果不能到达的对象则可视为能被回收的对象。它可以解决循环引用的问题,目前市面主流JVM都采取该算法

Java 中 GC Roots 一般包含哪些内容?

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

JVM中的三色标记法 --标记清除法的升级(使用了可达性分析)

目前JVM中CMS、G1垃圾回收器都用到了三色标记法。它的大致思想就是把JVM中内存对象分为三个颜色:
1.白色:表示还没有被垃圾回收器扫描的颜色
2.黑色:表示已经被垃圾回收器扫描过,且对象及其引用的对象其他对象都是存活的
3.灰色:表示已经被垃圾扫描过,但对象引用的其他对象尚未被扫描
在GC开始时,先将所有的对象标记为白色,然后从根对象开始遍历内存中的对象,接着把直接引用的标记为灰色。
再判断灰色集合中的对象是否存在子引用,不存在则直接放进黑色。存在则把其子引用放进灰色,自己放进黑色。
然后继续遍历灰色集合,直到灰色集合全部变成黑色,则表示一次标记结束。
此时还处于白色状态的对象就是不可达的对象,可以直接回收

三色标记法的问题:写屏障来解决

并发情况下有可能有问题:对象的丢失
1.黑色节点引用白色节点,黑色节点不会被扫描,导致白色节点的对象直接丢失了
2.灰色节点和可达关系的白色对象之间的引用遭到了破坏
解决方式:
强弱三色不等式来解决:
强三色不变式:黑色节点不能引用白色节点
弱三色不等式:黑色节点可以引用白色节点,但是白色节点必须有其他灰色节点的引用
实质是使用写屏障来实现的:
具体有:增量更新和原始快照
增量更新:记录黑色节点指向白色节点的引用。并发扫描后重新扫描记录的黑色节点,如果有则可以黑色变成灰色
原始快照:灰色节点删除白色节点引用时,记录下来 。并发扫描结束后,重新扫描这个灰色节点,能扫描的白色节点直接改为黑色。

对象有哪些引用类型?

  • 强引用
    被强引用关联的对象不会被回收。
    比如new 对象时,强引用通常用于实现单例模式、缓存等需要长时间存活的对象。JVM停止运行时终止
  • 软引用
    被软引用关联的对象只有在内存不够的情况下才会被回收。
    使用 SoftReference 类来创建软引用。
    软引用经常被用来实现内存敏感的缓存,例如,可以使用软引用来缓存最近使用的图片或文本,一旦内存不足,垃圾回收器就会回收这些对象
  • 弱引用
    被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。
    使用 WeakReference 类来实现弱引用。
    弱引用经常被用来实现对象监视器,例如,可以使用弱引用来监视一个对象的状态,当对象被回收时,该对象的状态也将被相应地清除。
  • 虚引用
    又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。一般不用,辅助咱们的Finaliza函数的使用
    使用 PhantomReference 来实现虚引用,虚引用必须和引用队列(ReferenceQueue)联合使用。
    主要用于byteBuffer回收堆外内存(直接内存)的流程中。–NIO

强引用、软引用、弱引用、虚引用的区别?

强引用 就是普通对象,只要还有强引用指向一个对象,就表示对象还存活,GC回收就无法回收这一类对象。
只有在没有其他引用关系,或者超过了引用的作用域,再或者显示的把引用赋值为null的时候,垃圾回收器才能进行内存回收。

软引用,是一种强引用弱化一些的应用,可以对象豁免一些垃圾收集,只有当JVM认为内存不足,才会试图去回收软引用指向的对象。它一般用来实现内存敏感的缓存

弱引用,相对强引用而言,它允许对象在有引用关联的情况下被垃圾回收。一旦一个对象只存在弱引用的情况下,无论内存是否充足,GC都会回收该内存

虚引用,它不会影响对象的生命周期,它更多是提供对象被finalize以后,去做某些事情的机制。它需要和引用队列联合使用。

虚引用(PhantomReference)的使用场景

使用的是ByteBuffer来操作堆外内存。
1.当byteBuffer被回收后,在进行GC垃圾回收的时候,发现虚引用对象Cleaner是PhantomReference类型的对象,并且被该对象引用的对象(ByteBuffer对象)已经被回收了
2.那么他就将将这个对象放入到(ReferenceQueue)队列中
3.JVM中会有一个优先级很低的线程会去将该队列中的虚引用对象取出来,然后回调clean()方法
4.在clean()方法里做的工作其实就是根据内存地址去释放这块内存(内部还是通过unsafe对象去释放的内存)

有哪些基本的垃圾回收算法?

jvm在对堆新生代,老年代占满时就是启动GC垃圾回收,以便腾出内存空间。目前市面主流的有5中垃圾回收算法:

  • 标记 - 清除
    将存活的对象进行标记,然后清理掉未被标记的对象(标记方法可以采取引用计数法,可达性分析等,目前主流是可达性分析)。这种方法存在不足:
    1.标记和清楚效率都不高
    2.这种方法会产生大量不连续的内存碎片,导致无法给大对象分配内存,很容易导致FULL GC
  • 标记 - 整理
    在标记清除算法基础上多了整理的步骤,就是把存活的对象都移动到一端,然后清除。这样虽然多了移动的开支,但是不会产生内存碎片。
  • 复制算法
    其思想就是将内存分成两个大小相同的区域,每次只有其中一块,当这一块内存满了,就把还存活的对象复制到另一块上,然后把当前使用的这部门内存全部清理。
    优点:不会产生内存碎片
    缺点:最大问题是这样每次只使用了一半的内存,相当于内存使用率最多50%
    因此,实际使用时会进行优化:堆的新生代分为Eden,from survivor,to survivor区,占比为8:1:1。每次使用都是Eden区加其中一块survivor区,内存使用率达到90%。
  • 分代收集
    这种算法的其实就是将内存分成几块,每块根据特点采用不同的算法
    • 新生代
      每次gc都是大批对象以死,少量存活。采取改良的复制算法,只需要对少量存活的对象复制就可以完成收集
    • 老年代
      对象存活率高,没有额外的空间操作,因此标记-清除或者标记-整理算法,不用腾出内存区复制。
      这也是目前市面主流商业虚拟机采用的算法思想,里面一般都是不同代采用不同垃圾收集器,新生区使用parNew,老年区使用CMS
  • 分区收集思想
    这个是HotSpot 开发团队新推出的算法,目的就是能取代不同代使用不同收集器,用G1收集器跨越老年,新生代进行垃圾收集。其思想就是将堆划分为连续的不同小区间,每个小区间独立使用,独立回收。
    好处:增加了高吞吐量的同时,减少一次GC所产生的停顿时间。
    (jdk11推出了一个ZGC,适用于大内存低延迟的内存管理,测试128G的堆,最大停顿1.68ms)

什么是Minor GC、Major GC、Full GC?

JVM在执行GC是,并不是每次都是堆内存一起回收,大部分时候回收主要发生在新生代。
Minor GC 只发生在新生代的垃圾回收,一般对应的垃圾处理器serial,ParNew等
Major GC 只发生在老年代的垃圾回收,一般对应的垃圾处理器CMS,serialOld等
Full GC 对整个java堆和方法区的垃圾进行收集

对象进入老年代对象的方式只有新生代年龄到达阈值吗?

不是的,一般来说有四种方式。

  • 一是通过新生代种多次GC达到年龄阈值后,进入老年代。
  • 动态年龄判断规则
  • 一种是新建时就是大对象(很长的字符串和数组那种),就会直接进入老年区,避免在新生代多次复制。具体大小可以通过 -xx:PretenureSzieThreshold来设置
  • 一次minor gc后,存活的对象大于survivor空间,直接进入老年代

动态年龄判断规则?

按照年龄从小到大对其所占用的大小进行累计,当累计的某个年龄大小超过Survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升阈年龄值。
比如年龄1 30%,年龄2 30%,年龄3 30%此时就是年龄1+年龄2=60%,那么年龄2,年龄3就会进入老年区

长期存活的对象进入老年代为对象定义年龄计数器

对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
-XX:MaxTenuringThreshold 用来定义年龄的阈值。
但是其实JVM里面有一个动态对象年龄判定。虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。

什么情况下会触发Full GC?

  • 调用 System.gc()
    只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
  • 老年代空间不足
    少创建大对象,大数组
  • JDK 1.7 及以前的永久代空间不足
    在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
  • Concurrent Mode Failure
    执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。

Hotspot中有哪些垃圾回收器?

除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。
年轻代:
1.serial收集器
单线程收集器,Client模式下默认的新生代收集器。每次收集都会暂停用户程序,可以配合CMS收集器工作
2.ParNew收集器
serial收集器的多线程版本,是server模式下首选新生代收集器。可以配合CMS收集器工作
3.Parallel Scavenge收集器
多线程收集器,“吞吐优先收集器”,主要适合一些在后台运算而不需要太多交互的任务
老年代:
1.Serial Old收集器
serial的老年版,也是适用Client,可以作为CMC预备方案(发生Concurrent Mode Failure时使用)
2.Parallel Old收集器
Parallel Scavenge 收集器的老年代版本,同样注重吞吐量和CPU资源
3.CMS收集器
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法
分为四个阶段:

  • 初始标记:仅仅标记GC ROOTs直接关联的对象,速度快,需要停顿
  • 并发标记:进行GC Roots Tracing的过程,整个回收过程耗时最长,不用停顿
  • 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记变动的那部分对象标记记录,需要停顿
  • 并发清除:清除死去的对象,不需要停顿

整个过程中耗时最长的并发标记,并发清理不停顿,收集器线程和用户线程一起工作。因此可能导致一些问题:

  • 吞吐量低:低停顿时间是以牺牲吞吐量为代价,CPU使用率不高
  • 可能存在浮动垃圾:出现 Concurrent Mode Failure。因为用户并发清理时,线程会继续运行,此时可能产生一部分垃圾,而且这部分只有下一次GC才能清理。由于浮动垃圾存在,导致需要预留一部分内存,因此CMS不能等老年区装满再回收。而且预览内存不足就会产品Concurrent Mode Faiure,这时就需要启动后备serial old收集器

4.G1收集器:
HotSpot推出该收集器就是为了未来代替CMS收集器,它可以面向服务应用的垃圾收集器,多CPU下,大内存的场景下有很好的性能。而且它可以跨越新生代,老年代一起收集。
通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。
G1 收集器的运作大致可划分为以下几个步骤:

  • 初始标记
  • 并发标记
  • 最终标记: 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
  • 筛选回收:
  • 首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
    具备如下特点:空间整合: 整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。可预测的停顿: 能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。

记忆集&卡表 针对老年代中有对象引用了新生代 解决跨代引用

为了防止每次young gc需要去扫描老年代,则使用一个记忆集(卡表是具体实现)来存储这种引用关系。专门用来解决跨代引用的问题。

JVM调优

JVM内存调优的目的是什么?

调优的最终目标是通过参数设置来达到快速、低延时的内存垃圾回收以提高应用吞吐量,尽可能的避免因内存回收不及时而触发的完整 GC(Full GC 会带来应用出现卡顿)。减少STW(stop-the-world)即减少停顿的时间

查看参数:

java -XX:+PrintFlagsFinal -version > flags.txt

JVM常用命令

  1. jps:查看本机java进程信息。
  2. jinfo :实时查看修改JVM参数
  3. jstat:性能监控工具
  4. jstack:打印线程的栈信息,制作线程dump文件。
  5. jmap:打印内存映射,制作堆dump文件
  6. jconsole:简易的可视化控制台
  7. jhat:内存分析工具
  8. jvisualvm:功能强大的控制台

调优参数:其实这个一般要根据垃圾收集器来说具体参数才有用

-Xmx:设置堆的最大值,一般为操作系统的 2/3 大小 MaxHeapSize

-Xms:设置堆的初始值,一般设置成和 Xmx 一样的大小来避免动态扩容。 InitialHeapSize项目启动直接Full GC
-Xmn:表示年轻代的大小,默认新生代占堆大小的 1/3。高并发、对象快消亡场景可适当加大这个区域,对半,或者更多
但是G1收集器不需要调整

-XX:+PrintGCDetails 查看GC详细信息

Parallel常用参数

-XX:SurvivorRatio
设置伊甸园空间大小与幸存者空间大小之间的比率。默认情况下,此选项设置为8
-XX:PreTenureSizeThreshold
对象到达一定的限定值的时候 会直接进入老年代
-XX:MaxTenuringThreshold
升代年龄,最大值15 并行(吞吐量)收集器的默认值为15,而CMS收集器的默认值为6。
-XX:+ParallelGCThreads
并行收集器的线程数,同样适用于CMS,一般设为和CPU核数相同

CMS常用参数

-XX:+UseConcMarkSweepGC
启用**CMS垃圾回收器
-XX:MaxTenuringThreshold
升代年龄,最大值15 并行(吞吐量)收集器的默认值为15,而CMS收集器的默认值为6。

-XX:CMSInitiatingOccupancyFraction 并发失败的模式
使用多少比例的老年代后开始CMS收集,默认是68%(近似值),如果频繁发生SerialOld卡顿,应该调小,(频繁CMS回收)

-XX:MaxGCPauseMillis
停顿时间,是一个建议时间,GC会尝试用各种手段达到这个时间,比如减小年轻代

G1常用参数

-XX:+UseG1GC
启用G1垃圾收集器

-XX:MaxGCPauseMillis
设置最大**GC暂停时间的目标(以毫秒为单位)。这是一个软目标,并且JVM将尽最大的努力(G1会尝试调整Young区的块数来)来实现它。默认情况下,没有最大暂停时间值。

-XX:GCPauseIntervalMillis
GC的间隔时间

-XX:+G1HeapRegionSize 你的堆内存小于2G的时候 4C8G起步
单个Region大小,取值是1M-32M,建议逐渐增大该值,1 2 4 8 16 32。随着size增加,垃圾的存活时间更长,GC间隔更长,但每次GC的时间也会更长-XX:G1NewSizePercent 新生代最小比例,默认为**1/2000

-XX:G1MaxNewSizePercent
新生代最大比例,默认为60%

-XX:GCTimeRatioGC
时间建议比例,G1会根据这个值调整堆空间

-XX:ConcGCThreads
初始标记线程数量

-XX:InitiatingHeapOccupancyPercent
启动G1的堆空间占用比例,根据整个堆的占用而触发并发GC周期

问题排查

文本操作
文本查找 - grep
文本分析 - awk
文本处理 - sed
文件操作
文件监听 - tail
文件查找 - find
网络和进程
网络接口 - ifconfig
防火墙 - iptables -L
路由表 - route -n
netstat
其它常用
进程 ps -ef | grep java
分区大小 df -h
内存 free -m
硬盘大小 fdisk -l |grep Disk
top
环境变量 env

CPU飆升的情況?怎么排查

可能原因:

1.死循环、递归,或者资源未释放
2.线程数增加
线程数增多,通常会伴随内存飙升,但是线程内本身无复杂业务,会呈现,只有CPU飙升,但内存增高不明显。
3.死锁
死锁会导致核心线程数无响应,间接导致线程数飙升。

排查步骤

短时间内大量服务器请求,导致处理过多,CPU飙升
根据情况排查是否是内存溢出导致的问题。简单的频繁发生full gc,或者full gc卡顿。
可以先执行top 命令查看当前cpu占用率最高的进程
然后shift+H找到进程中CPU消耗过高的线程,一般有两种情况:
1.CPU占用率过高的是同一个线程,说明程序中存在线程长期占用CPU没释放。这个就要把线程的DUMP日志下下来,利用MAT等工具分析,找到有问题的代码,作相应的修改
2.CPU占用率过高的线程ID不断变化,说明是线程创建过多,需要挑选几个线程ID,通过jstack去线程dump日志中排查
最后也有可能是程序正常,只是CPU飙升那段时间,用户的访问量太大,导致资源不够

常用问题

在这里插入图片描述

超大对象

代码中创建了很多大对象 , 且一直因为被引用不能被回收,这些大对象会进入老年代,导致内存一直被占用,很容易引发GC甚至是OOM

超过预期访问量

通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值。
比如如果一个系统高峰期的内存需求需要2个G的堆空间,但是堆空间设置比较小,导致内存不够,导致JVM发起频繁的GC甚至OOM。

过多使用Finalizer

过度使用终结器(Finalizer),对象没有立即被 GC,Finalizer线程会和我们的主线程进行竞争,不过由于它的优先级较低,获取到的CPU时间较少,因此它永远也赶不上主线程的步伐,程序消耗了所有的可用资源,最后抛出OutOfMemoryError异常。

内存泄漏

大量对象引用没有释放,JVM 无法对其自动回收。

长生命周期的对象持有短生命周期对象的引用

例如将ArrayList设置为静态变量,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏

连接未关闭

如数据库连接、网络连接和IO连接等,只有连接被关闭后,垃圾回收器才会回收对应的对象。

变量作用域不合理

例如,1.一个变量的定义的作用范围大于其使用范围,2.如果没有及时地把对象设置为null

内部类持有外部类

Java的非静态内部类的这种创建方式,会隐式地持有外部类的引用,而且默认情况下这个引用是强引用,因此,如果内部类的生命周期长于外部类的生命周期,程序很容易就产生内存泄漏

如果内部类的生命周期长于外部类的生命周期,程序很容易就产生内存泄漏(垃圾回收器会回收掉外部类的实例,但由于内部类持有外部类的引用,导致垃圾回收器不能正常工作)

解决方法:你可以在内部类的内部显示持有一个外部类的软引用(或弱引用),并通过构造方法的方式传递进来,在内部类的使用过程中,先判断一下外部类是否被回收;

Hash值改变

在集合中,如果修改了对象中的那些参与计算哈希值的字段,会导致无法从集合中单独删除当前对象,造成内存泄露

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值