JVM总结目录

  • JVM总结
    • 1. 内存结构
      • 线程私有区
      • 线程共享区
      • 堆栈的区别
      • 获取堆内存数据
    • 2. 垃圾回收
      • 垃圾回收机制
      • 垃圾回收算法
      • 分代垃圾回收的过程
      • 判断一个对象是否为垃圾
      • 引用类型
    • 3. 对象分配
      • 对象在内存中如何分配
      • 对象如何从年轻代进入老年代
    • 4. 垃圾收集器
      • 常见的垃圾收集器
      • CMS的垃圾回收过程
      • G1垃圾回收器参数
    • 5. 类加载器
      • 类加载器的流程
      • 常用的类加载器
      • 双亲委派机制
    • 6. JVM调优
      • 需要调优的场景
      • 调优步骤
      • 调优参数

1. 内存结构

线程私有区

程序计算器

  • 作用:是一块较小的内存空间,存储的是当前线程所执行的字节码文件的序号
  • 特点:线程私有,不会出现内存空间溢出

虚拟机栈

虚拟机栈是管理JAVA方法执行的内存模型,每个方法执行时都会创建一个当前栈桢,在当前栈桢里面存储方法的局部变量表,操作数栈,动态链接方法,返回值,返回地址等信息,栈大小决定了方法调用的可达深度(递归多少层,嵌套调用多少层其他方法,在idea中,-Xss参数可以设置虚拟机栈的大小)

  • 是线程私有的
  • 局部变量表存放了编译期可知的所有基本数据类型(byte,short,int,long,float,double,boolean,char),以及对象引用
  • 栈太小或者方法调用过深都将抛出StackOverFlowError异常

本地方法栈

为本地语言服务的栈,Native方法服务

线程共享区

堆内存

存放对象实例的区域,对象,数组,以及常量池(从java7开始常量池也会使用堆内存)

堆内存从GC角度可以分为:新生类(Eden区,From Survision区和To Survision区),老年代,永久代(在Java8的时候被移除了)

特点:是线程贡献,需要考虑线程安全问题,同时会产生内存溢出问题

-Xms 设置最小堆内存大小(不能小于1024K)

-Xmx:设置最大堆内存大小(不能小于1024K)

方法区

用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后代码等数据

特点:

  • 是一块线程共享的内存区域
  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出的错误
  • 现在说的方法区一般是指元数据区(元空间Metaspace,java8的时候添加的),如果不指定大小,默认情况下,虚拟机会耗尽系统的可用内存

堆栈的区别

  • 存储的东西不同,栈内存存储局部变量和方法调用,而堆内存存储对象,包括成员变量,局部变量,还有类变量
  • 共享不同,栈内存是线程私有的,堆内存是所有线程共有的
  • 异常错误不同:栈空间不足:java.lang.StackOverFlowError,堆空间不足:java.lang.OutOfMemoryError。
  • 大小不同,栈空间小于堆空间

获取堆内存数据

java.lang.Runtime类中包含了与内存先关的方法

获取剩余空间的的字节数:Runtime.freeMemory()

获取总内存的字节数:Runtime.totalMenory()

2. 垃圾回收垃圾回收机制

在Java中,程序员不需要显示的去释放一个对象的内存,而是由虚拟机自动去执行,在JVM中,有一个垃圾回收线程,他是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者在堆内存不足时才会触发执行,扫描那些被视为垃圾的对象,将他们添加到回收的集合中进行回收,也可以进行手动回收,通过System.gc(),通知GC运行

垃圾回收算法

  • 标记清除法

    标记出所有需要回收的对象,在标记完成后,统一回收的被标记的对象

    优点:速度比较快

    缺点:会产生内存碎片,碎片过多,仍会使得连续空间变少

  • 标记整理

    标记出所有需要回收的对象,在标记完成后统一进行整理,将存活对象进行一端移动,减少内存碎片,效率相对较低

    优点:无内存碎片

    缺点:效率较低

  • 复制算法

    开辟两份大小相等的空间,一份空间始终空着,垃圾回收时将存活对象拷贝进空闲时间

    优点:无内存碎片

    缺点:占用空间多

  • 分代回收

根据对象的存活周期不同,将对象划分为几块,比如堆内存的新生代和老年代,然后根据各个年代的特点采用最合适的算法进行回收;

新生代对象的存活时间比较短,因此使用的是复制算法,老年代对象存活的时间比较长,因此使用的是标记清除或者标记整理

分代垃圾回收的过程

分代垃圾回收器分两个区,新生代和老年代,新生代默认占1/3,老年代占2/3

新生代使用复制算法,新生代里面又分3个区(Eden,To Survivor, From Survicior)默认占比是8:1:1,当Eden区满了之后就会触发第一次MinorGC,将Eden区和From区存活的对象复制到To Survivor区域,然后to Survivor区域和From Survivor互换,原来的To Servivor区域成为下一次的From Survivor区域,然后清空Eden和From Survivor区中的对象,From中的对象每经过一次MinorGC,他的年龄值就会加1,达到15的移动到老年代,这里使用了复制算法,老年代满了或者是超过了临界值则会触发完全垃圾回收

判断一个对象是否为垃圾

引用计数法

堆中每个对象实例都有一个引用计数,当一个对象被创建的时候,且将这个对象实例分配给一个变量,该变量计数设置为1,当任何其他变量被赋值为这个对象的引用时,计数加1,但当一个对象实例的某个引用超过了生命周期,或者被设置为一个新值时,这个对象实例的引用计数将会减1,任何引用计数为0的对象都是可以被当做垃圾收集的对象,也就是一个垃圾,引用计数法容易产生循环引用的问题,如果两个对象相互引用,那么他们的引用就会一直存在,导致一直无法回收,为了解决这个问题,下面引入了可达性分析法

可达性分析算法

可达性分析算法又叫做根搜索法,就是通过一系列的称之为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain)影响链,当一个对象到GC Roots没有任何链相连时(即从 GC Roots节点到该节点是不可达的),则这个对象就是可以被当做垃圾收集的对象,也就是一个垃圾

可以被作为GC Roots的对象

  • 虚拟机栈中引用的对象
  • 方法区静态成员的引用对象
  • 方法区常量的引用对象
  • 本地方法栈引用的对象

引用类型

  • 强引用

    默认声明的就是强引用,只要强引用存在,垃圾回收器就永远不会回收该对象,哪怕内存不足时也不会去回收,所以强引用是造成Java内存泄漏的主要原因,如果想回收某个对象可以将值赋值为null,切断强引用关系

  • 软引用:一些非必需但是仍有用的对象,通过SoftReference实现,在内存足够的时候,软引用对象不会回收,在内存不足时则会回收,当回收了软引用对象内存还是不足时,会抛出内存溢出异常

  • 弱引用:比软引用的引用强度更低一些,通过WeakReference实现,无论内存是否足够,JVM都会被进行垃圾回收

  • 虚引用:最弱的引用关系,通过PhantomReference类实现,每次垃圾回收都会被回收,主要用于跟踪对象的垃圾回收状态,虚引用的对象始终返回一个null,虚引用对象始终和引用队列一起使用,当一个对象还存在虚引用时,会被加入到引用队列中,jvm通过引用队列是否包含这个虚引用来了解这个对象是否将要被进行垃圾回收

3. 对象分配对象在内存中如何分配

对象优先在Eden区中分配,当Eden区没有足够的空间时,会触发垃圾回收,把存活的对象移动到Survicor空间,如果Survivor空间也满了,则会把部分对象移动到老年代中,如果对象太大,是一个大对象,则这个对象会直接分配到老年代,不会发生GC

对象如何从年轻代进入老年代

  • 对象年龄够老,默认是15,可以通过XX:MaxTenuringThreshold设置,复制到老年代,同时年龄加1
  • 大对象会直接分配到老年代,大对象的定义和具体的JVM版本,堆大小,垃圾回收策略有关,一般为2K~128k,可以通过XX:pretenureSizeThreshold设置其大小

4. 垃圾收集器

JVM针对新生代和老年代分别提供了不同的垃圾收集器,新生代有Serial,ParNew,Parallel Scavenge,针对老年代提供的垃圾收集器有Serial Old,Parallel Old,CMS,还有针对不同区域的G1分区收集算法

常见的垃圾收集器

Serial垃圾收集器

Java虚拟机运行在Client模式下的新生代的默认垃圾收集器,基于复制算法实现,是一个单线程收集器,在进行垃圾收集时,必须暂停其他所有工作线程,直到垃圾收集结束,Serial收集器对于但CPU运行环境来说,没有线程交互开销,可以获得最高的单线程垃圾收集效率

ParNew垃圾收集器

Java虚拟机运行在Server模式下,新生代的默认垃圾收集器,基于复制算法实现,采用多线程模式工作,在进行垃圾收集时,必须暂停其他所有工作线程,直到垃圾收集结束,ParNew垃圾收集器默认开启与CPU同等数量的线程进行垃圾回收,可以通过-XX:ParallelGCThreads参数调节工作线程数

Parallel Scavenge垃圾收集器

Parallel Scavenge垃圾收集器是为提高新生代垃圾收集效率而设计的垃圾收集器,基于复制算法实现,采用多线程,在系统吞吐量上有很大的优化,,可以更高效的利用CPU完成垃圾回收任务,Parallel Scavenge提供了三个参数用于调节,控制垃圾回收的停顿时间及吞吐量,分别是:控制足最大垃圾收集停顿时间:-XX:MaxGCPauseMillis,控制吞吐量大小:-XX:GCTimeRation,控制是否开启自适应调节策略:UseAdaptiveSizePolicy

Serial Old垃圾收集器

Serial Old是JVM运行在Client模式下,老年代的默认垃圾收集器,Serial Old是Serial的老年代实现,同Serial一样采用单线程执行,不同的是Serial Old基于标记整理算法实现

Parallel Old垃圾收集器

Parallel Old垃圾收集器采用多线程并发进行垃圾回收,基于标记整理法实现,在设计上优先考虑系统吞吐量,其次考虑停顿时间等因素

CMS垃圾收集器

CMS垃圾收集器是为老年代设计的垃圾收集器,其主要目的是达到最短时间的垃圾回收停顿时间,基于线程的标记清除算法实现

CMS的垃圾回收过程

  • 初始标记:只标记GC Roots直接关联的对象,速度很快,需要暂停所有工作线程
  • 并发标记:和用户一起工作,执行GC Roots跟踪标记过程,不需要暂停工作线程
  • 重新标记:为了确保并发标记的正确性,重新标记已经标记的对象,需要暂停工作线程
  • 并发清除:和用户线程一起工作,执行清除GC Roots不可达对象的任务,不需要暂停工作线程

G1垃圾收集器

G1垃圾收集器是为了避免全区域垃圾收集引起的系统停顿,将堆内存划分大小固定的几个独立区域,独立使用这些区域跟踪垃圾收集进度,同时在维护一个优先级列表,根据系统允许的最长垃圾收集时间,优先回收垃圾最多的区域,相对于CMD垃圾收集器,G1垃圾收集器不产生内存碎片,可以精确的控制停顿时间,在不牺牲吞吐量的前提下实现停顿垃圾回收

G1垃圾回收器参数

  • -XX:MaxGCPauseMillis:暂停毫秒级,默认200毫秒
  • -XX:G1HeapRegionSize:区域大小,默认最多2048块,每块的大小需要为2的幂次方,最大为32M
  • -XX:G1NewSizePercent:新生代的最低百分比,默认5%
  • -XX:G1MaxNewSizePercent:新生代的最大百分比,默认60%

5. 类加载器

类加载器负责将class文件加载到java虚拟机中,并为之创建一个Class对象

类加载器的流程

  • 加载:将class文件加载到内存中,将静态数据结构转化成方法区中运行时的数据结构,在堆中生成一个代表这个类的对象,作为数据访问的入口
  • 验证:确保加载的类符合JVM规范和安全,保证被效验类的方法在运行时不会做出危害虚拟机的事件,做一个安全检查
  • 准备:为static变量在方法区中分配内存空间,设置变量的初始值,注意只设置静态变量,不包括实例变量,实例变量在对象初始化时赋值
  • 解析:虚拟机将常量池内的符号引用替换为直接引用的过程,符号引用中的符号可以是任何形式的字面量,只要能无歧义的定位到目标即可,直接引用是可以直接指向目标的指针,相对偏移量或者间接定位到目标的句柄
  • 初始化:初始化类变量和其他资源
  • 使用
  • 卸载:GCh将对象从内存中卸载

常用的类加载器

  • 启动类加载器(Bootstrap ClassLoader):虚拟机内置类加载器,加载java核心类库,如JAVA_HOME/jre/lib/rt.jar,resources.jar,sun.boot.class.path,使用C+/C++实现,他没有父类加载器,是扩展类加载器和应用程序加载器的父类加载器
  • 扩展类加载器(Extension classLoader):由java语言编写,从系统属性java.ext.dirs目录中或者JDK安装目录JRE/lib/ext加载类库
  • 应用程序类加载器(Application ClassLoader):应用程序类加载器,负责加载用户类路径上所指定的类,我们程序中默认的类加载器,可以通过ClassLoader#getSystemClassLoader()获取并操作这个加载器

双亲委派机制

双亲委派机制是当一个类加载器需要加载一个字节码文件时,首先会把这个任务委托给他的上级类加载器,上级类加载器又交给他的上级,以此递归,直到上级类加载器不能加载该字节码文件,然后再自己去加载这个字节码文件,如果自己也无法加载则抛出ClassNotFoundException异常,这样做的好处是防止一个字节码文件多次加载,保证了数据的安全性,即使重复加载了也不会是同一个class对象

6. JVM调优

在调优之前要先明确是否需要使用JVM调优,因为大多数Java应用是不需要调优的,大多数导致GC问题的原因是代码层面的问题,比如创建的对象数量过多,使用了大量的全局变量和大对象

需要调优的场景

  • Heap内存(老年代)持续上涨,达到设置的最大内存值
  • Full GC次数频繁
  • GC停顿时间过长
  • 应用出现OutOfMemory等内存异常
  • 系统吞吐量不高

调优步骤

  • 分析GC日志和Demp文件,判断是否需要优化,确定瓶颈问题点
  • 确定JVM调优量化目标
  • 调整JVM调优参数,包括内存,延迟,吞吐量
  • 观察调优前后的差异,不断分析和调整,找到最合适的参数
  • 将这些参数应用到服务器

调优参数

语法

  • -XX:+ ‘ +’表示启用该选项
  • -XX:- ‘-‘表示关闭该选项
  • -XX:= ‘=’给选项设置一个数字类型的值,可跟随单位,例如:m和M表示兆字节,k和K表示k字节,g和G表示千兆字节

参数示例

-Xms4g:设置最小堆内存大小为4g,堆内存初始大小-Xmx4g:设置最大堆内存大小为4g,堆内存最大值-Xmn1200m:设置年轻代大小为1200MB,增大年轻代后,将会减小老年代的大小,这个值对系统性能较大,官方推荐配置为整个堆的3/8-Xss512K:设置每个线程的堆栈大小,JDK5.0后每个线程堆栈大小为1MB,以前为256K,值越小,能创建的线程越多-XX:NewRatio=4:设置年轻代与老年代的比值为1:4,年轻代占整个堆栈的1/5-XX:SurvivorRatio=8:设置年轻代中Eden区和Survivor区的比值为2:8,一个Survivor区占整个年轻代的1/10-XX:MaxTenuringThreshold=15:设置Survivor区中对象年龄的最大值为15,超过这个值则进入老年代,如果设置为0,则年轻代不经过Survivor,直接进入老年代-XX:+UseParNewGC:年轻代-XX:+UseConcMarkSweepGC:老年代

常用调优参数

-Xms:初始化堆内存大小,默认为物理内存的1/64(小于1G)-Xmx:堆内存的最大值。默认(MaxHeapFreeRation参数可以调整)空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制-Xmn:新生代大小,包括Eden和两个Survivor区-XX:MaxDirectMemorySize=1G:直接内存,报java.lang.OutOfMemoryError:Direct buffer memory异常时可以上调这个值-XX:+DisableExplicitGC:禁止运行期间显示的调用System.gc()来触发full GC-XX:CMSlnitiatingOccupancyFraction=60:老年代内存回收阈值,默认为68-XX:ConcGCThreads=4:CMS垃圾回收器并行线程数,推荐值为CPU核心数-XX:ParallelGCThreads=8:新生代并行收集器的线程数-XX:MaxTenuringThreshold=10:Survivor区的最大年龄值,超过就会进入老年代