### JVM工作原理
JVM主要由**ClassLoader**和**执行引擎**两子系统组成,运行时数据区分为五个部分: 方法区、堆、栈、程序计数器、本地方法栈。方法区和堆是所有线程共享的,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
Java 虚拟机(JVM)是运行 Java 程序必不可少的机制。JVM实现了Java语言最重要的特征:即平台无关性。原理:编译后的 Java 程序指令并不直接在硬件系统的 CPU 上执行,而是由 JVM 执行。JVM屏蔽了与具体平台相关的信息,使Java语言编译程序只需要生成在JVM上运行的目标字节码(.class),就可以在多种平台上不加修改地运行。Java 虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。因此实现java平台无关性。JVM 是 编译后的 Java 程序(.class文件)和硬件系统之间的接口 ( 编译后:javac 是收录于 JDK 中的 Java 语言编译器。该工具可以将后缀名为. java 的源文件编译为后缀名为. class 的可以运行于 Java 虚拟机的字节码。)
JVM = 类加载器classloader + 执行引擎 execution engine + 运行时数据区域runtime data area,classloader 把硬盘上的class 文件加载到JVM中的运行时数据区域, 但是它不负责这个类文件能否执行,而是由执行引擎负责的。
## classloader 类加载器
作用:装载.class文件到JVM中的运行时数据区。classloader 有两种装载class的方式(时机):
- 显示加载:在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象。
- 隐式加载:不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。
### 类的加载过程
![](https://box.kancloud.cn/499300c5e84b26ec80fc3630c5431801_899x292.png)
1. 加载:通过一个类的全限定名来获取其定义的二进制字节流。二进制字节流可以从Class文件中获取,还可以从Jar、EAR、War包中获取、从网络中获取、由其他文件生成(JSP应用)、运行时计算生成(比如动态代理)等。将这个字节流所代表的静态存储结构(类信息、静态变量、字节码、常量这些)转化为方法区的运行时数据结构。在内存中生成一个代表这个.class文件的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。一般这个Class是在堆里的,不过HotSpot虚拟机比较特殊,这个Class对象是放在方法区中的。
2. 验证:确保Class文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。
3. 准备:为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0(如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值),这里不包含用final修饰的static,因为final在编译的时候就会分配了,注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
4. 解析:将常量池内的符号引用替换为直接引用的过程。Class文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中。
5. 初始化:给static变量赋予用户指定的值以及执行静态代码块。虚拟机规范严格规定了有且只有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的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要进行初始化。
以上5中场景称之为对一个类的主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。关于被动引用:比如如下的场景:子类引用父类的静态字段,不会导致子类初始化;通过数组定义来引用类,不会触发类的初始化;引用静态常量时,常量在编译阶段会存入该类的常量池中,本质上并没有直接引用到定义常量的类
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,例如数据库中和网络中。
### 双亲委派模型
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式即把请求交由父类处理。双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码,类加载器间的关系如下:
![](https://img-blog.csdn.net/20170625231013755?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvamF2YXplamlhbg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
**双亲委派模型的工作过程**:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
**双亲委派模式的优势**:Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子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体系中最基础的行为也将无法保证,应用程序也将会变得一片混乱。如果一个对象每次加载都是由不同的类加载器加载的,就会出现很多同名但不是同一个类的类。
**双亲委派模型的缺陷**:双亲委派模型解决了各个类加载器的基础类的统一问题,越基础的类由越上层的加载器进行加载,基础类的代码总是作为被用户代码调用的API,如果基础类要调用用户的代码,这就有问题了。比如JNDI服务(JNDI的目的是对资源进行集中管理和查找,要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI)的代码),启动类加载器不认可这些代码,所以引入了线程上下文类加载器(Thread Context ClassLoader),用父类加载器请求子类加载器完成类加载。JNDI,JDBC等都是采用这种方式。
**OSGI原理**:模块热部署,打破了双亲委派模型。OSGI实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(Bundle)都有自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉实现模块热部署。OSGI的类加载器不再是双亲委派模型中的树状结构,而是复杂的网状结构。例如bundleA、B都依赖于bundleC,当他们访问bundleC中的类时,就会委托给bundleC的类加载器,由它来查找类;如果它发现还要依赖bundleE中的类,就会再委托给bundleE的类加载器。
## 运行时数据区
JVM 运行时数据区 (JVM Runtime Area) 其实就是指 JVM 在运行期间,其对JVM内存空间的划分和分配。JVM在运行时将数据划分为了6个区域来存储。
### Java内存区域
* **程序计数器**:一块较小的内存空间,它是当前线程所执行的字节码的行号指示器,当线程在执行一个Java方法时,该计数器记录的是正在执行的虚拟机字节码指令的地址,当线程在执行的是Native方法时,该计数器的值为空。线程私有,不存在内存溢出的问题。
* **Java虚拟机栈**:线程私有,描述的是java方法执行的内存模型,每个方法执行时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法返回地址等信息,每一个方法从调用到执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。
* **本地方法栈**:调用本地方法(Native)服务。
* **Java堆**:所有线程共享,几乎所有的对象实例和数组都在这里分配内存。是垃圾收集器管理的主要区域。Java堆分为新生代(Eden、From survivor、To Survivor)和老年代。
* **方法区**:线程共享区域,用于存储被虚拟机加载的类信息、常量、静态变量,即时编译器编译后的代码等数据。Sun HotSpot虚拟机中方法区又称为永久代。运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有存有常量池(Class文件常量池),用于存放编译器生成的各种字面量和符号引用,这部分内容在类加载后存放到方法区的运行时常量池中。在JDK8之前的HotSpot虚拟机中,类的这些“永久的”数据存放永久代。在JVM启动之前可以通过设置-XX:MaxPermSize的值来控制永久代的大小,32位机器默认的永久代的大小为64M,64位的机器则为85M。永久代的垃圾回收和老年代的垃圾回收是绑定的,一旦其中一个区域被占满,这两个区都要进行垃圾回收。 在JDK7之前的HotSpot虚拟机中,纳入字符串常量池的字符串被存储在永久代中,因此导致了一系列的性能问题和内存溢出错误。Java8中移除了永久代。类的元数据信息被移到了一个与堆不相连的本地内存区域叫**元空间**。这项改动是很有必要的,因为对永久代进行调优是很困难的。永久代中的元数据可能会随着每一次Full GC发生而进行移动。并且为永久代设置空间大小也是很难确定的。将元数据从永久代剥离出来,可以简化Full GC以及对以后的并发隔离类元数据等方面进行优化。如果不设置元空间的大小,JVM会自动根据类的元数据大小动态增加元空间的容量。
* **直接内存**:不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,它直接从操作系统中分配,不受Java堆大小的限制,但是会受到本机总内存的大小及处理器寻址空间的限制,因此它也可能导致OutOfMemoryError异常出现。在JDK1.4中新引入了NIO机制,使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,可以直接从操作系统中分配直接内存,即在堆外分配内存,这样能在一些场景中提高性能,因为避免了在Java堆和Native堆中来回复制数据。直接内存也叫做堆外内存。
### 每个区域可能造成的内存溢出现象
* 堆内存OutOfMemoryError:只要不断创建对象并且对象不被回收,那么对象数量达到最大堆容量限制后就会产生内存溢出异常了。
* 栈溢出(StockOverflowError 和 OutOfMemoryError):
* 方法调用的深度太深,就会产生栈溢出。我们只要写一个无限调用自己的方法,就会出现方法调用的深度太深的场景。
* 过不断创建线程的方式可以产生OutOfMemoryError,因为每个线程都有自己的栈空间。
* 方法区和运行时常量池溢出:运行时常量池也是方法区的一部分。这个区域的OutOfMemoryError可以利用String.intern()方法来产生。这是一个Native方法,意思是如果常量池中有一个String对象的字符串就返回池中的这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中去,并且返回此String对象的引用。JDK1.7下是不会有这个异常的,while循环将一直下去,字符串常量池移动到堆中了。JDK1.8移除了永久代并采用元空间来实现方法区的规划了。
### Java对象创建
在语言层面上,创建对象(克隆、反序列化)就是一个new关键字而已,在虚拟机层面上创建对象的步骤:
1. 虚拟机遇到一条new指令,首先检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。如果没有,那么必须先执行类的初始化过程。
2. 类加载检查通过后,为新生对象分配内存。对象所需内存大小在类加载完成后便可以确定,为对象分配空间就是从Java堆中划分出一块确定大小的内存。
3. 内存分配结束,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。这一步保证了对象的实例字段在Java代码中可以不用赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值。
4. 对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息存放在对象的对象头中。
5. 执行对象构造器`<init>`方法,进行初始化,这样一个真正可用的对象才算完全产生出来。
### Java对象的访问定位
对内存分配情况分析最常见的示例便是对象实例化:
`Object obj = new Object();`
这段代码的执行会涉及java栈、Java堆、方法区。假设该语句出现在方法体中,obj会作为引用类型的数据保存在Java栈的本地变量表中,在Java堆中保存该引用的实例化对象,Java堆中还必须包含能查找到此对象类型数据的地址信息,这些类型数据则保存在方法区中。
另外,由于reference类型在Java虚拟机规范里面只规定了一个指向对象的引用,不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄池和直接使用指针。
通过句柄池访问的方式如下:使用句柄访问方式的最大好处就是reference中存放的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象)时只会改变句柄中的实例数据指针,而reference本身不需要修改
![](http://img.blog.csdn.net/20131226172011765)
通过直接指针访问的方式如下:
![](http://img.blog.csdn.net/20131226172113234)
使用直接指针访问方式的最大好处是速度快,它节省了一次指针定位的时间开销。目前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区的内存分配方式称为慢速分配方式。
#### 对象引用
- 强引用:如“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());
~~~
- 虚引用:最弱的一种引用关系,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。JDK1.2之后提供了PhantomReference类来实现虚引用。虚引用PhantomReference<T>的声明的借助强引用或者匿名对象,结合泛型ReferenceQueue<T>初始化,具体如下:
~~~
PhantomReference<User> phantomReference = new PhantomReference<User>(new User(),new ReferenceQueue<User>());
~~~
### 垃圾对象的判定
- 引用计数算法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效时,计数器值就减1,计数器为0的对象就是不可能再被使用的。引用计数算法的实现简单,判定效率也很高,Java语言并没有选择这种算法来进行垃圾回收,因为它很难解决对象之间的相互循环引用问题。
- 可达性分析算法:Java和C#中都是采用可达性分析算法来判定对象是否存活的。这种算法的基本思路是通过一系列名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的。可作为GC Roots的对象包括下面几种:
1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
2. 方法区中的类静态属性引用的对象。
3. 方法区中的常量引用的对象。
4. 本地方法栈中JNI(Native方法)的引用对象。
在可达性分析算法中,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,则没有必要执行。
### 方法区回收
虚拟机规范中不要求方法区一定要实现垃圾回收,而且方法区中进行垃圾回收的效率也确实比较低,但是HotSpot对方法区也是进行回收的,主要回收的是废弃常量和无用的类两部分。
- 废弃常量:只要当前系统中没有任何一处引用该常量就是废弃常量
- 无用的类需要同时满足以下三个条件:
1. 该类所有实例都已经被回收,Java堆中不存在该类的任何实例
2. 加载该类的ClassLoader已经被回收
3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载功能,以保证方法区不会溢出。
### 内存分配策略
1. 对象优先在Eden区分配:当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC
2. 大对象直接进入老年代:大对象是指需要大量连续内存空间的Java对象,比如很长的字符串以及数组,老年代发生Full GC
3. 长期存活的对象将进入老年代:如果对象在Eden区出生并且经过第一次Minor GC后任然存在,并且能被Survivor容纳,则被移动到Survivor空间中,并且对象年龄+1,当对象年龄达到“-XX:MaxTenuringThreshold”设置的值(默认15)的时候,对象就会被晋升到老年代中
### 垃圾收集算法
1. 标记-清除算法:首先标记出所有需要回收的对象,然后统一回收所有被标记的对象;缺点是效率不高且容易产生大量不连续的内存碎片, 当程序需要分配较大对象时无法找到连续内存而不得不触发另一次垃圾收集动作。
2. 复制算法:将可用内存分为大小相等的两块,每次只使用其中一块;当这一块用完了,就将还活着的对象复制到另一块上,然后把已使用过的内存清理掉。在HotSpot里,考虑到大部分对象存活时间很短,将内存分为Eden和两块Survivor,默认比例为8:1:1。代价是存在部分内存空间浪费,且可能存在空间不够需要分配担保的情况,所以适合在新生代使用;
3. 标记-整理算法:首先标记出所有需要回收的对象,然后让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。适用于老年代。
4. 分代收集算法:一般把Java堆分新生代和老年代,在新生代用复制算法,新生代每次垃圾收集时都会有大量对象死去,只有少量存活。老年代对象存活率高、没有额外额空间对他进行分配担保,用标记-清理或标记-整理算法,是现代虚拟机通常采用的算法。
### 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收集器:ParNew收集器其实就是Serial收集器的多线程版本。新生代并行,老年代串行;新生代复制算法、老年代标记-压缩。-XX:+UseParNewGC ParNew收集器,-XX:ParallelGCThreads 限制线程数量
3. Parallel收集器:Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;新生代复制算法、老年代标记-压缩。参数控制: -XX:+UseParallelGC 使用Parallel收集器+ 老年代串行
4. Parallel Old 收集器:Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.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. 并发清除:与应用线程一起运行,清理垃圾对象
优点: 并发收集、低停顿
缺点: 产生大量空间碎片、并发阶段会降低吞吐量
参数控制:
-XX:+UseConcMarkSweepGC 使用CMS收集器
-XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理
-XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)
![](http://mmbiz.qpic.cn/mmbiz_png/PgqYrEEtEnqLYgY6g5DgUKYUPgXXTjorfdaee1XLicg5ZLjhNiajxyD8X78TBrHpnfu3cdtu30apSxDF1PhYcRrw/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1)
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的集合。
![](http://mmbiz.qpic.cn/mmbiz_jpg/PgqYrEEtEnqLYgY6g5DgUKYUPgXXTjoru5HeMdP7OnkFlDIpg71gf6utSNzcoH5E3BpzwZ9ytvFKHBxvf929Dw/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1)
收集步骤:
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)。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。![](http://mmbiz.qpic.cn/mmbiz_png/PgqYrEEtEnqLYgY6g5DgUKYUPgXXTjorMmqL4XiaoBlyRy1ziascfFousWcicSvFxOrFN2GIIgyiagPaWljnbtMzibg/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1)
4. Remark, 再标记,会有短暂停顿(STW)。再标记阶段是用来收集 并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行);G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。
5. Copy/Clean up,多线程清除失活对象,会有STW。G1将回收区域的存活对象拷贝到新区域,清除Remember Sets,并发清空回收区域并把它返回到空闲区域链表中。![](http://mmbiz.qpic.cn/mmbiz_png/PgqYrEEtEnqLYgY6g5DgUKYUPgXXTjornTUQo5RqW79icN3rDRuZ4bl8Y2GU8q7CuM73hJwQpia5Ek52DicIpUDTQ/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1)
6. 复制/清除过程后。回收区域的活性对象已经被集中回收到深蓝色和深绿色区域。![](http://mmbiz.qpic.cn/mmbiz_png/PgqYrEEtEnqLYgY6g5DgUKYUPgXXTjor8XeOMiay8P27IQnWawjlyJdvYriba2ae07zxODmvMOgHqVxCiazYnGia1g/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1)
参考文章
1. https://mp.weixin.qq.com/s?__biz=MzI4NDY5Mjc1Mg==&mid=2247484530&idx=1&sn=149bfd8bbdcdeac0410d39ee42cc52dd&chksm=ebf6dc0ddc81551b6f258dcd060a4266dfd0eeeded6dae94a58020f927ecb1239d83633bd504&mpshare=1&scene=1&srcid=0312dWSUN8BkQxWvmwT8RDhc#rd