JVM

JVM虚拟机只关注字节码文件是否符合规范。
JVM字节码、 多语言混合编程、JVM作为运行平台,进行跨语言平台操作
虚拟机:系统虚拟机,程序虚拟机(Java虚拟机执行Java字节码,自动内存管理,垃圾回收)
Java源码–前段编译器–字节码文件–类加载器–字节码效验器–翻译字节码,JIT编译器
高级语言–汇编–机器指令
Java指令是基于基于栈的指令架构集,基于寄存器的指令架构集
jvm生命周期 Java虚拟机的启动通过类加载器创建一个初始类来完成 执行:Java程序的的执行其实是一个Java虚拟机的进程,虚拟机的退出
类加载器子系统 class文件标识 CAFE BABY , classloader只负责加载,运行由execution决定 加载的类信息存放在方法区
反编译 javap -v 文件名.class
类的加载过程 达到能基本的描述三个步骤
加载loading:生成一个代表这个类的class对象,作为方法区这个类的各种数据的访问入口
链接linking: 验证 CAFE BABY,保证加载类的正确性; 准备 类变量分配内存,默认零值 除非final static,静态变量的默认初始化 解析
初始化 :执行类构造器方法clinit 的过程,此方法无需定义,javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中语句合并而来(有静态就有这个文件)静态变量的前初始化和静态代码块的执行
init()是类的本身构造器
虚拟机保证一个类的clinit()方法在多线程下被同步加锁
加载器 引导类加载器bootstarp, 自定义加载器: 系统类加载器 扩展类加载器, 派生于抽象类classloader的类加载器为自定义加载器
自定义类 系统加载器; 核心类由引导类加载器加载(c/c++)
自定义加载器:隔离加载类(类的冲突), 修改类的加载方式, 扩展加载源, 防止源码泄露(类的加密)
实现步骤: 继承classloader ,重写findclas 或者直接继承URLClassLoder类
classloader 抽象类 除了bootstarp,其余都继承
类加载器的获取 getclassloder 线程获取 系统类加载器的getparent
双亲委派机制:Java虚拟机对class文件采取按需加载,加载类的class文件时采用双亲委派机制,即把请求交由父类处理,一种任务委派模式 优点:避免类的重复加载,保护程序安全,防止核心API被恶意篡改
类加载依次向上委托 由包名判断加载器
反向委派机制 jdbc的jar包加载
沙箱安全机制:对Java核心源代码的保护
JVM中表示两个class对象是否为同一个类的必要条件:类的完整类名包括包名一致,加载类的classloader一致
如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。
当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
Java程序对类的使用方式分为:主动使用和被动使用。 类的初始化,反射,调用类的静态方法
类的加载-> 验证 -> 准备 -> 解析 -> 初始化 这几个阶段完成后,就会用到执行引擎对我们的类进行使用,同时执行引擎将会使用到我们运行时数据区
内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁
每个线程:独立包括程序计数器、栈、本地栈。 线程间共享:堆、方法区
JVM允许一个应用有多个线程并行的执行。 在Hotspot JVM里,每个线程都与操作系统的本地线程直接映射。
守护线程和非守护线程(JVM停止)
后台系统线程:虚拟机线程、GC线程、周期任务线程
程序计数寄存器、PC寄存器: 存储下一条指令地址 一块很小的内存空间 运行速度最快的存储区域 每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法 唯一一个在Java虚拟机规范中没有规定任何outotMemoryError情况的区域。
堆 方法区 GC 堆 方法区 栈OOM
指令地址(偏移地址)–PC寄存器
使用PC寄存器存储字节码指令地址有什么用:CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
PC寄存器为什么被设定为私有:准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
CPU时间片 :CPU分配给各个程序的时间,宏观上多个应用程序同时运行。微观上引入时间片,每个程序轮流执行。
并行 vs 串行 并发

虚拟机栈
Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。 优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
栈是运行时的单位,而堆是存储的单位
每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应着一次次的Java方法调用 线程私有 生命周期和线程一致,主管Java程序的运行,它保存方法的局部变量(八 种基本数据类型,对象的引用地址)、部分结果,并参与方法的调用和返回。
局部变量 成员变量 基本数据类型变量 VS 引用类型变量(类、数组、接口)
JVM直接对Java栈的操作只有两个:入栈出栈
对于栈来说不存在垃圾回收问题(栈存在溢出的情况)
栈中可能出现的异常:Java 虚拟机规范允许Java栈的大小是动态的或者是固定不变的。
固定大小的Java虚拟机栈 StackoverflowError 异常
Java虚拟机栈可以动态扩展 无法申请到足够的内存,outofMemoryError 异常
使用参数 -Xss选项来设置线程的最大栈空间 默认11420
栈的存储单位:每个线程都有自己的栈,栈中的数据都是以栈帧的格式存在。线程上正在执行的每个方法都各自对应一个栈帧,栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
OOP(面向对象)的基本概念:类和对象
类中基本结构:field(属性、字段、域)、method
执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出
栈帧的内部结构:局部变量表,操作数栈,动态链接,方法返回地址,附加信息
并行每个线程下的栈都是私有的,因此每个线程都有自己各自的栈,并且每个栈里面都有很多栈帧,栈帧的大小主要由局部变量表 和 操作数栈决定的

局部变量表
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。
局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题,方法运行期间是不会改变局部变量表的大小的,方法嵌套调用的次数由栈的大小决定(栈帧的大小)。maximum local variables
关于Slot的理解 最基本的存储单元是Slot(变量槽)局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。
32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(1ong和double)占用两个slot。
JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。
如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的s1ot处,其余的参数按照参数表顺序继续排列。
静态方法不能使用this,应为this变量不存在于当前方法的局部变量表中
Slot的重复利用 栈帧中的局部变量表中的槽位是可以重用的
变量的分类: 按数据类型分:基本数据类型、引用数据类型;
按类中声明的位置分:成员变量(类变量,实例变量)、局部变量
类变量:linking的paper阶段,给类变量默认赋值,init阶段给类变量显示赋值即静态代码块
实例变量:随着对象创建,会在堆空间中分配实例变量空间,并进行默认赋值
局部变量:在使用前必须进行显式赋值,不然编译不通过。
在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。

操作数栈
栈:使用数组或者链表来实现
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问
Java虚拟机的执行引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
使用javap 命令反编译class文件: javap -v 类名.class

栈顶缓存技术:基于栈式架构的虚拟机所使用的零地址指令更加紧凑 内存读/写次数多 将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。

动态链接:每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Reference)保存在class文件的常量池里。
动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
为什么需要运行时常量池:在不同的方法,都可能调用常量或者方法,所以只需要存储一份即可,节省了空间
常量池的作用:就是为了提供一些符号和常量,便于指令的识别

方法调用:解析与分配 将符号引用转换为调用方法的直接引用与方法的绑定机制相关
方法返回地址:存放调用该方法的pc寄存器的值
方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
一些附加信息:对程序调试提供支持的信息

本地方法接口:Native Methodt是一个Java调用非Java代码的接囗
本地方法:native修饰 有方法体,不是Java实现
Java外面的环境交互,与操作系统的交互,Sun’s Java

本地方法栈
Java虚拟机栈于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。也是线程私有的,本地方法是使用C语言实现的。
当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一。
本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。


堆针对一个JVM进程来说是唯一的,也就是一个进程只有一个JVM,但是进程包含多个线程,他们是共享同一堆空间的。
Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。堆内存的大小是可以调节的
Java VisualVM查看堆空间的内容,通过 jdk bin提供的插件
堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的
所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区TLAB
几乎所有的对象实例以及数组都应当在运行时分配在堆上,还有一些对象是在栈上分配的
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
触发了GC的时候,才会进行回收,堆中对象马上被回收GC频率高,那么用户线程就会收到影响,因为有stop the word
堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。
堆内存细分:Java 8及之后堆内存逻辑上分为三部分:新生区+养老区+元空间
设置堆内存大小与OOM:通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在ava垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
如何查看堆内存的内存分配情况:cmd:jps -> jstat -gc 进程id ide : -XX:+PrintGCDetails
Java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(oldGen)1 : 2
其中年轻代又可以划分为Eden空间、Survivor0空间和Survivor1空间(有时也叫做from区、to区) 8:1:1 -xx:SurvivorRatio=8 默认为8但实际操作值为6
几乎所有的Java对象都是在Eden区被new出来的。绝大部分的Java对象的销毁都在新生代进行了。
在Eden区满了的时候,才会触发MinorGC,而幸存者区满了后,不会触发MinorGC操作
如果Survivor区满了后,将会触发一些特殊的规则,也就是可能直接晋升老年代
针对幸存者s0,s1:复制之后又交换,谁空谁是to
垃圾回收:新生代频繁,老年区很少,元空间不动
常用的调优工具:Visual VM ,Jprofiler
Minor GC,MajorGC、Full GC 我们需要尽量的避免垃圾回收,因为在垃圾回收的过程中,容易出现STW的问题 主要调优MajorGC、Full GC
JVM在进行GC时,并非每次都对上面三个内存区域(新生代,老年代,方法区)一起回收的
GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(FullGC)
新生代收集(MinorGC/YoungGC):只是新生代的垃圾收集
老年代收集(MajorGC/o1dGC):只是老年代的圾收集。
目前,只有CMSGC会有单独收集老年代的行为。很多时候Major GC会和FullGC混淆使用,需要具体分辨是老年代回收还是整堆回收。
混合收集(MixedGC):收集整个新生代以及部分老年代的垃圾收集。目前,只有G1 GC会有这种行为
整堆收集(FullGC):收集整个java堆和方法区的垃圾收集。
Minor GC:Eden代满,Survivor满不会引发GC
Major GC:在老年代空间不足时,会先尝试触发MinorGc。如果之后空间还不足,则触发Major GC,Major GC的速度一般会比MinorGc慢1e倍以上,STW的时间更长,如果Major GC后,内存还不足,就报OOM了
Full GC:五种 System.gc()老年代空间不足,方法区空间不足
GC 举例:设置JVM启动参数, -Xms10m -Xmx10m -XX:+PrintGCDetails
触发OOM的时候,一定是进行了一次Full GC,因为只有在老年代空间不足时候,才会爆出OOM异常
堆空间分代思想: 分代的唯一理由就是优化GC性能
内存分配策略 : 如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到survivor空间中,并将对象年龄设为1。对象在survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代
针对不同年龄段的对象分配原则: 优先分配到Eden 大对象直接分配到老年代 长期存活的对象分配到老年代 动态对象年龄判断 空间分配担保
经过Minor GC后,所有的对象都存活,因为Survivor比较小,所以就需要将Survivor无法容纳的对象,存放到老年代中。
为对象分配内存:TLAB: 在堆中划分出一块区域,为每个线程所独占 为每个线程单独分配了一个缓冲区
在并发环境下从堆区中划分内存空间是线程不安全的
TLAB : 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。 -Xx:UseTLAB”设置是否开启TLAB空间 默认开启。一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。
堆空间的参数设置
-XX:+PrintFlagsInitial:查看所有的参数的默认初始值
-XX:+PrintFlagsFinal:查看所有的参数的最终值(可能会存在修改,不再是初始值)
-Xms:初始堆空间内存(默认为物理内存的1/64)
-Xmx:最大堆空间内存(默认为物理内存的1/4)
-Xmn:设置新生代的大小。(初始值及最大值)
-XX:NewRatio:配置新生代与老年代在堆结构的占比
-XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例
-XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails:输出详细的GC处理日志
打印gc简要信息:①-Xx:+PrintGC ② - verbose:gc
-XX:HandlePromotionFalilure:是否设置空间分配担保
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。I
如果大于,则此次Minor GC是安全的
如果小于,则虚拟机会查看-xx:HandlePromotionFailure设置值是否允担保失败。
如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。
如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;
如果小于,则改为进行一次FullGC。
如果HandlePromotionFailure=false,则改为进行一次Ful1 Gc。
堆不是分配对象的唯一选择 : 逃逸分析 经过逃逸分后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配
GCIH(GC invisible heap)技术实现off-heap,将生命周期较长的Java对象从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的
当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。
如何快速的判断是否发生了逃逸分析,大家就看new的对象实体是否在方法外被调用。
使用逃逸分析,编译器可以对代码做如下优化:
栈上分配: 花费的时间快速减少,同时不会发生GC操作
同步省略 :线程同步的代价是相当高的,同步的后果是降低并发性和性能,如果一个对象被发现只有一个线程被访问到,那么对于这个对象的操作可以不考虑同步。这个取消同步的过程就叫同步省略,也叫锁消除。
分离对象和标量替换 : 有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
标量(scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。
相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。
如果经过逃逸分析,发现一个对象不会被外界访问的话,即时编译器就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换
参数-server:启动Server模式,因为在server模式下,才可以启用逃逸分析。
小结
年轻代是对象的诞生、成长、消亡的区域,一个对象在这里产生、应用,最后被垃圾回收器收集、结束生命。
老年代放置长生命周期的对象,通常都是从survivor区域筛选拷贝过来的Java对象。当然,也有特殊情况,我们知道普通的对象会被分配在TLAB上;如果对象较大,JVM会试图直接分配在Eden其他位置上;如果对象太大,完全无法在新生代找到足够长的连续空闲空间,JVM就会直接分配到老年代。当GC只发生在年轻代中,回收年轻代对象的行为被称为MinorGc。
当GC发生在老年代时则被称为MajorGc或者FullGC。一般的,MinorGc的发生频率要比MajorGC高很多,即老年代中垃圾回收发生的频率将大大低于年轻代。

方法区
方法区看作是一块独立于Java堆的内存空间。
ThreadLocal:如何保证多个线程在并发环境下的安全性?典型应用就是数据库连接管理,以及会话管理
方法区主要存放的是 Class,而堆中主要存放的是 实例化的对象
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。
方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutofMemoryError:PermGen space 或者java.lang.OutOfMemoryError:Metaspace; 加载大量的第三方的jar包,Tomcat部署的工程过多(30~50个),大量动态的生成反射类
关闭JVM就会释放这个区域的内存。
元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存
设置方法区大小与OOM :元数据区大小可以使用参数 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize指定
如何解决这些OOM: 分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)
内存泄漏就是 有大量的引用指向某些对象,但是这些对象以后不会使用了,但是因为它们还和GC ROOT有关联,所以导致以后这些对象也不会被回收,这就是内存泄漏的问题,如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象是通过怎样的路径与GCRoots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GCRoots引用链的信息,就可以比较准确地定位出泄漏代码的位置。
如果不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗
方法区的内部结构
用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
类型信息(类class、接口interface、枚举enum、注解annotation):
这个类型的完整有效名称(全名=包名.类名)
这个类型直接父类的完整有效名(对于interface或是java.lang.object,都没有父类)
这个类型的修饰符(public,abstract,final的某个子集)
这个类型直接接口的一个有序列表
域信息 :
JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
域的相关信息包括:域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient的某个子集)
方法(Method)信息:
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
方法名称
方法的返回类型(或void)
方法参数的数量和类型(按顺序)
方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
异常表(abstract和native方法除外)
non-final的类变量 :
静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分
类变量被类的所有实例共享,即使没有类实例时,你也可以访问它
全局常量:全局常量就是使用 static final 进行修饰,被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。

运行时常量池 VS 常量池
一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述符信息外,还包含一项信息就是常量池表(Constant Pool Table),包括各种字面量和对类型、域和方法的符号引用
常量池可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型
运行时常量池:运行时常量池(Runtime Constant Pool)是方法区的一部分,常量池表(Constant Pool Table)是Class文件的一部分在类加载后存放到方法区的运行时常量池中。
运行时常量池,相对于Class文件常量池的另一重要特征是:具备动态性。
只有Hotspot才有永久代,BEA JRockit、IBMJ9等来说,是不存在永久代的概念的
JDK1.6及以前 有永久代,静态变量存储在永久代上
JDK1.7 有永久代,但已经逐步 “去永久代”,字符串常量池,静态变量移除,保存在堆中
JDK1.8 :无永久代,类型信息,字段,方法,常量保存在本地内存的元空间,但字符串常量池、静态变量仍然在堆中。
为什么永久代要被元空间替代:为永久代设置空间大小是很难确定的。对永久代进行调优是很困难的。
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不在使用的类型
StringTable为什么要调整位置:
jdk7中将StringTable放到了堆空间中。因为永久代的回收效率很低,在full gc的时候才会触发。而ful1gc是老年代的空间不足、永久代不足时才会触发。
这就导致stringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。
静态变量存放在那里:静态引用对应的对象实体始终都存在堆空间
方法区的垃圾回收:
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。
类的卸载比较费劲

对象创建方式:
new:最常见的方式、单例类中调用getInstance的静态类方法,XXXFactory的静态方法
Class的newInstance方法:在JDK9里面被标记为过时的方法,因为只能调用空参构造器
Constructor的newInstance(XXX):反射的方式,可以调用空参的,或者带参的构造器
使用clone():不调用任何的构造器,要求当前的类需要实现Cloneable接口中的clone接口
使用序列化:序列化一般用于Socket的网络传输
第三方库 Objenesis
创建对象的步骤:判断对象对应的类是否加载、链接、初始化;为对象分配内存;处理并发问题;初始化分配到的内存,所有属性设置默认值,保证对象实例字段在不赋值可以直接使用;设置对象的对象头;执行init方法进行初始化显示初始化、代码块中的初始化、构造器初始化
对象实例化的过程:
加载类元信息
为对象分配内存
处理并发问题
属性的默认初始化(零值初始化)
设置对象头信息
属性的显示初始化、代码块中初始化、构造器中初始化
对象内存布局:
对象头:运行时元数据(哈希值 GC分代年龄 锁状态标志)和 类型指针(指向的其实是方法区中存放的类元信息)实例数据;
对象的访问定位:JVM是如何通过栈帧中的对象引用访问到其内部的对象实例:句柄访问(句柄池)、直接指针(HotSpot采用

直接内存 Direct Memory 元空间使用 来源于NIO,通过存在堆中的DirectByteBuffer操作Native内存
非直接缓存区和缓存区 原来采用BIO的架构,我们需要从用户态切换成内核态 NIO的方式使用了缓存区的概念,物理内存映射文件,访问直接内存的速度会优于Java堆。即读写性能高。
存在的问题:也可能导致outofMemoryError异常 分配回收成本较高 不受JVM内存回收管理

执行引擎:执行引擎属于JVM的下层,里面包括 解释器、及时编译器、垃圾回收器
虚拟机的执行引擎则是由软件自行实现的,能够执行那些不被硬件直接支持的指令集格式
JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM所识别的字节码指令、符号表,以及其他辅助信息。
想要让一个Java程序运行起来,执行引擎(Execution Engine)的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以
执行引擎的工作流程
执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于PC寄存器。
每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址。
当然方法在执行的过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息。
解释器:当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
JIT编译器:就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言
Java是半编译半解释型语言:既可以编译也可以解释
机器码:二进制编码方式表示的指令
指令:把机器码中特定的0和1序列,简化成对应的指令(一般为英文简写,如mov,inc等
指令集
当程序启动后,解释器可以马上发挥作用,省去编译的时间,立即执行。
当虚拟机启动的时候,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成再执行,这样可以省去许多不必要的编译时间。并且随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率。

String的基本特性:
string声明为final的,不可被继承
String实现了Serializable接口:表示字符串是支持序列化的。实现了Comparable接口:表示string可以比较大小
string在jdk8及以前内部定义了final char[] value用于存储字符串数据。JDK9时改为byte[]
String再也不用char[] 来存储了,改成了byte [] 加上编码标记,节约了一些空间
字符串常量池是不会存储相同内容的字符串的 在JDK8中,StringTable可以设置的最小值为1009,太多就会造成Hash冲突严重
Java 7 String的内存分配:字符串常量池的位置调整到Java堆内
为什么StringTable从永久代调整到堆中:永久代的默认比较小、永久代垃圾回收频率低
String的基本操作:Java语言规范里要求完全相同的字符串字面量,应该包含同样的Unicode字符序列(包含同一份码点序列的常量),并且必须是指向同一个String类实例。
字符串拼接操作:常量与常量的拼接结果在常量池,原理是编译期优化,变量拼接的原理是StringBuilder
如果拼接符号的前后出现了变量,则相当于在堆空间中new String(),具体的内容为拼接的结果
而调用intern方法,则会判断字符串常量池中是否存在字符串值,如果存在则返回常量池中的值,否者就在常量池中创建
s1 + s2的执行细节
StringBuilder s = new StringBuilder();
s.append(s1);
s.append(s2);
s.toString(); -> 类似于new String(“ab”);
左右两边如果是变量的话,就是需要new StringBuilder进行拼接,但是如果使用的是final修饰,则是从常量池中获取。所以说拼接符号左右两边都是字符串常量或常量引用 则仍然使用编译器优化。也就是说被final修饰的变量,将会变成常量,类和方法将不能被继承、
public static void test4() {
final String s1 = “a”;
final String s2 = “b”;
String s3 = “ab”;
String s4 = s1 + s2;
System.out.println(s3 == s4);//true
}
intern()的使用:intern是一个native方法
new String(“ab”)会创建几个对象:两个对象,一个对象是:new关键字在堆空间中创建,另一个对象:字符串常量池中的对象,看字节码ldc就知道是2个对象
new String(“a”) + new String(“b”) 会创建几个对象:创建了6个对象,
对象1:new StringBuilder();
对象2:new String(“a”);
对象3:常量池的 a;
对象4:new String(“b”);
对象5:常量池的 b
对象6:toString中会创建一个 new String(“ab”) 调用toString方法,不会在常量池中生成ab

总结string的intern()的使用:
JDK1.6中,将这个字符串对象尝试放入串池。
如果串池中有,则并不会放入。返回已有的串池中的对象的地址
如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址
JDK1.7起,将这个字符串对象尝试放入串池。
如果串池中有,则并不会放入。返回已有的串池中的对象的地址
如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址
intern():节省空间
G1中的String去重操作:这里说的重复,指的是在堆中的数据,而不是常量池中的,因为常量池中的本身就不会重复
许多大规模的Java应用的瓶颈在于内存,Java堆中存活的数据集合差不多25%是string对象,jdk8中不再用char[]转换为byte[]数组

垃圾回收概述
垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序的结束,被保留的空间无法被其它对象使用,甚至可能导致内存溢出。
内存泄露:本身不被使用,但无法进行垃圾回收
为什么需要GC:不进行垃圾回收,内存迟早都会被消耗完,JVM将整理出的内存分配给新的对象,应付的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序的正常进行
在早期的C/C++时代,垃圾回收基本上是手工进行的。开发人员可以使用new关键字进行内存申请,并使用delete关键字进行内存释放
Java堆是垃圾收集器的工作重点
频繁收集Young区,较少收集Old区,基本不收集Perm区(元空间)
垃圾回收相关算法:
判断对象存活一般有两种方式:引用计数算法和可达性分析算法。
引用计数算法:对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况,引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
可达性分析算法(根搜索算法、追踪性垃圾收集): 可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。引用链
GC Roots: 虚拟机栈中引用的对象, 本地方法栈内JNI(通常说的本地方法)引用的对象方法区中类静态属性引用的对象,方法区中常量引用的对象, 所有被同步锁synchronized持有的对象
总结一句话就是,除了堆空间外的一些结构,比如 虚拟机栈、本地方法栈、方法区、字符串常量池 等地方对堆空间进行引用的,都可以作为GC Roots进行可达性分析
除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。比如:分代收集和局部回收(PartialGC)。
由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root。
对象的finalization机制: 当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法。
永远不要主动调用某个对象的finalize()方法I应该交给垃圾回收机制调用。
在finalize()时可能会导致对象复活。
finalize()方法的执行时间是没有保障的,它完全由Gc线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会。,因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收
一个糟糕的finalize()会严重影响Gc的性能。
由于finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态。可触及,可复活的,不可触及的
可触及的:从根节点开始,可以到达这个对象。
可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。
不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次
具体过程:
判定一个对象objA是否可回收,至少要经历两次标记过程:
如果对象objA到GC Roots没有引用链,则进行第一次标记。
进行筛选,判断此对象是否有必要执行finalize()方法,如果对象objA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA被判定为不可触及的。如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法执行。finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记。如果objA在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA会被移出“即将回收”集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize方法只会被调用一次。

清除阶段:
当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在JVM中比较常见的三种垃圾收集算法是: 标记一清除算法(Mark-Sweep),复制算法(copying),标记-压缩算法(Mark-Compact)
标记一清除算法: 执行过程:当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
标记:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。标记的是引用的对象,不是垃圾!!
清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收
什么是清除:所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放覆盖原有的地址。
关于空闲列表是在为对象分配内存的时候 提过:
如果内存规整,采用指针碰撞的方式进行内存分配
如果内存不规整,虚拟机需要维护一个列表,空闲列表分配
复制算法:没有标记和清除过程,实现简单,运行高效,复制过去以后保证空间的连续性,不会出现“碎片”问题。
此算法的缺点也是很明显的,就是需要两倍的内存空间。
对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小
在新生代,对常规应用的垃圾回收,一次通常可以回收70% - 99% 的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。
标记-整理算法:第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象,第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。
标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法。标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的
分代收集算法:目前几乎所有的GC都采用分代收集算法执行垃圾回收的
分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
年轻代:复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。
老年代:一般是由标记-清除或者是标记-清除与标记-整理的混合实现,以HotSpot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对象的回收效率很高
Mark阶段的开销与存活对象的数量成正比。
Sweep阶段的开销与所管理区域的大小成正相关。
compact阶段的开销与存活对象的数据成正比。
增量收集算法:在stop the World状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。
增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作,线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
分区算法:为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
垃圾回收相关概念:
通过system.gc()者Runtime.getRuntime().gc() 的调用,会显式触发FullGC,同时对老年代和新生代进行回收
system.gc() )调用附带一个免责声明,无法保证对垃圾收集器的调用。(不能确保立即生效)

内存溢出:javadoc中对outofMemoryError的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。
没有空闲内存的情况:说明Java虚拟机的堆内存不够。原因有二:
Java虚拟机的堆内存设置不够:可能存在内存泄漏问题;也很有可能就是堆的大小不合理
代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用),老版本的oracle JDK,因为永久代的大小是有限的,并且JVM对永久代垃圾回收(如,常量池回收、卸载不再需要的类型)非常不积极,所以当我们不断添加新类型的时候,永久代出现OutOfMemoryError也非常多见,尤其是在运行时存在大量动态类型生成的场合;类似intern字符串缓存占用太多空间,也会导致OOM问题。对应的异常信息,会标记出来和永久代相关:“java.lang.OutOfMemoryError:PermGen space"。随着元数据区的引入,方法区内存已经不再那么窘迫,所以相应的ooM有所改观,出现ooM,异常信息则变成了:“java.lang.OutofMemoryError:Metaspace"。直接内存不足,也会导致OOM。
在抛出OutofMemoryError之前,通常垃圾收集器会被触发,尽其所能去清理出空间,也不是在任何情况下垃圾收集器都会被触发的,JVM可以判断出垃圾收集并不能解决这个问题,所以直接抛出OutofMemoryError。

内存泄漏: 严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。
宽泛意义上的“内存泄漏”: 生命周期变很长但无需长生命周期的对象,方法内变量设置为成员变量或者static,web程序中对象存储至会话级别本质无需
举例
单例模式:单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。
一些提供close的资源未关闭导致内存泄漏:数据库连接(dataSourse.getConnection() ),网络连接(socket)和io连接必须手动close,否则是不能被回收的

Stop The World:指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿。开发中不要用system.gc() 会导致stop-the-world的发生。
垃圾回收的并行与并发
并发:并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换,由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行。
并行:当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,我们称之为并行(Paralle1)
并发和并行对比
并发,指的是多个事情,在同一时间段内同时发生了。
并行,指的是多个事情,在同一时间点上同时发生了。
并发的多个任务之间是互相抢占资源的。并行的多个任务之间是不互相抢占资源的。
只有在多CPU或者一个CPU多核的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。
并发和并行,在谈论垃圾收集器的上下文语境中,它们可以解释如下:
并行(Paralle1):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行。用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;
安全点:程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点
如何在cc发生时,检查所有线程都跑到最近的安全点停顿下来呢:主动式中断:设置一个中断标志,各个线程运行到Safe Point的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。(有轮询的机制)
安全区域:线程处于sleep-状态或Blocked 状态,这时候线程无法响应JVM的中断请求,把Safe Region看做是被扩展了的Safepoint。

引用:强引用、软引用、弱引用、虚引用。这4种引用强度依次逐渐减弱
强引用:object obj=new Object 只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象
强引用的对象是可触及的,垃圾收集器就永远不会回收掉被引用的对象。强引用是造成Java内存泄漏的主要原因之一。虚拟机宁愿抛出OOM异常,也不会回收强引用所指向对象。
软引用:没有足够的内存,才会抛出内存流出异常
内存不足即回收,软引用通常用来实现内存敏感的缓存。比如:高速缓存就有用到软引用。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
弱引用:当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。 发现即回收,WeakHashMap用来存储图片信息,可以在内存不足的时候,及时回收,避免了OOM
虚引用:为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知(对象回收的跟踪)
第一次尝试获取虚引用的值,发现无法获取的,这是因为虚引用是无法直接获取对象的值,然后进行第一次gc,因为会调用finalize方法,将对象复活了,所以对象没有被回收,但是调用第二次gc操作的时候,因为finalize方法只能执行一次,所以就触发了GC操作,将对象回收了,同时将会触发第二个操作就是 将回收的值存入到引用队列中。
终结器引用:用于实现对象的finalize() 方法
三级缓存:内存–本地–网络

Java不同版本新特性
语法层面:Lambda表达式、switch、自动拆箱装箱、enum
API层面:Stream API、新的日期时间、Optional、String、集合框架
底层优化:JVM优化、GC的变化、元空间、静态域、字符串常量池位置变化
垃圾收集器分类
按线程数分(垃圾回收线程数),可以分为串行垃圾回收器和并行垃圾回收器。
按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器。
按碎片处理方式分,可分为压缩武垃圾回收器(指针碰撞)和非压缩式垃圾回收器(空闲列表)。
按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器。
评估GC的性能指标:
吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间)
垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。
暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
收集频率:相对于应用程序的执行,收集操作发生的频率。
内存占用:Java堆区所占的内存大小。
快速:一个对象从诞生到被回收所经历的时间。
主要抓住两点:吞吐量、暂停时间
吞吐量=运行用户代码时间 /(运行用户代码时间+垃圾收集时间)
暂停时间”是指一个时间段内应用程序线程暂停,让Gc线程执行的状态
吞吐量vs暂停时间:现在标准:在最大吞吐量优先的情况下,降低停顿时间
7种经典的垃圾收集器
串行回收器:Serial、Serial old
并行回收器:ParNew、Parallel Scavenge、Parallel old
并发回收器:CMS、G1
Parallel回收器:吞吐量优先
和ParNew收集器不同,ParallelScavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器。
自适应调节策略也是Paralle1 Scavenge与ParNew一个重要区别。
高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。执行批量处理、订单处理、工资支付、科学计算的应用程序。
JDK8:parallel回收器
JDK9:G1 GC

CMS回收器:低延迟 这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
CMS的垃圾收集算法采用标记-清除算法,并且也会"stop-the-world"
CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段。
初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“stop-the-world”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GCRoots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。
并发标记(Concurrent-Mark)阶段:从Gc Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
重新标记(Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
CMS收集器的垃圾收集算法采用的是标记清除算法
CMS为什么不使用标记整理算法?
答案其实很简答,因为当并发清除的时候,用Compact整理内存的话,原来的用户线程使用的内存还怎么用呢?要保证用户线程能继续执行,前提的它运行的资源不受影响嘛。Mark Compact更适合“stop the world” 这种场景下使用
优点:并发收集、低延迟
缺点:会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发FullGC。
CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
CMS收集器无法处理浮动垃圾。并发标记阶段产生新的垃圾对象
如果你想要最小化地使用内存和并行开销,请选Serial GC;
如果你想要最大化应用程序的吞吐量,请选Parallel GC;
如果你想要最小化GC的中断或停顿时间,请选CMs GC。

G1回收器:区域化分代式
官方给G1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才担当起“全功能收集器”的重任与期望。
业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序正常进行,而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化
G1 GC有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。这种方式的侧重点在于回收垃圾最大量的区间(Region),G1一个名字:垃圾优先(Garbage First)
G1(Garbage-First)是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足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除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
G1垃圾收集器的缺点
相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(overload)都要比CMS要高。
从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间。
G1收集器的常见操作步骤:
G1的设计原则就是简化JVM性能调优,开发人员只需要简单的三步即可完成调优:
第一步:开启G1垃圾收集器,第二步:设置堆的最大内存,第三步:设置最大的停顿时间
G1收集器的适用场景
面向服务端应用,针对具有大内存、多处理器的机器。(在普通大小的堆里表现并不惊喜)最主要的应用是需要低GC延迟,并具有大堆的应用程序提供解决方案
分区Region:化整为零
一个region有可能属于Eden,Survivor或者old/Tenured内存区域。但是一个region只可能属于一个角色。G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域, 主要用于存储大对象
G1GC的垃圾回收过程主要包括如下三个环节:年轻代GC(Young GC)、老年代并发标记过程(Concurrent Marking)、混合回收(Mixed GC)
单线程、独占式、高强度的Fu11GC还是继续存在的
young gc->young gc+concurrent mark->Mixed GC顺序,进行垃圾回收。
应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程;G1的年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期,G1GC暂停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到Survivor区间或者老年区间,也有可能是两个区间都会涉及。
当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程。
标记完成马上开始混合回收过程。对于一个混合回收期,G1GC从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。和年轻代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的Region就可以了。同时,这个老年代Region是和年轻代一起被回收的。
Remembered Set(记忆集):一个对象被不同区域引用的问题

G1回收过程-年轻代GC:YGC时,首先G1停止应用程序的执行(stop-The-Wor1d),G1创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代Eden区和Survivor区所有的内存分段。
回收过程:扫描根、更新RSet(dirty card queue)、处理RSet、复制对象、处理引用
G1回收过程-并发标记过程
G1回收过程 - 混合回收
Java垃圾收集器的配置对于JVM优化来说是一个很重要的选择,选择合适的垃圾收集器可以让JVM的性能有一个很大的提升。怎么选择垃圾收集器?
优先调整堆的大小让JVM自适应完成。
如果内存小于100M,使用串行收集器
如果是单核、单机程序,并且没有停顿时间的要求,串行收集器
如果是多CPU、需要高吞吐量、允许停顿时间超过1秒,选择并行或者JVM自己选择
如果是多CPU、追求低停顿时间,需快速响应(比如延迟不能超过1秒,如互联网应用),使用并发收集器
官方推荐G1,性能高。现在互联网的项目,基本都是使用G1。

GC日志分析
-XX:+PrintGc输出GC日志。类似:-verbose:gc
-XX:+PrintGcDetails输出Gc的详细日志
-XX:+PrintGcTimestamps 输出Gc的时间戳(以基准时间的形式)
-XX:+PrintGCDatestamps 输出Gc的时间戳(以日期的形式,如2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC在进行Gc的前后打印出堆的信息
-Xloggc:…/logs/gc.1og日志文件的输出路径

革命性的ZGC:在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停颇时间限制在十毫秒以内的低延迟
ZGC的工作过程可以分为4个阶段:并发标记 - 并发预备重分配 - 并发重分配 - 并发重映射 等。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

常用的日志分析工具有:GCViewer、GCEasy、

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值