java虚拟机就是二进制字节码运行的环境
特点:
shotdomn.halt()
并且Java安全管理器允许本次操作类加载器负责从文件系统或者网络中加载Class文件,class文件在开头有特定的文件标识(KA FE BA BE)
ClassLoader只负责class文件的加载,不负责运行。运行由Execution Engine决定
加载的类信息存放于在方法区的内存空间,除了类信息方法区还存放常量池信息,可能还包括字符串字面量和数字常量(常量池部分的内存映射)
class file加载到JVM中加载为DNA元数据模板,之后可以实例化多个实例调用构造器方法
静态代码块中的num、number已经在准备阶段被赋值为0,但是不能调用number,因为还没有声明变量
这四种加载器是包含关系,不是上下层也不是子父类的继承关系
java核心类库都是引导类加载器加载并且只加载此
引导类加载器无父加载器,使用C++编写
扩展类加载派生于ClassLoader类,父加载器为启动类加载器,加载从java.ext.dirs系统属性所指定的目录中加载类库或从JDK的安装目录jre/lib/ext子目录加载类库
系统类加载器(AppClassLoader)派生于ClassLoader,父加载器为扩展类加载器,程序默认加载器负责加载classpath指定路径下类库
自定义加载器:用于隔离加载类、修改类加载方式、扩展加载源、防止源码泄露
获取ClassLoader的途径:
获取当前类的ClassLoader
clazz.getClassLoader()
clazz => Class.forName(“全限定名”)
获取当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader
获取系统的ClassLoader
ClassLoader.getSystemClassLoader
获取调用者的ClassLoader
DriverManger.getCallerClassLoader
JVM对class文件采用按需加载 的方式,当需要使用该类才会将class文件加载到内存中生成class对象,需要加载某个类的class文件时,JVM采用双亲委派机制,即把请求交由父类处理,是一种任务委派模式
优势
此模式避免累的重复加载,保护程序安全,防止核心API被篡改
工作原理:
会将程序放置在一个独立空间内运行,这是对java核心代码的保护
例如自定义一个String类,加载自定义的String类的时候率先加载引导类加载器,先加载jdk自带文件(java.lang.String)
堆区和方法区(jdk1.8之后是元数据区 + 代码缓存->JIT编译产物)是多个线程共享(生命周期是进程),而程序计数器、本地方法栈、虚拟机栈是线程私有
每一个虚拟机实例对应一个Runtime实例(即运行时环境)
JVM主要的线程
Program Counter Register 是对物理PC寄存器的一种抽象模拟,用来存储指向将要执行的指令代码,执行引擎根据地址读取下一条指令 => 将每行代码挂起来被执行引擎读取
使用PC寄存器存储字节码指令地址有什么用?为什么使用PC寄存器记录当前线程的执行地址
PC寄存器为什么被设为线程私有(每个线程一个)
java指令根据栈来设计,优点:跨平台、指令集小,编译器容易实现,缺点:性能下降,实现同样的功能需要更多的指令
栈是运行时的单位(程序如何执行),堆是存储时的单位(数据放在哪里)
一个线程对应一个虚拟机栈,一个栈帧对应一次方法调用,当前线程只有一个活动的栈帧,他保存方法的局部变量(基本数据类型、对象的地址引用)、部分结果,方法的调用和返回
成员变量与局部变量的区别
1.定义的位置不一样【重点】
2.作用范围不一样【重点】
3.默认值不一样【重点】
4.内存的位置不一样(了解)
5.生命周期不一样(了解)
使用参数-Xss设置线层的最大栈空间,其大小决定函数调用最大可达深度
栈帧:局部变量表、操作数栈、动态链接、方法返回地址、一些附加信息
定义为一个数字数组,存储方法参数和定义方法体内的局部变量(包含对象实例引用);
由于局部变量表是建立在线程的栈上,是线程私有数据,不存在数据安全问题;
局部变量表所需容量是编译器确定下来的,运行时不会改变
方法嵌套调用的次数由栈的大小决定,线性正相关
局部变量表的变量制造当前方法调用中有效,方法执行时,JVM通过使用局部变量表完后曾参数值到参数变量列表的传递过程,当方法调用结束后,随着方法栈帧的销毁,局部变量表也会销毁
局部变量表的基本存储单位
32位以内的类型(byte、short、char转换为int,boolean转换为01,float)占用一个slot,64位类型(long、double)占用两位slot
参数值存放在局部变量数组的index0开始到数组长度-1的索引结束
JVM为局部变量表中的每一个slot分配一个访问索引,通过这个索引访问指定值
当一个方法被调用的时候他的方法参数和内部定义的局部变量会顺序复制到局部变量的solt上
当访问64位类型,只需要使用使用前一个索引
当前帧是构造方法或者实例方法(非静态方法),对象引用this会放在index0的slot处
slot是可以重复利用的,如果之前的变量过了作用域,之后的变量是可以占据过期变量的slot
在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据
定义为int类型的数据,底层是按照数的大小定义类型的,超出这个类型范围,才会顺序转变类型
将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读写次数,提升执行引擎的执行效率(操作数是存储在内存中的)
每个栈帧内部包含的一个指向运行时常量池中该栈帧的所述方法的引用
所有的变量和方法引用都作为符号引用(#1)保存在class文件的常量池里
动态连接的作用就是为了将这些符号引用转换为调用方法的直接引用
方法的调用:
JVM中符号引用转换为调用方法的直接引用与方法的绑定机制相关
java中的方法都特征具备虚函数的特征(晚期绑定)可以使用final去除这个
调用指令
invokestatic
:调用静态方法,解析阶段确定唯一方法invokespecial
:调用方法、私有及父类方法,解析阶段确定唯一方法invokevirtual
:调用所有虚方法以及final修饰的方法invokeinterface
:调用接口invokedynamic
:动态解析需要调用的方法,然后执行(java8-lambda表达式)前四条指令固化在JVM,方法的调用认为不可干预,但最后一条有用户确定
静态类型语言就是判断自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息
方法重写的本质
虚方法表
因为频繁使用动态分配,每次动态分配的过程中都要重新在类方法元数据中搜索合适的目标的话影响执行效率,为提高性能,JVM采用在类的方法区建立一个虚方法表,使用索引表来代替查找**(每个虚方法表都存放着方法的实际入口)**虚方法表会在链接-解析时创建并初始化
方法调用看你实现的是接口、父类还是自己的方法,再按照虚方法表找到对应的方法入口
方法返回地址:存放调用该方法的pc寄存器的值
当A方法调用B方法时,在B方法的方法返回地址存放pc寄存器的值,记录调用B方法后的下一条指令,当执行引擎调用方法返回地址后,B方法栈帧出栈,A方法来到下一条指令
无论是正常执行完成还是出现未处理的异常在方法退出后都返回该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址;而通过异常退出的,返回地址要通过异常表来确定,栈帧不会保存这部分信息
一些附加信息:比如对程序调试提供支持的信息
调用深度太深
递归没出口
JVM用于管理本地方法的调用
一个进程对应一个JVM实例,只存在一个堆内存,是线程共享的;在JVM启动时就被创建,空间大小确定但是可以调节
堆可以是物理上不连续的内存空间,但在逻辑上应该连续(在虚拟内存连续)
所有线程共享堆,但是可以划分线程私有的缓冲区TLAB,即在堆中每个线程都独有一分小空间
所有的对象实例以及数组都应该分配在堆上,数组和对象永远不会存储在栈上,只会保存引用
方法结束后,堆 中的对象不会立刻移除,当卡机回收的时候才会移除
堆是GC的重点区域
JDK1.7分为新生区、养老区、永久区
JDK1.8分为新生区(Eden区 + Survivor区)、养老区、元空间
-Xms
=>-XX:InitialHeapSize 堆区起始内存
-Xmx
=>-XX:MaxHeapSize 堆区最大内存
一旦堆区内不错呢大小超过最大内存就会出现OutOfMemoryError
默认堆空间的大小:初始内存是物理内存的1/64,最大内存是物理内存的1/4
开发中初始和最大的内存最好设置为相同的值,避免频繁的扩容和释放,为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能
查看所有进程:jps
查看堆空间内存大小:jstat -gc 进程
参数调整
jinfo -flag NewRatino 进程
查看占比
新生代Eden区与Survivor区默认应该是8:1:1,但是实际是6:1:1
几乎所有的对象都是Eden区被new出来的,绝大部分对象的销毁都在新生代中
-Xmn
:设置新生代最大内存大小
对象分配
如果再次进行垃圾回收,此时所有幸存对象会重新放回from区,再次GC接着再去to区(age+1)
当age达到一定次数的时候,默认15次,会放到养老区
-XX:MaxTenuringThreshold=
进行设置阈值
复制之后有交换,谁空谁是to区
GC频繁在新生区,很少在老年区,几乎不在永久区/元数据收集
特殊情况
如果存在超大对象,Eden区无法放下同时新生代也无法放下,直接放到老年区,如果老年区也放不下,执行FGC,还是放不下报OOM
如果新生代内存放不下,就会直接晋升老年代
每次GC的时候不是都对三个内存区域都回收
-XX:+PrintGCDetails
显示GC垃圾回收过程
堆空间的空间分代思想
对象提升规则
TLAB设置原因
TLAB:为每个相册个分配一个私有缓存区域
多线程同时分配时,可以避免非线程安全问题,同时哈能顾提升内存分配得吞吐量,这是快速分配策略
-XX:UseTLAB
开启TLAB
-XX:TLABWasteTargetPercent
设置TLAB空间所占用的Eden空间的百分比大小
一旦对象TLAB空间分配失败(对象比较大),JVM会使用加锁机制确保数据操作原子性,从而直接在Eden空间中分配内存
如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配
判断new的对象是否被外部方法调用
开发中能使用局部变量的,就不要使用在方法外定义
代码优化
逃逸分析必须在server环境下
方法区看作是独立于java堆的内存空间
方法区的大小决定系统可以保存多少个类,如果定义太多类会导致方法区溢出(OOM),比如加载大量第三方jar包、Tomcat部署的工程过多、大量动态的生成反射类
JDK1.7以前方法区成为永久代,JDK1.8之后元空间取代了永久代
元空间不在虚拟机设置的内存中,而是使用本地内存
设置方法区大小
-XX:MetaspaceSize
设置方法区初始大小
-XX:MaxMetaspaceSize
设置最大大小
如何解决OOM
方法区存放类型信息(类、接口、枚举、注解)、常量、编译后的代码缓存
为什么需要常量池
一个java源文件中的类、接口,编译后产生一个字节码文件,而java字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,所以存到常量池里,这个字节码会包含常量池的引用
常量池存储的数据类型
字节码中的常量池经过类加载之后,存储的到方法区的常量池就是运行时常量池
只有HotSpot才有永久代
去除永久代的原因
String Table为什么调整
static Variable
方法区的回收效果不好,尤其是类型的卸载
垃圾回收:常量池中废弃的常量和不再使用的类型
判定类型不再使用的条件
大量使用动态代理、反射的场景下,需要使用方法区的回收
一个本地方法就是一个java调用非Java代码的接口
本地接口就是融合不同编程语言为java所用
定义一个native method时,并不提供实现体,使用native关键字修饰
不能和abstract一起用
使用native method的原因
通过反射,只能调用空参的构造器,权限必须是public
通过反射,可以调用空参、带参的构造器,无权限要求
不调用构造器,但必须要实现Cloneable接口
从文件、网络中获取一个对象的二进制流
虚拟机在接受到一条new指令,首先检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。如果没有加载,在双亲委派模式下,使用当前类加载器ClassLoader+包名+类名为key查找对应的.class文件。如果没有找到这个文件,抛出ClassNotFoundException异常,如果找到,进行类加载,生成对应的Class类对象。
首先计算对象占用空间大小,接着在堆中划分一块内存给新对象,如果实例成员变量是引用变量,仅分配引用变量空间即可,四个字节大小(short、boolean、int、char、float都是四字节,lon、double都是八字节)
堆的规整取决于垃圾收集器是是否带有压缩整理功能
如果内存规整——指针碰撞
使用的内存在一边,空闲的内存在另一边,中间放着一个指针,分配内存时,就把指针向空闲那边移动与对象大小相等的距离;如果垃圾回收器选择的是Serial、ParNew这种压缩算法,虚拟机使用这种分配,一般使用conpact过程的收集器使用指针碰撞
如果内存不完整——空闲列表
使用内存和空闲内存交错,虚拟机后悔维护一个列表,记录纳西额内存可用于不可用,再分配一个足够大的空间划分给对象实例,并更新列表上的内容,例如CMS垃圾收集器
所有属性设置默认值,保证对象实例字段在不赋值时可以使用
给对象属性赋值方式
将对象的所属类、对象的HashCode和对象的GC信息、锁信息春初在对象的对象头中
正式开始初始化,初始化成员变量,执行实例化代码块,调用类的构造器,并把堆内对象的首地址赋值给引用变量,一般来说:new之后就是执行方法
指向元数据的INstanceKlass,确定该对象所属的类型
如果是数组还需记录数组的长度
对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(父类继承以及本身)
规则
占位符作用
完整实例化过程
JVM通过栈上的reference访问,使得栈帧中的对象引用访问其内部的对象实例
优点:如果对象实例发生移动,只需要更改句柄池
优点:不浪费空间
直接内存是在java堆外的直接向系统申请的内存区别
来源于NIO,通过存在堆中的DirectByteBuffer操作的Native内存
应用访问物理磁盘的时候不在需要用户态和内核态,而是直接访问内存的映射
默认与 -Xmx参数值一致
-MaxDirectMemorySize
设置直接内存大小
虚拟机的执行引擎由软件自行实现,能够执行那些不被硬件支持的指令集格式,解释将字节码指令解释或编译为对应平台的本地机器码
输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行结果
执行引擎
解释器:对字节码按采用装解释的方式执行
JIT编译器:虚拟机将源代码直接编译成和本地机器平台相关的机器语言
为什么java是半编译型半解释型语言
因为java执行引擎既编译既解释
机器码->指令->指令集->汇编语言->高级语言
解释器
字节码解释器和模板解释器(将每一条字节码和一个末班函数相关联)
为什么要保留解释器
当程序启动后解释器立马就可以发挥作用
一个被多次调用的方法或者和一个方法内部循环次数较多的循环体都是热点代码,将这些代码编译成本地机器指令就是栈上替换
采用热点探测方式使基于计数器(方法调用计数器C1500/S10000和回边计数器)的热点探测
JIT编译器内置两种-client
和-server
String声明为final,不可被继承
String实现Serializable和Comparable接口,表示序列化和比较大小
在java1.9之后存储String数据由char[]改为了byte[]
String不可变的字符序列
字符串常量池中不会存储相同内容的字符串
-XX:StringTableSize
设置StringTable长度String pool使用方式
newString(“”)创建的对象
会在堆中先new出对象,之后在常量池中再创建一个对象
new String(“”) + new String(“”)
注:new完两个对象后字符串常量池中并没有创建相应对象
对于存在大量重复的字符串,可以使用intern来减少内存消耗
在运行程序中没有任何指针指向的对象
为什么需要GC
如果不及时堆内存中垃圾进行回收,垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用,可能会导致内存溢出
System.gc() -> Runtime.getRuntime().gc()
堆是GC的收集重点
内存溢出
没有空闲内存,并且垃圾收集器也无法提供更多内存
在抛出OOM前,会进行一次GC处理
内存泄漏
对象不会被使用,但是GC不能回收
⭐️这里的存储空间不是指物理内存,而是虚拟内存,这个取决于磁盘交换区设定的大小
GC发生的时候,产生的应用程序的停顿
e.g:可达性分析算法中枚举GC Roots会导致吸纳成停顿
并发
在一个时间段内有几个程序都是已启动和运行完毕之间,且几个程序都是同一个处理器上运行
并行
多个CPU执行个线程,DUO个线程互不抢占资源,也互不影响,就是在并发
安全点:只有特殊位置才能停顿下来执行GC
选择一些执行时间较长的指令作为安全点,比如方法调用、循环跳转、异常跳转
如何在GC发生的时候,检查线程跑到最近的安全点停顿下来
主动式中断:设置一个中断标志,各个线程运行到萨芬Point的时候主动轮询这个标志,若为真,则中断挂起
安全区域:在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置来时GC都是安全的
执行过程
为了存在内存空间足够时,对象存在内存,如果内存空间不够,就抛弃这些对象(缓存)
只要存在强引用关系,就永远不会被回收的对象
在系统将要发生内存溢出之前,对这些对象二次回收
e.g:高速缓存的实现、mybatis的缓存
垃圾回收器在某个时刻决定回收软引用对象的时候,会清理软引用。并可选的把引用存放到一个引用队列
只被弱引用关联的对象只能生存到下一次垃圾收集的时候
构造弱引用的时候可以指定一个引用队列,当弱引用对象被回收的时候就会加入引用队列,通过队列可以跟踪对象的回收情况
为一个对象设置虚引用唯一目的就是追踪垃圾回收过程,当垃圾回收时可以收到一个系统通知
e.g:将一些资源释放操作放置在虚引用中执行和记录
用于实现finalize()方法
内部配合队列使用
对象存活判断
⭐️对每个对象爆粗一个整型的引用计数器属性,用于记录对象被引用的情况
优缺点
python使用引用计数器和垃圾收集机制,依靠手动解除和使用弱引用weakref来解决循环引用
GC Roots必须是一组活跃的引用
基本思路
GC Roots包含的对象
必须在保障一致性的快照中标记,所以JVM会停下来进行标记
对象终止机制提供对象被销毁前的自定义处理逻辑
垃圾回收对象之前会调用该对象的finalize(),用于在对象被回收时进行资源释放,比如关闭文件、套接字、数据库连接
由于finalize(),对象会处于三种状态
判断对象可回收的过程
标记与清除
标记:Collector从引用根节点开始便利,标记所有被引用对象
清除:Collection对堆内存从头到尾开始线性遍历,如果发现某个对象在Header中没有标记为可达对象就回收
⭐️清除就是把需要清除的对象地址保存在空闲的地址列表里,下次新对象需要加载时,判断垃圾的位置空是否足够,如果够就存放
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
一次可以回收70%~99%内存空间,存活对象少,垃圾对象多
标记与压缩
标记:从根节点标记所有被引用的对象,在清除未标记对象
压缩:将所有存活的对象压缩到内存的一端按顺序排放,清理边界外的所有空间
三种算法对比
增量收集算法
如果一次性所有的垃圾进行处理,需要造成系统长时间的停顿,那么可以让垃圾收集线程和引用程序交替执行,垃圾收集线程只收集一小片区域的内存空间,接着切换到引用程序线程,直至垃圾收集完成
对线程间冲突的妥善处理,允许垃圾线程以分阶段的方式完成标记、清理或复制工作
会使得垃圾回收的总体成本上升,造成系统吞吐量的下降
分区算法
将整个堆空间分成一块块小区间
每个小区间独立使用,独立回收
性能指标
准则:在最大吞吐量有限的情况下,降低停顿时间
垃圾回收器的搭配使用
针对不同的场景下,使用不同组合的垃圾回收器,以此提高垃圾收集效率
查看默认的垃圾回收器
-XX:+PrintCommandLineFlags
查看命令行相关参数(包含垃圾回收器)
Serial收集器采用复制算法、串行回收、STW机制执行内存回收
Serial Old收集器采用标记-压缩算法、串行回收、STW机制,在Client模式下默认老年代GC,在Server模式下,与新生代Parallel Scavenge配合使用或者作为CMS后备垃圾收集器
适用于桌面应用场景,内存不大,可以在较短时间内完成垃圾收集
与SerialGC相比只是 串行回收变成了并行回收
ParNew是运行在Server模式下的默认GC
一般不会使用,因为在1.9版本被移除了
Parallel Scavenge收集器采用复制算法、并行回收、STW机制
此收集器可以达到一个可控制的吞吐量,和自适应调节策略
高吞吐亮可以高效率低利用CPU时间,适合后台运算,比如鼻梁处理、订单处理、工资支付、科学计算等
Parallel Old采用标记-压缩算法
参数设置
-XX:+UseParallelGC
设置年轻代
-XX:+UseParallelOldGC
设置老年代
-XX:ParallelGCThreads
设置年轻代并行收集器线程数
-XX:MaxGCPauseMillis
设置垃圾收集器最大停顿时间
-XX:GCTimeRatio
垃圾收集时间占总时间的比例(=1/(N + 1)),默认99,不超过1%
-XX:+UseAdaptiveSizePolicy
:设置自适应调节策略,平衡吞吐量和垃圾回收停顿时间
采用标记-清除算法,尽可能缩短垃圾收集时用户线程的停顿时间
CMS过程
因为在垃圾收集的时候用户线程没有切断,所以不能等到老年代快填满时才收集,应该留出足够的内存给线程;当堆内存使用率达到某一个阈值时,便开始进行回收。如果内存无法满足,则临时启用Serial Old收集器进行垃圾收集
为什么不用标记-压缩
要保证用户线程的执行,不能改变对象的地址,同时压缩算法需要STW,这样就无法达到低延迟特点
参数设置
-XX:UseConcMarkSweepGC
设置CMS GC(老年代用)
-XX:CMSInitiaingOccupanyFraction
设置堆内存使用率的阈值,默认92%,降低Full GC的次数
-XX:UseCMSCompactAtFullCollection
指定在执行完FullGC后对内存空间进行压缩整理
-XX:CMSFullGCBeforeCompaction
设置执行多少次FullGC后压缩整理
-XX:ParallelCMSThreads
设置CMS的线程数量(默认(ParallelGCTHreads + 3)/4)
在延迟可控的情况下,尽可能获得高的吞吐量
G1垃圾回收
跟踪各个Region里面垃圾堆积的价值大小(回收所获得的空间以及回收所需时间),在后台维护一个优先列表。每次根据允许的收集时间,优先回收价值最大的Region
G1回收器特点
并行并发
并行性:G1在回收期间,可以有多个GC线程同时工作,多核处理
并发性:G1可以与应用程序交替执行
分代收集
堆空间分为若干区域,年轻代和老年代逻辑上连续,但内存空间上不连续
空间整合
每个Region之间使用复制算法,整体还可看做标记-压缩算法
可预测的停顿时间的模型
G1在大内存应用上优秀,在6~8G内存上
内存设置
-XX:+UseG1GC
设置G1
-XX:G1HeapRegionSize
设置每个Region大小,范围1~32MB,值2的幂,根据最小的堆大小划分2048个区域,默认堆内存1/2000
-XX:MaxGCPauseMillis
设置期望达到最大GC停顿时间,默认200ms
-XX:ParallelGCThread
设置STW时GC线程数
-XX:ConcGcThreads
设置并发标记的线程数
-XX:InitiatingHeapOccupancyPercent
设置触发并发GC周期的java堆占用率阈值,默认45
调优步骤
G1回收器的使用场景
分区Region
所有Region大小相同,且在JVM生命周期内不会改变
一个Region属于一个角色->eden、survivor、old、humongous
humongous内存区域专门存储大对象,超过1.5个Region
如果一个H区存不下大对象,就可以寻找连续的H区存储,
Region内部使用指正碰撞存储对象,还可实现TLAB
G1回收器垃圾回收过程
Young GC
1.JVM启动的时候,会先准备好eden区,不断创建对象到eden
2.当年轻代的eden区用尽时开始年轻代回收,此时是并行独占式回收,G1创建回收集(被回收的内存分段集合包含eden和survivor)
3.之后从年轻代存活的对象到survivor区或old区,也有可能两个区都有
老年代并发标记过程
当堆内存使用达到一定值(45%)开始老年代并发标记过程
Mixed GC
混合回收,老年代存活对象移动到空闲内存,G1的老年代回收器不需要整个老年代别回收,一次只需要扫描一部分Region
Full GC
Remembered Set
G1收集器优化
如何选择垃圾回收器
GC日志分析