虚拟机学习总结

 

目录

一.JVM架构图

1.JVM结构

程序计数器: 线程私有

Java虚拟机栈: 线程私有

本地方法栈: 线程私有

Java堆: 共享,内存回收主要区域

方法区:共享

二、类加载机制

1.流程:

加载:

验证:

准备:

解析

初始化

2、类加载器

虚拟机类加载器

自定义类加载器

3、双亲委派

什么是双亲委派

如何打断默认双亲委派机制

为什么使用双亲委派

三、垃圾回收:

1. 垃圾回收要点知识

引用计数:

可达性分析

四种引用

2. 垃圾回收算法

标记清除

标记整理

复制

HotSpot算法

3.垃圾回收机制

对象优先分配到Eden区

大对象直接进入老年代

长期存活的对象会进入老年代

动态年龄判断

4.关于GC

Minor GC

Major GC/Full GC

4. JDK垃圾回收器

Serial

ParNew

Parallel Scavenge

Serial Old

Parallel Old

Concurrent Mark Sweep

G1

5.附录:JVM内存管理参数


 

一.JVM架构图

在这里插入图片描述

1.JVM结构

程序计数器(Program Counter)、Java堆(Heap)、Java虚拟机栈(Stack)、本地方法栈(Native 、Stack)方法区(Method Area) 五大组成部分

程序计数器: 线程私有

一个寄存器,可以看作是代码行号指示器,用于指示,跳转下一条需要执行的命令

Java虚拟机栈: 线程私有

描述Java方法的内存模型,每个方法被执行时都会创建一个栈帧(Stack Frame)。栈帧会利用局部变量数组存储局部变量(Local Variables)操作栈(Operand Stack)方法出口(Return Value)

保存函数的参数以及局部变量用的,局部变量表中的变量只在当前函数调用中有效,当函数调用结束后,随着函数栈帧的销毁,局部变量表也会随之销毁。局部变量表在编译期确定大小

局部变量表的容量以 Variable Slot(变量槽)为最小单位,每个变量槽都可以存储 32 位长度的内存空间

基本类型数据以及引用和 returnAddress(返回地址)占用一个变量槽,long 和 double 需要两个

 

Java虚拟机规范规定了两种异常:

  • 线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverFlow异常
  • 对于支持动态扩展的虚拟机,当扩展无法申请到足够的内存时会抛出OutOfMemory异常

本地方法栈: 线程私有

执行的是Native方法,提供对底层其他语言的方法调用

Java堆: 共享,内存回收主要区域

存放对象实例和数组,在虚拟机启动时创建

是垃圾收集器管理的主要区域,也被称为CG堆。收集器采用的是分代回收法,有新生代老生代。新生代又包括Eden SpaceFrom Survivor Space、To Survivor Space

如果堆中没有内存 完成实例分配,并且堆无法再扩展时,抛出OutOfMemory异常,

方法区:共享

类信息、常量、静态变量、静态代码块的区域、构造函数、常量池、接口初始化、即时编译器编译后的代码等数据

相对而言,垃圾收集行为再这个区比较少出现,这个区域的内存回收目标主要针对常量池的回收和对类型的卸载,当方法区无法满足内存分配需要时,将抛出OutOfMemor、异常

二、类加载机制

1.流程:

类从被加载到虚拟机内存开始,到卸载内存为止,整个生命周期包括:加载Loading、验证Verification、准备Preparation、解析Resolution、初始化Initialization、适用Using和卸载Unloading

共7个阶段,其中准备、验证、解析统称为连接

 

加载:

1. 获取二进制字节流

2. 静态存储结构转化为方法区的运行时数据结构

3. 再Java堆里面生成一个类对象,作为方法区的访问入口

Java二级制字节流获取途径:

1.  从ZIP包中读取,JAR、WAR、EAR格式的基础

2. 从网络中获取,Applet应用

3. 运行时计算生成,动态代理技术

4. 由其他文件生成,JSP应用,由JSP文件生成对应的class类

验证:

验证是连接阶段第一步,确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机自身的安全,大致分下面4个动作

  • 文件格式验证

  • 元数据验证

  • 字节码验证

  • 符号引用验证

抛出异常大致几种错误:

  • IncompatibleClassChangeError

  • Unsupported major.minor version

  • IllegalAccessError

  • NoSuchFieldError

  • NoSuchMethodError

准备:

准备阶段是为类变量分配内存并设置类变量初始化的阶段,这些变量所使用的内存当将在方法区中进行分配。只对类变量进行内存分配(static修饰),不包括实例变量,实例变量将会在对象实例化是随着对象一起分配在Java堆中。

如:一个类变量的定义为

// n的初始化值是0,而不是2。因为这个时候还没执行任何初始化方法(<clinit>)。
public static int n = 2;

再例如:

// 编译时会为m生成ConstantValue属性,在准备阶段会根据ConstantValue将m值设置为2
public static final int m = 2;

类变量和实例变量

  • 类变量:也称为静态变量,在类中以static关键字声明,但必须在方法构造方法和语句块之外

  • 实例变量:属于该类的对象,必须产生该类对象,才能调用实例变量。

解析

解析的目的就是将常量池内的符号引用替换为直接引用。

符号引用是以字面量的形式明确定义在常量池中;直接引用是指向目标的指针,或者相对偏移量

解析动作主要对类或者接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行,分别对应于常量池

字段的解析

class A extends B implements C, D{
    private String str; //字段的解析
}

解析字段的顺序:

  1. 先查找本类A,如果包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。

  2. 否则,在接口中查找。将会按照集成关系从下往上递归搜搜各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都于目标相匹配的字段,则返回这个字段的直接引用,查找结束。

  3. 否则,在父类中查找,如果在父类中包含了简单名称和字段描述符都于目标相匹配的字段,则返回这个字段的直接引用,查找结束

  4. 否则,查找失败,抛出java.lang.NoSuchFieldError异常。

类方法的解析

class A extends B implements C, D{
    private void inc(); //方法的解析
}
  1. 如果在类方法表中发现class_index中索引的A是一个接口,哪就直接抛出java.lang.IncompatiableClassChangeError异常。

  2. 如果通过了第一步,先查找本类A,是否由简单名称和描述符都于目标相匹配的方法,如果有则返回方法的直接引用,查找结束。

  3. 否则,父亲中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。

  4. 否则,在类实现的接口列表及它们的父接口之中查找是否有简单名名称和描述符都与目标相匹配的方法,如果存在匹配的方法,说明类C是一个抽象类,这时查找结束,抛出java.lang.AbastractMethodError异常。

  5. 否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError。

接口方法的解析

  1. 与类的方法解析不同,如果在接口方法表中发现class_index中的索引A是个类而不是接口,那就直接抛出java.lang.IncompatiableClassError异常。

  2. 否则,先查找本接口,是否有简单名称和描述符都与目标匹配的方法,如果有则返回这个方法的直接引用,查找结束。

  3. 否则,在接口的父接口中递归查找,直到java.lang.Object类(查找范围包括Object类)为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。

  4. 否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError异常。

初始化

  • <clinit> 类的初始化。静态变量,静态块的初始化。所有的类变量初始化语句和类型的静态初始化器。Java在编译之后会在字节码文件中生成<clinit>方法,称之为类构造器,类构造器同实例构造器一样,也会对静态语句块,静态变量进行初始化

  • <init> 对象的初始化。Java在编译之后会在字节码文件中生成<init>方法,称之为实例构造器。该实例构造器会对语句块,变量进行初始化,并调用父类的构造器。

<clinit>方法是在类加载过程中执行的,而<init>是在对象实例化执行的,所以<clinit>一定比<init>先执行。所以整个顺序就是:

  • 父类静态变量初始化

  • 父类静态语句块

  • 子类静态变量初始化

  • 子类静态语句块

  • 父类变量初始化

  • 父类语句块

  • 父类构造函数

  • 子类变量初始化

  • 子类语句块

  • 子类构造函数

2、类加载器

通过类的全限定名来获取描述此类的二进制字节流,把类加载阶段中的这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取需要的类,实现这个动作的代码模块称为“类加载器”

public abstract class ClassLoader {
    // 父加载器
    private final ClassLoader parent;

    // 加载类的核心方法之一。如果没找到类则抛出ClassNotFoundException
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        // synchronized意味着:
        // 1. 同一时间只允许一个线程加载名字为name的类
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            // 2. 在加载之前先检查,是否已经加载过该类
            Class<?> c = findLoadedClass(name);
            // 3. c==null代表只有没有被加载过的类才会进行加载
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 双亲委派
                    // 父加载器能加载的绝不给子加载器
                    if (parent != null) {
                        // 如果父加载器不为null,则把类加载的工作交给父加载器
                        c = parent.loadClass(name, false);
                    } else {
                        // 如果父加载器为null,则把类加载的工作交给BootstrapClassLoader
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                // 如果父加载器不能加载,子加载器去尝试加载
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    // 子加载器去尝试加载
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
}

ClassLoader.loadClass核心代码总结:

  1. 同一时间只允许一个线程加载名字为name的类。

  2. 在加载之前先检查,是否已经加载过该类。只有没有加载过的才允许加载

  3. 双亲委派:父加载器能加载的绝不给子类加载

  4. 父加载器加载不到类的情况,交给子加载器去加载(即:findClass)

虚拟机类加载器

从Java虚拟机角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),由C++语言实现,是虚拟机自身的一部分;另一种是所有其他的类加载器,由Java语言实现,独立于虚拟机外部,全部继承抽象类java.lang.ClassLoader。

从开发人员角度来看,类加载器大致分一下三种:

  1. 启动类加载器(Bootstrap ClassLoader):负责将存放在\lib目录中的,或被-Xbootclasspath参数指定的路径中的,并且是虚拟机识别的类库加载到虚拟机中。如,rt.jar。名字不符合即使放在目录中也不被加载。如果需要把加载请求委派给引导类加载器,直接使用null代替即可。

  2. 扩展类加载器(Extension ClassLoader):由sum.misc.Launcher$ExtClassLoader实现,负责加载<Java_Home>\lib\ext目录中的,或者被java.ext.dir系统变量所指定的路径中的所有类库。开发者可以直接使用扩展类加载器。

  3. 应用程序类加载器(Application ClassLoader):由sun.misc.Launcher$App-ClassLoader实现。是ClassLoader中的getSystemClassLoader()方法的返回值,所以也称为系统类加载器。负责加载用户路径(ClassPath)上所指定的类库,如果应用程序中没有自定义过自己的类加载器,这个就是默认的加载器,开发人员可以直接使用这个类加载器。

自定义类加载器

public class CLoaderTest {

   public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException,
         InstantiationException {
      // 自定义类加载器
      // 目的:打断默认的双亲委任机制
      ClassLoader loader = new ClassLoader() {
         @Override
         // 自定义类加载器需要覆写loadClass函数
         public Class<?> loadClass(String name) throws ClassNotFoundException {
            try {
               // 根据类名获取文件名
               String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
               // 把文件转换为流
               InputStream is = getClass().getResourceAsStream(fileName);
               // 如果流为空,则交给父类调用loadClass去加载
               if (is == null) {
                  return super.loadClass(name);
               }
               byte[] b = new byte[is.available()];
               // 把流转换为字节数组
               is.read(b);
               // 把字节码转化为Class并返回
               return defineClass(name, b, 0, b.length);
            } catch (IOException e) {
               throw new ClassNotFoundException();
            }
         }
      };

      String className = "ai.yunxi.cl.CLoaderTest";
      Object o1 = CLoaderTest.class.getClassLoader().loadClass(className).newInstance();
      // 默认AppClassLoader
      System.out.println(o1.getClass().getClassLoader());

      // 类加载器为自定义类加载器
      Object o2 = loader.loadClass(className).newInstance();
      System.out.println(o2.getClass().getClassLoader());
      System.out.println(o1 == o2);
      System.out.println(o1.equals(o2));
      System.out.println(o1 instanceof CLoaderTest);
      System.out.println(o2 instanceof CLoaderTest);
   }
}

 

自定义加载器有何用途:

  • 加密

Java代码很容易被反编译,如果你需要对你的代码进行加密以防止反编译,则可以将编译后的代码用加密算法加密。但加密后的类就不能再使用Java默认的类加载器进行加载,这时候就需要自定义类加载器。

  • 非标准的来源加载代码

字节码是放在数据库或者网络位置,需要自定义类加载器

3、双亲委派

什么是双亲委派

  • 如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,因此所有的类加载请求最终都会传送到顶端的启动类加载器;

  • 只有当父加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子加载器,子加载器会尝试去自己加载。

总结:父加载器能加载的绝不给子加载器加载,只有父加载器找不到所需的类才让子加载器尝试加载。

如何打断默认双亲委派机制

如果想打破双亲委派模型,只需要重写loadClass方法即可。如:

ClassLoader loader = new ClassLoader() {
    @Override
    // 自定义类加载器需要覆写loadClass函数
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
            // 根据类名获取文件名
            String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
            // 把文件转换为流
            InputStream is = getClass().getResourceAsStream(fileName);
            // 如果流为空,则交给父类调用loadClass去加载
            if (is == null) {
                return super.loadClass(name);
            }
            byte[] b = new byte[is.available()];
            // 把流转换为字节数组
            is.read(b);
            // 把字节码转化为Class并返回
            return defineClass(name, b, 0, b.length);
        } catch (IOException e) {
            throw new ClassNotFoundException();
        }
    }
};

为什么使用双亲委派

对任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。判断两个类是否"相等",必须是在这两个类被同一个类加载器加载的前提下。

基于双亲委派模型设计,那么Java中基础类,如Object类重复多次的问题就不会存在了,因为经过层层传递,加载请求最终都会被BootstrapClassLoader所响应。加载的Object类也会只有一个,否则如果用户自己编写了一个java.lang.Object类,并把它放到了ClassPath中,会出现很多个Object类,这样Java类型体系中最最基础的行为都无法保证,应用程序也将一片混乱。

三、垃圾回收:

1. 垃圾回收要点知识

引用计数:

给对象添加一个引用计数器。每当有一个地方引用这个对象,这个计数器就加1;每当引用失效,这个计数器就减1;当计数器为0的时候就代表该对象不能再被使用

public class ReferenceCountingGC{
    public Object instance = null;
    private static final int_1MB = 1024*1024;
    /**
    * 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否被回收过
    */
    private byte[] bigSize = new byte[2*_1MB];
        public static void testGC() {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
        // 假设在这行发生GC,objA和objB是否能被回收?
        System.gc();
    }
}

上面的例子,两个对象虽然再无任何引用,实际上这两个对象已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。

可达性分析

JVM中对内存进行回收时,需要判断对象是否仍在使用中,可以通过GC Roots Tracing辨别。

通过一系列名为”GCRoots”的对象作为起始点,从这个节点向下搜索,搜索走过的路径称为ReferenceChain,当一个对象到GCRoots没有任何ReferenceChain相连时,(图论:这个对象不可到达),则证明这个对象不可用。

在Java语言中,可作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。

  • 方法区中类静态属性引用的对象。

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

  • 本地方法栈中JNI(即一般说的Native方法)引用的对象

四种引用

引用分为强引用(Strong Reference)、 软引用(Soft Reference)、 弱引用(Weak Reference)、 虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。

2. 垃圾回收算法

标记清除

最基础的收集算法是“标记-清除”(Mark-Sweep)算法。该算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

该算法有两个主要不足:

  • 一个是效率问题,标记和清除的效率都不高;

  • 另一个是空间问题,标记清除之后会产生大量不连续的内存碎片。

标记整理

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

复制

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

缺点:内存缩小为了原来的一半,内存利用率太低

HotSpot算法

枚举根节点

从可达性分析中从GC Roots节点找引用链这个操作为例,可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中,现在很多应用仅仅方法区就有数百兆,如果要逐个检查这里面的引用,那么必然会消耗很多时间。

另外,可达性分析对执行时间的敏感还体现在GC停顿上,因为这项分析工作必须分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果准确性就无法得到保证。这点是导致GC进行时必须停顿所有Java执行线程(Sun将这个称为“Stop The World”)的其中一个重要原因,即使是在号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。

GC分类

保守式GC

JVM选择不记录任何类型的数据,那么它就无法区分内存里某个位置上的数据到底应该解读为引用类型还是整型还是别的什么。这种条件下,实现出来的GC就会是“保守式GC(conservative GC)”。在进行GC的时候,JVM开始从一些已知位置开始扫描内存,扫描的时候每看到一个数字就判断它“像不像是一个指向GC堆中的指针”。

半保守式GC

JVM可以选择在对象上记录类型信息。这样的话,扫描到GC堆内的对象时因为对象带有足够类型信息了,JVM就能够判断出在该对象内什么位置的数据是引用类型了。这种是“半保守式GC”,也称为“根上保守(conservative with respect to the roots)”。

为了支持半保守式GC,运行时需要在对象上带有足够的元数据。如果是JVM的话,这些数据可能在类加载器或者对象模型的模块里计算得到,但不需要JIT编译器的特别支持。

准确式GC

从外部记录下类型信息,存成映射表。现在三种主流的高性能JVM实现,HotSpot、JRockit和J9都是这样做的。其中,HotSpot把这样的数据结构叫做OopMap。

OopMap

在HotSpot中,对象的类型信息里有记录自己的OopMap,记录了在该类型的对象内什么偏移量上是什么类型的数据。所以从对象开始向外的扫描可以是准确的;这些数据是在类加载过程中计算得到的。每个被JIT编译过后的方法也会在一些特定的位置记录下OopMap。

安全点

在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举,但一个很现实的问题随之而来:可能导致引用关系变化,或者说OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会变得很高。

安全点太多,GC 过于频繁,增大运行时负荷;安全点太少,GC 等待时间太长。一般会在如下几个位置选择安全点:

  1. 循环的末尾

  2. 方法临返回前

  3. 调用方法之后

  4. 抛异常的位置

安全区域

假如线程处于Sleep或者Blocked状态,这时候线程无法响应JVM的中断请求,也就无法到达Safepoint的地方中断挂起。对于这种情况,就需要安全区域(Safe Region)来解决。

安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。 也可以把Safe Region看做是Safepoint的扩展。

 

 

3.垃圾回收机制

对象优先分配到Eden区

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

大对象直接进入老年代

所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组(比遇到大对象更坏的就是一群“朝生夕灭”的“短命大对象”。写程序的时候应当避免)。经常出现大对象容易导致内存还有不少空间时就提前触发GC以获取足够的连续空间来“安置”它们。

长期存活的对象会进入老年代

虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。

动态年龄判断

虚拟机并不是永远要求对象的年龄必须达到了MaxTenuringThreshold才能晋升到老年代。动态年龄判断基于如下两点:

  1. 如果在Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半

  2. 年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

注意:动态对象年龄判断和TargetSurvivorRatio有很大关系。该参数的含义是:根据年龄从小到大累加对象的字节大小,如果累加值超过Survivor区域大小*TargetSurvivorRatio(默认50%),则这个年龄之上的对象都要晋升到老年代。

4.关于GC

Minor GC

指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。

MinorGC用于清理新生代,MajorGC清理老年代。FullGC清理整个堆+方法区(Metaspace)

MinorGC触发条件:当新生代无法给新对象分配空间时。如:Eden区满了。

Major GC/Full GC

指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

MajorGC触发条件:老年代没有空间。

Full GC本身不会先进行Minor GC,可以配置Full GC之前先进行一次Minor GC,因为老年代很多对象都会引用到新生代的对象,先进行一次Minor GC可以提高老年代GC的速度。比如老年代使用CMS时,设置CMSScavengeBeforeRemark优化,可以让CMS remark之前先进行一次Minor GC。

Full GC 触发条件:

  1. System.gc

  2. 老年代空间不足

  3. 方法区空间不足

  4. 经过MinorGC后,运行的对象大小大于老年代的可用空间。如:Eden+From到To复制的时候,对象的大小大于To区域大小,当把对象转到老年代,这个时候老年代的空间小于对象大小。

4. JDK垃圾回收器

Serial

单线程的收集器,它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,最重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束

  • 开启参数:-XX:+UseSerialGC

  • 适用场景:用户的桌面应用场景

ParNew

Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、 收集算法、 Stop The World、 对象分配规则、 回收策略等都与Serial收集器完全一样

  • 开启参数:-XX:+UseParNewGC

  • 适用场景:Server首选的新生代收集器

Parallel Scavenge

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,它的目标是达到一个可控制的吞吐量(Throughput)

吞吐量计算公式:运行用户代码时间/(运行用户代码时间+垃圾收集时间)

垃圾收集停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Parallel Scavenge提供了两个参数用于精确控制吞吐量

  • -XX:MaxGCPauseMillis // 最大垃圾收集停顿时间(大于0毫秒数)

  • -XX:GCTimeRatio // 吞吐量大小(大于0且小于100的整数,吞吐量百分比)

  • -XX:+UseAdaptiveSizePolicy // 内存调优委托给虚拟机管理。当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、 晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量

  • 开启参数:-XX:+UseParallelGC

  • 适用场景:后台计算不需要太多交互

Serial Old

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。 这个收集器的主要意义也是在于给Client模式下的虚拟机使用

  • 适用场景:用户的桌面应用场景

 

Parallel Old

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器从JDK 1.6中才开始提供的。

  • 开启参数:-XX:+UseParallelOldGC

  • 适用场景:用户的桌面应用场景

Concurrent Mark Sweep

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记—清除”算法实现的。整个过程分为4个步骤

  1. 初始标记(CMS initial mark)

  2. 并发标记(CMS concurrent mark)

  3. 重新标记(CMS remark)

  4. 并发清除(CMS concurrent sweep)

  1. 初始标记 仅仅只是标记一下GC Roots能直接关联到的对象,速度很快

  2. 并发标记 该阶段就是进行GC RootsTracing的过程

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

  4. 并发清理 清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。

缺点

  1. CMS收集器对CPU资源非常敏感。

  2. CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。

  3. CMS是一款基于“标记—清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。

  • 开启参数:-XX:+UseConcMarkSweepGC

  • 适用场景:互联网站或者WEB服务端

G1

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

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

老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片问题的存在了。

G1提供了两种GC模式(两种都是Stop The World的)

  • Young GC

  • Mixed GC

YoungGC

阶段1:根扫描。静态和本地对象被扫描

阶段2:更新RS(Remembered Set)。找出老年代到年轻代的引用并更新RS

阶段3:处理RS。检测从年2轻代指向年老代的对象

阶段4:对象拷贝。拷贝存活的对象到survivor/old区域

阶段5:处理引用队列。软引用,弱引用,虚引用处理

Mix GC

  1. 初始标记 该阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值。

  2. 并发标记 该阶段是从GCRoot开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。

  3. 最终标记 该阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录。

  4. 筛选回收 该阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。因为只回收一部分Region,所以时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

  • 开启参数:-XX:+UseG1GC

  • 适用场景:服务端应用

Full GC

如果对象内存分配速度过快Mixed GC来不及回收,就会导致老年代被填满,从而触发一次Full GC。G1的Full GC就是使用单线程的SerialOld,这会导致长时间的暂停时间。

5.附录:JVM内存管理参数

参数名称含义默认值 
-Xms初始堆大小物理内存的1/64(<1GB)默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制.
-Xmx最大堆大小物理内存的1/4(<1GB)默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制
-Xmn年轻代大小(1.4or lator) 注意:此处的大小是(eden+ 2 survivor space).与jmap -heap中显示的New gen是不同的。
整个堆大小=年轻代大小 + 年老代大小 + 持久代大小.
增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8
-XX:NewSize设置年轻代大小(for 1.3/1.4)  
-XX:MaxNewSize年轻代最大值(for 1.3/1.4)  
-XX:PermSize设置持久代(perm gen)初始值物理内存的1/64 
-XX:MaxPermSize设置持久代最大值物理内存的1/4 
-Xss每个线程的堆栈大小 JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.更具应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右
一般小的应用, 如果栈不是很深, 应该是128k够用的 大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。(校长)
和threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:"”
-Xss is translated in a VM flag named ThreadStackSize”
一般设置这个值就可以了。
-XX:ThreadStackSizeThread Stack Size (0 means use default stack size) [Sparc: 512; Solaris x86: 320 (was 256 prior in 5.0 and earlier); Sparc 64 bit: 1024; Linux amd64: 1024 (was 0 in 5.0 and earlier); all others 0.]
-XX:NewRatio年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代) -XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。
-XX:SurvivorRatioEden区与Survivor区的大小比值 设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10
-XX:LargePageSizeInBytes内存页的大小不可设置过大, 会影响Perm的大小 =128m
-XX:+UseFastAccessorMethods原始类型的快速优化  
-XX:+DisableExplicitGC关闭System.gc() 这个参数需要严格的测试
-XX:MaxTenuringThreshold垃圾最大年龄 如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代. 对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率
该参数只有在串行GC时才有效.
-XX:+AggressiveOpts加快编译  
-XX:+UseBiasedLocking锁机制的性能改善  
-Xnoclassgc禁用垃圾回收  
-XX:SoftRefLRUPolicyMSPerMB每兆堆空闲空间中SoftReference的存活时间1ssoftly reachable objects will remain alive for some amount of time after the last time they were referenced. The default value is one second of lifetime per free megabyte in the heap
-XX:PretenureSizeThreshold对象超过多大是直接在旧生代分配0单位字节 新生代采用Parallel Scavenge GC时无效
另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象.
-XX:TLABWasteTargetPercentTLAB占eden区的百分比1% 
-XX:+CollectGen0FirstFullGC时是否先YGCfalse 

 

 

垃圾回收相关参数

 

DisableExplicitGC

 

默认关闭

 

忽略来自System.gc()方法触发的垃圾收集

 

ExplicitGCInvokesConcurrent

 

默认关闭

 

当收到System.gc()方法提交的来机收集申请时,使用CMS收集器进行收集

 

UseSerialGC

 

Client模式的虚拟机默认开启

其他模式关闭

 

虚拟机运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收

 

UseParNewGC

 

默认关闭

 

打开此开关后,使用ParNew + Serial Old的收集器组合进行内存回收

 

UseConcMarkSweepGC

 

默认关闭

 

打开此开关后,使用ParNew + CMS + Serial Old的收集器组合进行内存回收.如果CMS收集器出现Concurrent Mode Failure,则Serial Old收集器将作为后备收集器

 

UseParallelGC

 

Server模式的虚拟机默认开启

其他模式关闭

 

虚拟机运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old的收集器组合进行内存回收

 

UseParallelOldGC

 

默认关闭

 

打开此开关后,使用Parallel Scavenge + Parallel Old的收集器组合进行内存回收

 

SurvivorRatio

 

默认为8

 

新生代中Eden区域与Survivor区域的容量比

 

PretenureSizeThreshold

 

无默认值

 

直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配

 

MaxTenuringThreshold

 

默认值为15

 

晋升到老年代的对象年龄,每个对象在坚持过一次Minor GC之后,年龄就+1,当超过这个参数值时就进入老年代

 

UseAdaptiveSizePolicy

 

默认开启

 

动态调整java堆中各个区域的大小及进入老年代的年龄

 

HandlePromotionFailure

 

Jdk1.5及以前是默认关闭

Jdk1.6默认开启

 

是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活的极端情况

 

ParallelGCThreads

 

少于或等于8个CPU时默认值为CPU数量值,多于8个CPU时比CPU数量值小

 

设置并行GC时进行内存回收的线程数

 

GCTimeRatio

 

默认值99

 

GC时间占总时间的比率.仅在使用Parallel Scavenge收集器时生效

 

MaxGCPauseMills

 

无默认值

 

设置GC最大停顿时间.仅在使用Parallel Scavenge收集器时生效

 

CMSInitiatingOccupancyFraction

 

默认值68

 

设置CMS收集器在老年代空间被使用多少后触发垃圾收集

 

UseCMSCompactAtFullCollection

 

默认开启

 

设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理

 

CMSFullGCsBeforeCompaction

 

无默认值

 

设置CMS收集器在进行若干次垃圾收集后再启动一次内存碎片整理

 

ScavengeBeforeFullGC

 

默认开启

 

在Full GC发生之前触发一次Minor GC

 

UseGCOverheadLimit

 

默认开启

 

禁止GC过程无限制的执行,如果过于频繁,就直接发生OutOfMemory

 

UseTLAB

 

Server模式默认开启

 

优先在本地线程缓冲区中分配对象,避免分配内存时的锁定过程

 

MaxHeapFreeRatio

 

默认值70

 

当Xmx值比Xms值大时,堆可以动态收缩和扩展,这个参数控制当堆空闲大于指定比率时自动收缩

 

MinHeapFreeRatio

 

默认值40

 

当Xmx值比Xms值大时,堆可以动态收缩和扩展,这个参数控制当堆空闲小于指定比率时自动收缩

 

MaxPermSize

 

大部分情况下默认值是64MB

 

永久代的最大值

 

UnlockExperimentalVMOptions

 

默认不开启(JDK7/8)

 

是否启用实验性功能,必须显式开启

 

G1HeapRegionSize

 

--

 

Region的大小。若未指定则默认则最多生成2048块,每块的大小需要为2的幂次方。Region块最小1M,最大32M

 

G1NewSizePercent

 

默认值5

 

年轻代最小占用堆空间的百分比。

 

G1MaxNewSizePercent

 

默认值60

 

年轻代最大占用堆空间的百分比。

 

G1MixedGCLiveThresholdPercent

 

默认值85

 

当Region中的存活对象占比不超过该阈值时,则表示要被回收

 

InitiatingHeapOccupancyPercent

 

默认值45

 

老年代占用整堆内存的比率。默认占用率是整个Java堆的 45%

 

G1HeapWastePercent

 

默认值5

 

允许整个堆内存中空间被浪费的比例。可回收的垃圾占堆内存的比例低于某个比例,则不再进行混合回收

 

G1MixedGCCountTarget

 

默认值8

 

对要执行垃圾回收的候选Old区域收集完毕需要执行Mixed GC的目标次数

 

即时编译参数

 

CompileThreshold

 

Client模式下默认值1500

Server模式下默认值10000

 

触发即时编译的阈值

 

OnStackReplacePercentage

 

Client模式下默认值933

Server模式下140

 

OSR比率,它是OSR即时编译阈值计算公司的一个参数,用于代替BackEdgeThreshold参数控制回边计数器的实际溢出阈值

 

ReservedCodeCacheSize

 

大部分情况下默认值32MB

 

即时编译器编译的代码缓存使得最大值

 

类型加载参数

 

UseSplitVerifier

 

默认开启

 

使用依赖StackMapTable信息的类型检查代替数据流分析,以加快字节码校验速度

 

FailOverToOldVerifier

 

默认开启

 

当类型校验失败时,是否允许回到老的类型推到校验方式进行校验,如果开启则允许

 

RelaxAccessControlCheck

 

默认开启

 

在校验阶段放松对类型访问性的限制

 

多线程相关参数

 

UseSpinning

 

Jdk1.6默认开启

Jdk1.5默认关闭

 

开启自旋锁以免线程频繁的挂起和唤醒

 

PreBolckSpin

 

默认值10

 

使用自旋锁时默认的自旋次数

 

UseThreadPriorities

 

默认开启

 

使用本地线程优先级

 

UseBiasedLocking

 

默认开启

 

是否使用偏向锁,如果开启则使用

 

UseFastAccessorMethods

 

默认开启

 

当频繁反射执行某个方法时,生成字节码来加快反射的执行速度

 

性能参数

 

AggressiveOpts

 

Jdk1.6默认开启

Jdk1.5默认关闭

 

使用激进的优化特征,这些特征一般是具备正面和负面双重影响的,需要根据具体应用特点分析才能判定是否对性能有好处

 

UseLargePage

 

默认开启

 

如果可能,使用大内存分页,这项特性需要操作系统的支持

 

LargePageSizeInBytes

 

默认值4MB

 

使用指定大小的内存分页,这项特性需要操作系统的支持

 

StringCache

 

默认开启

 

是否使用字符串缓存,开启则使用

 

调试参数

 

HeapDumpOnOutOfMemoryError

 

默认关闭

 

在发生内存溢出异常时是否生成堆转储快照,关闭则不生成

 

OnOutOfMemoryError

 

无默认值

 

当虚拟机抛出内存溢出异常时,执行指令的命令

 

OnError

 

无默认值

 

当虚拟机抛出ERROR异常时,执行指令的命令

 

PrintClassHistogram

 

默认关闭

 

使用[ctrl]-[break]快捷键输出类统计状态,相当于jmap-histo的功能

 

PrintConcurrentLocks

 

默认关闭

 

打印J.U.C中的状态

 

PrintCommandLineFlags

 

默认关闭

 

打印启动虚拟机时输入的非稳定参数

 

PrintFlagsFinal

 

----

 

显示所有可设置的参数及它们的值(***从JDK 6 update 21开始才可以用)

 

PrintFlagsInitial

 

----

 

显示在处理参数之前所有可设置的参数及它们的值,然后直接退出程序

 

PrintCompilation

 

默认关闭

 

打印方法即时编译信息

 

PrintGC

 

默认关闭

 

打印GC信息

 

PrintGCDetails

 

默认关闭

 

打印GC的详细信息

 

PrintGCTimeStamps

 

默认关闭

 

打印GC停顿耗时

 

PrintTenuringDistribution

 

默认关闭

 

打印GC后新生代各个年龄对象的大小

 

TraceClassLoading

 

默认关闭

 

打印类加载信息

 

TraceClassUnloading

 

默认关闭

 

打印类卸载信息

 

PrintInlining

 

默认关闭

 

打印方法内联信息

 

PrintCFGToFile

 

默认关闭

 

将CFG图信息输出到文件,只有DEBUG版虚拟机才支持此参数

 

PrintIdealGraphFile

 

默认关闭

 

将Ideal图信息输出到文件,只有DEBUG版虚拟机才支持此参数

 

UnlockDiagnosticVMOptions

 

默认关闭

 

让虚拟机进入诊断模式,一些参数(如PrintAssembly)需要在诊断模式中才能使用

 

PrintAssembly

 

默认关闭

 

打印即时编译后的二进制信息

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值