JVM-内存结构-垃圾回收

AAAAA 垃圾回收过程与垃圾回收算法

JVM 垃圾回收过程
先判断对象是否可以回收,再进行清除。
1、首先判断对象是否可以被回收:引用计数法、可达性分析法。
>>引用计数器算法
给对象添加一个引用计数器,每当有一个地方引用它时,计数器就会加1;当引用失效时,计数器就减1,当计数器为0就是没有被使用的对象,但主流的Java虚拟机并没有选择用引用计数法来管理内存,因为无法解决对象之间相互循环引用的问题,就是两个对象相互引用,除此之外,两个对象并没有其他引用,这两个对象已经不可能被访问了,但他们的引用计数都不为0,所以无法被垃圾收集器回收。
>>可达性分析算法
可达性分析法就是通过一系列被称为”GC Roots“的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径被称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明该对象是不可用的,也就是可回收的,某些对象object 1、object 2、object 3虽然有关联,但他们到GC Roots是不可达的,所以也会判定是可以回收的,这样解决了对象之间相互引用导致不能回收的问题。
在Java语言中,可以作为GC Roots的对象主要有以下几种:
虚拟机栈中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的属性
本地方法栈中Native中引用的对象

2、其次具体的垃圾回收算法有:垃圾回收算法主要有标记-清除算法、标记-复制算法、标记-整理算法、分代收集算法。
>>标记-清除算法
标记-清除算法主要包含标记和清除两个阶段,首先标记出所有需要回收的对象,在标记完成后同一回收所有被标记的对象。
标记-清除算法有两个明显的缺点:第一就是效率低,标记和清除两个过程的效率都不高;第二是空间问题,标记清除后会产生大量不连续内存碎片,空间碎片太多会导致以后在程序运作过程中需要分配大对象时,无法找到足够的连续内存进而提前触发另一次垃圾收集动作。
>>标记-复制算法
标记-复制算法 为了解决标记-清除算法的效率问题,标记-复制算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这块的内存用完了,就将活着的对象复制到另一块上面,再将已使用过的内存一次清理掉。
标记-复制算法的好处显而易见,每次都是对半个区进行内存回收,内存分配时也不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效,缺点也是显而易见的,每次可以使用的内存只有原来的一半。
>>标记-整理算法
如果对象存活率比较高时使用标记-复制算法就要进行比较多的复制操作,效率会变低,针对这种场景,提出了一种标记-整理算法,和标记-清除算法不同的是,标记完后不直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉其他地方的内存。
>>分代收集算法
按照前面讲的,将Java堆分为新生代和老年代,根据每个年代的特点采用合适的收集算法。例如,在新生代每次垃圾回收时会有大量对象死亡,只有少量存活,所以在新生代选择用标记-复制算法,在老年代每次垃圾回收会有大量对象存活,考虑使用标记-清除或标记-整理算法。在商业虚拟机中一般都是采用分代收集算法。

垃圾回收器
在这里插入图片描述

1. JVM是什么?

 概念
JVM全称是Java虚拟机,运行在操作系统上。执行一个所谓的Java程序的时候,真正在执行的是一个叫做Java虚拟机的进程。

 基本工作流程
 Java源码文件经过Java编译器(前端)编译后生成.class字节码文件,每一个.class字节码文件对应Java层面的一个类
 类加载器将.class字节码文件加载到内存中生成一个Class对象,同时对一些必要的属性进行初始化;
 此时,运行时数据区中的方法区就有对应的Class实例对象了;
 当真正执行字节码指令时,执行引擎开始工作,按照程序的字节码指令依次执行,这里就涉及到运行时数据区中的五个部分。其中虚拟机栈中局部变量表取数据,操作操作数栈等,创建对象需要用到堆空间,指令依次往下执行时,需要用到程序计数器,如果调用了本地C的类库,需要用到本地方法栈。换言之,运行时数据区的五个部分开始工作了。
 执行引擎中解释器负责翻译字节码,解析执行;即时编译器(后端)负责编译执行。目前主流虚拟机都采用解释执行和JIT即时编译(编译热点代码并缓存,提升性能)并存的方式。
 总结
 类装载子系统(类加载器):加载字节码文件到内存中
 运行时数据区:JVM的内存区域
 执行引擎:将字节码指令解释执行和即时编译为操作系统识别能够的机器指令码
 本地库方法接口:与其他语言交互时,调用本地方法所使用的;
 注意
实际上操作系统只能识别机器码,而非字节码,执行引擎负责将高级语言翻译成机器语言。

2. JVM运行时数据区

 概念
Java程序在运行时,会为JVM单独划出一块内存区域,而这块内存区域又可以再次划分出一块运行时数据区,运行时数据区域可以分为如下五个部分。
 方法区
 属性:属于线程共享区域,因此是线程不安全的。
 存储内容:类元信息(类的定义信息)。存储的是从.class文件加载进来的类信息(版本、字段、方法、接口、静态变量、静态方法)、运行时常量池以及编译器编译后的代码。 (只有HotSpot虚拟机有方法区)
 异常:可能会发生OutOfMemoryError
 堆空间
 属性:属于线程共享区域,因此是线程不安全的。
 存储内容:存储的是new来的对象,不存放基本类型和对象引用。
 由于创建了大量的对象,垃圾回收器主要工作在这块区域,也是数据区中最大的一块内存空间。
 异常:可能会发生OutOfMemoryError。
 虚拟机栈
 属性:属于线程私有区域,是线程安全的。
 存储内容:存放基本类型、对象引用。
 内部结构:每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame)。每一个Java方法执行的时候会在虚拟机栈中创建一个相应栈帧,方法执行完毕后该栈帧就会被销毁。栈帧是以先进后出的方式退出虚拟机栈的。
 每一个栈帧又可以划分为局部变量表、操作数栈、动态链接、方法返回地址(方法出口)以及额外的附加信息。
 异常:如果线程请求的栈深度大于虚拟机所允许的深度,则抛出StackOverflowError异常(通常是递归导致的);JVM动态扩展时无法申请到足够内存,则抛出OutOfMemoryError异常。
 虚拟机栈中栈帧中的方法与方法区中运行时常量池中的方法一一对应,动态链接指向方法区运行时常量池中对应当前方法的引用,就知道当前栈帧中的方法引用的方法区运行时常量池中某个具体的方法。这里所谓的“动态”是因为有些类只有在运行时才能确定具体加载那个类,即多态。
 程序计数器
 属性:属于线程私有区域,是线程安全的。
 存储内容:记录当前线程所执行的位置。当线程重新获得CPU的执行权的时候,直接从记录的位置开始执行,另外,分支、循环、跳转、异常处理也都依赖这个程序计数器来完成。
 异常:唯一一块不存在OutOfMemoryError的区域,可能是设计者觉得没必要。
 本地方法栈
 属性:属于线程私有区域,是线程安全的。
 存储内容:本地方法栈其实可以和Java虚拟机栈进行对比理解,唯一不同的是本地方法栈是Java程序在调用本地方法的时候创建栈帧的地方。
 异常:和JVM栈一样,这个区域也会抛出StackOverflowError和OutOfMemoryError
 总结

3. 虚拟机栈

 基本内容
栈是运行时的单位,而堆是存储的单位。即栈解决程序的运行问题,程序如何执行,如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
 Java虚拟机栈是什么?
Java虚拟机栈(Java Virtual Machistack) ,早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈其内部保存一个个的栈帧(stack Frame) ,对应着一次次的Java方法调用,是线程私有的。
 生命周期
生命周期和线程一致。
 作用
主管Java程序的运行,它保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。
 局部变量vs 成员变量(或属性)
 基本数据变量vs 引用类型变量(类、数组、接口)
 栈的特点(优点)
 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
 JVM直接对Java栈的操作只有两个:
 每个方法执行,伴随着进栈(入栈、压栈)
 执行结束后的出栈工作
 对于栈来说不存在垃圾回收问题
 栈中可能出现的异常
 Java虚拟机规范允许Java栈的大小是固定不变的或者是动态的。
 如果采用固定大小的Java虚拟机栈,每一个线程的Java虚拟机栈容量可以在线程创建时独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverFlowError异常。
 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法中请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。
 栈中存储什么?
每个线程对应着一个Java虚拟机栈,栈中的数据都是以栈帧(stack Frame)的格式存在,一个栈帧对应着一个方法。
 栈运行原理
 JVM直接对Java栈的操作只有两个,就是对栈帧的压栈(方法调用)和出栈(方法结束),遵循“先进后出”或“后进先出”原则。
 在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(current Frame),与当前栈帧相对应的方法就是当前方法(CurrentMethod),定义这个方法的类就是当前类( current class) 。
 执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。

 不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
 Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
 栈帧内部结构
 局部变量表(Local variables)
 操作数栈(operand stack)(或表达式栈)
 动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
 一些附加信息

 局部变量表
 局部变量表也被称之为局部变量数组或本地变量表。
 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。
 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。
 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。
 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
 关于Slot的理解
 局部变量表,最基本的存储单元是slot(变量槽),index从0开始,到数组长度-1的索引结束。
 局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。
 在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot。
 byte、short、char在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true。
 long和double 则占据两个slot。
 JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。

 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上。
 如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问long或double类型变量)
 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。
 栈帧中的局部变量表中的槽位是可以重用的。
 补充说明
 在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
 操作数栈
 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop)
 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈。
 比如:执行复制、交换、求和等操作
 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
 操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的code属性中,为max_stack的值。
 栈中的任何一个元素都是可以任意的Java数据类型。32bit的类型占用一个栈单位深度,64bit的类型占用两个栈单位深度
 操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据访问。
 另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
 动态链接(或指向运行时常量池的方法引用)
 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接( Dynamic Linking)。比如: invokedynamic指令
 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

 方法的调用
 在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。
 静态链接:当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。
 动态链接:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。
 对应的方法的绑定机制为:早期绑定(EarlBinding)和晚期绑定(Late Binding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。根本原因是面向对象的多态性。
 早期绑定:如果被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
 晚期绑定:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。
 非虚方法:如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的,这样的方法称为非虚方法(不符合多态性)。
 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。其他方法称为虚方法(符合多态性)。
 子类对象的多态性的使用前提:类的继承关系;方法的重写
 虚方法表
 在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要一层层往上找会影响到执行效率。因此,为了提高性能,JVM采用在类的方法区建立一个虚方法表( virtual method table)(非虚方法不会出现在表中)来实现。使用索引表来代替查找。
 每个类中都有一个虚方法表,表中存放着各个方法的实际入口。虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。
 方法返回地址
 存放调加该方法的程序计数器(PC寄存器)的值。
 一个方法的结束,有两种方式:
 正常执行完成;
 出现未处理的异常,非正常退出。
 无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的PC寄存器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
 本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置程序计数器值等,让调用者方法继续执行下去。
 正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。
 常见问题
 举例栈溢出的情况?(StackOverflowError)
 通过-Xss设置栈的大小;O0M
 调整栈大小,就能保证不出现溢出吗?不能分配的栈内存越大越好吗?不是!
 垃圾回收是否会涉及到虚拟机栈?不会的!
 方法中定义的局部变量是否线程安全?具体问题具体分析

4. 本地方法接口

 什么是本地方法?
一个Native Method就是一个Java调用非Java代码的接口。标识符native可以与所有其它的java标识符连用,但是abstract除外。
 为什么要使用Native Method ?
 与Java环境外交互:有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因。作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序。
 与操作系统交互: 通过使用本地方法,我们得以用Java实现了JRE的与底层系统的交互,甚至JVM的一些部分就是用C写的。
 sun’s Java:sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。
 目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用socket通信,也可以使用web service等等,不多做介绍。

5. 本地方法栈

 Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
 本地方法是使用c语言实现的,也是线程私有的,允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)。
 如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个 stackoverflowError 异常。
 如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个outofMemoryError异常。
 它的具体做法是Native Method stack中登记native方法,在Execution Engine 执行时加载本地方法库。
 当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界,它和虚拟机拥有同样的权限。
 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。
 它甚至可以直接使用本地处理器中的寄存器。
 直接从本地内存的堆中分配任意数量的内存
 并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。
 在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一。

6. 堆

 堆的核心概述
 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
 Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
 堆内存的大小是可以调节的。
 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(ThreadLocal Allocation Buffer, TLAB)
 《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。(The heap is the run-time data area fromwhich memory for all class instances and arrays is allocated )
从实际使用角度看,“几乎”所有的对象实例都在这里分配内存。
 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾回收的时候才会被移除。具体过程:方法结束后,对应虚拟机栈中的方法栈帧将会出栈,栈帧的局部变量表对应销毁,而堆空间中对应的垃圾对象并不会马上被回收,这取决于GC垃圾回收器被触发时机,比如当内存不足时,GC开始工作回收垃圾对象。原因是:如果GC随着方法结束开始工作,方法频繁被调用,那么GC也会频繁工作,而GC频繁回收垃圾,则会影响到用户线程的执行效率,因为守护线程GC与用户线程不能并行执行,每次GC工作前需要进行STW(所有线程进入SafePoint),将用户线程(业务线程)挂起,进而影响用户线程执行效率,从而要减少GC工作频率。
 堆,是GC(Garbage collection,垃圾收集器)执行垃圾回收的重点区域。所有垃圾回收器的所有垃圾回收策略都会触发STW,只是时间长短不同而已。
 内存细分
从逻辑上划分,堆空间分为新生代、老年代和永久区/元空间。
但实际上,堆中只有新生代和老年代。所谓的永久区/元空间实际上是方法区的实现。如字符串常量池和运行时常量池在逻辑上属于方法区,但实际上存放在堆中。

 堆空间分代思想
为什么需要把Java堆分代?不分代就不能正常工作了吗?
经研究,不同对象的生命周期不同。70%-99%的对象是临时对象。
 新生代:有Eden、两块大小相同的survivor(又称为from/to,s0/s1)构成,to总为空。
 老年代:存放新生代中经历多次Gc仍然存活的对象。
 其实不分代完全可以,分代的唯一理由就是优化GC性能。如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室。GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC 的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。

 年轻代与老年代
存储在JVM中的Java对象可以被划分为两类:
 生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速;
 生命周期却非常长的对象,在某些极端的情况下还能够与JVM的生命周期保持一致。
 Java堆区进一步细分的话,可以划分为年轻代( YoungGen)和老年代((oldGen),其中年轻代又可以划分为Eden空间、Survivor0空间和survivor1空间(有时也叫做from区、to区)。

 堆空间的参数设置
 Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,
 -Xms:初始堆内存,默认为物理内存的1/64,等价于-XX:InitialHeapsize
 -Xmx:最大堆内存,默认为物理内存的1/4,等价于-XX:MaxHeapsize
 -XX:NewRatio:设置新生代与老年代的比例,默认值是2,新:老=1:2
 -XX:SurvivorRatio:设置新生代中Eden区与Survivor区的比例,默认值是8,Eden:Survivor=8:1:1
 -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
 -XX:+PrintGCDetails:输出详细的cc处理日志
打印gc简要信息: ①-XX:+PrintGC; ②-verbose: gc
 -XX:HandlePromotionFailure:是否设置空间分配担保
 开发中建议将初始堆内存和最大堆内存设置成相同的值。通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
Minor GC、Major GC与Full GC
JVM调优,目的在于降低GC行为的频率,从而尽可能减少对用户线程的影响。

JVM在进行Gc时,并非每次都对上面三个内存(新生代、老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代。
注意:方法区是概念,永久代或元空间是对方法区的具体实现。
针对HotSpot VM的实现,它里面的Gc按照回收区域又分为两大种类型:一种是部分收集( Partial Gc) ,一种是整堆收集(Fu1l Gc)
部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:
≥新生代收集(Minor Gc / Young Gc) :只是新生代的垃圾收集老年代收集(Major Gc / old Gc):只是老年代的垃圾收集。
√目前,只有cMS GC会有单独收集老年代的行为。
√注意,很多时候Major GC会和Full Gc混淆使用,需要具体分辨是老年代
回收还是整堆回收。
混合收集(Mixed Gc):收集整个新生代以及部分老年代的垃圾收集。
√目前,只有G1 Gc会有这种行为
整堆收集(Fu1l Gc):收集整个java堆和方法区的垃圾收集。

7. 对象的创建方式?对象创建的过程?对象创建,如何分配内存?如何处理并发安全问题?

 创建对象的方式

 对象实例化的过程
 加载类元信息;
 为对象分配内存;
 处理并发问题
 属性的默认初始化(零值初始化)
 设置对象头的信息–
 属性的显式初始化、代码块中初始化、构造器中初始化

 类加载检查:具体来说,当JVM遇到一条字节码 new 指令时,它会首先在 class文件中的常量池表(Constant Pool Table)检查能否找到这个类对应的符号引用,然后去方法区中的运行时常量池中查找该符号引用所指向的类是否已被 JVM 加载、解析和初始化过。 如果没有,那就先执行相应的类加载过程。如果有,那么进入下一步,为新生对象分配内存
 分配内存:在类加载检查后,就要为新生对象分配内存了。对象内存所需大小在类加载完成后便可以确定,内存分配方式根据Java堆中内存是否完整主要分为指针碰撞和空闲列表两种。
 初始化零值(属性的默认初始化):内存分配完成之后,JVM 会将分配到的内存空间(当然不包括对象头)都初始化为零值,比如boolean字段都初始化为false,int字段都初始化为0等。这保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的零值。如果使用了TLAB的话,初始化零值这项工作可以提前至TLAB分配时就顺便进行。
 设置对象头:对象在内存中存储的布局可以分为3块区域:对象头、实例数据和对齐填充。对齐填充仅仅起占位作用,没啥特殊意义,实例数据我们在上一步操作中进行了初始化零值,那么对于剩下的对象头中的信息来说,自然也是要进行一些赋值操作的。对象头包括两部分信息
 第一部分用于存储对象自身的运行时数据,如哈希值、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
 第二部分是类型指针,即对象指向它的类元数据的指针,确定该对象是哪个类的实例。
 执行init方法(属性的显示初始化):上述操作完成后,从JVM的角度看,一个新的对象已经产生了。但从我们程序开发人员的角度看,对象创建才刚刚开始,但是还没按照我们定义的构造函数来进行赋值,所有的字段都还是默认的零值。所以,一般执行完new指令后还会接着执行init方法,把对象按照程序员的意愿进行显示初始化(赋值),这样一个真正可用的对象才算完全生产出来。

 创建对象时,分配内存的过程?

 具体过程(一般情况)

new的对象先放新生代的伊甸园区;

如果伊甸园区放得下,直接为新对象分配内存;
如果伊甸园区放不下,垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),之后

如果伊甸园区放得下,直接为新对象分配内存;
如果对象较大,依然放不下,晋升老年代;
注意,垃圾回收器对伊甸园区进行垃圾回收的同时,还会对from区进行垃圾回收,只不过它是被动触发的,同时将伊甸园中的存活对象移动到to区;
如果to区放得下伊甸园区存活对象且对象存活没有超过阈值,就放在to区;
如果to区放不下或能放下但是对象存活超过阈值,伊甸园区存活对象就晋升到老年代;
以上情况中,可能会出现新对象或伊甸园区剩余存活对象晋升到老年代

如果老年代放得下,直接为对象分配内存;
如果依然放不下,垃圾回收器将对老年代进行垃圾回收(Major GC),之后

如果能放下,直接为对象分配内存;
如果依然放不下

如果不支持扩容,抛出OOM异常;
如果支持扩容,扩容后,依然放不下,抛出OOM异常。
JDK6
在发生Minor Gc之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。
如果大于,则此次Minor Gc是安全的
如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败。
如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。
如果大于,则尝试进行一次Minor Gc,但这次Minor cc依然是有风险的;
如果小于,则改为进行一次Full GC。
如果HandlePromotionFailure=false,则改为进行一次Full GC。
在JDK6 Update24之后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,JDK6 Update24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor Gc,否则将进行Full GC。
 注意:
 针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to.
 关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集。
 对象内存分配策略(特殊情况)
针对不同年龄段的对象分配原则如下所示:
 优先分配到Eden;
 大对象直接分配到老年代,设计程序时,尽量避免程序中出现过多的大对象;
 长期存活的对象分配到老年代;
 动态对象年龄判断:如果survivor区中相同年龄的所有对象大小的总和大于survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等至MaxTenuringThreshold中要求的年龄。
 空间分配担保:-XX:HandlePromotionFailure

 创建对象时,分配内存的方式?
为对象分配内存空间的任务通俗来说把一块确定大小的内存块从 Java 堆中划分出来给这个对象用。对象的内存分配方式会根据Java内存是否完整分为指针碰撞(完整)和空闲列表(不完整)两种:
 指针碰撞:假设为Java堆中内存是绝对完整的,所有用过的内存放到一边,空闲的内存放到另一边,中间放着一个指针作为分界点的指示器,所分配的内存就是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为指针碰撞。

 空闲列表:假设Java堆中的内存并不是完整的,已使用的内存和空闲内存都混在一起了,这时虚拟机需要维护一个列表,用来记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为空闲列表。
 注:选择哪种分配方式由Java堆是否完整决定,而Java堆是否完整由所采用的垃圾收集器是否带有压缩整理功能决定(或者说由垃圾收集器采用的垃圾收集算法来决定的)。
 因此,当使用Serial、ParNew等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效。
 而当使用 CMS 这种基于清除(Sweep)算法的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存。

 创建对象时,是如何处理并发安全问题的?
并发场景中,进行内存分配时,如果两个线程先后把对象引用指向了同一个内存区域,则出现线程安全问题。解决这个问题有两种可选方案:
 对分配内存空间的动作做同步处理。采用CAS机制,配合失败重试的方式保证更新操作的线程安全性(原子性)。该方案每次分配时都需要进行同步控制,这种是比较低效的;
 TLAB分配,即本地线程分配缓冲(Thread Local Allocation Buffer)

 从内存模型而不是垃圾收集的角度,对堆中新生代中的Eden区继续进行划分,JVM为每个线程分配了一个私有缓存区域。
 JVM会将TLAB作为对象内存分配的首选策略,如果对象被分配到了TLAB中就不用考虑线程安全问题,而当TLAB(1%)满的时候,对象就只能放在Eden区其它共享区域,此时就需要加锁,乐观锁:CAS+失败重试。
 TLAB中的对象并不是私有的,这个技术只是为了解决在对给对象分配内存的时候,不需要加锁,Eden区其它共享区域为对象分配内存分配是需要加锁的。换句话说,这块内存只是在“分配”这个动作上是多个线程独占的,至于在读取、垃圾回收等动作上都是线程共享的,而且在使用上也没有什么区别。
 另外,它包含且仅作用于新生代的Eden Space,对象被创建时,会优先放到这里,但是还是会被垃圾回收或者被移到幸存者区、老年代等,而且新生代分配不了内存的大对象会直接进入老年代。因此,通常为多个小的对象分配内存,比大的对象分配起来更加高效。设计程序时,尽量避免程序中出现过多的大对象。
 虽然使用TLAB可以解决为多线程分配内存空间的问题,但TLAB这块内存自身从堆中划分出来的过程也可能存在线程安全问题,可能会出现多个线程竞争同一块内存空间作为TLAB,所以在对于TLAB的分配过程,还是需要进行同步控制(加锁)的,但是这种开销相比于每次为单个对象划分内存时候对进行同步控制的要低的多。
 总结:多线程同时分配内存时,使用TLAB可以解决为多线程分配内存空间的冲突,同时还能够提升内存分配的吞吐量,因此又称快速分配策略。
 为什么有TLAB ( Thread Local Allocation Buffer ) ?
 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据。
 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。
 为避免多个线程操作同一地址,需要使用加锁等(串行)机制,进而影响分配速度。
 堆是分配对象存储的唯一选择吗?
 在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。
 逃逸分析的基本行为就是分析对象动态作用域
 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。
 使用逃逸分析,编译器可以对代码做如下优化
 栈上分配。JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。
 同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
 分离对象或标量替换。经过逃逸分析,发现一个对象不逃逸时, JIT编译器在编译期间进行优化,使用标量替换聚合量(对象),然后将这些标量存储在该方法栈中的栈帧的局部变量表中。
 开发中能使用局部变量的,就不要使用在方法外定义。
 总结:这项技术到如今也并不是十分成熟的。虽然经过逃逸分析可以做标量替换、栈上分配和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析,这其实也是一个相对耗时的过程。据我所知,Oracle Hotspot在JVM中并未真正应用栈上分配,最终结论是所有对象实例都是分配在堆上,这是一个否定之否定的过程。

8. 对象的内存布局(对象头里面有什么?)

 对象头里有什么信息?
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头、实例数据和对齐填充。
 对象头:对象头包含两部分信息
 一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
 另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
 实例数据:实例数据部分是对象真正存储的有效信息,也是代码中所定义的各种类型的字段内容。
 对齐填充:HotSpot虚拟机的自动内存管理系统要求对象起止地址必须是8字节的整数倍,也就是说对象的大小必须是8字节的整数倍,对象头部分正好是8字节的整数倍,所以,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全,对齐填充并不是必然存在的,也没有特殊的含义,只是起到了占位符的作用。

9. 对象的访问定位(对象的访问方式有哪些?)

 对象的访问方式有哪些?
建立对象就是为了使用对象,JVM是通过栈上的 reference访问到其内部的对象实例(堆中的对象实例)。目前主流的访问方式有使用句柄和直接指针两种。
 句柄访问
 Java堆中会划分出一块内存来作为句柄,reference中存储的是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
 使用句柄访问对象的优势:reference中存储的是稳定的句柄地址,在对象移动时(垃圾回收时会经常移动对象)只会改变句柄中的实例数据指针,无需改变reference。

 直接指针(HotSpot默认)
 如果使用直接指针访问,reference中存储的就是对象地址,堆中对象存放的是对象的实例数据和指向对象类型数据的指针。
 因为对象访问时节省了一次指针定位的时间开销,速度更快,由于对象访问非常频繁,所以性能表现更好。

10. 方法区

 栈、堆、方法区的交互关系

 方法区的基本理解
《Java虚拟机规范》中明确说明:尽管所有的方法区在逻辑上是属于堆的一部分,但对于HotSpotJVM具体落地实现而言,方法区还有一个别名叫做Non-Heap (非堆),目的就是要和堆分开。所以,方法区看作是一块独立于Java堆的内存空间。
 方法区与Java堆一样,是各个线程共享的内存区域。
 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。逻辑上连续,物理上可以不连续。
 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误: java.lang. outOfMemoryError:PermGen space 或者java.lang.OutOfMemoryError: Metaspace
 加载大量的第三方的jar包; Tomcat部署的工程过多(30-50个)大量动态的生成反射类
 关闭JVM就会释放这个区域的内存。

 Hotspot中方法区的演进
 方法区可以看做是一种规范,jdk1.7及以前用永久代(JVM内存)实现,jdk1.8开始用元空间(本地内存,非JVM内存)实现。
 本质上,方法区和永久代并不等价,仅是对hotspot而言的。《Java虚拟机规范》对如何实现方法区,不做统一要求。

 方法区的内部结构
它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

 类型信息
对每个加载的类型(类claps、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:
 这个类型的完整有效名称(全名=包名.类名
 这个类型直接父类的完整有效名(对于interface或是java.lang.0bject,都没有父类
 这个类型的修饰符(public,abstract,final的某个子集)
 这个类型直接接口的一个有序列表
 域(Field)信息
 JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
 域的相关信息包括:域名称、域类型、域修饰符(public, private,protected,static,final,volatile,transient的某个子集)
 方法(Methon)信息
 JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
 方法名称
 方法的返回类型(或void)·方法参数的数量和类型(按顺序)
 方法的修饰符(public, private, protected, static, final,synchronized, native, abstract的一个子集)
 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
 异常表(abstract和native方法除外)
 每个异常处理的开始位置、结束位置代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
 常量池运行时常量池
 常量池
 一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池(Constant Pool),包括各种数量值、字面量和对类型、域和方法的符号引用。
 常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。
 运行时常量池
 常量池(Constant Pool Table)是字节码文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
 JVM为每个已加载的类型(类或接口)都创建并维护一个对应的运行时常量池,池中数据项和数组元素一样,通过索引下标访问。
 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。
 运行时常量池,相对于class文件常量池的另一重要特征是:具备动态性。
 non-final的类变量(静态变量)
 静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分。
 类变量被类的所有实例共享,即使没有类实例时你也可以访问它。
 补充说明:全局常量: static final
被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。
 方法区的演进细节
 首先明确:只有Hotspot才有永久代。BEA JRockit、IBM J9等来说,是不存在永久代的概念的。原则上如何实现方法区属于虚拟机实现细节,不受《Java虚拟机规范》管束,并不要求统一。
 Hotspot中方法区的变化

 JDK1.6

 JDK1.7

此时方法区还是在JVM内存(虚拟内存与物理内存存在映射关系)中实现的
 JDK1.8

此时方法区已经独立出来,不再试JVM内存中实现,而在本地内存中实现的。
 为什么要用元空间替换永久代?
由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。这项改动是很有必要的,原因有
 为永久代设置空间大小是很难确定的。在某些场景下,如果动态加载类过多,容易产生Perm区的OOM。比如某个实际web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内因此,默认情况下,元空间的大小仅受本地内存限制。
 对永久代进行调优是很困难的。因为对方法区进行垃圾回收主要收集常量和类型信息,在判断常量和类不在被使用其实也挺耗时的,Full GC其实很浪费时间,所以调优也会变得困难,尽量少出现Full GC,相比之下,选择本地内存实现方法区还是会好一点。
 字符串常量池(StringTable)为什么要调整位置?
JDK7中将StringTable放到了堆空间中。因为永久代的回收效率很低,在full gc的时候才会触发。而full gc是老年代的空间不足、永久代不足时才会触发。这就导致stringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。
 方法区的垃圾回收
 有些人认为方法区,如HotSpot虚拟机中的元空间或者永久代是没有垃圾回收行为的,其实不然。《Java虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾回收。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在,如JDK 11时期的ZGC收集器就不支持类卸载。
 一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。
 方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。
 方法区内运行时常量池中主要存放的两大类常量:字面量和符号引用。字面量比较接近Java语言层面的常量概念,如文本字符串、被声明为final的常量值等,而符号引用则属于编译原理方面的概念,包括下面三类常量:
 类和接口的全限定名
 属性的名称和描述符。注意描述符的概念是针对Java字节码的,用来描述字段的数据类型、方法的参数列表,包括数量、类型以及顺序和返回值。
 方法的名称和描述符。方法描述符用来描述方法,用来描述方法的参数列表和返回值。
 HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。所以回收废弃常量与回收Java堆中的对象实例非常类似。
 判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:
 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
 加载该类的类加载器已经被回收,这个条件通常是很难达成的。
 该类对应的java.lang.class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

11. StringTable

 String基本特性
 使用一对""引起来表示。
 声明为final的,不可被继承
 实现了serializable接口:表示字符串是支持序列化的。
 实现了Comparable接口:表示string可以比较大小。
 在jdk8及以前内部定义了final char[ ] value用于存储字符串数据,jdk9时改为byte[ ],节约了一些空间。
 字面量创建字符串会从字符串常量池中获取,如果没有再创建,new创建不会从常量池中获取,每次都会新开辟一个空间,不会加入到字符串常量池中。
 代表不可变的字符序列。简称不可变性
 当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。
 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
 当调用string的replace ()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
 字符串常量池存的是字符串本身,不会存储相同内容的字符串。运行时常量池里存的是字符串引用,指向字符串常量池。字符串常量池逻辑上属于运行时常量池,但具体的实现在JDK7及以后是在堆中。
 String的String Pool是一个固定大小的Hashtable,默认值大小长度是1009。如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降;
 使用一xx : StringTablesize可设置stringTable的长度;
 在Jdk6中stringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。StringTablesize设置大小没有要求
 在Jdk7中,StringTable的长度默认值是60013,stringTablesize设置大小没有要求;
 Jdk8开始,设置stringTable的长度的话,1009是可设置的最小值。
 String内存分配
 在Java语言中有8种基本数据类型和一种比较特殊的类型string。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。
 常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种。
 直接使用双引号声明出来的String对象会直接存储在常量池中。比如:string info = “atguigu.com” ;
 如果不是用双引号声明的string对象,可以使用string提供的intern ()方法。
 Java 6及以前,字符串常量池存放在永久代。
 Java 7 中oracle 的工程师对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到Java堆内。
 所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样可以让你在进行调优应用时仅需要调整堆大小就可以了。
 字符串常量池概念原本使用得比较多,但是这个改动使得我们有足够的理由让我们重新考虑在Java 7 中使用string.intern ( )。
 Java8元空间,字符串常量在堆。

 StringTable为什么要调整?
 PermSize默认较小
 永久代垃圾回收概率低,字符串数量较多时,容易发生内存溢出。
 字符串拼接操作
 常量与常量的拼接结果在常量池,原理是编译期优化;

 常量池中不会存在相同内容的常量;
 字符串拼接,只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder;

 如果拼接的结果调用intern(),判断字符串常量池中是否存在结果值,如果存在,则返回常量池中字符串的地址;如果字符串常量池中不存在,则在常量池中创建,并返回此字符串的地址。
 注意:
 字符串拼接操作不一定使用的是StringBuilder! 如果拼接符号左右两边都是字符串常量或常量引用,则仍然使用编译期优化,即非StringBuilder的方式。
 针对于final修饰类、方法、基本数据类型、引用数据类型的量的结构时,能使用上final的时候建议使用上。
 intern()的使用
 通俗点讲,intern()的使用就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(string Intern Pool)。
 如何保证变量s指向的是字符串常量池中的数据呢?有两种方式:
 方式一: string s = “shkstart”;//字面量定义的方式
 方式二: 调用intern()
string s = new String( “shkstart”).intern();
string s = new stringBuilder( “shkstart”).tostring().intern();等等,只要调用了intern();

 问题一:new String( “ab”)会创建几个对象呢? 看字节码,就知道是两个。
 对象1: new关键字在堆空间创建的
 对象2: 字符串常量池中的对象。字节码指令: ldc
 问题二:new string( “a”) +new string( “b”)会创建几个对象呢?
 对象1: new stringBuilder()
 对象2:new string(“a”)对象
 对象3:常量池中的"a"对象
 对象4:new string( “b”)对象
 对象5:常量池中的"b”
 深入剖析:stringBuilder的tostring( ): 对象6 : new String(“ab”)
强调一下, tostring()的调用,在字符串常量池中,没有生成"ab"

 问题三

 问题三的拓展

 总结string的intern()的使用
 Jdk1.6中,将这个字符串对象尝试放入字符串常量池中
 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
 如果串池没有,则会在串池中创建该字符串对象,并返回串池中的对象地址。
 Jdk1.7/1.8中,将这个字符串对象尝试放入字符串常量池中
 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
 如果串池没有,则不会创建该字符串对象,而是创建一个引用,指向堆空间中该字符串对象的内存地址,并返回该引用地址。
 使用intern()测试执行效率:空间使用上
结论:对于程序中大量存在的字符串,尤其其中存在很多重复字符串时,使用intern()可以节省内存空间。
通俗点讲,Interned string就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(string Intern Pool)。

12. 垃圾回收概述

 什么是垃圾?
垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
 为什么需要垃圾回收(GC)?
 在Java中垃圾回收的目的是回收释放不再被引用的实例对象,这样做可以减少内存泄漏、内存溢出问题的出现;
 除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便JVM将整理出的内存分配给新的对象;
 随着应用程序所应付的业务越来越庞大、复杂,用户越来越多,没有Gc就不能保证应用程序的正常进行。
 Java垃圾回收机制
 自动内存管理。无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险。
 自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发。
 垃圾回收器可以对年轻代回收,也可以对老年代回收,甚至是全堆和方法区的回收。
 其中,Java堆是垃圾收集器的工作重点。
 从次数上讲,频繁收集Young区,较少收集old区,基本不动Perm区(元空间)
 总结
只有在方法区和堆中才存在垃圾回收行为(GC),而另外三部分不存在垃圾回收行为,其中虚拟机栈和本地方法栈可能会出现内存溢出(StackOverFlowError)问题,程序计数器不仅不存在垃圾回收行为,而且会也不会出现内存溢出问题,内存较小。

什么时候进行垃圾回收?(什么情况下触发垃圾回收?)
如何回收?
JVM有哪几种常用的垃圾回收器?各有什么优劣?
如何选择合适的垃圾回收算法?
垃圾回收算法的原理是什么?

13. 垃圾回收的相关算法

 垃圾标记阶段
 在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。
 那么在JVM中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
 判断对象存活一般有两种方式:引用计数算法和可达性分析算法。
 引用计数算法
 引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
 对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
 优点
 实现简单,垃圾对象便于辨识;
 判定效率高,回收没有延迟性。
 缺点
 空间:它需要单独的字段存储计数器,增加了存储空间的开销。
 时间:每次赋值都需要更新计数器,伴随着加法和减法操作,增加了时间开销。
 引用计数器有一个严重的问题,即无法处理循环引用问题。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。

 可达性分析算法(根搜索算法、追踪性垃圾收集算法)
 相对于引用计数算法而言,可达性分析算法同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
 相较于引用计数算法,可达性分析算法就是Java选择的。
 所谓"GC Roots"根集合就是一组必须活跃的引用。
 基本思路:
 可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)。
 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。

 在Java语言中,GC Roots包括以字几类元素: 除堆以外的周边的引用都可以看成是GC Roots。
 虚拟机栈中对象的引用
比如:各个线程被调用的方法中使用到的参数、局部变量等。本地方法栈内JNI(通常说的本地方法)引用的对象
 方法区中类静态属性对堆中对象的引用
比如: Java类的引用类型静态变量
 方法区中的常量引用
比如:字符串常量池(String Table)里的引用
 所有被同步锁synchronized持有的对象
 Java虚拟机内部的引用
基本数据类型对应的class对象,一些常驻的异常对象(如:NullPointerException、OutOfMemoryError),系统类加载器。

 局部回收,回收区域之外的引用都可临时作为GC Roots
除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整Gc Roots集合。比如:分代收集和局部回收(Partial GC)。
如果只针对Java堆中的某一块区域进行垃圾回收(比如:典型的只针对新生代),必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的,这个区域的对象完全有可能被其他区域的对象所引用,这时候就需要一并将关联的区域对象也加入GC Roots集合中去考虑,才能保证可达性分析的准确性。
 对象的finalization机制
 Java语言提供了对象终止(finalization)机制来允许开发入员提供对象被销毁之前的自定义处理逻辑。
 当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法。
 finalize ()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
 永远不要主云动调用某个对象的finalize()方法,应该交给垃圾回收机制调用。理由包括下面三点:
 在finalize( )时可能会导致对象复活。
 finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize ()方法将没有执行机会。
 一个糟糕的finalize ()会严重影响Gc的性能。
 由于finalize ()方法的存在,虚拟机中的对象,一般处于三种可能的状态。
如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用了。一般来说,此对象需要被回收。但事实上,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段。一个无法触及的对象有可能在某一个条件下“复活”自己,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态。如下:
 可触及的:从根节点开始,可以到达这个对象。
 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。
 不可触及的:对象的finalize ()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize ()只会被调用一次。
以上3种状态中,是由于finalize ()方法的存在,进行的区分。只有在对象不可触及时才可以被回收。
 判定一个对象objA是否可回收,至少要经历两次标记过程:
 如果对象objA到 Gc Roots没有引用链,则进行第一次标记。
 进行筛选,判断此对象是否有必要执行finalize ()方法
 如果对象objA没有重写finalize ()方法或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA被判定为不可触及的。
 如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法执行。
 finalize()方法是对象逃脱死亡的最后机会,稍后cc会对F-Queue队列中的对象进行第二次标记。如果objA在finalize ()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA会被移出“即将回收”集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize方法只会被调用一次。
 垃圾清除阶段
当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。
目前在JVM中比较常见的三种垃圾收集算法是标记―清除算法(Mark-Sweep )、复制算法(copying )、标记–压缩算法(Mark-Compact )。
 标记–清除算法(Mark-Sweep)
它是一种非常基础和常见的垃圾收集算法,该算法被J.McCarthy等人在1960年提出并并应用于Lisp语言。
 执行过程:当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
 标记: collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
 清除:collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。

 缺点
 效率不算高
 在进行cc的时候,需要停止整个应用程序,导致用户体验差
 这种方式清理出来的空闲内存是不连续的,产生内存碎片。需要维护一个空闲列表
 注意:何为清除?
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放。
 复制(Copying)算法
 背景:为了解决标记-清除算法在垃圾收集效率方面的缺陷。
 核心思想:将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
 优点
 没有标记和清除过程,实现简单,运行高效
 复制过去以后保证空间的连续性,不会出现“碎片”问题。
 缺点
 此算法的缺点也是很明显的,就是需要两倍的内存空间。
 对于C1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。
 特别的
复制算法需要复制的存活对象数量并不会太大,或者说非常低才行。即特别适合垃圾对象很多,存活对象很少的场景;例如:Young区(新生代)的Survivor0和Survivor1区。
 应用场景
在新生代,对常规应用的垃圾回收,一次通常可以回收70%-99%的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。

 标记-压缩(或标记-整理、Mark - Compact)算法
 背景:复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
标记―清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以JVM 的设计者需要在此基础之上进行改进。标记–压缩(Mark - Compact)算法由此诞生。
 执行过程
 第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象
 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。
 之后,清理边界外所有的空间。
 标记-压缩算法与标记清除算法的区别
 标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法。
 二者的本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策。
 可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,使用指针碰撞的方式为新对象分配内存,这比维护一个空闲列表显然少了许多开销。
 优点
 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
 消除了复制算法当中,内存减半的高额代价。
 缺点
 从效率上来说,标记-整理算法要低于复制算法。
 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。
 移动过程中,需要全程暂停用户应用程序。即:STW,因为需要移动存活对象,所以暂停时间较长。

 总结:对比三种算法

 效率上来说,复制算法是当之无愧的老大但是却浪费了太多内存。而为了尽量兼顾上面提到的三个指标,标记-整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段。
 注意:复制算法(标记-复制 JVM第三版) 实际实现是在GC ROOT遍历过程中进行的对象复制,没有在遍历的过程进行标记,才说没有标记过程,时间复杂度O(n),而可达性分析算法不仅是遍历GC Root,还进行了标记。
换句话说,复制算法是在GC ROOT遍历过程中进行的对象复制,可达性分析算法是在GC ROOT遍历过程中进行了标记。
 分代收集算法
 背景:前面所有这些算法中,并没有一种算法可以完全替代其他算法,它们都具有自己独特的优势和特点。分代收集算法应运而生。不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。目前几乎所有的GC都是采用分代收集(Generational Collecting)算法执行垃圾回收的。在HotSpot中,基于分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代各自的特点。
 年轻代(Young Gen)
 年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。(小、短、低、频)
这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。
 老年代(Tenured Gen)
 老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。(大、长、高、频)
 这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记-清除或者是标记-清除与标记-整理的混合实现。
 Mark标记阶段的开销与存活对象的数量成正比。
 Sweep清除阶段的开销与所管理区域的大小成正相关。
 Compact整理阶段的开销与存活对象的数据成正比。
以HotSpot中的CMS回收器为例,CMS是基于Mark-Sweep标记清除(正主)实现的,对于对象的回收效率很高。而对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old回收器作为补偿措施(备胎):当由于内存碎片导致空间不足时才考虑内存整理(碎片导致的Concurrent Mode Failure时),将采用Serial Old执行Full GC以达到对老年代内存的整理。分代的思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年代。
 增量收集算法(低延迟)
 背景
在STW(stop the world)状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。引入增量收集(Incremental Collecting)算法解决该问题(延迟时间过长)。
 基本思想
如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程(守护线程)和用户线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到用户线程。依次反复,直到垃圾收集完成。
总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法着重处理线程间冲突问题,协调用户线程和垃圾回收线程交替执行,允许垃圾回收线程以分阶段的方式完成标记、清理或复制工作。
 缺点
使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程上下文切换比较消耗性能,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
 分区算法(低延迟)
 一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
 分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。
 每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。

 最后,总结一下,以上这些算法只是基本的算法思路,实际GC实现过程要复杂的多,目前还在发展中的前沿GC都是复合算法,并且并行和并发兼备。

14. 垃圾回收相关概念

 System.gc()的理解
 在默认情况下,通过System. gc ()或Runtime.getRuntime ().gc ()的调用,会显式触发Full Gc,同时对老年代和新生代进行回收,尝试释放被手弃对象占用的内存。
 然而System.gc ()调用附带一个免责声明,无法保证对垃圾收集器的调用。
 JVM实现者可以通过System.gc ()调用来决定JVM的Gc行为。而一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。在一些特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用System.gc (),提醒jvm的垃圾回收器执行GC,不能保证GC执行时间,也不能保证一定会执行【finalize()】。
 Stop The World
 Stop-The-World,简称STW,指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。
 可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿。
 分析工作必须在一个能确保一致性的快照中进行。
 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上。
 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证。
 被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以我们需要减少STW的发生。
 STW事件和采用哪款GC无关,所有的GC都有这个事件。哪怕是G1也不能完全避免Stop-The-World情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。
 STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。开发中不要用system.gc ( ),会导致stop–the-world的发生。
 垃圾回收的并行与并发
 并发(Concurrent)
并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换,由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行。

从一段时间来看,有多个任务同时在执行,从单一时间片上看,只有一个任务在执行,实际上就是CPU在快速切换任务交替执行。
 并行(Parellel)
当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,我们称之为并行(Parallel)。其实决定并行的因素不是CPU的数量,而是CPU的核心数量,比如一个CPU多个核也可以并行。适合科学计算,后台处理等弱交互场景。

 并发VS并行
 并发,指的是多个任务,在同一时间段内同时发生了。(宏观角度,时间段)
 并行,指的是多个任务,在同一时间点上同时发生了。(微观角度,时间点)
 并发的多个任务之间是互相抢占资源的。
 并行的多个任务之间是不互相抢占资源的
 只有在多CPU或者一个CPU多核的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。
 垃圾回收的并发与并行
 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行,注意这里只是理论上的概念,毕竟还存在STW。如:CMS、G1

 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。如ParNew、Parallel Scavenge、Parallel old;
 串行(serial)
 相较于并行的概念,单线程执行。
 如果内存不够,则程序暂停,启动JVM垃圾回收器进行垃圾回收。回收完,再启动程序的线程。

 安全点与安全区域
安全点(Safe Point)
 程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点(Safe Point) ”。
 Safe Point的选择很重要,如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题。大部分指令的执行时间都非常短暂,通常会根据“是否具有让程序长时间执行的特征”为标准。比如:选择一些执行时间较长的指令作为Safe Point,如方法调用、循环跳转和异常跳转等。
 如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?
 抢先式中断:(目前没有虚拟机采用了)首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。
 主动式中断:设置一个中断标志,各个线程运行到Safe Point的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。
安全区域(Safe Region)
 Safe Point机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safe Point。但是,程序“不执行”的时候呢?例如线程处于sleep 状态或Blocked 状态,这时候线程无法响应JVM的中断请求,“走”到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况,就需要安全区域(Safe Region)来解决。
 安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始cc都是安全的。我们也可以把 Safe Region看做是被扩展了的Safe Point。
 实际执行时
当线程运行到Safe Region的代码时,首先标识已经进入了Safe Region,如果这段时间内发生GC,JVM会忽略标识为Safe Region状态的线程,STW暂停此状态的用户线程,开始GC。
当线程即将离开Safe Region时,会检查JVM是否已经完成GC,如果完成了,则继续运行,否则线程必须等待,直到JVM已经完成GC工作,收到可以安全离开Safe Region的信号为止。
 Java引用 强、软、弱、虚

 你开发中使用过WeakHashMap吗?
作为容器,WeakHashMap.Entry和HashMap.Node 的不同点在于,WeakHashMap.Entry 继承了WeakReference。HashMap 作为强引用对象在没有主动将 key 删除时是不会被 JVM 回收的,可能会出现OOM 错误,而WeakHashMap 内部是通过弱引用来管理 Entry 的,在进行二次垃圾回收时(内存不足)进行回收。
同样,ThreadLocal中一个设计亮点是ThreadLocalMap中的Entry结构的Key用到了弱引用。试想如果使用强引用,等于ThreadLocalMap中的所有数据都是与Thread的生命周期绑定,这样很容易出现因为大量线程持续活跃导致的内存泄漏。使用了弱引用的话,JVM触发GC回收弱引用后,ThreadLocal在下一次调用get()、set()、remove()方法就可以删除那些ThreadLocalMap中Key为null的值,起到了惰性删除释放内存的作用。

15. Java内存泄漏和内存溢出是什么?如何避免?

 内存泄露 memory leak
动态分配的内存空间,在使用完毕后未释放,结果导致一直占据该内存单元,直到程序结束。一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。即memory leak可能会导致out of memory!
 举例
 单例模式:单例的生命周期和应用程序是一样长,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。
 资源未关闭:一些提供close的连接资源未手动关闭导致内存泄漏。如数据库连接(dataSourse.getConnection() )网络连接(socket)和IO连接必须手动close,否则是不能被回收的。
 内存泄露的原因
 单例模式造成的内存泄漏
由于单例模式的静态特性使得其生命周期和程序的生命周期一样长,如果一个对象已经不再需要使用了,而单例对象还持有该对象的引用,就会使得该对象不能被正常回收,从而导致了内存泄漏。
 非静态内部类或匿名内部类中创建静态实例造成的内存泄漏
因为在Java中,非静态内部类或匿名类内部类都会隐式地持有其所属外部类的一个强引用,但是静态内部类却不会。而非静态内部类或匿名类内部类如果又创建了一个静态的实例,该静态实例的生命周期和程序的一样长,则该静态实例一直会持有外部类的强引用,从而导致JVM就无法回收外部类内存资源,造成内存泄漏。
解决办法:外部类内声明一个非静态的内部类时,如果考虑防止内存泄露的话,应当显示地声明此内部类持有外部类的一个弱引用。
 集合容器中的内存泄露
通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。
解决方法:在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。
 改变哈希值造成的内存泄露
例如在一个对象存储到HashSet后,改变了对象中参与计算哈希值的字段,那么会导致对象的哈希值发生变化,和之前存入HashSet的哈希值不同,也就无法通过当前对象的引用在HashSet中找到这个对象,无法从HashSet中删除对象,则造成内存泄漏。
解决办法:利用String类型的变量当作HashMap的key。这也是为什么通常利用String类型的变量当作HashMap的key,因为String类型是不可变的。
 资源未关闭造成的内存泄漏
每当建立一个连接,JVM就会为这些资源分配内存。比如数据库连接、文件输入输出流、网络连接、IO连接等等。
解决方法:关闭资源连接。例如在数据库连接后不再使用时,必须调用close()释放与数据库的连接,否则会造成大量对象无法被回收进而造成内存泄漏。
 ThreadLocal的错误使用
ThreadLocal主要用于创建本地线程变量,不合理的使用也有可能会造成内存泄漏。ThreadLocalMap中key设计为弱引用,所以被key被弱引用关联的对象也会被回收,而value还存在着强引用。简而言之,ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。
解决方法:使用完ThreadLocal后,执行remove操作,清除value值,避免出现内存泄露。
总结:因此良好的代码规范,可以有效地避免这些错误。

 内存溢出out of memory
指程序在申请内存时,没有足够的内存空间供其使用。javadoc中对outOfMemoryError的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。即发生OOM之前,一般至少会进行一次Full GC。
当然,也不是在任何情况下垃圾收集器都会被触发的。比如,我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接抛出OutOfMemoryError。
 OutOfMemoryError:PermGen space
Permanent Generation space 这个区域主要用来保存加来的Class的一些信息,在程序运行期间属于永久占用的,Java的GC不会对他进行释放,所以如果启动的程序加载的信息比较大,超出了这个空间的大小,就会发生溢出错误;
解决办法:无非就是增加空间分配了——增加java虚拟机中的XX:PermSize和XX:MaxPermSize参数的大小,其中XX:PermSize是初始永久保存区域大小,XX:MaxPermSize是最大永久保存区域大小。
 OutOfMemoryError:Java heap space
heap 是Java内存中的堆区,主要用来存放对象,当对象太多超出了空间大小,GC又来不及释放的时候,就会发生溢出错误。
Java中对象的创建是可控的,但是对象的回收是由GC自动的,一般来说,当已存在对象没有引用(即不可达)的时候,GC就会定时的来回收对象,释放空间。但是因为程序的设计问题,导致对象可达但是又没有用(即前文提到的内存泄露),当这种情况越来越多的时候,问题就来了。
针对这个问题,我们需要做一下两点:
1、检查程序,减少大量重复创建对象的死循环,减少内存泄露。
2、增加Java虚拟机中Xms(初始堆大小)和Xmx(最大堆大小)参数的大小。
 StackOverFlowError:Java stack space
stack是Java内存中的栈空间,主要用来存放方法中的变量,参数等临时性的数据的,发生溢出一般是因为分配空间太小,或是执行的方法递归层数太多创建了占用了太多栈帧导致溢出。针对这个问题,除了修改配置参数-Xss参数增加线程栈大小之外,优化程序代码也同样重要。
 内存溢出的原因
 Java虚拟机的堆内存设置不合理,太小。
 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
 内存中加载的数据量过于庞大,如一次从数据库取出过多数据,内存不够用了;
 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
 代码中存在死循环或循环产生过多重复的对象实体;
 启动参数内存值设定的过小
 长期的内存泄漏也会导致内存溢出
 内存溢出的解决方案:
 第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)
 第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。
 第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。重点排查前面提到的常见造成内存溢出情况的代码:
 检查对数据库查询中,是否有一次获得全部数据的查询;
 检查List、map等集合对象是否有使用完后,未清除的问题;
 检查代码中是否有死循环或递归调用或因循环产生过多重复的对象实体;
 第四步,使用内存查看工具动态查看内存使用情况。

16. 垃圾回收器

 GC分类
 按线程数分,可以分为串行垃圾回收器和并行垃圾回收器。

 串行回收指的是在同一时间段内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。
 在诸如单CPU处理器或者较小的应用内存等硬件平台不是特别优越的场合,串行回收器的性能表现可以超过并行回收器和并发回收器。所以,串行回收默认被应用在客户端的client模式下的JVM中
 在并发能力比较强的CPU上,并行回收器产生的停顿时间要短于串行回收器。
 和串行回收相反,并行回收可以运用多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了“stop-the-world”机制。
 按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器。
 并发式垃圾回收器与应用程序线程交替工作以尽可能减少应用程序的停顿时间。
 独占式垃圾回收器(Stop the world)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束。

 按碎片处理方式分,可分为整理式垃圾回收器和整理垃圾回收器。
 整理式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片。
 非整理式的垃圾回收器不进行这步操作。
 按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器。

 评估GC的性能指标
 STW暂停时间:执行垃圾回收时,程序的工作线程被暂停的时间。
 吞吐量:运行用户代码的时间占总运行时间的比例
(总运行时间:用户线程的运行时间+垃圾回收线程的时间)
 内存占用:Java堆区所占的内存大小。
 回收频率:相对于应用程序的执行,回收操作发生的频率。
 回收开销:吞吐量的补数,垃圾回收所用时间与总运行时间的比例。
 快速:一个对象从诞生到被回收所经历的时间。
 吞吐量VS STW
 吞吐量优先(高吞吐量),意味着在单位时间内,STW的时间最短:0.2 + 0.2 = 0.4
 暂停时间优先(低延迟),意味着尽可能让单次STW的时间最短:0.1 + 0.1 + 0.1 +0.1 + 0.1 = 0.5

 两者不可兼得,最好取一个折中点,平衡内存回收(GC)的执行频率。
 现在标准:在最大吞吐量优先的情况下,降低停顿时间。

 不同的垃圾回收器概述
 三种分类,七款经典垃圾回收器
 串行回收器:Serial、Serial Old
 并行回收器:ParNew、Parallel Scavenge、Parallel Old
 并发回收器:CMS、G1
 七款经典收集器与垃圾分代之间的关系

 新生代收集器: Serial、ParNew、Parallel Scavenge
 老年代收集器: Serial Old、Parallel old、CMS
 整堆收集器:G1

 Serial回收器∶串行回收 + 复制算法 + STW机制
 Serial收集器作为HotSpot中client模式下默认新生代垃圾回收器。

 Serial Old回收器∶串行回收 + 标记-整理算法 + STW机制
 Serial Old是运行在client模式下默认老年代的垃圾回收器。
 Serial Old在server模式下主要有两个用途:
①与新生代的Parallel Scavenge配合使用 ②作为老年代CMS收集器的后备垃圾回收方案。
 这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

 总结:这种垃圾收集器大家了解,现在已经不用串行的了。而且在限定单核CPU才可以用。现在都不是单核的了。对于交互较强的应用而言,这种垃圾收集器是不能接受的。一般在Javaweb应用程序中是不会采用串行垃圾收集器的。

 ParNew回收器∶并行回收 + 复制算法 + STW机制
 如果说Serial GC是年轻代中的单线程垃圾收集器,那么ParNew收集器则是Serial收集器的多线程版本。
Par是Parallel的缩写,New:只能处理的是新生代。
 ParNew是很多JVM运行在server模式下新生代的默认垃圾收集器。
 对于新生代,回收次数频繁,使用并行方式高效。
 对于老年代,回收次数少,使用串行方式节省资源。(CPU并行需要切换线程,串行可以省去切换线程的资源)

 由于ParNew收集器是基于并行回收,那么是否可以断定ParNelw收集器的回收效率在任何场景下都会比serial收集器更高效?
 如果在多CPU或多核CPU的环境下,ParNew基于并行回收可以更快速地完成垃圾回收,提升程序的吞吐量。
 如果在单个CPU的环境下,ParNewl回收器并不比Serial 回收器更高效。虽然Serial回收器是基于串行回收,但是由于CPU不需要频繁地做任务切换,因此可以有效避免多线程交互过程中产生的一些额外开销。因为除Serial外,目前只有ParNew GC能与CMS回收器配合工作。

 Parallel Scavenge回收器∶并行回收 + 复制算法 + STW机制 吞吐量优先
 Parallel Scavenge和 ParNew都是采用并行回收 + 复制算法 + STW机制,那么ParallelScavenge回收器的出现是否多此一举?
 和ParNew回收器不同,Parallel Scavenge回收器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾回收器。自适应调节策略也是Parallel scavenge与ParNew一个重要区别。
 高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。

 Parallel Old回收器∶并行回收 + 复制算法 + STW机制 吞吐量优先
 JDK1.6时提供了用于执行老年代垃圾回收的Parallel Old回收器,代替老年代的Serial Old回收器。
 在程序吞吐量优先的应用场景中,Parallel + Parallel Old回收器的组合,在Server模式下的内存回收性能很不错。
 在Java8中,默认是此垃圾回收器。

 CMS回收器∶并发回收 + 标记-清除算法 + STW机制 低延迟
 JDK 1.5时,HotSpot推出适合强交互应用的垃圾回收器: CMS (concurrent-Mark-Sweep),是HotSpot虚拟机中第一款真正意义上的并发回收器,第一次实现了让垃圾回收线程与用户线程同时工作。
 CMS回收器的重点是低延迟。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。
 目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS回收器就非常符合这类应用的需求。
 但是,CMS作为老年代的回收器,却无法与JDK 1.4.0中已经存在的新生代回收器Parallel Scavenge 配合工作,所以在JDK 1.5中使用CMS来回收老年代的时候,新生代只能选择ParNew或者Serial回收器中的一个。
 在G1出现之前,CMS使用还是非常广泛的。一直到今天,仍然有很多系统使用CMS GC。

 CMS的工作原理
CMS整个过程比之前的回收器要复杂。
 初始标记(Initial-Mark)阶段:此阶段,所有用户线程都将会因为STW机制出现短暂的暂停,主要任务只是标记出GCRoots能直接关联到的对象。一旦标记完成之后,用户线程恢复工作。由于直接关联对象比较小,所以这里的速度非常快。
 并发标记(concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图,此过程耗时较长,但无需暂停用户线程,可与垃圾回收线程一起并发运行。
 重新标记(Remark)阶段:由于在并发标记阶段中,用户线程会和垃圾回收线程同时运行或交叉运行,为了修正并发标记期间,因用户线程继续工作而导致标记产生变动的那一部分对象的标记记录,此阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
变动的那一部分对象:由于标记阶段是从 GC Roots开始标记可达对象,那么在并发标记阶段可能产生两种变动:
①本来可达的对象,变得不可达了
②本来不可达的对象,变得可达了
重新标记只处理第2种变动,因为如果处理第1种变动需要重新从GC Roots开始遍历,相当于再次完成初始标记和并发标记的工作,这样会造成增加重新标记阶段的开销,所带来的暂停时间是追求低延迟的CMS不能容忍的。
 并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以此阶段也是可以与用户线程同时并发执行的。
 目前所有的垃圾回收器都做不到完全不需要STW机制,只是尽可能地缩短暂停时间。
 由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低延迟的。
 由于在垃圾回收阶段,垃圾回收线程和用户线程可以并发执行,没有中断,所以在CMS回收过程中,还应该确保用户线程有足够的内存可用。因此,CMS回收器不能像其他回收器那样等到老年代几乎完全被填满了再进行回收,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。当CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial old 回收器来重新进行老年代的垃圾回收,这样停顿时间就很长了。
 CMS回收器采用标记-清除算法,不可避免地会产生一些内存碎片,因此CMS在为新对象分配内存空间时,只能选择空闲列表(Free List)方式执行内存分配。

 有人会觉得既然Mark Sweep会造成内存碎片,那么为什么不把算法换成Mark Compact呢?
答案其实很简单,因为当并发清除的时候,用Mark Compact整理内存的话,原来的用户线程使用的内存还怎么用呢?要保证用户线程能继续执行,前提的它运行的资源不受影响。Mark Compact更适合“Stop the World”这种场景下使用。
 CMS的优点
 2,4阶段,并发回收。
 1,3阶段STW时间较短,低延迟
 CMS的弊端
 会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发Full GC。
 CMS回收器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
 CMS回收器无法处理浮动垃圾。可能出现“Concurrent Mode Failure"失败而导致另一次 Full GC的产生。
① 在并发标记阶段,用户线程和垃圾回收线程是并发执行的,所以标记工作可能会早于所有用户线程执行结束之前,在这个期间,可能还有用户线程被执行从而产生新的垃圾。换言之有一部分的对象引用关系会发生变动。由于并发标记阶段是从 GC Roots开始标记可达对象,那么在并发标记阶段可能产生两种变动:
1)本来可达的对象,变得不可达了
2)本来不可达的对象,变得可达了
② 在并发清理阶段,用户线程和垃圾回收线程也是并发执行的,这段时间还会可能产生新的垃圾。
③ 总结:以上两阶段可能产生的新的垃圾对象,而重新标记阶段只会处理并发阶段第2部分对象引用关系(不可达可达)的变动,而对于并发标记阶段的第1部分对象引用关系(可达不可达)的变动,并发清理阶段产生的浮动垃圾,CMS在当次垃圾回收中无法处理掉它们,只好留待下一次垃圾回收时再清理掉。
 CMS的发展
 JDK5新特性:推出CMS
 JDK9新特性:CMS被标记为Deprecate (EP291),如果使用它,用户会收到一个警告信息,提示CMS未来将会被废弃。
 JDK14新特性:删除CMS垃圾回收器(JEP363),如果使用它,JVM不会报错,只是给出一个warning信息,但是不会exit。

 小结:HotSpot有这么多的垃圾回收器,那么如果有人问,Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC有什么不同呢?
 如果你想要最小化地使用内存和并行开销,Serial GC + Serial Old。
 如果你想要最大化应用程序的吞吐量,Parallel GC + Parallel Old。
 如果你想要最小化GC的中断或停顿时间,ParNew + CMS GC。

 G1回收器∶并发、并行回收 + 复制算法(新)、标记-整理(老) + STW机制 低延迟
 官方给G1设定的目标:在延迟可控的情况下获得尽可能高的吞吐量。
 为什么名字叫做Garbage First (G1)呢?
 因为G1回收器,每次根据允许回收时间,优先回收垃圾最大量的区间(Region),因此被称作Garbage First。注意,这里针对的是老年代。
 具体而言,它把堆内存分割为很多不相关的区域(Region)(物理上不连续的),使用不同的Region来表示Eden、幸存者0区,幸存者1区,老年代等,有计划地避免在整个Java堆中进行全区域的垃圾回收。
 G1 跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的回收时间,优先回收价值最大的Region。由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以我们给G1一个名字:垃圾优先(Garbage First) 。
 G1 (Garbage-First)面向服务端应用,主要针对配备多核CPU及大容量内存的机器,以极高概率满足低延迟和高吞吐量的性能特征。JDK1.7版本正式启用,JDK9默认垃圾回收器,取代了CMS回收器以及Parallel + Parallel old组合。被Oracle官方称为“全功能的垃圾收集器”。
 G1回收器的特点(优势)
与其他GC收集器相比,G1使用了全新的分区算法,其特点如下所示:
 并行与并发
 并行性: G1在回收期间,可以有多个Gc线程同时工作,有效利用多核计算能力。此时用户线程STW。
 并发性: G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况。
 分代收集
 从分代上看,G1依然属于分代型垃圾回收器,只不过是是逻辑分代不是物理划分。它会区分年轻代和老年代,年轻代依然有Eden区和survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。
 将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。对比其他回收器,它同时兼顾年轻代和老年代。
 空间整合
 CMS:标记-清除算法、内存碎片、若干次GC后进行一次碎片整理。
 G1将内存划分为一个个的region。内存的回收是以region作为基本单位的。Region之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显。
 可预测的停顿时间模型(即:软实时soft real-time)
 这是 G1 相对于CMS 的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
 G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。相比于CMS GC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。
 G1回收器的缺点
 相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(Overload)都要比CMS要高。
 从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上优势更加明显。平衡点在6-8GB之间。
 G1回收器的应用场景
 面向服务端应用,针对具有大内存、多处理器的机器。(在普通大小的堆里表现并不惊喜),最主要的应用是GC行为需要低延迟,并具有大堆的应用程序提供解决方案。
 在下面的情况时,使用G1可能比CMS好:
 超过50%的Java堆被活动数据占用;
 对象分配频率或年代提升频率变化很大;
 GC停顿时间过长(长于0.5至1秒)。
 HotSpot 垃圾收集器里,除了G1以外,其他的垃圾收集器使用内置的JVM线程执行GC的多线程操作,而G1可以采用应用线程承担后台运行的GC工作,即当JVM的GC线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程。
 G1回收器工作模式
G1收集器只有两种工作模式:YGC 和Mixed GC(包括YGC、并发标记和混合回收三个过程),都是STW。
 Young GC:选定所有年轻代Region添加到CSet中。通过控制年轻代region的个数来,即年轻代内存的大小,来控制young GC的时间开销。
 Mixed GC:选定所有年轻代Region,外加根据global concurrent marking统计结果得出的收益高的若干老年代Region添加到CSet中。在用户指定开销返回内尽可能选择收益高的老年代Region。
 G1回收器垃圾回收过程
 G1 GC的垃圾回收过程主要包括如下三步:
 年轻代GC (Young GC)
 老年代并发标记过程(Concurrent Marking)
 混合回收((Mixed GC)
 如果需要,单线程、独占式、高强度的Full Gc还是继续存在的。它针对GC的评估失败提供了一种失败保护机制,即强力回收。
 三个过程概述:
 应用程序分配内存,当年轻代的Eden区用尽时,开始年轻代回收过程。G1的年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期,G1 GC暂停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到survivor区间或者老年区间,也有可能是两个区间都会涉及。
 当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程。
 标记完成马上开始混合回收过程。对于一个混合回收期,G1 GC从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。和年轻代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描、回收一小部分老年代的Region就可以了。即回收部分老年代Region和全部年轻代Region。
 举个例子:一个web服务器,Java进程最大堆内存为4G,每分钟响应1500个请求,每45秒钟会新分配大约2G的内存。G1会每45秒钟进行一次年轻代回收,每31个小时整个堆的使用率会达到45%,会开始老年代并发标记过程标记完成后开始四到五次的混合回收。
 回收过程一:年轻代GC
 第一阶段,扫描根GC Root
 扫描根GC Root连同RSet记录的外部引用作为扫描存活对象的入口。这一阶段可以体现Rset作用:避免全堆扫描。
 第二阶段,更新RSet
 处理dirty card queue(见备注)中的card,更新RSet。此阶段完成后,RSet可以准确的反映老年代对所在的内存分段中(新生代)对象的引用。这一阶段可以保证Rset中的数据准确性。
 第三阶段,处理RSet
 识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象。这一阶段本质就是根可达性遍历的一部分。
 第四阶段,复制对象
 此阶段,对象树被遍历,Eden区内存段中存活的对象会被复制到Survivor区中空的内存分段,Survivor区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到o1d区中空的内存分段。如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。这一阶段本质就是使用复制算法新生代进行垃圾回收工作。
 第五阶段,处理引用
 处理软、弱、虚等引用。最终Eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。这一阶段Eden变成空的,那它就变成了无主Region,因此会被记录到空链表中(LinkedList),等待下一次被分配。
 回收过程二:老年代并发标记过程
 初始标记阶段:标记从根节点直接可达的对象。这个阶段是STW的,并且会触发一次年轻代GC。
 根区域扫描(Root Region Scanning):G1 GC扫描Survivor区直接可达的老年代区域对象,并标记被引用的对象。这一过程必须在youngGC之前完成。这一阶段是为了标记可以移动到老年代的对象,发生GC的时候把这些对象移入老年代。
 为什么只扫描Survivor区,为什么必须在youngGC之前完成?
 因为刚执行完一次YoungGC,这里eden区里没东西了。毕竟我们进行Young GC的时候会移动Survivor区,移动之后就找不到哪些老年代对象是可达的了。
 并发标记(Concurrent Marking):在整个堆中进行并发标记(和应用程序并发执行),此过程可能被youngGC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。
 再次标记(Remark):由于应用程序持续进行,需要修正上一次的标记结果。是STW的。G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning(SATB)。原因:和CMS类似,并发标记并不准确。
 独占清理(Cleanup,STW):是STW的。计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。为下阶段做铺垫。这个阶段本质是一个统计计算过程,并不会实际上去做垃圾的收集。
 并发清理阶段:如果发现区域对象中的所有对象都是垃圾,那么这个区域会被立即回收。这个阶段其实就是第三阶段的一部分工作。
 G1回收过程三:混合回收
当越来越多的对象晋升到老年代Old Region时,为了避免堆内存被耗尽,虚拟机会触发混合回收Mixed GC,该算法会回收整个Young Region,还会回收一部分的Old Region。回收哪些Old Region是可选择的,从而控制垃圾回收的耗时时间。Mixed GC并不是Full GC,因为Mixed GC只收集部分老年代Region,如果在Mixed GC期间出现老年代被占用完的情况,JVM会采用Serial Old(Full GC)收集器来收集整个Heap。

 并发标记结束以后,老年代中百分百为垃圾的Region被回收了,部分为垃圾的Region被计算了出来。默认情况下,这些老年代的Region会分8次被回收。
 混合回收的回收集(Collection Set)包括八分之一的老年代Region,所有新生代Region(Eden+Survivor)。混合回收的算法和年轻代回收的算法完全一样,只是回收集多了老年代的Region。
 由于老年代中的Region默认分8次回收,G1会优先回收垃圾多的Region。垃圾占Region比例越高优先被回收,并且有一个阈值(默认为65%)会决定内存分段是否被回收,意思是垃圾占内存分段比例要达到65%才会被回收。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间。
 混合回收并不一定要进行8次。有一个阈值(默认值为10%),意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可回收垃圾占堆内存的比例低于10%,则不再进行混合回收。因为GC会花费很多的时间,但是回收到的内存却很少,即性价比不高,出力不讨好。
 G1回收过程四(可选):Full GC
 G1的初衷就是要避免Fu11GC的出现。但是如果上述方式不能正常工作,G1会停止应用程序的执行(stop-The-world),使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。
 避免Full GC的发生,可以通过增大堆内存。触发Full GC发生的原因:Mirror GC时没有足够的to-space来存放晋升的对象;并发处理过程完成之前空间耗尽。
 G1回收的优化建议
 年轻代大小
避免使用-Xmn或-XX:NewRatio等相关选项显式设置年轻代大小。
固定年轻代的大小会覆盖暂停时间,给JVM动态调节,不然是达不到控制暂停时间的目的。
 目标暂停时间目标不要太过严苛(过短)
G1 GC的吞吐量目标是90%的应用程序时间和10%的垃圾回收时间评估G1GC的吞吐量时,暂停时间目标不要太严苛。目标太过严苛,导致垃圾回收频率变高,进而引起更多的垃圾回收开销,最终直接影响到吞吐量。
 垃圾回收器总结
在这里插入图片描述

Serial(串行) Parallel(并行)CMS(并发)G1(并发+并行,追求可控暂停时间内,提高吞吐量)ZGC(低延迟)
JDK14

 怎么选择垃圾回收器?
Java垃圾收集器的配置对于JVM优化来说是一个很重要的选择,选择合适的垃圾收集器可以让JVM的性能有一个很大的提升。怎么选择垃圾收集器?
 优先调整堆的大小让JVM自适应完成。
 如果内存小于100M,使用串行收集器
 如果是单核、单机程序,并且没有停顿时间的要求,串行收集器
 如果是多CPU、需要高吞吐量、允许停顿时间超过1秒,选择并行或者JVM自己选择
 如果是多CPU、追求低停顿时间,需快速响应(比如延迟不能超过1秒,如互联网应用),使用并发收集器
 官方推荐G1,性能高。现在互联网的项目,基本都是使用G1。
最后需要明确一个观点:没有最好的收集器,更没有万能的收集;调优永远是针对特定场景、特定需求,不存在一劳永逸的收集器
 面试
 通用、基础性的部分如下:
 垃圾收集的算法有哪些?如何判断一个对象是否可以回收?
 垃圾收集器工作的基本流程。
 各种常用的参数:
 内存(堆、方法区)
 垃圾回收器

进入老年代有很多种情况,age到达,过大对象,当幸存者区相同年龄大于其内存一般,该年龄及以上也会进入,幸存者区满了也会进入

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值