JVM——Java虚拟机归纳整理

类的加载

加载器

  • 启动类加载器(Bootstrap ClassLoader)c++: 将<JAVA_HOME>\lib目录中的或者被-Xbootclasspath参数所指定的路径中的并且是虚拟机识别的类库i今安在到虚拟机内存中
  • 扩展类加载器(Extension ClassLoader) java :由sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>\libext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库
  • 应用程序类加载器(Application ClassLoader) java:由sun.misc.Launcher$AppClassLoader实现,是ClassLoader中的getSystemClassLoader()方法的返回值,负责加载用户类路径(ClassPath)上所指定的类库
  • 自定义类加载器(User ClassLoader)

类的生命周期

加载

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

将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

连接

  • 验证:
    若输入字节流不符合Class文件格式的约束,会java.lang.VerifyError异常或子异常
    1、 文件格式验证
    目的:保证输入的字节流能正确地解析并存储于 方法区之内,格式上符合描述一个Java类型信息的要求(该阶段的验证基于二进制字节流进行)
    验证点:
    是否以魔数0xCAFEBABE开头
    主、次版本号是否在当前虚拟机处理范围之内
    常量池中的常量中是否有不被支持的常量类型(检查常量tag标志)
    指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
    CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据
    Class文件中各个部分及文件本身是否有被删除的或附加的其他信息
    2、元数据验证
    目的:对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息
    验证点:
    这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)
    这个类的父类是否继承了不允许被继承的类(被final修饰的类)
    如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
    类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)
    3、 字节码验证
    目的:通过数据流和控制流分析,确定程序语义时合法的、符合逻辑的
    验证点:
    保证任意时刻的操作数栈的数据类型与指定代码序列都能配合工作
    保证跳转指令不会跳转到方法体以外的字节码指令上
    保证方法体中的类型转换是有效的
    StackMapTable:描述了方法体中所有的基本块(按照控制流拆分的代码块)开始时本地变量表和操作栈应有的状态,在字节码验证期间,就不需要根据程序推导这些状态的合法性,只需检查StackMapTable属性中的记录是否合法即可
    4、 符号引用验证
    发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验
    验证点:
    符号引用中通过字符串描述的全限定名是否能找到对应的类
    在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
    符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可以被当前类访问
    无法通过验证会抛出java.lang.IncompatibleClassChangeError异常的子类
  • 准备
    准备阶段是正式为类变量在方法区中分配内存并设置类变量初始值的阶段
    该阶段进行内存分配的仅包括类变量,不包括实例变量
    初始值指的是数据类型的零值(final修饰的变量除外):
    int:0
    long:0L
    short:(short)0
    char:’\u0000’
    byte:(byte)0
    boolean:false
    float:0.0f
    double:0.0d
    reference:null
  • 解析
    解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
    符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现地内存布局无关,引用的目标并不一定已经加载到内存中
    直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
    直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同
    主要针对七类:
    类或接口:CONSTANT_Class_info
    字段:CONSTANT_Fieldref_info
    类方法:CONSTANT_Methodref_info
    接口方法:CONSTANT_InterfaceMethodref_info
    方法类型:CONSTANT_MethodType_info
    方法句柄:CONSTANT_MethodHandle_info
    调用点限定符:CONSTANT_InvokeDynatic_info

初始化

触发规则(有且只有五种对一个类进行主动引用)

  • 遇到new、getstatic、putstatic或invokstatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化;
  • 使用new关键字实例化对象的时候。
  • 读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候。
  • 调用一个类型的静态方法的时候。
  • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化;但是一个接口在初始化时,并不要求其父接口全部完成了初始化只有在真正使用到父接口的时候才会初始化
  • 当虚拟机启动时,用户需要指定一个要执行的主类(main、test),虚拟机会先初始化这个主类
  • 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokstatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其的初始化。

初始化阶段是执行类构造器clinit()方法的过程
clinit()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问;

clinit()方法与类的构造函数(init()方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的clinit()方法执行之前,父类的clinit()方法已经执行完毕(因此在虚拟机中第一个被执行的clinit()方法的类肯定是java.lang.Object);

clinit()方法对于类或者接口来说不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成clinit()方法;

接口中不能使用静态语句块,但仍然有变量初始化的的赋值操作,因此接口与类一样都会生成clinit()方法。但接口与类不同的是,执行接口的clinit()方法不需要先执行父接口的clinit()方法。接口的实现类在初始化时也一样不会执行接口的clinit()方法;

虚拟机会保证一个类的clinit()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的clinit()方法,其他线程都需要阻塞等待,直到活动线程执行clinit()方法完毕*

使用

卸载

双亲委派模型(Parents Delegation Model)

双亲委派模型的工作过程:如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求,子加载器才会尝试自己去加载

运行时数据区

线程共享

方法区(Method Area)/非堆(Non-Heap)

物理可不连续,逻辑上必连续;
可选择固定大小或者可扩展;
存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;
GC目标主要是针对常量池的回收和对类型的卸载;
永久代是方法区的一个实现:
jdk1.7中已经将原本放在永久代的字符串常量池移走
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据(它存储的是运行环境必须的类信息,被装在进此区域的数据是不会被GC掉的,关闭JVM才会释放此区域所占用的内存)
运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存放编译期(加载时)生成的各种字面量和符号引用、翻译出来的直接引用也存储在运行时常量池中。
java.lang.Error:OOM—— 方法区无法满足内存分配需求时

堆(heap)

简单来说,堆管存储;
线程共享,虚拟机启动时创建;
此内存的唯一目的就是存放对象实例。
(规范中描述:所有的对象实例以及数组都要在堆上分配内存。但随着JIT编译器的发展和逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一系列微妙的变化发生,所有的对象都分配在堆上也渐渐变得不那么绝对了)
内存空间(物理可不连续,逻辑上必连续;可选择固定大小或者可扩展——通过参数-Xmx和-Xms设定)
- 新生代 (New Generation Space)占1/3
伊甸区(Eden Space)占8/10
(Eden Space满了,开启MinorGC(YGC),幸存去幸存0区)
幸存0区(Survivor 0 Space)/from区 占1/10
幸存1区(Survivor 1 Space)/to区 占1/10
- 老年代(Tenure Generation Space) 占2/3
【old满了,开启MajorGC(FullGC)——若FullGC多次执行之后依旧满,则报OOM(代码中创建了大量对象且长时间不能被GC——存在被引用)】
不得不提java8 元空间(Metaspace)——java7 永久代(Permanent generation)
逻辑上属于堆,是方法区的实现(JVM规范将方法区描述为堆的一个逻辑部分,但它的别名Non-Heap,目的就是要和堆分开)
元空间大小受本地内存限制。类的元数据放入native memory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制
备注: java.lang.Error:OOM:
如果在堆上没有内存完成实例分配,并且堆也无法再扩展时
代码中创建了大量对象且长时间不能被GC——存在被引用

线程私有

JVM虚拟机栈(Java Virtual Machine Stacks)

(栈管运行)
线程私有,生命周期与线程相同;
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息
局部表量表存放了编译器可知的基本数据类型、对象引用(reference类型)和returnAdress类型(指向了一条字节码指令的地址)
栈内存:
8种基本类型的变量
对象的引用变量
实例方法
java.lang.Error:
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度
OutOfMemoryError(OOM):如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存

本地方法栈(Native Method Stack)

java.lang.Error:
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度
OutOfMemoryError(OOM):如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存

程序计数器(Program Counter Register)

1、是当前线程所执行的字节码的行号指示器。
2、字节码解释器工作时就是通过改变这个计数器的值来选择下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要通过这个计数器来完成。
3、如果线程正在执行的是一个java方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器的值为空。
** 没有OOM情况**:唯一没有OOM的区域。

GC

(需要排查各种内存溢出、内存泄漏问题 / 垃圾收集成为系统达到更高并发量的瓶颈)
主要针对针对堆和方法区进行:
首先说一下四种引用:

  • 强引用
    指在程序代码之中普遍存在的,类似"Object obj = new Object()"这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象
  • 软引用
    用来描述一些还有用但非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之内进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
  • 弱引用
    也是用来描述非必需对象的,但是强度比软引用更弱一些,被弱引用关联的对象只能存活到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
  • 虚引用
    也成为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知
    确定对象是否存活算法

引用计数算法(Reference Counting)

给对象中添加一个引用计数器,每当一个地方引用它时,计数器值加1;当引用失效时,值减1;计数器值为0的对象不可能再被使用

  • 优点:实现简单,判定效率高
  • 缺点:每次对象赋值时均要维护引用计数器,且计数器本身也有一定消耗;
    很难解决对象之间相互循环引用的问题

可达性分析算法(Reachability Analysis)

通过一系列的成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即从GC Roots到该对象不可达时),证明此对象不可用。
可作为GC Roots的对象:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。
  • 方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量
  • 方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用
  • 本地方法栈中JNI(即Native方法)引用的对象
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器
  • 所有被同步锁(synchronized关键字)持有的对象。
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

标记过程:

  • 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。
  • 假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。
  • 如果这个对象被判定为确有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的 队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize() 方法。
  • finalize()方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对F-Queue中的对象进行第二次小规模的标记,如果对 象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己 (this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的要被回收了。

分代收集算法

标记—清除(Mark-Sweep)

首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象

缺点:

  • 执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;
  • 空间问题,标记清除之后会产生大量不连续的内存碎片(空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作)

标记—复制算法(Copying)(一般用于新生代)

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

  • 优点:实现简单,运行高效
  • 缺点:将内存缩小为了原来的一半,代价高;
    对象存活率较高时就要进行较多的复制操作,效率会变低。
  • 优化——Appel式回收
    把新生代分为一块较大的Eden空间和两块较小的 Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍 然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。
    当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实 际上大多就是老年代)进行分配担保(Handle Promotion)。

标记—整理(Mark-Compact)(老年代)

首先标记出所有需要回收的对象,在标记完成后让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。

补充概念(了解):
为了解决对象跨代引用所带来的问题,垃圾收集器在新生代中建立了名为记忆集(Remembered Set)的数据结构,用以避免把整个老年代加进GC Roots扫描范围(记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构)
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。
字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的32位或64位,这个 精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。
对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针
卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。
是用一种称为“卡表”(Card Table)的方式去实现记忆集;
卡表最简单的形式可以只是一个字节数组;
字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个 内存块被称作“卡页”(Card Page)。
一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代 指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。在垃 圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。
在HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的。

垃圾收集器

设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄分配到不同的区域之中存储。
(显而易见,如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那 么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对 象,就能以较低代价回收到大量的空间;如果剩下的都是难以消亡的对象,那把它们集中放在一块, 虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。)

衡量垃圾收集器的三项最重要的指标是:内存占用(Footprint)、吞吐量(Throughput)和延迟 (Latency)
(其中,随着硬件的发展,内存占用和吞吐量的标准可以略微降低,但是对延迟提出了更高的要求。)

Serial

  • 原理:
    这个收集器是一个单线程工作的收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强 调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。“Stop The World”工作是由虚拟机在后台自动发起和自动完成的
  • 特性:
    简单而高效(与其他收集器的单线程相比)
    对于内存资源受限的环境,它是所有收集器里额外内存消耗(Memory Footprint)最小的;
    对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销
    能与CMS 收集器配合工作。
  • 使用场景:
    是HotSpot虚拟机运行在客户端模式下的默认新生代收集器

ParNew

  • 原理:
    是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX: PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致
  • 使用场景:
    是不少运行在服务端模式下的HotSpot虚拟机,尤其是JDK 7之前的遗留系统中首选的新生代收集器

Parallel Scavenge

  • 原理:
    基于标记-复制算法实现
    也是能够并行收集的多线程收集器
  • 特性:
    它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐 量(Throughput)

【-XX:+UseAdaptiveSizePolicy被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区 的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数 了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时 间或者最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics)。】

  • 使用场景:
    适合在后台运算而不需要太多交互的分析任务。

Serial Old

  • 原理:
    是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法
  • 使用场景:
    供客户端模式下的HotSpot虚拟机使用
    在服务端模式下,它也可能有两种用途:一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用,另外一种就是作为CMS 收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。

Parallel Old

  • 原理:
    是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实 现。
  • 特性:
    无法与CMS配合工作
    Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器
  • 使用场景:
    在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。

CMS(Concurrent Mark Sweep)

  • 目标:
    主要目标是获取短垃圾回收停顿时间

  • 原理:
    多线程的标记-清除算法

  • 工作机制:
    1、 初始标记:只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
    2、 并发标记:进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
    3、 重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
    4、 并发清除:清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。

  • 特性:
    一款在强交互应用中几乎可称为具有划时代意义的垃圾收集器 (并发收集、低停顿)
    是HotSpot虚拟机中第一款真正意义上支持并发的垃圾收集器,它首次 实现了让垃圾收集线程与用户线程(基本上)同时工作。(由于耗时长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看 CMS收集器的内存回收和用户线程是一起并发地执行。)

  • 缺点:
    CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK 5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者 Serial收集器中的一个。

CMS收集器对处理器资源非常敏感;

由于CMS收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。
在JDK 5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果 在实际应用中老年代增长并不是太快,可以适当调高参数-XX:CMSInitiatingOccu-pancyFraction的值 来提高CMS的触发百分比,降低内存回收频率,获取更好的性能。到了JDK 6时,CMS收集器的启动 阈值就已经默认提升至92%。但这又会更容易面临另一种风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集, 但这样停顿时间就很长了。所以参数-XX:CMSInitiatingOccupancyFraction设置得太高将会很容易导致大量的并发失败产生,性能反而降低,用户应在生产环境中根据实际应用情况来权衡设置。)

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

CMS收集器提供了一个-XX:+UseCMS-CompactAtFullCollection开关参数*(默认是开启的,此参数从 JDK 9开始废弃)*,用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程,由于这个 内存整理必须移动存活对象,(在Shenandoah和ZGC出现前)是无法并发的。这样空间碎片问题是解 决了,但停顿时间又会变长,因此虚拟机设计者们还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction(此参数从JDK 9开始废弃),这个参数的作用是要求CMS收集器在执行过若干次(数量 由参数值决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理(默认值为0,表 示每次进入Full GC时都进行碎片整理)。

  • 使用场景:
    目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。

Garbage First(G1)

  • 目标:
    未来可以替换掉JDK 5中发布的CMS收集器。
  • 原理:
    基于标记-整理算法 ;
    G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得高的垃圾收集效率。
    G1的记忆集在存储结构的本质上是一 种哈希表,Key是别的Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。
    用户线程改变对象引用关系时,必须保证其不能打破原本的对象图结构,导致标记结果出现错误。G1 收集器则是通过原始快照(SATB)算法来实现的。
    G1收集器的停顿 预测模型是以衰减均值(Decaying Average)为理论基础来实现的,在垃圾收集过程中,G1收集器会记 录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得 出平均值、标准偏差、置信度等统计信息。
  • 工作机制:
    1、 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
    2、 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以 后,还要重新处理SATB记录下的在并发时有引用变动的对象。
    3、 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留 下来的最后那少量的SATB记录。
    4、 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回 收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
  • 特性:
    基于标记-整理算法,不产生内存碎片。
    可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
    可以由用户指定期望的停顿时间是G1收集器很强大的一个功能,设置不同的期望停顿 时间,可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。
    开创了收集 器面向局部收集的设计思路和基于Region的内存布局形式。
    面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而 是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式
    (虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的 分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的 Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的 旧对象都能获取很好的收集效果。
    Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个 Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设 定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象, 将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待)
  • 优点:
    从G1开始,最先进的垃圾收集器的设计导向都不约而同地变为追求能够应付应用的内存分配速率 (Allocation Rate),而不追求一次把整个Java堆全部清理干净。这样,应用在分配,同时收集器在收集,只要收集的速度能跟得上对象分配的速度,那一切就能运作得很完美。这种新的收集器设计思路 从工程实现上看是从G1开始兴起的,所以说G1是收集器技术发展的一个里程碑
    相比CMS,G1的优点有很多,暂且不论可以指定最大停顿时间、分Region的内存布局、按收益动 态确定回收集这些创新性设计带来的红利,单从最传统的算法理论上看,G1也更有发展潜力
  • 缺点:
    在用户程序运行过程 中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载 (Overload)都要比CMS要高
  • 使用场景:
    G1是一款主要面向服务端应用的垃圾收集器;
    目前在小内存应用上CMS的表现大概率仍然要会优于G1,而在大内存应用上G1则大多能发挥其 优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间。

Shenandoah

ZGC

堆参数调优

参数
	-Xms:设置初始分配大小,默认为物理内存的1/64
	-Xmx:最大分配内存,默认为物理内存的1/4
	-XX:+PrintGCDetails:输出详细的GC处理日志

内存分配与回收策略

对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起 一次Minor GC;
无法放入Survivor空间则通过分配担保机制提前转移到老年代去。

大对象直接进入老年代

大对象就是指需要大量连续内存空间的Java对象,最典型的大对象便是那种很长的字符串,或者 元素数量很庞大的数组;

在Java虚拟机中要避免大对象的原因是,在分配空间时,它容易 导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们,而当复 制对象时,大对象就意味着高额的内存复制开销。

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

对象通常在Eden区里诞生,如果经过第一次 Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象 年龄设为1岁。
对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程 度(默认为15),就会被晋升到老年代中。

对象晋升老年代的年龄阈值,可以通过参数-XX: MaxTenuringThreshold设置。

特殊情况——动态对象年龄判定:

如果在Survivor空间中相同年龄所有对象大小的总和大于 Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX: MaxTenuringThreshold中要求的年龄。

空间分配担保

在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。
如果不成立,则虚拟机会先查看XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure):

  • 如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XX: HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC。
    JDK 6 Update 24之 后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行Full GC。

内存模型与线程

Java内存模型

Java内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节。

此处的变量(V ariables)与Java编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。

为了获得更好的执行效能,Java内存模型并没有限制执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器 是否要进行调整代码执行顺序这类优化措施

内存操作
·lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
·unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量 才可以被其他线程锁定。
·read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以 便随后的load动作使用。
·load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的 变量副本中。
·use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚 拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作
·assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量, 每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
·store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随 后的write操作使用。
·write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

内存操作规则:
·不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内 存不接受,或者工作内存发起回写了但主内存不接受的情况出现。
·不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
·不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
·一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或 assign)的变量,换句话说就是对一个变量实施use、store操作之前,必须先执行assign和load操作。
·一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执 行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
·如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量 前,需要重新执行load或assign操作以初始化变量的值。
·如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个 被其他线程锁定的变量。
·对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。
特殊规则:
volatile型变量

  • 特性:
    第一项是保证此变量对所有线程的可见性
    这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。(但是Java里面的运算操作符并非原子操作, 这导致volatile变量的运算在并发下一样是不安全的)
    第二项是禁止指令重排序优化
    普通的变量仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。因为在同一个线程的方法执行过程中无法感知到这点,这就是Java内存模型中描述的 所谓“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics)。
  • 应用场景:
    ·运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
    ·变量不需要与其他的状态变量共同参与不变约束。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值