JVM 篇

目录

一、知识点汇总

二、知识点详解

2.1 JVM 的主要组成部分及其作用 

2.2 JVM内存模型 

2.3 堆与栈的区别 

2.4 JVM 加载 class 文件的原理机制 

2.5 类的生命周期 

2.6 Java 对象结构

2.7 Java 对象创建过程 

2.8 指针碰撞

2.9 空闲列表

2.10 TLAB+CAS 

2.11 说说对象内存分配

2.12 什么是标量与聚合量? 

2.13 Minnor GC 与 Full GC

2.14 Stop The World、OopMap、safepoint

2.15 垃圾收集算法

2.16 垃圾回收器

2.17 如何判断对象可以被回收? 

2.18 如何选择垃圾收集器? 

2.19 虚拟机为什么使用元空间替换了永久代 

2.20 调优命令

2.21 调优参数

2.22 调优工具


一、知识点汇总

其中内存模型,类加载机制,GC是重点方面。性能调优部分更偏向应用,重点突出实践能力。编译器优化和执行模式部分偏向于理论基础,重点掌握知识点。

  • 内存模型:各部分作用,保存哪些数据。
  • 类加载:双亲委派加载机制,常用加载器分别加载哪种类型的类。
  • GC:分代回收的思想和依据以及不同垃圾回收算法的回收思路和适合场景。
  • 性能调优:常有JVM优化参数作用,参数调优的依据,常用的JVM分析工具能分析哪些问题以及使用方法。
  • 执行模式:解释/编译/混合模式的优缺点,Java7提供的分层编译技术,JIT即时编译技术,OSR栈上替换,C1/C2编译器针对的场景,C2针对的是server模式,优化更激进。新技术方面Java10的graal编译器。
  • 编译器优化:javac的编译过程,ast抽象语法树,编译器优化和运行器优化。

二、知识点详解

2.1 JVM 的主要组成部分及其作用 

  • Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到运行时数据区的方法区中。
  • Execution engine(执行引擎):执行class的指令。
  • Native Interface(本地接口):与native lib交互,是其它编程语言交互的接口。
  • Runtime data area(运行时数据区域):即我们常说的JVM的内存。 

首先通过编译器把 Java源代码转换成字节码,Class loader(类装载)再把字节码加载到内存中,将其放在运行时数据区的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。 

2.2 JVM内存模型 

栈:又称方法栈,线程私有的,线程执行方法是都会创建一个栈帧,用来存储局部变量表,操作栈,动态链接,方法出口等信息。调用方法时执行入栈,方法返回时执行出栈.。

本地方法栈:与栈类似,也是用来保存执行方法的信息。执行 Java 方法是使用栈,执行 Native 方法时使用本地方法栈。

程序计数器:保存着当前线程执行的字节码位置,每个线程工作时都有独立的计数器,只为执行Java方法服务,执行 Native 方法时,程序计数器为空。

堆:JVM 内存管理最大的一块,对被线程共享,目的是存放对象的实例,几乎所有的对象实例都会放在这里,当堆没有可用空间时,会抛出 OOM 异常。根据对象的存活周期不同,JVM 把对象进行分代管理,由垃圾回收器进行垃圾的回收管理。

方法区:又称非堆区,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器优化后的代码等数据。1.7 的永久代和1.8 的元空间都是方法区的一种实现。

2.3 堆与栈的区别 

栈:是运行时单位,代表着逻辑,内含基本数据类型和堆中对象引用,所在区域连续,没有碎片。

堆:是存储单位,代表着数据,可被多个栈共享(包括成员中基本数据类型、引用和引用对象),所在区域不连续,会有碎片。

  • 功能不同:栈内存用来存储局部变量和方法调用,而堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中。 
  • 共享性不同:栈内存是线程私有的。 堆内存是所有线程共有的。
  • 异常错误不同:如果栈内存或者堆内存不足都会抛出异常。
    栈空间不足:java.lang.StackOverFlowError。
    堆空间不足:java.lang.OutOfMemoryError。
  • 空间大小:栈的空间大小远远小于堆的。

2.4 JVM 加载 class 文件的原理机制 

JVM 中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java 中的类加载器是一个重要的 Java 运行时系统组件,它负责在运行时查找和装入类文件中的类。 由于Java 的跨平台性,经过编译的 Java 源程序并不是一个可执行程序,而是一个或多个类文件。当 Java 程序需要使用某个类时,JVM 会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的 .class 文件中的数据读入到内存中,通常是创建一个字节数组读入 .class 文件,然后产生与所加载类对应的 Class 对象。加载完成后,Class 对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后JVM对类进行初始化,包括:1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;2)如果类中存在初始化语句,就依次执行这些初始化语句。

从Java 2 开始,类加载过程采取了父亲委托机制(PDM)。PDM 更好的保证了 Java 平台的安全性,在该机制中,JVM 自带的 Bootstrap 是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM 不会向 Java 程序提供对 Bootstrap 的引用。 

类加载器包括:

  • 根加载器(BootStrap):一般用本地代码实现,负责加载 JVM 基础核心类库(rt.jar)。
  • 扩展加载器(Extension):从 java.ext.dirs 系统属性所指定的目录中加载类库,它的父加载器是 Bootstrap。
  • 系统加载器(System):又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量 classpath 或者系统属性 java.class.path 所指定的目录中记载类,是用户自定义加载器的默认父加载器。
  • 用户自定义类加载器(java.lang.ClassLoader的子类)。

2.5 类的生命周期 

加载:通过类的完全限定名,查找此类字节码文件,利用字节码文件创建 Class 对象.。

验证:确保 Class 文件符合当前虚拟机的要求,不会危害到虚拟机自身安全。

准备:为 static 修饰的类变量分配内存,并设置初始值(0或null),不包含 final 修饰的静态变量,因为final变量在编译时分配。

解析:将常量池中的符号引用替换为直接引用的过程.直接引用为直接指向目标的指针或者相对偏移量等。

初始化:主要完成静态块执行以及静态变量的赋值。先初始化父类,再初始化当前类。只有对类主动使用时才会初始化。触发条件包括:

  • 创建类的实例的时候。
  • 访问类的静态方法或静态变量的时候。
  • 使用 Class.forName 反射类的时候。
  • 某个子类初始化的时候。

使用:new 出对象程序中使用。

卸载:执行垃圾回收,Java 自带的加载器加载的类,在虚拟机的生命周期中是不会被卸载的,只有用户自定义的加载器加载的类才可以被卸载。

其中验证,准备,解析合称链接

2.6 Java 对象结构

Java 对象由三个部分组成:对象头、实例数据、对齐填充。 

对象头:由两部分组成,Mark Word 中的内容会随着锁标志位而发生变化,所以只说存储结构就好了。

  • 第一部分存储对象自身的运行时数据,也被称为 Mark Word,也就是用于轻量级锁和偏向锁的关键点。具体的内容包含对象的 hashcode、分代年龄、轻量级锁指针、重量级锁指针、GC标记、偏向锁线程 ID(一般占32/64 bit)、偏向锁时间戳。
  • 第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。

实例数据:用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)。

对齐填充:JVM 要求对象起始地址必须是8字节的整数倍(8字节对齐)。

2.7 Java 对象创建过程 

  1. JVM 遇到一条新建对象的指令时首先去检查这个指令的参数是否能在常量池中定义到一个类的符号引用,然后加载这个类。
  2. 为对象分配内存。一种办法“指针碰撞”、一种办法“空闲列表”,通过TLAB+CAS解决并发问题。
  3. 将除对象头外的对象内存空间初始化为0。
  4. 对对象头进行必要设置。

2.8 指针碰撞

一般情况下,JVM的对象都放在堆内存中(发生逃逸分析除外)。当类加载检查通过后,Java虚拟
机开始为新生对象分配内存。如果Java堆中内存是绝对规整的,所有被使用过的的内存都被放到一
边,空闲的内存放到另外一边,中间放着一个指针作为分界点的指示器,所分配内存仅仅是把那个
指针向空闲空间方向挪动一段与对象大小相等的实例,这种分配方式就是指针碰撞Bump the Pointer。 

2.9 空闲列表

如果Java堆内存中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,不可以进行指针碰撞啦,虚拟机必须维护一个列表,记录哪些内存是可用的,在分配的时候从列表找到一块大的空间分配给对象实例,并更新列表上的记录,这种分配方式就是空闲列表Free List。 

2.10 TLAB+CAS 

TLAB: 可以把内存分配的动作按照线程划分在不同的空间之中进行,每个线程在Java堆中预先分配一小块内存,这就是TLAB(Thread Local Allocation Buffer:本地线程分配缓存) 。
开启TLAB:-XX:UseTLAB。
指定TLAB大小:-XX:TLABSize。 

CAS(compare and swap):虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。

JVM 虚拟机采用 TLAB + CAS 的方式分配内存。即每个线程在分配内存时先会分配在自己预内存上,如果自己的预内存分配完了,再分配到公共内存中,此时发生并发才采用 CAS 进行分配。

2.11 说说对象内存分配

对象内存分配流程:

  • 对象在栈上分配:先根据逃逸分析和标量替换判断,对象是否可以在栈上分配。 
  • 对象在Eden区分配:尝试使用TLAB方式分配内存到Eden区,如果分配失败,则继续尝试使用CAS的方式分配内存到Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC,将存活的对象移动到为空的s0区,下一次Eden区满后触发Minor GC,剩余存活的对象挪至另外一块为空的s1区,s0与s1来回存储。
  • 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象):这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
  • 长期存活的对象进入老年代:虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值(默认15)对象进入老年区。
  • 动态判断对象的年龄:如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
  • 空间分配担保:每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。 

我们知道Java对象都是在堆上进行分配的,当对象没有被引用的时候,需要依靠GC进行内存回收,如果对象数量较多的时候,会给给GC带来较大压力,也间接影响了应用性能。为了减少对象在堆内分配的数量,JVM通过逃逸分析确定该对象会不会被外部访问,如果不会逃逸则可以将该对象在栈上分配内存,这样该对象所占用的内存空间就可以随着栈帧出栈而销毁,从而减轻了垃圾回收的压力。 

逃逸分析:就是分析对象动态作用域。

开启逃逸分析参数(JDK 7之后默认开启):-XX:+DoEscape Analysis
关闭逃逸分析参数:-XX:-DoEscape Analysis

标量替换:通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该对象的成员变量进行分解。分解后的成员变量在栈帧上分配空间,这样就不会应为没有一大块连续空间导致对象内存不够分配。

开启标量替换参数(JDK 7之后默认开启):-XX:+EliminateAllocations 
关闭标量替换参数:-XX:-EliminateAllocations

/**
 * 逃逸分析,标量替换
 * 代码调用了1亿次methodA,如果分配到堆上,大概需要1GB以上空间,如果堆空间小于该值,必然会触发GC。
 *
 * 使用如下参数不会发生GC,开启逃逸分析、标量替换
 * -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
 * 
 * 使用如下参数配置大概率会发生大量GC
 * 没有开启逃逸分析
 * -Xmx15m -Xms15m -XX:-DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
 * 没有开启标量替换
 * -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations
 *
 * @param args
 */
public static void main(String[] args) {
    for (int i=0;i<100000000;i++) {
        methodB();
    }
}

/**
 *  User 作为调用参数传递到其它地方中,它的作用域范围变得不确定。
  * @return
 */    
public User methodA() {
    User user = new User();
    user.setAge(1);
    user.setName("ww");
    // TODO
    
    return user;
}

/**
 *  当方法结束后user对象成为无效对象了,对于这样的对象我们其实可以将其分配在栈内存里,让其方法结束时随栈内存一起被回收调。
  */    
public void methodB() {
    User user = new User();
    user.setAge(1);
    user.setName("ww");
    // TODO
}

总结:对象不一定分配在堆中,JVM通过逃逸分析、标量替换,那些逃不出方法的对象在栈上分配。

2.12 什么是标量与聚合量? 

标量:即不可被进一步分解的量,比如Java的基本数据类型就是标量。

聚合量:相对于标量,聚合量就是可以被进一步分解的量,在Java中对象就是可以被进一步分解的聚合量。 

2.13 Minnor GC 与 Full GC

当年轻代中的 Eden 区满时,会触发 Minnor GC。 

除直接调用 System.gc 外,触发 Full GC 执行的情况有如下四种:

  • 老年代空间不足 

新生代对象转入老年代空间或创建大对象、大数组时,如果老年代空间不足,会触发 Full GC,当执行Full GC后空间仍然不足,则抛出如下错误: java.lang.OutOfMemoryError: Java heap space。为避免以上两种状况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收,让对象在新生代多存活一段时间及不要创建过大的对象及数组。

  • 方法区(Permanet Generation)空间不足

Permanet Generation 中存放的为一些 class 的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation 可能会被占满,在未配置为采用 CMS GC 的情况下会执行Full GC。如果经过 Full GC 仍然回收不了,那么JVM会抛出如下错误信息: java.lang.OutOfMemoryError: PermGen space 为避免 Perm Gen 占满造成 Full GC 现象,可采用的方法为增大 Perm Gen 空间或转为使用 CMS GC。 

  • 空间分配担保机制:统计得到的 Minor GC 晋升到老年代的平均大小大于老年代的剩余空间

这是一个较为复杂的触发情况,Hotspot 为了避免由于新生代对象晋升到老年代导致老年代空间不足的现象,在进行 Minor GC 时,做了一个判断,如果之前统计所得到的 Minor GC 晋升到老年代的平均大小大于老年代的剩余空间,那么就直接触发 Full GC。 例如程序第一次触发 MinorGC 后,有6MB的对象晋升到老年,那么当下一次 Minor GC 发生时,首先检查老年代的剩余空间是否大于6MB,如果小于6MB,则执行 Full GC。 当新生代采用 PS GC 时,方式稍有不同,PS GC 是在 Minor GC 后也会检查,例如上面的例子中第一次 Minor GC 后,PS GC 会检查此时老年代的剩余空间是否大于6MB,如小于,则触发对老年代的回收。

  • CMS GC 时出现 promotion failed 和 concurrent mode failure

对于采用 CMS 进行老年代 GC 的程序而言,尤其要注意 GC 日志中是否有 promotion failed 和concurrent mode failure 两种状况,当这两种状况出现时可能会触发 Full GC。promotion failed 是在进行 Minor GC 时,survivor space 放不下,对象只能放入老年代,而此时老年代也放不下造成的。concurrent mode failure 是在执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足造成的。 应对措施为:增大survivor space、旧生代空间或调低触发并发 GC 的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置 -XX:CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。

除了以上4种状况外,对于使用 RMI 来进行 RPC 或管理的 Sun JDK 应用而言,默认情况下会一小时执行一次 Full GC。可以在启动时通过 - java -Dsun.rmi.dgc.client.gcInterval=3600000 来设置 Full GC 执行的间隔时间或通过 -XX:+DisableExplicitGC 来禁止 RMI 调用 System.gc。 

2.14 Stop The World、OopMap、safepoint

Stop The World:
进行垃圾回收的过程中,会涉及对象的移动。为了保证对象引用更新的正确性,必须暂停所有的用
户线程,像这样的停顿,虚拟机设计者形象描述为「Stop The World」。也简称为STW。
 

OopMap:
在HotSpot中,有个数据结构(映射表)称为「OopMap」。一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,记录到OopMap。在即时编译过程中,也会在「特定的位置」生成 OopMap,记录下栈上和寄存器里哪些位置是引用。 

safepoint:
循环的末尾(非 counted 循环)、方法临返回前 / 调用方法的call指令后、可能抛异常的位置,这些特定的位置叫做安全点
「safepoint」。

总结:程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是必须是执行到安全点才能够暂停。

2.15 垃圾收集算法

GC 最基础的算法有三种: 标记--清除算法、复制算法、标记--压缩算法,我们常用的垃圾回收器一般都采用分代收集算法。

标记--清除算法:“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。

复制算法:“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

标记--压缩算法:标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法:“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。 

2.16 垃圾回收器

如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了
7种作用于不同分代的收集器,其中用于回收新生代的收集器包括Serial、PraNew、Parallel、Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不同收集器之间的连线表示它们可以搭配使用。 

新生代收集器

  • Serial收集器 (复制算法):新生代单线程收集器,标记和清理都是单线程,优点是简单高效。
  • ParNew收集器 (复制算法):新生代收并行集器,实际上是Serial收集器的多线程版本,在多核
    CPU环境下有着比Serial更好的表现,常用于和CMS搭配使用。
  • Parallel Scavenge收集器 (复制算法):新生代并行收集器,JDK 8 新生代默认收集器追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景。

老年代收集器

  • Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本。
  • Parallel Old收集器 (标记-整理算法): 老年代并行收集器,JDK 8 默认老年代收集器。吞吐量优先,Parallel Scavenge收集器的老年代版本。
  • CMS(Concurrent Mark Sweep)收集器 (标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。

整堆收集器

  • G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,JDK 9 设为默认收集器。G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。
  • ZGC(Z Garbage Collector) 是一款由Oracle公司研发的,以低延迟为首要目标的一款垃圾收集器。它是基于动态Region内存布局,(暂时)不设年龄分代,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的收集器。在 JDK 11 以实验性的特性引入,JDK 15 可投入生产。主要特点是:回收TB级内存(最大4T),停顿时间不超过10ms。
    优点:低停顿,高吞吐量,ZGC 收集过程中额外耗费的内存小。
    缺点:浮动垃圾。前使用的非常少,真正普及还是需要时间。 

2.17 如何判断对象可以被回收? 

判断对象是否存活一般有两种方式:

  • 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计 数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
  • 可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引 用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对 象。

GC Roots根节点:线程的本地变量、静态变量、本地方法栈的变量等等。 

2.18 如何选择垃圾收集器? 

  • 如果你的堆大小不是很大(比如 100MB ),选择串行收集器一般是效率最高的。
    参数: -XX:+UseSerialGC 。
  • 如果你的应用运行在单核的机器上,或者你的虚拟机核数只有单核,选择串行收集器依然是合适的,这时候启用一些并行收集器没有任何收益。
    参数: -XX:+UseSerialGC 。
  • 如果你的应用是“吞吐量”优先的,并且对较长时间的停顿没有什么特别的要求。选择并行收集器是比较好的。
    参数: -XX:+UseParallelGC 。
  • 如果你的应用对响应时间要求较高,想要较少的停顿。甚至 1 秒的停顿都会引起大量的请求失败,那么选择 G1 、 ZGC 、 CMS 都是合理的。虽然这些收集器的 GC 停顿通常都比较短,但它需要一些额外的资源去处理这些工作,通常吞吐量会低一些。
    参数:-XX:+UseConcMarkSweepGC 、-XX:+UseG1GC 、-XX:+UseZGC 等。

总结:从上面这些出发点来看,我们平常的 Web 服务器,都是对响应性要求非常高的。选择性其实就集中在 CMS 、 G1 、 ZGC 上。而对于某些定时任务,使用并行收集器,是一个比较好的选择。 

2.19 虚拟机为什么使用元空间替换了永久代 

什么是永久代?它和方法区有什么关系呢?
如果在HotSpot虚拟机上开发、部署,很多程序员都把方法区称作永久代。可以说方法区是规范,永久代是Hotspot针对该规范进行的实现。在Java7及以前的版本,方法区都是永久代实现的。 

什么是元空间?它和方法区有什么关系呢?
对于Java8,HotSpots取消了永久代,取而代之的是元空间(Metaspace)。换句话说,就是方法区还是在的,只是实现变了,从永久代变为元空间了。 

为什么使用元空间替换了永久代?

  • 永久代的方法区,和堆使用的物理内存是连续的。

对于「永久代」,如果动态生成很多class的话,就很可能出现「java.lang.OutOfMemoryError:
PermGen space错误」,因为永久代空间配置有限嘛。最典型的场景是,在web开发比较多jsp页
面的时候。 

  • JDK8之后,方法区存在于元空间(Metaspace)。物理内存不再与堆连续,而是直接存在于本地内存中,理论上机器「内存有多大,元空间就有多大」。 

总结:表面上看是为了避免OOM异常。因为通常使用PermSize和MaxPermSize设置永久代的大小就决定了永久代的上限,但是不是总能知道应该设置为多大合适,,如果使用默认值很容易遇到
OOM错误。当使用元空间时,可以加载多少类的元数据就不再由MaxPermSize控制,而由系统
的实际可用空间来控制啦。 

2.20 调优命令

Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo。

  • jps:JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
  • jstat:JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
     
  • jmap:JVM Memory Map命令用于生成heap dump文件
  • jhat:JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看
  • jstack:用于生成java虚拟机当前时刻的线程快照。
  • jinfo:JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。 

2.21 调优参数

堆栈内存相关

  • -Xms 设置初始堆的大小。
  • -Xmx 设置最大堆的大小。
  • -Xmn 设置年轻代大小,相当于同时配置-XX:NewSize和-XX:MaxNewSize为一样的值。
  • -Xss 每个线程的堆栈大小。
  • -XX:NewSize 设置年轻代大小(for 1.3/1.4)。
  • -XX:MaxNewSize 年轻代最大值(for 1.3/1.4)。
  • -XX:NewRatio 年轻代与年老代的比值(除去持久代)。
  • -XX:SurvivorRatio Eden区与Survivor区的的比值。
  • -XX:PretenureSizeThreshold 当创建的对象超过指定大小时,直接把对象分配在老年代。
  • -XX:MaxTenuringThreshold 设定对象在Survivor复制的最大年龄阈值,超过阈值转移到老年代。

垃圾收集器相关

  • -XX:+UseParallelGC:选择垃圾收集器为并行收集器。
  • -XX:ParallelGCThreads=20:配置并行收集器的线程数。
  • -XX:+UseConcMarkSweepGC:设置年老代为并发收集。
  • -XX:CMSFullGCsBeforeCompaction=5 由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行5次GC以后对内存空间进行压缩、整理。
  • -XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片。 

辅助信息相关

  • -XX:+PrintGCDetails 打印GC详细信息。
  • -XX:+HeapDumpOnOutOfMemoryError让JVM在发生内存溢出的时候自动生成内存快照,排查问题用。
  • -XX:+DisableExplicitGC禁止系统System.gc(),防止手动误触发FGC造成问题。.
  • -XX:+PrintTLAB 查看TLAB空间的使用情况。 

2.22 调优工具

jdk自带监控工具

  • jconsole:是从java5开始,在JDK中自带的java监控和管理控制台,用于对JVM中内存,线程和类等的监控。
  • jvisualvm:可以分析内存快照、线程快照;监控内存变化、GC变化等。

第三方工具

  • MAT(Memory、Analyzer Tool):一个基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。
  • GChisto:一款专业分析gc日志的工具。
  • arthas:阿里开源工具,可以反编译线上代码,可以查看线上系统变量的值,甚至可以修改变量的值。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值