JVM详解

什么是JVM


JVM 全称是 Java Virtual Machine,中文译名 Java虚拟机。
                                 

JVM 本质上是一个运行在计算机上的程序,他的职责是运行Java字节码文件。


 JVM即时编译:


Java语言如果不做任何优化,性能不如C、C++等语言

Java需要实时的通过java虚拟机解释成机器码才可以交给计算机去运行,这个过程随着程序的运行要去反复的执行,每次执行都要花费一定的时间,而C/C++等其他语言可以直径将源代码文件转换成机器码去执行,节省了一个解释的过程。

Java需要实时解释,主要是为了支持跨平台特性,将一份字节码指令交给Windows、Linux不同平台,这些平台会安装不同的Java虚拟机,它们分别将字节码解释成不同的机器码。

JVM提供了即时编译(Just-In-Time 简称JIT) 进行性能的优化,最终能达到接近C、C++语言的运行性能

虚拟机发现热点代码,就是在短时间被多次调用,它会认为这段代码有必要优化,并解释成计算机可以运行的机器码,然后将机器码保存在内存中,当下次使用的时候直接从内存中调用,会省略一次解释的过程,就可以提高性能。

常见的JVM:


              

JDK默认HotSpot,可以使用java -version命令查看安装的JDK所使用的JVM版本。
  


字节码文件


Java虚拟机的组成

字节码文件的组成

以下使用idea中jclasslib插件查看字节码组成

字节码组成:

基本信息:魔数、字节码文件对应的版本号,访问标识,父类和接口

 魔数:
 - 文件是无法通过文件扩展名来确定文件类型的,文件扩展名可以随意修改,不影响文件的内容。
 - 软件使用文件的头几个字节(文件头)去校验文件的类型,如果软件不支持该类型就会出错。
 - Java字节码文件中,将文件头称为magic魔数。

主副版本号:

 - 主副版本号指的是编译字节码文件的JDK版本号,主版本号用来标识大版本号,JDK1.0-1.1使用了
45.0-45.3,JDK1.2是46之后每升级一个大版本就加1;副版本号是当主版本号相同时作为区分不同
版本的标识,一般只需要关心主版本号。

 - 版本号的作用主要是判断当前字节码的版本和运行时的JDK是否兼容。

 访问标识:
 - 标识是类还是接口、注解、枚举、模块标识public final abstract
 类、父类、接口索引:

 - 通过这些索引可以找到类、父类、接口的信息

常量池:保存字符串常量、类或接口名、字段名,主要在字节码指令中使用
常量池作用:避免相同内容重复定义,节省空间。
常量池中的数据都有一个编号,编号从1开始。在字段或者字节码指令中通过编号可以快速的找到对应的数据。
字节码指令中通过编号引用到常量池的过程称之为符号引用。



字段:当前类或接口声明的字段信息   




方法:方法中源代码信息转换成字节码指令,通过编译器完成
字节码中的方法区域是存放字节码指令的核心位置,字节码指令的内容存放在方法的Code属性中。
操作数栈是临时存放数据的地方,局部变量表是存放方法中局部变量的位置。


属性:类的属性,比如源码的文件名,内部类的列表

类的生命周期


声明周期概述


加载---连接--初始化--使用--卸载
连接中三个小阶段:验证--准备--解析

加载

 - 加载阶段第一步是类加载器根据类的全限定名通过不同渠道以二进制流的方式获取字节码信息。
 - 类加载器在加载完类之后,Java虚拟机会将字节码中的信息保存在方法区中。
 - 生成一个InstanceKlass对象,保存类的所有信息,里面还包含实现特定功能比如多态的信息。
 - 同时,Java还会在堆中生成与方法区数据类似的java.lang.Class对象。作用是在Java代码中去获取类的信息以及存储静态字段的数据(Java8及以后)
 - java.lang.Class对象中包含基本内容有:字段、方法、静态字段数据
 - 方法区中InstanceKlass对象和堆中的java.lang.Class对象互相关联

 为什么需要在堆中创建java.lang.Class类的对象:


    因为InstanceKlass对象是使用C++编写的对象,java不能直接操作使用C++编写的对象,所以Java虚拟机就在堆上创建一个java.lang.Class这样一个使用Java语言包装之后的对象,可以让我们在Java中获取到。
    java.lang.Class对象中的字段要少于InstanceKlass对象中的字段,InStanceKlass中的很多数据是不需要去使用的,所以就要考虑开发者访问的安全性,只让开发者访问一部分数据,把InStanceKlass中的数据复制出来放到堆对象中,开发者只访问堆中的对象,堆中的对象只包含需要用到的数据,InStanceKlass对象为了安全性就不让开发者去访问。通过这样的设计就很好的控制开发者访问数据的范围,提升数据的安全性。

连接


 1.验证阶段:验证内容是否满足《Java虚拟机规范》
 2.准备阶段:给静态变量赋初值,如果变量使用final修饰,准备阶段会直接将代码中的值进行赋值
 3.解析阶段:将常量池中的符号引用替换成指向内存的直接引用。

        符号引用:就是在字节码文件中使用编号来访问常量池中的内容。

        直接引用:不再使用编号而是使用内存中的地址进行访问具体的数据


 
 初始化
     

 - 初始化会执行静态代码块中的代码,并为静态变量赋值
 - 初始化阶段会执行字节码文件中clinit部分的字节码指令, clinit就是类的初始化
类初始化的几种方式:
    1.访问一个类的静态变量或静态方法,变量是final修饰的并且等号右边是常量不会触发初始化
    2.调用Class.forName()方法      initialize值默认为true,传入false就是告诉虚拟机不要初始化类



  

    3.new一个该类的对象时
    4.执行Main方法的当前类
类在特定情况下不会初始化:
    1.无静态代码块且无静态变量赋值语句。
    2.有静态变量的声明,但是没有赋值语句。
    3.静态变量的定义使用final关键字,这类变量会在准备阶段进行初始化。
    4.数组的创建不会数组中元素的类初始化。

类加载器

什么是类加载器

        类加载器(ClassLoader)是Java虚拟机提供给应用程序去实现获取类和接口字节码 数据的技术。

        类加载器只参与加载过程中的字节码获取并加载到内存的这一部分。

类加载器的分类

加载器分为两类,

        Java代码中实现:

        JDK默认提供或自定义,JDK中默认提供了多中处理不同渠道的类加载器,程序员也可以自己根据需求定制

        继承自抽象类ClassLoader,所有Java中实现的类加载器都需要继承ClassLoader这个抽象类。

        Java虚拟机底层实现:

        源码位于Java虚拟机的源码中,实现语言和虚拟机底层语言一致,比如Hotspot使用C++。

        加载程序运行时的基础类,保证Java运行时的基础类被正确的加载,比如java.lang.String,确保其可靠性。

JDK8及8之前

  Java虚拟机底层实现:

        启动类加载器(Bootstrap) 加载Java最核心的类,是由Hotspot虚拟机提供的,使用C++编写的类加载器

  Java实现:

        它们的源码都位于sun.misc.Launcher中,是一个静态内部类。继承自URLClassLoader。具备通过目录或指定jar包将字节码文件加载到内存中。

        扩展类加载器(Extension)Java中比较通用的类

        应用类程序加载器(Application)加载应用使用的类,比如自己写的一些类,第三方引用的jar包也由它加载

       

双亲委派机制

由于Java虚拟机中有多个类加载器,双亲委派机制的核心是解决一个类到底由哪个类加载器加载。

双亲委派机制有什么用

        1.保证类加载的安全性,通过双亲委派机制避免恶意代码替换JDK中核心类库,比如java.lang.string,确保核心类库的完整性和安全性。

        2.避免重复加载,双亲委派可以避免一个类被多次加载。

类加载器的双亲委派机制

        双亲委派机制指的是:当一个类加载器接收到加载类的任务时,会自底向上查找是否加载过,再由顶向下进行加载。

        向上查找如果已经加载过,就直接返货Class对象,加载过程结束,这样就能避免一个类重复加载。

        如果所有父类加载器都没有加载过该类,则由当前类加载器自己尝试加载,所以看上去是自顶向下加载。

        第二次加载相同的类,仍然会向上进行委派,如果某个加载器加载过就会直接返回。

        向下委派加载起到了一个加载优先级的作用。

在Java中使用代码主动去加载一个类

方式1:使用Class.forName方法,使用当前类的类加载器去加载指定的类。

方式2:获取到类加载器,通过类加载器的loadClass方法指定某个类加载器加载。

每个Java实现的类加载器中都保存了一个成员变量叫 “父” (Parent)类加载器,可以理解为它的上级,并不是继承关系。

打破双亲委派机制

自定义加载器

        自定义加载器并重写loadClass方法就可以将双亲委派机制的代码去除。

线程上下文加载器

        以JDBC为例:

        1.启动类加载器加载DriverManager。

        2.在初始化DriverManager时,通过SPI机制加载jar包中的mysql驱动。

        3.SPI中利用了线程上下文类加载器(应用程序类加载器)去加载类并创建对象。

                

Osgi框架的类加载器

历史上,OSGi模块化框架,它存在同级之间的类加载器的委托加载。

JDK9之后的类加载器

JDK8及之前的加载器

        JDK8及之前的版本中,扩展类加载器和应用类加载器的源码位于rt.jar包中的sun.misc.Launcher.java。

JDK9之后

        JDK9引入了module的概念,类加载器在设计上发生了很多变化。

        把Java的类放到一个个jmod文件中,并且放到jmods文件夹中保存,原先类加载器从jar包中进行加载转变成jmods文件中加载。

        1.启动类加载器使用Java编写,位于java.inter.loader.ClassLoaders类中。Java中的BootClassLoader继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件。启动类加载器依然无法通过Java代码获取到,返回的依然是null,保持了统一。

        2.扩展类加载器被替换成了平台类加载器(PlatForm Class Loader)。平台类加载器遵循模块化方式加载字节码文件,所以继承关系从URLClassLoader变成了BuiltinClassLoader,BuiltinClassLoader实现了从模块中加载字节码文件。

运行时数据区

Java虚拟机在运行Java程序过程中管理的内存区域,称为运行时数据。

程序计数器

程序计数器(Program Counter Register)也叫PC寄存器,每个线程会通过程序计数器记录当前要执行的字节码指令的地址。

在加载阶段,虚拟机将字节码文件中的指令读取到内存之后,会将原文件中的偏移量转换成内存地址。

问题:程序计数器在运行中会出现内存溢出吗?

因为每个线程只存储一个固定长度的内存地址,所以程序计数器是不会发生内存溢出的。

程序员无需对程序计数器做任何处理。

Java虚拟机栈

java虚拟机栈(Java Virtual Machine Stack)采用栈的数据结构来管理方法调用中的基本数据,先进后出(First In Last Out),每一个方法的调用使用一个栈帧(Stack Frame)来保存。

Java虚拟机栈随着线程的创建而创建,而回收会在线程销毁时进行,由于方法可能会在不同方法中执行每个线程都包含一个虚拟机栈。

栈帧的组成
局部变量表

        局部变量表的作用是在方法执行过程中存放所有的局部变量。编译成字节码文件时就可以确定局部变量表的内容。

字节码文件中的局部变量表:

字节码文件中的局部变量表

Nr:编号

起始PC和长度:限制变量生效范围

序号:栈帧局部变量表中槽的起始编号。

栈帧中的局部变量表是一个数组,数组每一个位置称之为槽(slot),long和double占用两个槽,其他类型占用一个槽。

实例方法中序号为0的位置存放的是this,指的是当前调用方法的对象,运行时会在内存中存放实例对象的地址。

方法参数也会保存在局部变量表中,其顺序与参数定义的顺序一致。

局部变量表保存的内容有:实例的this对象,方法的参数,方法体中声明的局部变量。

为了节省空间,局部变量表中的槽是可以复用的,一旦某个局部变量不在生效,当前槽就可以再次被使用。

操作数栈

        操作数栈是栈帧中虚拟机在执行指令过程中用来存放中间数据的一块区域,他是一种栈式的数据结构,如果一条指令将一个值压入操作数栈,则后面的指令可以弹出并使用该值。

        在编译期就可以确定操作数栈的最大深度,从而在执行时正确分配内存大小。

帧数据

        当前类的字节码指令引用了其他类的属性或方法时,需要将符号引用转换成对应的运行时常量池中的内存地址。动态连接就保存了编号到运行时常量池的内存地址的映射关系。

方法出口:

        方法出口指的是方法在正确或异常结束时,当前栈会被弹出,同时程序计数器应该指向上一个栈帧中下一条指令的地址。所以在当前栈帧中,需要存储此方法出口的地址。

异常表:

        异常表存放代码中异常的处理信息,包含了try代码块和catch代码块执行后跳转到的字节码指令位置。

问题:Java虚拟机栈会不会出现栈溢出?

        Java虚拟机如果栈帧过多,占用的内存超过栈内存可以分配的最大大小就会出现内存溢出。

        Java虚拟机栈内存溢出时会出现StackOvreflowError的错误。

虚拟机栈默认大小:如果不指定栈的大小,JVM将创建一个具有默认大小的栈,大小取决与操作系统和计算机的体系结构。

要修改Java虚拟机栈的大小,可以使用虚拟机参数 -Xss。

语法:-Xss栈大小

单位:字节(默认,必须是1024的倍数)、k或K(KB)、m或M(MB)、g或G(GB)

Windows(64位)JDK8测试最小值为180k,最大值为1024m。

局部变量过多、操作数栈的深度过大也会影响栈内存的大小。

本地方法栈

        Java虚拟机栈存储了Java方法调用的栈帧,而本地方法存储的是native本地方法的栈帧。

        在Hotspot虚拟机中,Java虚拟机栈和本地方法栈实现上使用了同一个栈空间,本地方法会在栈内存上生成一个栈帧,临时保存方法的参数同时方便出现异常时也把本地方法的栈信息打印出来。

        一般Java程序中堆内存时是空间最大的一块内存区域,创建出来的对象都存在堆上。

        栈上的局部变量表中,可以存放堆上的对象引用。静态变量也可以存放堆对象的引用,通过静态变量就可以实现对象在线程之间共享。

        堆内存大小是有上限的,当对象一直向堆中放入对象达到上限后,就会抛出OutOfMemory错误。

堆内存需要关注的三个值:

        used:当前已使用的堆内存。

        total:Java虚拟机已分配的可用堆内存。

        max:Java虚拟机可分配的最大堆内存。

        

        随着堆中的对象增多,当total可使用的内存即将不足时,Java虚拟机会继续分配内存给堆。

        如果内存不足,Java虚拟机就会不断的分配内存,total的值就会变大,total最多只能与max相等。

        问题:当 used=max=total 时,堆内存是否就溢出了?

        否。  具体原因在垃圾回收器部分解释。

        

        如果不设置任何虚拟机参数,max默认是系统内存1/4,total默认是系统内存1/64.。一般都需要设置max和total的值。

方法区

        JDK7中将方法区存与堆区域中的永久代空间中,堆的大小由虚拟机参数控制。

        JDK8及之后的版本将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不超过系统承受上限,可以一直分配。可以使用-XX:MaxMetaspaceSize=值将元空间最大大小进行限制。

类的元信息

        方法区用来存储每个类的基本信息(元信息),一般称为InstanceKlass对象。在类的加载阶段完成。

运行时常量池

        除了存储类的元信息,还存放了运行时常量池,常量池中存放的是字节码中的常量池内容。

        字节码文件中通过编号查表的方式找到常量,这中常量池称为静态常量池。当常量池加载到内存中,可以通过内存地址快速定位到常量池中的内容,这种常量池称为运行时常量池。

字符串常量池

        JDK7以前运行池常量池逻辑包含字符串常量池,hotspot虚拟机对方法区的实现为永久代。

        JDK7字符串常量池被从方法区拿到了堆中,运行常量池剩下的东西还在方法区中。

        JDK8之后hotspot移除了永久代改用元空间,字符串常量池还在堆内。

        早期字符串常量池属于运行时常量池的一部分,它们存储的位置也是一样的,后续做出了调整,将字符串常量池和运行时常量池做了拆分。

        JDK7及以后静态变量是存放在Class对象中,脱离了永久代。

直接内存

        直接内存(Direct Memory)并不在《Java虚拟机规范》中存在,所以并不属于Java运行时的内存区域。在JDK1.4之后引入了NIO机制,使用了直接内存,主要为了解决两个问题。

        1.Java堆中的对象如果不再使用要回收,回收时会影响对象的创建和引用。

        2.IO操作比如读文件,需要先把文件读入直接内存(缓冲区)再把数据复制到Java堆中。现在直接放入直接内存中即可,同时Java堆上维护直接内存的引用,减少了数据复制的开销,写文件也是类似的思路。

垃圾回收

C/C++的内存管理

        在C/C++这类没有自动回收机制的语言中,一个对象如果不再使用,需要手动释放,否则就会出现内存泄露,我们称这类释放对象的过程为垃圾回收,而需要程序员编写代码进行回收的方式为手动回收。

        内存泄漏指的是不再使用的对象在系统中未被回收,内存泄漏的积累可能会导致内存溢出。

Java的内存管理

        Java中为了简化对象的释放,引入了自动的垃圾回收(Garbage Collection 简称GC)机制。通过垃圾回收器对不再使用的对象完成自动的回收,垃圾回收器主要对堆上的内存进行回收。其他很多现代的语言比如C#、Python、Go都拥有自己的垃圾回收器。

方法区的回收

判断一个类可以被卸载,需要同时满足三个条件。

        1.此类所有实例对象都已经被回收,在堆内不存在任何该类的实例对象以及子对象。

        2.加载该类的类加载器已经被回收。

        3.该类对应的java.lang.Class对象没有在任何地方被引用。

 手动触发回收

        如果需要手动触发回收,可以调用System.gc()方法

        调用System.gc()并不一定会立即回收垃圾,仅仅是向Java虚拟机发送一个垃圾回收的请求,具体是否需要执行垃圾回收Java虚拟机会自行判断。

堆回收

引用计数法和可达性分析算法

如何判断堆上的对象可以回收

        Java中的对象是否能被回收,是根据对象是否被引用来决定的,如果对象被引用了,说明对象还在使用,不允许回收。

常见的有两种判断方式:引用计数法和可达性分析算法

引用计数法

        引用计数法会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1.

        引用计数法的优点是实现简单,C++中的智能指针就采用了引用计数法。

        但也有缺点,主要有两点。

        1.每次引用和取消都需要维护计数器,对系统性能有一定的影响。

        2.存在循环引用的问题,所谓循环引用就是当A引用B,B同时引用A会出现对象无法回收的问题。  

可达性分析算法

        Java使用的是可达性分析算法来判断对象是否可以回收,可达性分析算法将对象分为两类:垃圾回收的根对象(GC Root)和普通对象,对象与对象之间存在引用关系。

        可达分析算法指的是如果从某个到GC Root对象是可达的,对象就不可被回收。

哪些对象被称为GC Root对象:

        1.线程Thread对象。

        2.系统类加载器加载的java.lang.Class对象。

        3.监视器对象,用来保存同步锁synchronized关键字持有的对象。

        4.本地方法调用时使用的全局对象。

五种对象引用

强引用

        可达性算法中描述的对象引用一般是强引用,即是GC Root对象对普通对象有引用关系,只要这层关系存在,普通对象就不会被回收。

软引用

        软引用相对于强引用是一种相对比较弱的引用关系,如果一个对象只有软引用关联到它,当程序内存不足时,就会将软引用中的数据进行回收。

        在JDK1.2版之后提供了SoftReference类来实现软引用,软引用常用于缓存中。

        软引用执行过程:

        1.将对象使用软引用包装起来,new SoftReference<对象类型>(对象)

        2.内存不足时,虚拟机尝试进行垃圾回收

        3.如果垃圾回收任然不能解决内存不足的问题,回收软引用的对象。

        4.如果内存仍然不足,抛出OutOfMemory异常。

        软引用对象如果在内存不足时回收,SoftReference对象本身也需要被回收。

        软引用当作HashMap的value时,软引用被回收,也要清理HashMap的key。

弱引用

        弱引用的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会被直接回收。

        在JDK1.2版之后提供了WeakReference类来实现弱引用,弱引用主要在ThreadLocal中使用。

        弱引用对象本身也可以使用引用队列进行回收。

虚引用

        虚引用也叫幽灵引用/幻影引用不能通过虚引用对象获取到包含的对象。虚引用唯一用途是当对象被垃圾回收器回收时可以接收到对应的通知。Java中使用PhantomReference实现虚引用,直接内存中为了及时知道直接内存对象不再使用,从而回收内存,使用虚引用来实现。

终结器引用

        终结器引用指的是在对象需要被回收时,对象将会被放置在Finalizer类中的引用队列中,并在稍后由一条FinalizerThread线程从队列中获取对象,然后执行对象的finalize方法,在这个过程中可以在finalize方法中再使用强引用关联上,但不建议这样做,如果耗时过长会影响其他对象的回收。

垃圾回收算法

从三个方面判断GC算法是否优秀:

        1.吞吐量:吞吐量指的是CPU用于执行用户代码的时间与CPU总执行时间的比值,即吞吐量=执行用户代码时间/(执行用户代码时间+GC时间)。吞吐量数值越高,垃圾回收的效率就越高。

        2.最大暂停时间:最大暂停时间指的是所有在垃圾回收过程中的STW时间最大值,最大暂停时间越短用户使用系统时受到的影响就越短。

        3.堆使用效率:不同的垃圾回收算法,对堆内存的使用方式是不同的。比如标记清除算法可以使用完整的堆内存,而复制算法会将堆内存一分为二,每次只能使用一半内存。从堆使用效率上来说,标记清除算法优于复制算法。

        上述三种评价标准:堆使用效率,最大暂停时间,吞吐量不可兼得。

        一般来说,堆内存越大,最大暂停时间就越长。想要减少最大暂停时间,就会降低吞吐量。

        不同的垃圾回收算法适用于不同的场景。

标记清楚算法(Mark Sweep GC)

核心思想分为两个阶段:

        1.标记阶段:将所有存活的对象进行标记。Java使用可达性分析算法,从Gc Root开始通过引用链遍历出所有存活对象。

        2.清除阶段:从内存中删除没有被标记也就是非存活对象。

优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除即可。

缺点:1.碎片化问题:由于内存是连续的所以在对象被删除后,内存中会出现很多细小的内存单元,如果我们需要的是一个比较大的空间,很有可能这些内存单元过小无法分配。

        2.分配速度慢:由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次需遍历到链表最后才能获得合适的内存空间。

复制算法(Copying GC)

核心思想:

        1.准备两块空间,From空间和To空间,每次在对象分配阶段,只能使用其中一块空间(From空间)。

        2.在垃圾回收GC阶段,将From空间存活对象复制到To空间。

        3.将两块空间的From和To名字互换。

优点:

        1.吞吐量高:复制算法只需要遍历一次存活对象复制到To空间即可,比标记-整理 算法少了一次遍历的过程,而性能较好,但是不如标记-清除算法, 因为标记清除算法不需要进行对象的移动。

        2.不会发生碎片化:复制算法在复制之后就会将对象按顺序放 入To空间中,所以对象以外的区域都是可用空间,不存在碎片化内存空间。

缺点:内存使用效率低:每次只能让一半的内存空间来为创建对象使用。

标记整理算法(Mack Compact GC)

核心思想分为两个阶段:

        1.标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出 所有存活对象。

        2.整理阶段,将存活对象移动到堆的一端。清理掉存活对象的内存空间。

优点:

        1.内存使用效率高:整个堆内存都可以使用,不会像复 制算法只能使用半个堆内存。

        2.不会发生碎片化:在整理阶段可以将对象往内存的一侧进行 移动,剩下的空间都是可以分配对象的有效空间。

缺点:

        整理阶段效率不高:整理算法有很多种,比如Lisp2整 理算法需要对整个堆中的对象搜索3 次,整体性能不佳。可以通过TwoFinger、表格算法、ImmixGC等高效的整理算法优化此阶段的性能

分代GC(Generational Gc)
分代垃圾回收将整个内存区域划分为年轻代和老年代
可以根据以下参数调整堆的大小
        分代回收时,创建出来的对象,首先会被放入Eden伊甸园区。
        随着对象在Eden中越来越多,如果Eden区满,新创建的对象已经无法放入,就会触发年轻代
的GC,称为Minor GC或者Young GC。
        Minor GC会把需要eden中和from中需要回收的对象回收,把没有回收的对象放入To区。
        接下来From和To区互换名字,当eden区满时再往里面放入对象,依然会发生Minor GC。
        
        每次Minor GC都会为对象记录它的年龄,初始值为0,每次GC加1.
        如果GC过后,对象的年龄达到阈值(或最大15,默认值与垃圾回收器有关)对象就会被晋升
老年代。
        
        当老年代中空间不足,无法放入新的对象时,先尝试Minor GC,如果还是不足,就会触发Full
GC,Full GC会对整个堆进行垃圾回收。
        如果Full GC后依然无法回收掉老年代的对象,那么当对象继续放入老年代时,就会抛出
OutOfMemory异常。

垃圾回收器

问题:为什么分代GC算法要把堆分成年轻代和老年代?

        系统中大部分对象,都是创建出来之后很快不再使用可以被回收,比如用户获取的订单数据,订单数据返回给用户之后就可以释放了。

        老年代中会存放长期存活的对象,比如Spring的大部分bean对象,在程序启动之后就不会被回收了。

        在虚拟机的默认配置中,新生代大小要远小于老年代大小。

        分代GC算法将堆分为年轻代和老年代的主要原因:

        1.可以通过调整年轻代和老年代的比例来适应不同类型的应用程序,提高内存的利用率和性能。

        2.新生代和老年代使用不同的垃圾回收算法,新生代一般选择复制算法,老年代可以选择标记-清除算法和标记-整理算法,由程序员来选择灵活度较高。

        3.分代的设计允许只回收新生代(Minor  GC),如果满足对象分配的要求就不需要对整个堆进行回收(Full GC),STW时间就会减少。

垃圾回收器组合关系

        由于垃圾回收器分为年轻代和老年代,除了G1之外其他垃圾回收器成对组合进行使用。

Serial和Serial Old

年轻代-Serial垃圾回收器:是一种单线程串行回收年轻代的垃圾回收器。

        回收年代和算法:年轻代,复制算法。

        优点:单CPU处理器下吞吐量非常出色。

        缺点:多CPU吞吐量不如其他垃圾回收器,堆如果偏大会让用户线程处于长时间的等待。

        适用场景:Java编写的客户端程序或硬件配置有限的场景。

老年代-Serial Old垃圾回收器:Serial Old是Serial垃圾回收器的老年代版本,采用单线程串行回收。

        回收年代和算法:老年代,标记-整理算法。

        优点:单CPU处理器下吞吐量非常出色。

        缺点:多CPU吞吐量不如其他垃圾回收器,堆如果偏大会让用户线程处于长时间的等待。

        适用场景:与Serial垃圾回收器搭配使用,或者在SMC特殊情况下使用。

ParNew和CMS

年轻代-ParNew垃圾回收器:ParNew垃圾回收器本质上是对Serial在多CPU下的优化,使用多线程进行垃圾回收。

        回收年代和算法:年轻代,复制算法。

        优点:多CPU处理器下停顿时间较短。

        缺点:吞吐和停顿时间不如G1,所以在JDK9之后不建议使用。

        适用场景:JDK8及之前的版本,与CMS老年代垃圾回收器搭配使用。

老年代-CMS垃圾回收器:CMS垃圾回收器关注的是系统的暂停时间,允许用户线程和垃圾回收线程在某些步骤中同时执行减少用户线程的等待时间。

        回收年代和算法:老年代,标记清除算法。

        优点:系统由于垃圾回收出现的停顿时间较短,用户体验好。

        缺点:内存碎片化问题,退化问题,浮动垃圾问题。

        适用场景:大型的互联网系统中用户请求数据量大,频率高的场景,比如订单接口,商品i恶口等。

Parallel Scavenge和Parallel Old

年轻代-Parallel Scavenge垃圾回收器:是JDK8默认的垃圾回收器,多线程并行回收,关注的是系统的吞吐量,具备自动调整堆内存大小的特点。

        回收年代和算法:年轻代,复制算法。

        优点:吞吐量高,而且手动可控。为了提高吞吐量,虚拟机会动态调整堆的参数。

        缺点:不能保证单词停顿时间。

        适用场景:后台任务,不需要与用户交互,并且容易产生大量对象。

老年代-Parallel Old:是Parallel Scavenge收集器设计的老年代版本,利用多线程并发收集。

        回收年代和算法:老年代,标记整理算法。

        优点:并发收集,在多核CPU下效率较高。

        缺点:暂停时间较长。

        适用场景:与Parallel Scavenge配套使用。

G1

        1.支持巨大的堆空间回收。

        2.支持多CPU并行垃圾回收。

        3.允许用户设置最大暂停时间。

       G1内存结构:

        G1的整个堆会被划分成多个大小相等的区域,称之为区Region,区域不要求是连续的,分为Eden,Survivor,Old区,Region的大小通过堆空间大小/2048计算得到,也可以通过参数-XX:G1 HeapRegionSize=32m指定,Region size必须是2的指数幂,取值范围从1m到32m。

G1垃圾回收有两种方式:

        1.年轻代回收(Young GC)

        回收Eden和Survivor区中不用的对象,会导致STW,G1中可以通过参数-XX:MaxGCPauseMillis=n(默认200)设置每次垃圾回收时最大暂停时间毫秒数,G1垃圾回收器尽可能的保证暂停时间。

        G1垃圾回收执行流程:

        1.新创建的对象会存放在Eden区,当G1判断年轻代不足(max默认60%),无法分配对象时需要回收时会执行 Young GC。

        2.标记Eden和Survivor区域中存活的对象。

        3.根据配置的最大暂停时间选择某些区域将存活对象复制到一个新的Survivor区中(年龄+1),清空这些区域。

        4.后续Young GC时与之前相同,只不过Survivor区中存活的对象会被搬运到另一个Survivor区。

        5.当某个存活对象的年龄到达阈值(默认15),将放入老年代。

        6.部分对象大小超过Region的一半,会直接放入老年代,这类老年代被称为Humongous区。比如堆内存时4G,每个Region就是2M,只要一个大对象超过1M就会被放入Humongous区,如果对象过大会横跨多个Region。

        7.多次回收之后,会出现很多Old老年代区,此时总堆占有率达到阈值,会触发混合回收,回收所有年轻代和部分老年代的对象以及大对象区。采用复制算法完成。

        2.混合回收(Mixed GC)

        混合回收分为:初始标记、并发标记、最终标记、并发清理

        G1老年代的清理会选择存活度最低的区域来进行回收,这样可以保证回收效率最高,最后清理阶段使用复制算法,不会产生内存碎片。

        

        注意:如果清理过程中发现没有足够的空Region存放转移的对象,会出现Full GC,单线程执行标记整理算法,此时会导致用户线程暂停。

        

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值