Java面试看这一篇就够了:JVM(上)

什么是虚拟机

Java 虚拟机(JVM)是运行 Java 程序必不可少的机制。JVM实现了Java语言最重要的特征:平台无关性。原理:编译后的 Java 程序指令并不直接在硬件系统的 CPU 上执行,而是由 JVM 执行。JVM屏蔽了与具体平台相关的信息,使Java语言编译程序只需要生成在JVM上运行的目标字节码(.class),就可以在多种平台上不加修改地运行。Java 虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。因此实现java平台无关性。JVM = 类加载器classloader + 执行引擎 execution engine + 运行时数据区域runtime data area,classloader 把硬盘上的class 文件加载到JVM中的运行时数据区域, 但是它不负责这个类文件能否执行,而是由执行引擎负责的。不同平台下需要安装不同版本的 JVM 。


image

JVM能够跨计算机体系结构来执行 Java 字节码,主要是由于 JVM 屏蔽了与各个计算机平台相关的软件或者硬件之间的差异,使得与平台相关的耦合统一由 JVM 提供者来实现。

JVM工作原理

任何一个Java类的main函数运行都会创建一个JVM实例,JVM实例启动时默认启动几个守护线程,比如:垃圾回收的线程,而main方法的执行在一个单独的非守护线程中执行。只要非守护线程结束JVM实例就销毁了。那么在Java类main函数运行过程中,JVM的工作原理如下:

  1. 根据系统环境变量,创建装载JVM的环境与配置;
  2. 寻找JRE目录,寻找jvm.dll,并装载jvm.dll;
  3. 根据JVM的参数配置,如:内存参数,初始化jvm实例;
  4. JVM实例产生一个引导类加载器实例(Bootstrap Loader),加载Java核心库,然后引导类加载器自动加载扩展类加载器(Extended Loader),加载Java扩展库,最后扩展类加载器自动加载系统类加载器(AppClass Loader),加载当前的Java类;
  5. 当前Java类加载至内存后,会经过 验证、准备、解析三步,将Java类中的类型信息、属性信息、常量池存放在方法区内存中,方法指令直接保存到栈内存中,如:main函数;
  6. 执行引擎开始执行栈内存中指令,由于main函数是静态方法,所以不需要传入实例,在类加载完毕之后,直接执行main方法指令;
  7. main函数执行主线程结束,随之守护线程销毁,最后JVM实例被销毁;

JVM由哪些部分组成

image
  • 类加载器:在 JVM 启动时或者类运行时将需要的 class 加载到 JVM 中。
  • 内存区:将内存划分成若干个区以模拟实际机器上的存储、记录和调度功能模块,如实际机器上的各种功能的寄存器或者 PC 指针的记录器等。
  • 执行引擎:执行class文件中包含的字节码指令,相当于实际机器上的CPU。
  • 本地方法调用:调用 C 或 C++ 实现的本地方法的代码返回结果。

Java运行时内存区域

该图是基于 JDK6 版本的运行内存的分类。

image
  • 程序计数器:线程私有,不存在内存溢出的情况,是当前线程所执行的字节码的行号指示器,当线程在执行一个Java方法时,该计数器记录的是正在执行的虚拟机字节码指令的地址,当线程在执行的是Native方法时,该计数器的值为空。
  • Java虚拟机栈(栈内存):线程私有,描述的是java方法执行的内存模型,每个方法执行时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法返回地址等信息,每一个方法从调用到执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。
  • 本地方法栈:和Java虚拟机栈的作用类似,区别是该区域为JVM提供使用 Native方法的服务。
  • Java堆:所有线程共享,几乎所有的对象实例和数组都在这里分配内存。是垃圾收集器管理的主要区域。目前主要的垃圾回收算法都是分代收集算法,所以 Java 堆中还可以细分为:新生代和老年代;再细致一点的有 Eden 空间、From Survivor 空间、To Survivor 空间等,默认情况下新生代按照 8:1:1 的比例来分配。
    根据 Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘一样。
  • 方法区:线程共享,用于存储被虚拟机加载的类信息、常量、静态变量,即时编译器编译后的代码等数据。Sun HotSpot虚拟机中方法区又称为永久代。运行时常量池是方法区的一部分,用于存放编译器生成的各种字面量和符号引用。
  • 直接内存:不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,它直接从操作系统中分配,不受Java堆大小的限制,但是会受到本机总内存的大小及处理器寻址空间的限制,因此它也可能导致OutOfMemoryError异常出现。在JDK1.4中新引入了NIO机制,使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,可以直接从操作系统中分配直接内存,即在堆外分配内存,这样能在一些场景中提高性能,因为避免了在Java堆和Native堆中来回复制数据。直接内存也叫做堆外内存。

直接内存(堆外内存)与堆内存比较?

  1. 直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显。
  2. 直接内存 IO 读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显。

JDK后续版本对方法区做的调整
JDK7:的改变:存储在永久代的部分数据(常量池和静态变量)就已经转移到了 Java Heap。但永久代仍存在于 JDK7 中,但是并没完全移除。
JDK8 的改变:废弃 PermGen(永久代),新增 Metaspace(元数据区)。方法区在 Metaspace 中。MetaSpace 大小默认没有限制,一般根据系统内存的大小。JVM 会动态改变此值。

为什么要废弃永久代?
由于永久代内存经常不够用或发生内存泄露,字符串存在永久代中,容易出现性能问题和内存溢出。类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

Java 内存堆和栈区别?
栈内存用来存储基本类型的变量和对象的引用变量;堆内存用来存储Java中的对象,无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中。栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存;堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。

image

每个区域可能造成的内存溢出现象

  • 堆内存OutOfMemoryError:只要不断创建对象并且对象不被回收,那么对象数量达到最大堆容量限制后就会产生内存溢出异常了。
  • 栈溢出(StockOverflowError 和 OutOfMemoryError):
    • 方法调用的深度太深,就会产生栈溢出。我们只要写一个无限调用自己的方法,就会出现方法调用的深度太深的场景。
    • 过不断创建线程的方式可以产生OutOfMemoryError,因为每个线程都有自己的栈空间。
  • 方法区和运行时常量池溢出:运行时常量池也是方法区的一部分。这个区域的OutOfMemoryError可以利用String.intern()方法来产生。这是一个Native方法,意思是如果常量池中有一个String对象的字符串就返回池中的这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中去,并且返回此String对象的引用。JDK1.7下是不会有这个异常的,while循环将一直下去,字符串常量池移动到堆中了。JDK1.8移除了永久代并采用元空间来实现方法区的规划了。

classloader 类加载器

作用:装载.class文件到JVM中的运行时数据区。classloader 有两种装载class的方式(时机):

  • 显示加载:在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象。
  • 隐式加载:不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。

类加载的时机

虚拟机严格规定,有且仅有 5 种情况必须对类进行加载(些文章会称为对类进行“初始化”。)

  1. 使用new关键字实例化对象时、读取或设置一个类的静态字段(static)时(被static修饰又被final修饰的,已在编译期把结果放入常量池的静态字段除外)、以及调用一个类的静态方法时。
  2. 使用Java.lang.refect包的方法对类进行反射调用时。
  3. 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个主类,虚拟机会先执行该主类。
  5. 当使用Jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析的结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要进行初始化。

类的加载过程

image
  1. 加载:通过一个类的全限定名来获取其定义的二进制字节流。二进制字节流可以从Class文件中获取,还可以从Jar、EAR、War包中获取、从网络中获取、由其他文件生成(JSP应用)、运行时计算生成(比如动态代理)等。将这个字节流所代表的静态存储结构(类信息、静态变量、字节码、常量这些)转化为方法区的运行时数据结构。在Java堆中生成一个代表这个.class文件的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
  2. 验证:确保Class文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。
  3. 准备:为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0(如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值),这里不包含用final修饰的static,因为final在编译的时候就会分配了。这里也不会为实例变量分配内存,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
  4. 解析:将常量池内的符号引用替换为直接引用的过程。Class文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中。
  5. 初始化:给static变量赋予用户指定的值以及执行静态代码块。JVM 初始化步骤:假如这个类还没有被加载和连接,则程序先加载并连接该类。假如该类的直接父类还没有被初始化,则先初始化其直接父类。假如类中有初始化语句,则系统依次执行这些初始化语句。
  6. 使用
  7. 卸载:卸载是对象被GC的阶段,JVM中的Class只有满足下面的三个条件,才会被卸载(回收),该类所有的实例都被GC,不存在该类的任何实例;加载该类的ClassLoader已经被GC;该类的java.lang.Class对象没有在任何地方被引用,比如不能再任何地方通过反射访问该类的方法

类加载器

通过一个类的全限定名来获取描述此类的二进制字节流的代码块称之为类加载器。
比较两个类是否"相等",只有在这两个类是由同一个类加载器加载的前提下才有意义,否则即使这两个类来源于同一个.class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,这两个类必定不相等。"相等"包括代表类的.class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。类加载器可以大致划分为以下三类:

  • 启动类加载器:Bootstrap ClassLoader。负责加载存放在JDK\jre\lib,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.* 开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
  • 扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.* 开头的类),开发者可以直接使用扩展类加载器。
  • 应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是从本地文件系统加载标准的java class文件,如果编写了自己的ClassLoader,便可以做到如下几点:

  • 在执行非置信代码之前,自动验证数字签名。
  • 动态地创建符合用户特定需要的定制化构建类。
  • 从特定的场所取得Java class,例如数据库中和网络中。

双亲委派模型

类加载器 ClassLoader 是具有层次结构的,也就是父子关系,父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码,类加载器间的关系如下:


image
  1. 双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,当前 ClassLoader首先从自己已经加载的类中,查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了。如果当前 ClassLoader 的缓存中没有找到被加载的类的时候,会委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到 bootstrap ClassLoader。当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。
  2. 双亲委派模式的优势:可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。可能你会想,如果我们在classpath路径下自定义一个名为java.lang.SingleInterge类(该类是胡编的)呢?该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做是不允许,因为java.lang是核心API包,需要访问权限,强制加载将会报出异常。相反,如果没有双亲委派模型,由各个类自己去加载的话,如果用户自己编写了一个java.lang.Object,并放在CLASSPATH下,那系统中将会出现多个不同的Object类,Java体系中最基础的行为也将无法保证,应用程序也将会变得一片混乱。如果一个对象每次加载都是由不同的类加载器加载的,就会出现很多同名但不是同一个类的类。
    3 双亲委派模型的缺陷:双亲委派模型解决了各个类加载器的基础类的统一问题,越基础的类由越上层的加载器进行加载,基础类的代码总是作为被用户代码调用的API,如果基础类要调用用户的代码,这就有问题了。比如JNDI服务(JNDI的目的是对资源进行集中管理和查找,要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI)的代码),启动类加载器不认可这些代码,所以引入了线程上下文类加载器(Thread Context ClassLoader),用父类加载器请求子类加载器完成类加载。JNDI,JDBC等都是采用这种方式。
  3. 破坏双亲委派模型:破坏双亲委托模型,需要做的是,#loadClass(String name, boolean resolve) 方法中,不调用父 parent ClassLoader 方法去加载类,那么就成功了。那么我们要做的仅仅是,错误的覆盖 ##loadClass(String name, boolean resolve) 方法,不去使用父 parent ClassLoader 方法去加载类即可。
  4. OSGI原理:模块热部署,打破了双亲委派模型。OSGI实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(Bundle)都有自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉实现模块热部署。OSGI的类加载器不再是双亲委派模型中的树状结构,而是复杂的网状结构。例如bundleA、B都依赖于bundleC,当他们访问bundleC中的类时,就会委托给bundleC的类加载器,由它来查找类;如果它发现还要依赖bundleE中的类,就会再委托给bundleE的类加载器。

Java对象创建

在语言层面上,创建对象(克隆、反序列化)就是一个new关键字而已,在虚拟机层面上创建对象的步骤:

  1. 检测类是否被加载:检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。如果没有,那么必须先执行类的加载过程。
  2. 为对象分配内存:类加载检查通过后,为新生对象分配内存。对象所需内存大小在类加载完成后便可以确定,为对象分配空间就是从Java堆中划分出一块确定大小的内存。
  3. 虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。这一步保证了对象的实例字段在Java代码中可以不用赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值。
  4. 对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息存放在对象的对象头中。
  5. 执行对象构造器init方法,进行初始化


    image

Java对象的访问定位

对内存分配情况分析最常见的示例便是对象实例化:
Object obj = new Object();
这段代码的执行会涉及java栈、Java堆、方法区。假设该语句出现在方法体中,obj会作为引用类型的数据保存在Java栈的本地变量表中,在Java堆中保存该引用的实例化对象,Java堆中还必须包含能查找到此对象类型数据的地址信息,这些类型数据则保存在方法区中。
另外,由于reference类型在Java虚拟机规范里面只规定了一个指向对象的引用,不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄池和直接使用指针。

  • 通过句柄池访问的方式:使用句柄访问方式的最大好处就是reference中存放的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象)时只会改变句柄中的实例数据指针,而reference本身不需要修改
    [图片上传失败...(image-bb2ae3-1600958929592)]
  • 通过直接指针访问的方式:
    [图片上传失败...(image-ee5181-1600958929592)]
    使用直接指针访问方式的最大好处是速度快,它节省了一次指针定位的时间开销。目前Java默认使用的HotSpot虚拟机采用的是直接指针进行对象访问的。

对象在堆中的布局

HotSpot虚拟机中,对象在堆内存中的布局分为三块区域:对象头、实例数据和对齐填充。

  1. 对象头:包括两部分:Mark Word 和 类型指针。
    • Mark Word:存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,占用内存大小与虚拟机位长一致。
    • 类型指针:指向方法区中的对象类型数据,虚拟机通过这个指针确定该对象是哪个类的实例。
  2. 实例数据:对象真正存储的有效信息。
  3. 对齐填充:由于HotSpot虚拟机要求对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数,当对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全。对齐填充不是必然存在的。

如何保证new对象时候的线程安全性。

因为可能出现虚拟机正在给对象A分配内存,指针还没有来得及修改,对象B又同时使用了原来的指针来分配内存的情况。虚拟机采用了CAS配上失败重试的方式保证更新操作的原子性和TLAB两种方式来解决这个问题。TLAB:内存分配的动作,每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程需要分配内存,就在哪个线程的TLAB上分配。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。这么做的目的之一,也是为了并发创建一个对象时,保证创建对象的线程安全性。TLAB比较小,直接在TLAB上分配内存的方式称为快速分配方式,而TLAB大小不够,导致内存被分配在Eden区的内存分配方式称为慢速分配方式。

垃圾收集器

垃圾对象的判定

  • 引用计数算法:每个对象有一个引用计数属性,新增一个引用时计数加 1 ,引用释放时计数减 1 ,计数为 0 时可以回收。此方法简单,无法解决对象相互循环引用的问题。目前在用的有 Python、ActionScript3 等语言。

  • 可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。不可达对象。目前在用的有 Java、C# 等语言。可作为GC Roots的对象包括下面几种:

    1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
    2. 方法区中的类静态属性引用的对象。
    3. 方法区中的常量引用的对象。
    4. 本地方法栈中JNI(Native方法)的引用对象。

    在可达性分析算法中,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,则没有必要执行。

对象引用

  • 强引用:如“Object obj = new Object()”,这类引用是Java程序中最普遍的。只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用:它用来描述一些可能还有用,但并非必须的对象。在系统内存不够用时,这类引用关联的对象将被垃圾收集器回收。JDK1.2之后提供了SoftReference类来实现软引用。
SoftReference<User> softReference = new SoftReference<User>(new User());  
strangeReference = softReference.get(); //通过get方法获得强引用
  • 弱引用:被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2之后,提供了WeakReference类来实现弱引用。jdk中的ThreadLocal就是弱引用的
WeakReference<User> weakReference = new WeakReference<User>(new User());  
  • 虚引用:“虚引用”顾名思义,就是形同虚设,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。JDK1.2之后提供了PhantomReference类来实现虚引用。虚引用PhantomReference<T>的声明的借助强引用或者匿名对象,结合泛型ReferenceQueue<T>初始化,具体如下:
PhantomReference<User> phantomReference = new PhantomReference<User>(new User(),new ReferenceQueue<User>()); 

为什么要有不同的引用类型?

不像 C 语言,我们可以控制内存的申请和释放,在 Java 中有时候我们需要适当的控制对象被回收的时机,因此就诞生了不同的引用类型,可以说不同的引用类型实则是对 GC 回收时机不可控的妥协。有以下几个使用场景可以充分的说明:利用软引用和弱引用解决 OOM 问题。用一个 HashMap 来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM 会自动回收这些缓存图片对象所占用的空间,从而有效地避免了 OOM 的问题;通过软引用实现 Java 对象的高速缓存。比如我们创建了一 Person 的类,如果每次需要查询一个人的信息,哪怕是几秒中之前刚刚查询过的,都要重新构建一个实例,这将引起大量 Person 对象的消耗,并且由于这些对象的生命周期相对较短,会引起多次 GC 影响性能。此时,通过软引用和 HashMap 的结合可以构建高速缓存,提供性能。

方法区回收

虚拟机规范中不要求方法区一定要实现垃圾回收,而且方法区中进行垃圾回收的效率也确实比较低,但是HotSpot对方法区也是进行回收的,主要回收的是废弃常量和无用的类两部分。

  • 废弃常量:只要当前系统中没有任何一处引用该常量就是废弃常量
  • 无用的类需要同时满足以下三个条件:
    1. 该类所有实例都已经被回收,Java堆中不存在该类的任何实例
    2. 加载该类的ClassLoader已经被回收
    3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载功能,以保证方法区不会溢出。

垃圾收集算法

  1. 标记-清除算法:首先标记出所有需要回收的对象,然后统一回收所有被标记的对象;缺点是效率不高且容易产生大量不连续的内存碎片, 当程序需要分配较大对象时无法找到连续内存而不得不触发另一次垃圾收集动作。
    image
  2. 复制算法:将可用内存分为大小相等的两块,每次只使用其中一块;当这一块用完了,就将还活着的对象复制到另一块上,然后把已使用过的内存清理掉。在HotSpot里,考虑到大部分对象存活时间很短,将内存分为Eden和两块Survivor,默认比例为8:1:1。代价是存在部分内存空间浪费,且可能存在空间不够需要分配担保的情况,所以适合在新生代使用;
    image
  3. 标记-整理算法:首先标记出所有需要回收的对象,然后让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。适用于老年代。
    image
  4. 分代收集算法:一般把Java堆分新生代和老年代,在新生代用复制算法,新生代每次垃圾收集时都会有大量对象死去,只有少量存活。老年代对象存活率高、没有额外额空间对他进行分配担保,用标记-清理或标记-整理算法,是现代虚拟机通常采用的算法。
    image

内存分配策略

  1. 对象优先在Eden区分配:当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC
  2. 大对象直接进入老年代:大对象是指需要大量连续内存空间的Java对象,比如很长的字符串以及数组,老年代发生Full GC
  3. 长期存活的对象将进入老年代:如果对象在Eden区出生并且经过第一次Minor GC后任然存在,并且能被Survivor容纳,则被移动到Survivor空间中,并且对象年龄+1,当对象年龄达到“-XX:MaxTenuringThreshold”设置的值(默认15)的时候,对象就会被晋升到老年代中

什么是安全点

SafePoint安全点,顾名思义是指一些特定的位置,当线程运行到这些位置时,线程的一些状态可以被确定,比如记录OopMap 的状态,从而确定 GC Root 的信息,使 JVM 可以安全的进行一些操作,比如开始 GC。SafePoint 指的特定位置主要有:
循环的末尾 (防止大循环的时候一直不进入 Safepoint ,而其他线程在等待它进入 Safepoint )、方法返回前、调用方法的 Call 之后、抛出异常的位置。

Minor GC和Full GC的区别

  1. 新生代GC(Minor GC):发生在新生代的垃圾收集动作
  2. 老年代GC(Major GC/Full GC):发生在老年代的垃圾收集动作,出现了Major GC,经常会伴随至少一次的Minor GC(但并不是绝对的)。Major GC的速度一般要比Minor GC慢上10倍以上

垃圾收集器

  1. Serial收集器:复制算法的单线程的收集器,它只会使用一条线程去完成垃圾收集工作,进行垃圾收集时必须暂停其他线程的所有工作,直到它收集结束为止。垃圾收集的过程中会Stop The World(服务暂停)。参数控制: -XX:+UseSerialGC 串行收集器

  2. ParNew收集器:Serial收集器的多线程版本。新生代并行,老年代串行;新生代复制算法、老年代标记-压缩。-XX:+UseParNewGC ParNew收集器,-XX:ParallelGCThreads 限制线程数量

  3. Parallel收集器:Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;新生代复制算法、老年代标记-压缩。参数控制: -XX:+UseParallelGC 使用Parallel收集器+ 老年代串行

  4. Parallel Old收集器:Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK1.6中才开始提供,参数控制:-XX:+UseParallelOldGC 使用Parallel收集器+ 老年代并行

  5. CMS收集器:Concurrent Mark Sweep,是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验,缺点是会产生大量空间碎片、并发阶段会降低吞吐量。从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括:

    1. 初始标记:仅标记一下GC Roots能直接关联到的对象,速度很快,单线程,会发生Stop The World
    2. 并发标记:与应用线程一起运行,是CMS最主要的工作阶段,通过直达对象,扫描全部的对象,进行标记
    3. 重新标记:STW,修正并发标记时由于应用程序还在并发运行产生的对象的修改,多线程,速度快,需要全局停顿
    4. 并发清除:与应用线程一起运行,清理垃圾对象
  6. G1收集器:G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与CMS收集器相比G1收集器有以下特点:

    • 空间整合:G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
    • 可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。收集步骤:

  1. 标记阶段: 首先初始标记(Initial-Mark),这个阶段是停顿的(Stop the World Event),并且会触发一次普通Mintor GC。对应GC log:GC pause (young) (inital-mark)
  2. Root Region Scanning,程序运行过程中会回收survivor区(存活到老年代),这一过程必须在young GC之前完成。
  3. Concurrent Marking,在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那个这个区域会被立即回收(图中打X)。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。
  4. Remark, 再标记,会有短暂停顿(STW)。再标记阶段是用来收集 并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行);G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。
  5. Copy/Clean up,多线程清除失活对象,会有STW。G1将回收区域的存活对象拷贝到新区域,清除Remember Sets,并发清空回收区域并把它返回到空闲区域链表中。
  6. 复制/清除过程后。回收区域的活性对象已经被集中回收到深蓝色和深绿色区域。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267