Java虚拟机

1. JVM 的主要组成部分及其作用?

JVM是Java Virtual Machine(Java虚拟机),在我们运行本地编写的Java程序后,编译器将Java文件编译成Java .class文件,然后将.class文件输入JVM, JVM加载并执行类文件。下面是JVM的架构图,

在这里插入图片描述

在这里插入图片描述

JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。

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

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

JNI:Java Native Interface

一般情况下,我们完全可以使用 Java 语言编写程序,但某些情况下,Java 可能会不满足应用程序的需求,或者是不能更好的满足需求,比如:

  • 标准的 Java 类库不支持应用程序平台所需的平台相关功能。
  • 我们已经用另一种语言编写了一个类库,如何用Java代码调用?
  • 某些运行次数特别多的方法代码,为了加快性能,我们需要用更接近硬件的语言(比如汇编)编写

为了使Java 代码能够调用不同语言编写的代码,就产生了JNI

从Java 1.1开始,Java Native Interface (JNI) 标准就成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是为 C 和 C++ 而设计 的,但是它并不妨碍你使用其他语言,只要调用约定受支持就可以了。使用 Java 与本地已编译的代码交互,通常会丧失平台可移植性。

native 用来修饰方法,用 native 声明的方法表示告知 JVM 调用,该方法在外部定义,我们可以用任何语言去实现它。 简单地讲,一个 Native Method 就是一个 Java 调用非 Java 代码的接口。

比如在 Thread 类中的 start() 方法中,最终调用了start0()来启动线程,该方法使用了 native关键字进行修饰,表明是一个非 Java 本地方法。

使用该关键字的方法,首先会进入本地方法栈,调用本地方法接口-JNI,最终通过JNI调用本地方法库进行执行。

public class Thread implements Runnable {
	public synchronized void start() {
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        group.add(this);
        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
            }
        }
    }
    //本地方法
    private native void start0();
}

2. 虚拟机类加载器

Java虚拟机的类加载器: 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。

2.1 类加载的时机

Java类从被加载到虚拟机内存开始,到卸载出内存位置,他的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initializtion)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备和解析统称为连接(Linking),如下图所示:

在这里插入图片描述

那么类加载过程的第一个阶段-加载在什么情况下开始呢? Java虚拟机并没有进行强制的规定,但是虚拟机规定了有且只有5种情况下必须立即对类进行初始化,当然加载、验证、准备过程在此之前开始。

  • 1) 遇到 new、 getstatic、 putstatic 或 invokestatic 这4条字节码指令是,会先触发类的初始化。生成这4条指令的最常见的Java代码场景是:

    • 使用 new 关键字实例化对象
    • 读取或设置一个类的静态字段(被final修饰已在编译器把结果放入常量池的静态字段除外)
    • 调用一个类的静态方法时
    • *注:对于静态字段,只有直接定义这个字段的类才会初始化,而其他类引用这个类中的静态字段,只会触发被应用类的初始化
  • 2)使用 java.lang.reflect 包的方法对类进行反射调用的时候,若类没有进行初始化,会先触发其初始化

  • 3)当初始化一个类时,若其父类还没有进行初始化,则先触发其父类的初始化

  • 4) 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类

2.2 类加载的过程

加载阶段:

​ 1) 通过一个类的全限定名来获取定义此类的二进制字节流。

​ 2) 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

​ 3) 在Java堆中生成一个代表这个类的java.lang.class对象,作为方法区这些数据的访问入口。

验证阶段:

验证时连结阶段的第一步,这一阶段目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机自身的安全。

​ 1) 文件格式验证(是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理)

​ 2) 元数据验证(对字节码描述的信息进行语意分析,以保证其描述的信息符合Java语言规范要求)

​ 3) 字节码验证(保证被校验类的方法在运行时不会做出危害虚拟机安全的行为)

​ 4) 符号引用验证(虚拟机将符号引用转化为直接引用时,解析阶段中发生)

准备阶段:

​ 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。将对象初始化为“零”值

解析阶段:

​ 解析阶段时虚拟机将常量池内的符号引用替换为直接引用的过程。

初始化阶段:

​ 初始化阶段时加载过程的最后一步,而这一阶段也是真正意义上开始执行类中定义的Java程序代码。

2.1 JVM加载Class文件的原理机制

Java中的所有类都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。

类装载方式,有两种

  • 隐式装载:程序在运行过程中当碰到通过 new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,
  • 显式装载:通过 class.forname() 等方法,显式加载需要的类

Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载,以节省内存开销。

2.2 什么是类加载器,类加载器有哪些?

类加载器是实现通过类的权限定名获取该类的二进制字节流的代码块

主要有一下四种类加载器:

  • 启动类加载器 (Bootstrap ClassLoader) : 用来加载java核心类库,即<JAVA_HOME>\lib 目录下的类库加载到虚拟机内存中。任何类的加载都要经过它的访问,该加载器无法被java程序直接引用。
  • 扩展类加载器 (Extensions ClassLoader) : 它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在 <JAVA_HOME>lib/ext 目录下或者被 java.ext,dirs 系统变量所指定的路径中查找并加载 Java 类, 开发者可以直接使用该加载器。
  • 系统类加载器(System ClassLoader/ APP ClassLoader) :它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
  • 用户自定义类加载器 (Custom ClassLoader) : 通过继承 java.lang.ClassLoader类的方式实现。

1) 双亲委派模型

在这里插入图片描述
每⼀个类都有⼀个对应的类加载器。系统中的 ClassLoder 在协同⼯作的时候会默认使⽤双亲委派模型

  • 双亲委派模型

    如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。

  • 使用双亲委派模型的好处:

    此机制使得Java类随着它的类加载器一起具备了一种带有优先级的层次关系。它保证JDK核心类的优先加载;使得Java程序的稳定运⾏,可以避免类的重复加载,也保证了 Java 的核⼼ API 不被篡改。如果不⽤没有使⽤双亲委派模型,⽽是每个类加载器加载⾃⼰的话就会出现⼀些问题,⽐如我们编写⼀个称为 java.lang.Object 类的话,那么程序运⾏的时候,系统就会出现多个不同的Object 类。

  • 如何破坏双亲委派机制:

    可以⾃⼰定义⼀个类加载器,重写loadClass方法;

2. JVM运行时内存划分?

JVM运行时数据区域:堆、方法区(元空间)、虚拟机栈、本地方法栈、程序计数器
1.PNG

2.1 Heap(堆):

Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,

对象的实例以及数组的内存都是要在堆上进行分配的,堆是线程共享的一块区域,用来存放对象实例,也是垃圾回收(GC)的主要区域

2.1.1 堆结构

​堆细分:新生代、老年代,对于新生代又分为:Eden区和From Survivor和To Survivor区;

1.PNG

新生代:用来存放新生的对象,一般占据1/3的空间由于频繁创建对象,所以新生代会频繁出发MinorGC进行垃圾回收

  • Eden区:java新对象的出生地(如果新创建的对象占用内存很大,直接分配到老年区)。当Eden内存不够的时候,就会触发MinorGC对新生代进行一次垃圾回收
  • To Servivor:保留了一次MinorGC过程中的幸存者
  • From Servivor:上一次GC的幸存者,作为这一次GC的被扫描者

永久区
永久区一直存在与内存中,用来存储JDK本身的Class对象,Interface元数据,存储的时Java运行时的环境或类信息,这个区域不进行垃圾回收,关闭JVM才会释放该区域的内存。

当项目中一个启动类加载了过多的第三方jar包,Tomcat部署了太多的应用,或者程序中大量动态生成的放射类,使得永久区不断被占用就会出现

永久区的变化:

  • jdk1.6 之前: 存在永久代,常量池在方法区
  • jdk1.7: 存在永久代,在慢慢退化 ( 去永久代 ), 常量池在堆中
  • jdk1.8:无永久代,被元空间替换,常量池在元空间

当JVM无法为新对象分配内存空间时(Eden满了),Minor GC被触发,因此新生代空间占有率越高,Minor GC越频繁

2.2 方法区:

​对于JVM的方法区也可以称之为永久区,它储存的是已经被java虚拟机加载的类信息、常量(final)、静态变量(static) 以及常量池;Jdk1.8以后取消了方法区这个概念,称之为元空间(MetaSpace);

  • 运行时常量池

    运行时常量池是方法区的一部分,用于存放编译器生成的各种字面量和符号引用

2.3 虚拟机栈:

​虚拟机栈是线程私有的,他的生命周期和线程的生命周期是一致的。里面装的是一个一个的栈帧,每一个方法在执行的时候都会创建一个栈帧,栈帧中用来存放局部变量表、操作数栈 、动态链接 、返回方法地址,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程

1.PNG

  • 局部变量表:局部变量表是一组变量值存储空间,用来存放方法参数、对象引用以及方法内部定义的局部变量。局部变量表的容量是以变量槽(variable slot)为最小的单位。Java虚拟机没有明确规定一个slot所占的空间大小。只是导向性的说了每一个slot能存放8中基本数据类型中的一种(long 和double这种64位的需要两个slot);

  • 操作数栈是用来记录一个方法在执行的过程中,字节码指令向操作数栈中进行入栈和出栈的过程。大小在编译的时候已经确定了,当一个方法刚开始执行的时候,操作数栈中是空发的,在方法执行的过程中会有各种字节码指令往操作数栈中入栈和出栈。

  • 动态链接:因为字节码文件中有很多符号的引用,这些符号引用一部分会在类加载的解析阶段或第一次使用的时候转化成直接引用,这种称为静态解析;另一部分会在运行期间转化为直接引用,称为动态链接。

  • 返回地址(returnAddress):类型(指向了一条字节码指令的地址)

该区域可能抛出以下异常:

  • 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
  • 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。

2.4 本地方法栈:

​本地方法栈和虚拟机栈类似,不同的是虚拟机栈服务的是Java方法,而本地方法栈服务的是Native方法。在HotSpot虚拟机实现中是把本地方法栈和虚拟机栈合二为一的,同理它也会抛出StackOverflowError和OOM异常。

本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。

2.PNG

2.5 PC程序计数器:

PC指的是存放下一条指令的位置的这么一个区域,可以看作是当前线程所执行的字节码的行号指示器,字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要以来这个计数器来完成。

它是一块较小的内存空间,且是线程私有的。由于线程的切换,CPU在执行的过程中,一个线程执行完了,接下来CPU切换到另一个线程去执行,另外一个线程执行完再切回到之前的线程,这时需要记住原线程的下一条指令的位置,所以每一个线程都需要有自己的PC。

如果线程正在执行一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址,如果执行的是Native方法,这个计数器的值为空。并且此区域是唯一没有规定任何OutofMemoryError的区域。

2.6 元空间

元空间: 本质与永久代类似,都是对JVM规范中方法区的实现,不过元空间与永久代之间最大的区别在于:元空间不在虚拟机中,而是使用本地内存,因此,默认情况下元空间的大小仅受本地内存限制,类的元数据放入native memory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而是由系统的实际可用空间来控制, 可以通过以下参数来指定元空间的大小:

-XX:MetaspaceSize:始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize:最大空间,默认是没有限制的。 除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio:在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio:在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

去除永久代,设置元空间的原因:

  • 字符串存在永久代中,现实使用中易出问题, 由于永久代内存经常不够用或发生内存泄露,爆出异常 java.lang.OutOfMemoryError: PermGen
  • 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出
  • 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低
  • 移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代

2.7 字符串在 JVM 中如何存放?

字符串对象在JVM中可能有两个存放的位置:字符串常量池堆内存

  • 使用常量字符串初始化的字符串对象,它的值存放在字符串常量池中;
    字符串常量池存储位置随着jdk版本的变化也发生了改变
    • jdk 6.0 字符串常量池在方法区,方法区的具体体现可以看做是堆中的永久区。
    • jdk 7.0 java 虚拟机规范中不再声明方法区,字符串常量池存放在堆空间中
    • jdk 8.0 java 虚拟机规范中又声明了元空间,字符串常量池存放在元空间中
  • 使用字符串构造方法创建的字符串对象,它的值存放在堆内存中;

String提供了一个API, java.lang.String.intern(),这个API可以手动将一个字符串对象的值转移到字符串常量池中

在1.7之前,字符串常量池是在PermGen区域(方法区),这个区域的大小是固定的,不能在运行时根据需要扩大,也不能被垃圾收集器回收,因此如果程序中有太多的字符串调用了intern方法的话,就可能造成OOM。

在1.7以后,字符串常量池移到了堆内存中,并且可以被垃圾收集器回收,这个改动降低了字符串常量池OOM的风险。

在这里插入图片描述

intern() 是一个native修饰的方法,如果常量池中已经存在这个字符串,直接返回,如果不存在就把字符串存入常量池再返回

 /**
     * Returns a canonical representation for the string object.
     * <p>
     * A pool of strings, initially empty, is maintained privately by the
     * class {@code String}.
     * <p>
     * // 如果常量池中已经存在这个字符串,直接返回,如果不存在就把字符串存入常量池再返回
     * When the intern method is invoked, if the pool already contains a
     * string equal to this {@code String} object as determined by
     * the {@link #equals(Object)} method, then the string from the pool is
     * returned. Otherwise, this {@code String} object is added to the
     * pool and a reference to this {@code String} object is returned.
     * <p>
     * It follows that for any two strings {@code s} and {@code t},
     * {@code s.intern() == t.intern()} is {@code true}
     * if and only if {@code s.equals(t)} is {@code true}.
     * <p>
     * All literal strings and string-valued constant expressions are
     * interned. String literals are defined in section 3.10.5 of the
     * <cite>The Java&trade; Language Specification</cite>.
     *
     * @return  a string that has the same contents as this string, but is
     *          guaranteed to be from a pool of unique strings.
     */
    public native String intern();

3.深拷贝和浅拷贝

  • 浅拷贝(shallowCopy) 只是增加了一个指针指向已存在的内存地址,
  • 深拷贝(deepCopy) 是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,

使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。

  • 浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。
  • 深复制:在计算机中开辟一块新的内存地址用于存放复制的对象。

4. 说一下堆栈的区别?

1)物理地址

  • 堆的物理地址分配对对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记——压缩)
  • 栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。

2)内存分配

  • 堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。
  • 栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的

3)存放的内容

  • 堆存放的是对象的实例和数组。因此该区更关注的是数据的存储
  • 栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。

PS:静态变量放在方法区,静态的对象还是放在堆。

4)程序的可见度

  • 堆对于整个应用程序都是共享、可见的
  • 栈只对于线程是可见的。所以也是线程私有,他的生命周期和线程相同。

5. 队列和栈是什么?有什么区别?

队列和栈都是被用来预存储数据的。

  • 操作的名称不同。队列的插入称为入队,队列的删除称为出队。栈的插入称为进栈,栈的删除称为出栈。
  • 可操作的方式不同。队列是在队尾入队,队头出队,即两边都可操作。而栈的进栈和出栈都是在栈顶进行的,无法对栈底直接进行操作。
  • 操作的方法不同。队列是先进先出(FIFO),即队列的修改是依先进先出的原则进行的。新来的成员总是加入队尾(不能从中间插入),每次离开的成员总是队列头上(不允许中途离队)。而栈为后进先出(LIFO),即每次删除(出栈)的总是当前栈中最新的元素,即最后插入(进栈)的元素,而最先插入的被放在栈的底部,要到最后才能删除。

6. 内存溢出

6.1 堆溢出

Java堆用于储存对象实例。当需要为对象实例分配内存,而堆的内存占用又已经达到-Xmx设置的最大值。将会抛出OutOfMemoryError异常。例子如下:

1.JPG

解决方法:

如果不存在内存泄漏,检查虚拟机的堆参数(-Xmx 和 -Xms),与及其物理内存对比看是否可以调大。并期望从代码上检查是都存在某些对象生命周期过长、持有状态时间过长的情况,减少程序运行期的内存消耗。

若要检查定位出错的代码行,需要使用内存快照分析工具 - JProfiler

  • 分析Dump内存文件,快速定位i内存泄漏
  • 获取堆中的数据
  • 获取大的对象

参数:

  • Xmx Java Heap最大值,默认值为物理内存的1/4,最佳设值应该视物理内存大小及计算机内其他内存开销而定;

    用来设置你的应用程序能够使用的最大内存数(看好,致使你的应用程序,不是整个jvm),如果你的程序要花很大内存的话,那就需要修改缺省的设置,

  • Xms Java Heap初始值,Server端JVM最好将-Xms和-Xmx设为相同值,开发测试机JVM可以保留默认值;

    用它来设置程序初始化的时候内存栈的大小,增加这个值的话你的程序的启动性能会得到提高。不过同样有前面的限制,以及受到xmx的限制。

  • Xmn Java Heap Young区大小,不熟悉最好保留默认值;

  • Xss 每个线程的Stack大小,不熟悉最好保留默认值;

  • XX:PermSize 永久区的大小。

  • XX:+UseParNewGC 使用并行收集算法。

6.2 虚拟机栈和本地方法栈溢出

函数的调用过程都体现在入栈和出栈上,调用构造函数的 “层”太多了,以致于把栈区溢出了。 通常来讲,一般栈区远远小于堆区的,因为函数调用过程往往不会多于上千层,而即便每个函数调用需要 1K的空间,那么栈区也不过是需要1MB的空间。通常栈的大小是1-2MB的。常递归也不要递归的层次过多,很容易溢出。

对于栈,Java虚拟机规范中描述了两种异常:

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常;
  • 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError。

解决方法:

  • 修改程序
  • 通过 -Xss 来设置每个线程的栈的大小

6.3 方法区和运行时常量池溢出

6.4 JVM的几个参数

  • -Xms:java Heap初始大小, 默认是物理内存的1/64。
  • -Xmx:java Heap最大值,不可超过物理内存。
  • -Xmn:young generation的heap大小,一般设置为Xmx的3、4分之一 。增大年轻代后,将会减小年老代大小,可以根据监控合理设置。
  • -Xss:每个线程的Stack大小,而最佳值应该是128K,默认值好像是512k。
  • -XX:PermSize:设定内存的永久代初始大小,缺省值为64M。
  • -XX:MaxPermSize:设定内存的永久代最大大小,缺省值为64M。
  • -XX:SurvivorRatio:Eden区与Survivor区的大小比值,设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10。
  • -XX:+UseParallelGC:F年轻代使用并发收集,而年老代仍旧使用串行收集。
  • -XX:+UseParNewGC:设置新生代为并行收集,JDK5.0以上,JVM会根据系统配置自行设置,所无需再设置此值。
  • -XX:ParallelGCThreads:并行收集器的线程数,值最好配置与处理器数目相等 同样适用于CMS。
  • -XX:+UseParallelOldGC:老年代垃圾收集方式为并行收集(Parallel Compacting)。
  • -XX:MaxGCPauseMillis:每次新生代垃圾回收的最长时间(最大暂停时间),如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。
  • -XX:+ScavengeBeforeFullGC:Full GC前调用YGC,默认是true。

6.5

-Xms1024m -Xmx1024m -XX:+PrintGCDetails 设置堆的大小,并打印GC垃圾回收的详情
-Xms20m -Xmx20m -XX:HeapDumpOnOutOfMemoryError 设置堆的大小,并Dump堆信息文件进行分析

7. 垃圾收集

垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收

而Java堆和方法区内存的分配和回收是动态,因为只有在程序处于运行期间时才能知道会创建哪些对象,编译器并不确定。

7.1 简述Java垃圾回收机制

在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收

7.2 GC是什么?为什么要GC?

GC 是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存

回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动

回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法

7.3 垃圾回收的优点和原理。并考虑2种回收机制

java语言最显著的特点就是引入了垃圾回收机制,它使java程序员在编写程序时不再考虑内存管理的问题。

由于有这个垃圾回收机制,java中的对象不再有“作用域”的概念,只有引用的对象才有“作用域”。

垃圾回收机制有效的防止了内存泄露,可以有效的使用可使用的内存

垃圾回收器通常作为一个单独的低级别的线程运行,在不可预知的情况下对内存堆中已经死亡的或很长时间没有用过的对象进行清除和回收。

程序员不能实时的对某个对象或所有对象调用垃圾回收器进行垃圾回收。

垃圾回收有分代复制垃圾回收、标记垃圾回收、增量垃圾回收

7.4 垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?

对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。

通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。

可以。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。

7.5 Java 中都有哪些引用类型?

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。

Java 提供了四种强度不同的引用类型。

1) 强引用

被强引用关联的对象不会被回收。

使用 new 一个新对象的方式来创建强引用。

Object obj = new Object();
2) 软引用

被软引用关联的对象只有在内存不够的情况下才会被回收。

使用 SoftReference 类来创建软引用。

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;  // 使对象只被软引用关联
3) 弱引用

被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。

使用 WeakReference 类来创建弱引用。

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
4) 虚引用(幽灵引用/幻影引用):

又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。

为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。

使用 PhantomReference 来创建虚引用。

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;

7.6 *怎么判断对象是否可以被回收?

垃圾收集器在进行垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是「存活」的,是不可以被回收的;哪些对象已经「死掉」了,需要被回收。

一般有两种方法来判断:

1) 引用计数器法

为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。

两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。

public class Test {	
    public Object instance = null;
    public static void main(String[] args) {
        Test a = new Test();
        Test b = new Test();
        a.instance = b;
        b.instance = a;
        a = null;
        b = null;
        doSomething();
    }
}

在上述代码中,a 与 b 引用的对象实例互相持有了对象的引用,因此当我们把对 a 对象与 b 对象的引用去除之后,由于两个对象还存在互相之间的引用,导致两个 Test 对象无法被回收。

2)可达性分析算法

算法的基本思想时通过一系列的称为“GC Roots”的对象作为起始点,从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。

Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般包含以下内容:

  • 虚拟机栈中局部变量表中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中的常量引用的对象
  • 本地方法栈中 JNI(即一般说的Native方法)中引用的对象

1.PNG

3)方法区的回收

因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高。

主要是对常量池的回收和对类的卸载

为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能。

类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载:

  • 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
4)finalize()

类似 C++ 的析构函数,用于关闭外部资源。 但是 try-finally 等方式可以做得更好,并且该方法运行代价很高,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。

当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会再调用该方法。

7.8 *垃圾回收算法

1)复制算法:

​将内存分为大小相同的两块,每次使用其中的一块。当一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收;

  • 优点:实现简单,内存效率高,不易产生碎片
  • 缺点:内存压缩了一半,倘若存活对象多,Copying 算法的效率会大大降低

现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。

HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 就不够用了,此时需要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。

1.PNG

2)标记清除:

标记-清除算法(Mark-Sweep)是一种常见的基础垃圾收集算法,它将垃圾收集分为两个阶段:

  • 标记阶段:标记出可以回收的对象。
  • 清除阶段:回收被标记的对象所占用的空间。

​缺点:标记和清除的两个过程效率低,标记清除后会产生大量不连续的碎片,可能发生大对象不能找到可利用空间的问题,不得不提前触发另一次垃圾回收动作。

3)标记整理:

​标记过程仍然与“标记-清除”算法一样,再让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存;解决了产生大量不连续碎片问题

优点:不会产生内存碎片; 不足:需要移动大量对象,处理效率比较低。

4)分代收集:

​根据各个年代的特点选择合适的垃圾收集算法。

新生代采用复制算法,新生代每次垃圾回收都要回收大部分对象,存活对象较少,即要复制的操作比较少,一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中。

​老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法对老年代进行垃圾收集

7.9 *垃圾收集器

垃圾收集器:Serial、Parnew、parallel Scavenge、Serialold 、Parnewold、CMS、G1

1.jpg

以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。

单线程与多线程:单线程指的是垃圾收集器只使用一个线程,而多线程使用多个线程;
串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。

1) Serial 收集器

1.jpg

Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束

它的优点是简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率。

它是 Client 场景下的默认新生代收集器,因为在该场景下内存一般来说不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿时间是可以接受的。

2) ParNew 收集器

1.jpg

ParNew 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和 Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。

ParNew 收集器在单 CPU 的环境下不会有比 Serial 收集器更好的效果,因为存在线程交互的开销。但随着 CPU 数量的增加,它对于 GC 时系统资源的有效利用还是有好处的。

可以使用参数:-XX:UseParNewGC使用该收集器,使用 -XX:ParallelGCThreads可以限制线程数量。

它是 Server 场景下默认的新生代收集器,除了性能原因外,主要是因为在 JDK1.5 中除了 Serial 收集器,只有它能与 CMS 收集器配合使用。

  • 并行(Parallel):指多条垃圾收集器线程并行工作,但此时用户线程仍然处于等待状态
  • 并发(Concurrent):之用户线程和垃圾收集器线程同时执行,用户线程在继续运行,而垃圾收集器程序运行于另一个CPU上。
3) Parallel Scavenge 收集器

与 ParNew 一样是多线程收集器。

其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,因此它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值

可以通过-XX:MaxGCPauseMillis参数控制最大垃圾收集停顿时间;通过-XX:GCTimeRatio参数直接设置吞吐量大小;通过-XX:+UseAdaptiveSizePolicy参数可以打开GC自适应调节策略,该参数打开之后虚拟机会根据系统的运行情况收集性能监控信息,动态调整虚拟机参数以提供最合适的停顿时间或者最大的吞吐量。自适应调节策略是Parallel Scavenge收集器和ParNew的主要区别之一

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

4) Serial Old 收集器

1.jpg

是 Serial 收集器的老年代版本,也是给 Client 场景下的虚拟机使用。如果用在 Server 场景下,它有两大用途:

  • 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
  • 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
5) Parallel Old 收集器

1.jpg

是 Parallel Scavenge 收集器的老年代版本。

在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。

6) CMS 收集器

1.jpg

​CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记-清除算法。 CMS收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法,通常和ParNew一起使用。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段:

  • 初始标记:只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
  • 并发标记:进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
  • 重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
  • 并发清除:清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。

优点: 并发收集、低停顿

具有以下缺点:

  • **CMS收集器对CPU资源非常敏感、总吞吐量低:**低停顿时间是以牺牲吞吐量为代价的,垃圾回收线程占用了一部分CPU资源,导致 CPU 利用率不够高。
  • CMS无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
  • 标记-清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
7) G1 收集器

G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。

堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。

1.PNG

G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。

2.PNG

通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。

通过使用Region划分内存空间以及有优先级的区域回收方式,保证G1收集器在有限的时间内可以获取尽可能高的收集效率。

但是很难真的以 Region 为单位进行垃圾收集,因为 Region之间的对象见会发生引用关系,那么在做可达性判断对象是否存活的时候,还得扫描整个Java堆才能保证准确性。

为了避免上述问题,虚拟机都是使用 Remembered Set来避免全堆的扫描。 G1 中每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同Region之中,如果是,便通过CardTable包相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC roots的枚举范围中加入 Remembered Set即可保证不对全堆扫描也不会有遗漏, 在做可达性分析的时候就可以避免全堆扫描。

1.jpg

G1收集器的特点:

  • **并行与并发:**G1能充分利用多CPU,多核环境下的硬件优势,来缩短Stop the World,是并发的收集器。
  • **分代收集:**G1不需要其他收集器就能独立管理整个GC堆,能够采用不同的方式去处理新建对象、存活一段时间的对象和熬过多次GC的对象。
  • **空间整合:**G1从整体来看是基于标记-整理算法,从局部(两个Region)上看基于复制算法实现,G1运作期间不会产生内存空间碎片。
  • **可预测的停顿:**能够建立可以预测的停顿时间模型,预测停顿时间。

如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:

  • 初始标记:标记与GC Roots能直接关联到的对象;
  • 并发标记:从GC Root 开始对堆中对象进行可达性分析,找出存活的对象,可以与用户程序并发执行;
  • 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
  • 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。

相比与 CMS 收集器,G1 收集器两个最突出的改进是:

​ 【1】基于标记-整理算法,不产生内存碎片

​ 【2】可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收

​ G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。

垃圾收集器参数总结

1.JPG
2.JPG

7.10 新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么区别?

  • 新生代回收器:Serial、ParNew、Parallel Scavenge
  • 老年代回收器:Serial Old、Parallel Old、CMS
  • 整堆回收器:G1

新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。

7.11 简述分代垃圾回收器是怎么工作的?

分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。

新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:

  • 把 Eden + From Survivor 存活的对象放入 To Survivor 区;
  • 清空 Eden 和 From Survivor 分区;
  • From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。

每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。

老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。

7.12 Full GC触发条件

​每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小,则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC

8. 内存分配与回收策略

8.1 Minor GC 和 Full GC

  • Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
  • Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。

8.2 内存分配策略

1) 对象优先在 Eden 分配

大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。

2) 大对象直接进入老年代

大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。

经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。

-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。

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

为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。

-XX:MaxTenuringThreshold 用来定义年龄的阈值。

4) 动态对象年龄判定

虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。

5) 空间分配担保

在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。

如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。

8.3 Full GC 的触发条件

对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:

1) 调用 System.gc()

只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存

2) 老年代空间不足

老年代空间不足的常见场景为前文所讲的大对象直接进入老年代长期存活的对象进入老年代等。

为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

3) 空间分配担保失败

使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC

4) JDK 1.7 及以前的永久代空间不足

在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。

当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。

为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。

5) Concurrent Mode Failure

执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值