JVM(完成度95%,不断更新)
一、HotSpot
HotSpot是最新的虚拟机,替代了JIT,提高Java的运行性能。Java原先是将源代码编译为字节码在虚拟机运行,HotSpot将常用的部分代码编译为本地代码。
对象创建过程
类加载 |
---|
分配内存 |
初始化 |
设置对象头 |
执行init |
对象的内存布局
| 对象头 | 记录一个对象的实例名字、ID和实例状态
普通对象占用 8 bytes,数组占用 12 bytes (8 bytes 的普通对象头 + 4 bytes 的数组长度)
1.第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,官方称为“Mark Word”。
2.类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象的哪个实例。 |
| --- | --- |
| 基本类型 | boolean,byte 占用 1 byte
char,short 占用 2 bytes
int,float 占用 4 bytes
long,double 占用 8 bytes |
| 引用类型 | 每个引用类型占用 4 bytes |
| 对齐填充物 | 对齐填充不是必然存在的,也没有特别的含义,仅仅起着占位符的作用。
以 8 的倍数计算,不足 8 的倍数会自动补齐 |
计算 User 对象占用空间大小?
User 对象有两个属性,一个 int 类型的 id 占用 4 bytes,一个引用类型的 name 占用 4bytes,在加上 8 bytes 的对象头,正好是 16 bytes
对象的访问
Java程序需要通过JVM栈上的引用访问堆中的具体实例
方法 | 概念 | 优点 |
---|---|---|
句柄 | 堆中划分一块内存作为句柄池,引用中存放对象的句柄地址,句柄池中存放对象实例的指针以及对象类信息的指针。![]() |
引用中存放的是稳定的句柄地址,当对象移动时,引用本身不需要修改 |
直接指针 | 引用中存放的是对象的地址,对象的类信息指针也存在实例中。 如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。 ![]() |
速度更快,节省了一次指针定位的时间开销 (HotSpot采用该方式) |
二、类加载机制
虚拟机将编译好的字节码文件加载进JVM内存,也就是运行时数据区,并对数据进行校验、解析、初始化,然后由执行引擎将字节码指令转为底层系统指令。类加载器只负责加载,不负责运行。
原理
Java中的所有类,都是有误类加载器加载到JVM然后运行的,类加载器本身也是一个类,负责将class文件从硬盘加载到内存。
类加载是动态的,不会一次性将所有的类都加载,而是对于程序运行必须的类会先加载,其他类在使用的时候才进行加载。这样做可以节省内存开销。
类加载器
概念:通过类的全限定类名获取该类的二进制字节流。
启动类加载器BootStrap | 用于加载java核心类库,lib下的jar包的类,提供初始化环境,比如Object、String |
---|---|
扩展类加载器Extension | 用于加载Java的扩展库,JavaX包下的类,比如swing、awt等 |
应用程序类加载器Application | 用于加载应用类路径classpath下的类,比如自己定义的Hello.class类 |
用户自定义类加载器 | 通过继承java.lang.ClassLoader实现 |
类加载过程
加载 | 类加载器加载class文件到内存 |
---|---|
验证 | 验证class文件的正确性,class文件的开头有特定的标识 --> cafe babe |
准备 | 给类中的静态变量分配内存空间并且初始默认值 |
解析 | 将类常量池中的符号引用解析为直接引用 |
初始化 | 初始化阶段就是类的构造器 |
类初始化时机
主动引用 右边几种方式会触发类的初始化 |
使用new关键字实例化对象时 |
---|---|
调用一个类的静态方法时 | |
使用反射获取对象时,如果对象没有初始化,会先进行初始化 | |
当初始化一个类时,如果父类还没初始化,会先初始化父类 | |
虚拟机启动时,会先初始化主类(main方法) | |
被动引用 右边几种方式不会触发类的初始化 |
通过子类调用父类的静态字段,子类不会初始化 |
通过数组引用类,不会触发此类的初始化 | |
调用类的常量,不会触发类初始化,因为常量是存在类的常量池中,会转为调用常量的那个类中 |
实例初始化过程
- 父类的类构造器clinit
- 子类的类构造器clinit
- 父类的实例变量和实例代码块
- 父类的构造函数
- 子类的实例变量和实例代码块
- 子类的构造函数
如果一个类的成员变量用static修饰,则它被称为类变量(静态变量),否则它被称为实例变量。
Java的实例变量和实例代码块都是按照顺序执行的,并且实例变量和实例代码块优先于构造函数执行。
java的静态变量和静态代码块是按照顺序执行的。
实例初始化不一定要在类初始化结束之后才开始初始化,比如static StaticDemo st=new StaticDemo(); 实际上是把实例初始化嵌入到了静态初始化流程中。
双亲委派机制
概念 | 好处 |
---|---|
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。 采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object对象。 |
沙箱机制: 保证类的唯一性,防止自己写的代码污染Java原生的方法。 比如自定义一个java.lang,String类,JVM从Bootstrap启动器开始寻找String类,但是其中找不到我们写的方法,这就会抛出ClassNotFoundException |
三、JVM内存结构图
运行流程图(精简)
运行流程图(详细)
JDK7、8内存空间区别
四、程序计数器
线程私有
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器 的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处 理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因 此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程 之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。
此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。
可以使用javap 来查看类的字节码文件
程序计数器在当前指令执行时记录下一个指令的标记,方便当前指令完成后快速切换
五、Java虚拟机栈
线程私有
**_栈负责运行、堆负责存储 _
栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。8种基本类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配。
存储的数据类型
方法==栈帧
方法的本地变量(Local Variables) | 输入参数和输出参数以及方法内的变量 |
---|---|
栈操作(Operand Stack) | 记录出栈、入栈的操作;类似PC寄存器、计数器) |
栈帧数据(Frame Data) | 包括类文件(方法里面可以写新的类)、方法等等 |
内存结构
栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集。
每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。栈的大小和具体JVM的实现有关,通常在256K~756K之间,约等于1Mb左右。
“栈”通常就是指这里讲的虚拟机栈,或者更多的情况下只是指虚拟机栈中局部变量表部分。
| 局部变量表 |
局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示。
long、double等64位数据类型占两个变量槽
其余数据类型占一个
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。这里说的“大小”是指变量槽的数量,虚拟机真正使用多大的内存空间(譬如按照1个变量槽占用32个比特、64个比特,或者更多)来实现一个变量槽,这是完全由具体的虚拟机实现自行决定的事情。
|
| --- | --- |
| 操作数栈 | 每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的操作数栈,也可以称之为表达式栈(Expression Stack)。操作数栈和局部变量表在访问方式上存在着较大差异,操作数栈并非采用访问索引的方式来进行数据访问的,而是通过标准的入栈和出栈操作来完成一次数据访问。每一个操作数栈都会拥有一个明确的栈深度用于存储数值,一个32bit的数值可以用一个单位的栈深度来存储,而2个单位的栈深度则可以保存一个64bit的数值,当然操作数栈所需的容量大小在编译期就可以被完全确定下来,并保存在方法的Code属性中。 |
| 动态链接 | 每一个栈帧内部除了包含局部变量表和操作数栈之外,还包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。在运行时常量池,一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息,那就是常量池表(Constant Pool Table),那么运行时常量池就是字节码文件中常量池表的运行时表示形式。在一个字节码文件中,描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用(Symbolic Reference)来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。 |
| 方法出口 | 一个方法在执行的过程中将会产生两种调用结果:一种是方法正常调用完成,而另外一种则是方法异常调用完成。如果是方法正常调用完成,那么这就意味着,被调用的当前方法在执行的过程中将不会有任何的异常被抛出,并且方法在执行的过程中一旦遇见字节码返回指令时,将会把方法的返回值返回给它的调用者,不过一个方法在正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定。在字节码指令中,返回指令包含ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn以及areturn,另外还有一个return指令供声明为void的方法、实例初始化方法、类和接口的初始化方法使用。 |
执行顺序
先进后出原则(弹夹)
栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个函数1被调用时就产生了一个栈帧 1,并被压入到栈中, 1方法又调用了 2方法,于是产生栈帧 2 也被压入栈, 2方法又调用了 3方法,于是产生栈帧 3 也被压入栈 …… 执行完毕后,先弹出5栈帧,再弹出4栈帧……最后弹出1栈帧……
☆堆、栈、方法区的交互关系
HotSpot是使用指针的方式来访问对象:
Java堆中会存放访问类元数据的地址(方法区)
reference存储的就直接是对象的地址
常见错误产生原因
StackOverFlowError | 线程请求的栈深度大于虚拟机允许的深度时则报此错 |
---|---|
OutOfMemoryError | 如果虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存则报此错 |
六、本地方法栈&本地方法接口
本地方法栈:****线程私有
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
因此具体的虚拟机可以根据需要自由实现它,甚至有的Java虚拟机(譬如Hot-Spot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。
与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。
本地接口的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++程序,Java 诞生的时候是 C/C++横行的时候,要想立足,必须有调用 C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是 Native Method Stack中登记 native方法,在Execution Engine 执行时加载native libraies。 目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用 Socket通信,也可以使用Web Service等等
本地方法接口:****线程共享
它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。
七、方法区
线程共享(有少量GC)
1.它存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容。
2.方法区是规范,在不同虚拟机里头实现是不一样的,最典型的就是永久代(PermGen space)和元空间(Metaspace)。
3.实例变量存在堆内存中,和方法区无关
方法区内存的数据类型
八、堆空间
一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。
内存结构
Young Generation Space | 新生区 Young/New (新生区还分伊甸区、幸存者0、1区) |
---|---|
Tenure generation space | 养老区 Old/Tenure |
MetaSpace/Permanent Space | 元空间/永久区 MetaSpace/Perm (元空间JDK8出来后替代永久区) |
- 逻辑上划分:新生区+养老区+元空间
- 物理上划分:新生区+养老区
- 元空间存在堆内存外的物理内存空间中。
MinorGC的过程(复制->清空->互换)
①eden、SurvivorFrom 复制到 SurvivorTo,年龄+1 | 首先,当Eden区满的时候会触发第一次GC,把还活着的对象拷贝到SurvivorFrom区,当Eden区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域(如果有对象的年龄已经达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1 |
---|---|
②清空 eden、SurvivorFrom | 然后,清空Eden和SurvivorFrom中的对象,也即复制之后有交换,谁空谁是to |
**③SurvivorTo和 SurvivorFrom 互换 ** | 最后,SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一次GC时的SurvivorFrom区。部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代 |
真相:98%的对象是临时对象,不会进入到old区
对象进入老年代规则
- 躲过15次GC之后进入老年代(-XX:MaxTenuringThreshold设置)
- 大对象直接进入老年代(-XX:PretenureSizeThreshold设置)
- 动态对象年龄判断(年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n以上的对象都放入老年代)
- Gc对象太多无法放入Survivor区,会将这些对象放到老年代 (通过老年代空间分配担保规则)
New对象GC流程、老年代空间分配担保规则
常量池的区别
| **常量池/class常量池 **
**(Constant Pool) ** | java文件被编译成 class文件,class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项就是常量池(Constant Pool),用于存放编译器生成的各种字面量( Literal )和 符号引用(Symbolic References)。
|
| --- | --- |
| 字符串常量池/全局字符串池
(String Pool) | String Pool 中存的是 引用值,而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。
在类加载完成,经过验证,准备阶段之后 在 堆 中生成字符串对象实例,然后 将该字符串对象实例的 引用值 存到 String Pool 中。
在 HotSpot VM 里实现的 String Pool 功能的是一个 StringTable 类,它是一个哈希表,里面存的是 驻留字符( 也就是用双引号括起来的部分)的 引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个 StringTable 引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个 HotSpot VM 的实例只有一份,被所有的类共享。 |
| 运行时常量池
(Runtime Constant Pool) | jvm在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。
在类加载完成之后,JVM会将每个class常量池
中的符号引用值转存到 运行时常量池
中,也就是说,每个class都有一个 运行时常量池
,类在 解析阶段 ,将 符号引用 替换成 直接引用 ,与 字符串常量池
中的引用值保持一致。 |
符号、直接引用
符号引用 | 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。 一般包含以下三类常量 - 类和接口的全限定名 - 字段的名称和描述符 - 方法的名称和描述符 |
---|---|
直接引用 | 直接引用可以使直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。 |
字面量、常量、变量
int a; //变量
const int b = 10; //b为常量,10为字面量
string str = “hello world!”; // str 为变量,hello world!为字面量
字面量是指由字母,数字等构成的字符串或者数值,它只能作为右值出现,(右值是指等号右边的值,如:int a=123这里的a为左值,123为右值。)
常量和变量都属于变量,只不过常量是赋过值后不能再改变的变量,而普通的变量可以再进行赋值操作。
元空间
元空间是方法区的是实现
Klass MetaSpace | Klass Metaspace就是用来存klass的,klass是我们熟知的class文件在jvm里的运行时数据结构,不过有点要提的是我们看到的类似A.class其实是存在heap里的,是java.lang.Class的一个对象实例。这块内存是紧接着Heap的,和我们之前的perm一样,这块内存大小可通过-XX:CompressedClassSpaceSize参数来控制,这个参数前面提到了默认是1G,但是这块内存也可以没有,假如没有开启压缩指针就不会有这块内存,这种情况下klass都会存在NoKlass Metaspace里,另外如果我们把-Xmx设置大于32G的话,其实也是没有这块内存的,因为会这么大内存会关闭压缩指针开关。还有就是这块内存最多只会存在一块。 |
---|---|
NoKlass MetaSpace | NoKlass Metaspace专门来存klass相关的其他的内容,比如method,constantPool等,这块内存是由多块内存组合起来的,所以可以认为是不连续的内存块组成的。这块内存是必须的,虽然叫做NoKlass Metaspace,但是也其实可以存klass的内容,上面已经提到了对应场景。 |
Klass Metaspace和NoKlass Mestaspace都是所有classloader共享的,所以类加载器们要分配内存,但是每个类加载器都有一个SpaceManager,来管理属于这个类加载的内存小块。如果Klass Metaspace用完了,那就会OOM了,不过一般情况下不会,NoKlass Mestaspace是由一块块内存慢慢组合起来的,在没有达到限制条件的情况下,会不断加长这条链,让它可以持续工作。
元空间的特点
- 充分利用了Java语言规范中的好处:类及相关的元数据的生命周期与类加载器的一致。
- 每个加载器有专门的存储空间
- 只进行线性分配
- 不会单独回收某个类
- 省掉了GC扫描及压缩的时间
- 元空间里的对象的位置是固定的
- 如果GC发现某个类加载器不再存活了,会把相关的空间整个回收掉
常见错误产生原因
如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:
(1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。
(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
九、垃圾回收
垃圾回收算法总览
垃圾回收算法图形化流程
垃圾回收器
Serial 串行垃圾回收器 |
串行垃圾回收器,它为单线程环境设计且值使用一个线程进行垃圾收集,会暂停所有的用户线程,只有当垃圾回收完成时,才会重新唤醒主线程继续执行。所以不适合服务器环境 | ![]() |
---|---|---|
Parallel 并行垃圾回收器 |
并行垃圾收集器,多个垃圾收集线程并行工作,此时用户线程也是阻塞的,适用于科学计算 / 大数据处理等弱交互场景,也就是说Serial 和 Parallel其实是类似的,不过是多了几个线程进行垃圾收集,但是主线程都会被暂停,但是并行垃圾收集器处理时间,肯定比串行的垃圾收集器要更短 注意:并行垃圾回收在_单核_CPU下可能会更慢 |
![]() |
CMS 并发标记清除 回收器 |
并发标记清除,用户线程和垃圾收集线程同时执行(不一定是并行,可能是交替执行),不需要停顿用户线程,互联网公司都在使用,适用于响应时间有要求的场景。并发是可以有交互的,也就是说可以一边进行收集,一边执行应用程序。 |
这是一种以最短回收停顿时间为目标的收集器
适合应用在互联网或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短。
CMS非常适合堆内存大,CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器 | |
| | 优点:并发收集低停顿
缺点:并发执行,对CPU资源压力大,采用的标记清除算法会导致大量碎片
由于并发进行,CMS在收集与应用线程会同时增加对堆内存的占用,也就是说,CMS必须在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,将触发担保机制,串行老年代收集器将会以STW方式进行一次GC,从而造成较大的停顿时间
CMS无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽,最后将不得不通过担保机制对堆内存进行压缩,CMS也提供了参数 -XX:CMSFullGCSBeForeCompaction(默认0,即每次都进行内存整理)来指定多少次CMS收集之后,进行一次压缩的Full GC | 四个步骤
- 初始标记(CMS initial mark)
- 只是标记一个GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程
-
并发标记(CMS concurrent mark)和用户线程一起
- 进行GC Roots跟踪过程,和用户线程一起工作,不需要暂停工作线程。主要标记过程,标记全部对象 -
重新标记(CMS remark)
- 为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程,由于并发标记时,用户线程依然运行,因此在正式清理前,在做修正 -
并发清除(CMS concurrent sweep)和用户线程一起
- 清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。基于标记结果,直接清理对象,由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS收集器的内存回收和用户线程是一起并发地执行。
|
|
G1垃圾回收器
JDK9默认 | G1垃圾回收器将堆内存分割成不同区域,然后并发的进行垃圾回收 | |
| ZGC垃圾回收器
JDK11默认 | zgc是为大内存、多cpu而生,它通过分区的思路来降低STW,比如原来我有一个巨大的房间需要打扫卫生,每打扫一次就需要很长时间的STW,因为房间太大了,后来我改变了思路,把这个巨大的房间打隔断,最终出来100个小房间,这些小房间可以是eden区、可以是s区、也可以是old区,没有固定,每次只选择最需要打扫的房间(垃圾最多的)进行打扫,最终把这个房间的存活对象转移到其它房间,同时其它房间还可以继续供客户使用,也就是并行、并发的打扫,只在某几个必须的阶段进行很短暂的STW,其它时间都是和用户线程并行工作,这样可以很好的控制STW时间,同时也因为占用了很多cpu时间并发干活导致吞吐量降低,所以如果硬件充足的情况下 可以考虑 ZGC。 | |
默认垃圾回收器
使用下面JVM命令,查看配置的初始参数
-XX:+PrintCommandLineFlags
然后运行一个程序后,能够看到它的一些初始配置信息
-XX:InitialHeapSize=266376000 -XX:MaxHeapSize=4262016000 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
-XX:+UseParallelGC
移动到最后一句,就能看到 -XX:+UseParallelGC
说明使用的是并行垃圾回收
-XX:+UseParallelGC
垃圾收集器
Java中一共有7大垃圾收集器
- UserSerialGC:串行垃圾收集器
- UserParallelGC:并行垃圾收集器
- UseConcMarkSweepGC:(CMS)并发标记清除
- UseParNewGC:年轻代的并行垃圾回收器
- UseParallelOldGC:老年代的并行垃圾回收器
- UseG1GC:G1垃圾收集器
- UserSerialOldGC:串行老年代垃圾收集器(已经被移除)
垃圾收集器使用范围
参数说明
- DefNew:Default New Generation
- Tenured:Old
- ParNew:Parallel New Generation
- PSYoungGen:Parallel Scavenge
- ParOldGen:Parallel Old Generation
G1垃圾收集器各区都能使用
新生代 | 老年代 |
---|---|
- Serial Copying: UserSerialGC,串行垃圾回收器 |
-
ParNew:UserParNewGC,新生代并行垃圾收集器
- Parallel Scavenge:UserParallelGC,并行垃圾收集器
|
- Serial Old:UseSerialOldGC,老年代串行垃圾收集器 -
Parallel Compacting(Parallel Old):UseParallelOldGC,老年代并行垃圾收集器
-
CMS:UseConcMarkSwepp,并行标记清除垃圾收集器
|
垃圾收集器组合选择、参数配置
- 单CPU或者小内存,单机程序
- -XX:+UseSerialGC
- -XX:+UseSerialGC
- 多CPU,需要最大的吞吐量,如后台计算型应用
- -XX:+UseParallelGC(这两个相互激活)
- -XX:+UseParallelOldGC
- -XX:+UseParallelGC(这两个相互激活)
- 多CPU,追求低停顿时间,需要快速响应如互联网应用
- -XX:+UseConcMarkSweepGC
- -XX:+ParNewGC
- -XX:+UseConcMarkSweepGC
| 参数 | 新生代垃圾收集器 | 新生代算法 | 老年代垃圾收集器 | 老年代算法 |
-XX:+UseSerialGC | SerialGC | 复制 | SerialOldGC | 标记整理 |
---|---|---|---|---|
-XX:+UseParNewGC | ParNew | 复制 | SerialOldGC | 标记整理 |
-XX:+UseParallelGC | Parallel [Scavenge] | 复制 | Parallel Old | 标记整理 |
-XX:+UseConcMarkSweepGC | ParNew | 复制 | CMS + Serial Old的收集器组合,Serial Old作为CMS出错的后备收集器 | 标记清除 |
-XX:+UseG1GC | G1整体上采用标记整理算法 | 局部复制 |
G1垃圾收集器
开启G1垃圾收集器
-XX:+UseG1GC
以前收集器的特点
- 年轻代和老年代是各自独立且连续的内存块
- 年轻代收集使用单eden + S0 + S1 进行复制算法
- 老年代收集必须扫描珍整个老年代区域
- 都是以尽可能少而快速地执行GC为设计原则
#### G1是什么 Garbage-First 收集器,是一款面向服务端应用的收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能满足垃圾收集暂停时间的要求。另外,它还具有一下特征:
- 像CMS收集器一样,能与应用程序并发执行
- 整理空闲空间更快
- 需要更多的时间来预测GC停顿时间
- 不希望牺牲大量的吞吐量性能
- 不需要更大的Java Heap
G1收集器设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色
- G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。
- G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。
CMS垃圾收集器虽然减少了暂停应用程序的运行时间,但是它还存在着内存碎片问题。于是,为了去除内存碎片问题,同时又保留CMS垃圾收集器低暂停时间的优点,JAVA7发布了一个新的垃圾收集器-G1垃圾收集器
G1是在2012版本才在JDK1.7中可用,Oracle官方在JDK9中将G1变成默认的垃圾收集器以替代CMS,它是一款面向服务端应用的收集器,主要应用在多CPU和大内存服务器环境下,极大减少垃圾收集的停顿时间,全面提升服务器的性能,逐步替换Java8以前的CMS收集器
主要改变时:Eden,Survivor和Tenured等内存区域不再是连续了,而是变成一个个大小一样的region,每个region从1M到32M不等。一个region有可能属于Eden,Survivor或者Tenured内存区域。
特点
- G1能充分利用多CPU,多核环境硬件优势,尽量缩短STW
- G1整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片
- 宏观上看G1之中不再区分年轻代和老年代。把内存划分成多个独立的子区域(Region),可以近似理解为一个围棋的棋盘
- G1收集器里面将整个内存区域都混合在一起了,但其本身依然在小范围内要进行年轻代和老年代的区分,保留了新生代和老年代,但他们不再是物理隔离的,而是通过一部分Region的集合且不需要Region是连续的,也就是说依然会采取不同的GC方式来处理不同的区域
- G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代和老年代的区别,也不需要完全独立的Survivor(to space)堆做复制准备,G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换。
#### 底层原理 Region区域化垃圾收集器,化整为零,打破了原来新生区和老年区的壁垒,避免了全内存扫描,只需要按照区域来进行扫描即可。
区域化内存划片Region,整体遍为了一些列不连续的内存区域,避免了全内存区的GC操作。
核心思想是将整个堆内存区域分成大小相同的子区域(Region),在JVM启动时会自动设置子区域大小
在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可,每个分区也不会固定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数`-XX:G1HeapRegionSize=n` 可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。
大小范围在1MB~32MB,最多能设置2048个区域,也即能够支持的最大内存为:32MB*2048 = 64G内存
Region区域化垃圾收集器
Region区域化垃圾收集器
G1将新生代、老年代的物理空间划分取消了,同时对内存区域进行了划分
G1算法将堆划分为若干个区域(Reign),它仍然属于分代收集器,这些Region的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。
这些Region的一部分包含老年代,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片的问题存在了。
在G1中,还有一种特殊的区域,叫做Humongous(巨大的)区域,如果一个对象占用了空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象,这些巨型对象默认直接分配在老年代,但是如果他是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响,为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H区来存储,为了能找到连续的H区,有时候不得不启动Full GC。
回收步骤
针对Eden区进行收集,Eden区耗尽后会被触发,主要是小区域收集 + 形成连续的内存块,避免内碎片
- Eden区的数据移动到Survivor区,加入出现Survivor区空间不够,Eden区数据会晋升到Old区
- Survivor区的数据移动到新的Survivor区,部分数据晋升到Old区
- 最后Eden区收拾干净了,GC结束,用户的应用程序继续执行
小区域收集 + 形成连续的内存块,最后在收集完成后,就会形成连续的内存空间,这样就解决了内存碎片的问题
四步过程
- 初始标记:只标记GC Roots能直接关联到的对象
- 并发标记:进行GC Roots Tracing(链路扫描)的过程
- 最终标记:修正并发标记期间,因为程序运行导致标记发生变化的那一部分对象
- 筛选回收:根据时间来进行价值最大化回收
参数配置
开发人员仅仅需要申明以下参数即可
三步归纳:-XX:+UseG1GC -Xmx32G -XX:MaxGCPauseMillis=100
-XX:MaxGCPauseMillis=n:最大GC停顿时间单位毫秒,这是个软目标,JVM尽可能停顿小于这个时间
G1和CMS比较
- G1不会产生内碎片
- 是可以精准控制停顿。该收集器是把整个堆(新生代、老年代)划分成多个固定大小的区域,每次根据允许停顿的时间去收集垃圾最多的区域。
### # 十、确定垃圾、什么是GC Roots? ### 什么是垃圾? 简单来说就是内存中不再被使用的空间就是垃圾 ### 如何判断一个对象是否可以被回收 | 引用计数法 | Java中,引用和对象是有关联的。如果要操作对象则必须用引用进行。
因此,很显然一个简单的办法就是通过引用计数来判断一个对象是否可以回收。简单说,给对象中添加一个引用计数器
每当有一个地方引用它,计数器值加1
每当有一个引用失效,计数器值减1
任何时刻计数器值为零的对象就是不可能再被使用的,那么这个对象就是可回收对象。
那么为什么主流的Java虚拟机里面都没有选用这个方法呢?其中最主要的原因是它很难解决对象之间相互循环引用的问题。
该算法存在但目前无人用了,解决不了循环引用的问题,了解即可。 | | --- | --- | | 枚举根节点做可达性分析 | 为了解决引用计数法的循环引用个问题,Java使用了可达性分析的方法:

所谓 GC Roots 或者说 Tracing Roots的“根集合” 就是一组必须活跃的引用
基本思路就是通过一系列名为 GC Roots的对象作为起始点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连,则说明此对象不可用。也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活,没有被遍历到的对象就被判定为死亡

必须从GC Roots对象开始,这个类似于linux的 / 也就是根目录
蓝色部分是从GC Roots出发,能够循环可达
而白色部分,从GC Roots出发,无法到达 | | 可以被当做GC Roots的对象 |
- 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中的引用对象
- 方法区中的类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中的JNI(Native方法)的引用对象
|
GC前对于重写的finalize方法的处理流程
一个案例理解GC Roots
假设我们现在有三个实体,分别是 人,狗,毛衣
然后他们之间的关系是:人 牵着 狗,狗穿着毛衣,他们之间是强连接的关系
有一天人消失了,只剩下狗狗 和 毛衣,这个时候,把人想象成 GC Roots,因为 人 和 狗之间失去了绳子连接,
那么狗可能被回收,也就是被警察抓起来,被送到流浪狗寄养所
假设狗和人有强连接的时候,狗狗就不会被当成是流浪狗
/**
* 在Java中,可以作为GC Roots的对象有:
* - 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中的引用对象
* - 方法区中的类静态属性引用的对象
* - 方法区中常量引用的对象
* - 本地方法栈中的JNI(Native方法)的引用对象
*/
public class GCRootDemo {
// 方法区中的类静态属性引用的对象
// private static GCRootDemo2 t2;
// 方法区中的常量引用,GC Roots 也会以这个为起点,进行遍历
// private static final GCRootDemo3 t3 = new GCRootDemo3(8);
public static void m1() {
// 第一种,虚拟机栈中的引用对象
GCRootDemo t1 = new GCRootDemo();
System.gc();
System.out.println("第一次GC完成");
}
public static void main(String[] args) {
m1();
}
}
如何判断常量、类被废弃了
十一、强引用、软引用、弱引用、虚引用
概念列表
强引用 StrongReference |
当内存不足的时候,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,打死也不回收~! 强引用是我们最常见的普通对象引用,只要还有一个强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到,JVM也不会回收,因此强引用是造成Java内存泄漏的主要原因之一。 对于一个普通的对象,如果没有其它的引用关系,只要超过了引用的作用于或者显示地将相应(强)引用赋值为null,一般可以认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾回收策略) |
|
---|---|---|
软引用 SoftReference |
软引用是一种相对弱化了一些的引用,需要用Java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集,对于只有软引用的对象来讲: - 当系统内存充足时,它不会被回收 |
- 当系统内存不足时,它会被回收
软引用通常在对内存敏感的程序中,比如Mybatis高速缓存就用到了软引用,内存够用 的时候就保留,不够用就回收 | 它们的区别主要在于GC时对对象回收时:软引用指向的对象只在内存不足时被回收,而只被弱引用指向的对象在下一次GC时被回收
弱/软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中,进行其他与该对象相关内存的回收,如HashMap中的Key被回收,则Entry应该被回收。这就是JVM的工作了。
弱引用对于构造弱集合最有用,如WeakHashMap ,它对键(而不是值)使用弱引用。
软引用解决图片缓存的问题
弱应用解决HashMap中的oom问题 |
| 弱引用
WeakReference | 不管内存是否够,只要有GC操作就会进行回收
弱引用需要用 java.lang.ref.WeakReference
类来实现,它比软引用生存期更短
对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的空间。 | |
| 虚引用
PhantomReference | 虚引用又称为幽灵引用,需要java.lang.ref.PhantomReference
类来实现
顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。
如果一个对象持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列ReferenceQueue联合使用。
虚引用的主要作用和跟踪对象被垃圾回收的状态,仅仅是提供一种确保对象被finalize以后,做某些事情的机制。
PhantomReference的get方法总是返回null,因此无法访问对象的引用对象。其意义在于说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比finalization机制更灵活的回收操作
换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候,收到一个系统通知或者后续添加进一步的处理,Java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前,做必要的清理工作
这个就相当于Spring AOP里面的后置通知
一般用于在回收时候做通知相关操作
| |
代码实现
StrongReference
public class SoftReferenceDemo {
/**
* 内存够用的时候
*/
public static void softRefMemoryEnough() {
// 创建一个强应用
Object o1 = new Object();
// 创建一个软引用
SoftReference<Object> softReference = new SoftReference<>(o1);
System.out.println(o1);
System.out.println(softReference.get());
o1 = null;
// 手动GC
System.gc();
System.out.println(o1);
System.out.println(softReference.get());
}
/**
* JVM配置,故意产生大对象并配置小的内存,让它的内存不够用了导致OOM,看软引用的回收情况
* -Xms5m -Xmx5m -XX:+PrintGCDetails
*/
public static void softRefMemoryNoEnough() {
System.out.println("========================");
// 创建一个强应用
Object o1 = new Object();
// 创建一个软引用
SoftReference<Object> softReference = new SoftReference<>(o1);
System.out.println(o1);
System.out.println(softReference.get());
o1 = null;
// 模拟OOM自动GC
try {
// 创建30M的大对象
byte[] bytes = new byte[30 * 1024 * 1024];
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(o1);
System.out.println(softReference.get());
}
}
public static void main(String[] args) {
softRefMemoryEnough();
softRefMemoryNoEnough();
}
}
我们写了两个方法,一个是内存够用的时候,一个是内存不够用的时候
我们首先查看内存够用的时候,首先输出的是 o1 和 软引用的 softReference,我们都能够看到值
然后我们把o1设置为null,执行手动GC后,我们发现softReference的值还存在,说明内存充足的时候,软引用的对象不会被回收
java.lang.Object@14ae5a5
java.lang.Object@14ae5a5
[GC (System.gc()) [PSYoungGen: 1396K->504K(1536K)] 1504K->732K(5632K), 0.0007842 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 504K->0K(1536K)] [ParOldGen: 228K->651K(4096K)] 732K->651K(5632K), [Metaspace: 3480K->3480K(1056768K)], 0.0058450 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
null
java.lang.Object@14ae5a5
下面我们看当内存不够的时候,我们使用了JVM启动参数配置,给初始化堆内存为5M
-Xms5m -Xmx5m -XX:+PrintGCDetails
但是在创建对象的时候,我们创建了一个30M的大对象
// 创建30M的大对象
byte[] bytes = new byte[30 * 1024 * 1024];
这就必然会触发垃圾回收机制,这也是中间出现的垃圾回收过程,最后看结果我们发现,o1 和 softReference都被回收了,因此说明,软引用在内存不足的时候,会自动回收
java.lang.Object@7f31245a
java.lang.Object@7f31245a
[GC (Allocation Failure) [PSYoungGen: 31K->160K(1536K)] 682K->811K(5632K), 0.0003603 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 160K->96K(1536K)] 811K->747K(5632K), 0.0006385 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 96K->0K(1536K)] [ParOldGen: 651K->646K(4096K)] 747K->646K(5632K), [Metaspace: 3488K->3488K(1056768K)], 0.0067976 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 646K->646K(5632K), 0.0004024 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] [ParOldGen: 646K->627K(4096K)] 646K->627K(5632K), [Metaspace: 3488K->3488K(1056768K)], 0.0065506 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
null
null
SoftReference
public class WeakReferenceDemo {
public static void main(String[] args) {
Object o1 = new Object();
WeakReference<Object> weakReference = new WeakReference<>(o1);
System.out.println(o1);
System.out.println(weakReference.get());
o1 = null;
System.gc();
System.out.println(o1);
System.out.println(weakReference.get());
}
}
我们看结果,能够发现,我们并没有制造出OOM内存溢出,而只是调用了一下GC操作,垃圾回收就把它给收集了
java.lang.Object@14ae5a5
java.lang.Object@14ae5a5
[GC (System.gc()) [PSYoungGen: 5246K->808K(76288K)] 5246K->816K(251392K), 0.0008236 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 808K->0K(76288K)] [ParOldGen: 8K->675K(175104K)] 816K->675K(251392K), [Metaspace: 3494K->3494K(1056768K)], 0.0035953 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
null
null
软引用和弱引用的使用场景
场景:假如有一个应用需要读取大量的本地图片
- 如果每次读取图片都从硬盘读取则会严重影响性能
- 如果一次性全部加载到内存中,又可能造成内存溢出
此时使用软引用可以解决这个问题
设计思路:使用HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占的空间,从而有效地避免了OOM的问题
Map<String, SoftReference<String>> imageCache = new HashMap<String, SoftReference<Bitmap>>();
** WeakHashMap是什么?**
比如一些常常和底层打交道的,mybatis等,底层都应用到了WeakHashMap
WeakHashMap和HashMap类似,只不过它的Key是使用了弱引用的,也就是说,当执行GC的时候,HashMap中的key会进行回收,下面我们使用例子来测试一下
我们使用了两个方法,一个是普通的HashMap方法
我们输入一个Key-Value键值对,然后让它的key置空,然后在查看结果
public class WeakHashMapDemo {
public static void main(String[] args) {
myHashMap();
System.out.println("==========");
myWeakHashMap();
}
private static void myHashMap() {
Map<Integer, String> map = new HashMap<>();
Integer key = new Integer(1);
String value = "HashMap";
map.put(key, value);
System.out.println(map);
key = null;
System.gc();
System.out.println(map);
}
private static void myWeakHashMap() {
Map<Integer, String> map = new WeakHashMap<>();
Integer key = new Integer(1);
String value = "WeakHashMap";
map.put(key, value);
System.out.println(map);
key = null;
System.gc();
System.out.println(map);
}
}
-------------------------------------------------
{1=HashMap}
{1=HashMap}
==========
{1=WeakHashMap}
{}
从这里我们看到,对于普通的HashMap来说,key置空并不会影响,HashMap的键值对,因为这个属于强引用,不会被垃圾回收。
但是WeakHashMap,在进行GC操作后,弱引用的就会被回收
PhantomReference
引用队列 ReferenceQueue
软引用,弱引用,虚引用在回收之前,需要在引用队列保存一下
我们在初始化的弱引用或者虚引用的时候,可以传入一个引用队列
那么在进行GC回收的时候,弱引用和虚引用的对象都会被回收,但是在回收之前,它会被送至引用队列中
public class PhantomReferenceDemo {
public static void main(String[] args) {
Object o1 = new Object();
// 创建引用队列
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
// 创建一个弱引用
WeakReference<Object> weakReference = new WeakReference<>(o1, referenceQueue);
// 创建一个弱引用
// PhantomReference<Object> weakReference = new PhantomReference<>(o1, referenceQueue);
System.out.println(o1);
System.out.println(weakReference.get());
// 取队列中的内容
System.out.println(referenceQueue.poll());
o1 = null;
System.gc();
System.out.println("执行GC操作");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(o1);
System.out.println(weakReference.get());
// 取队列中的内容
System.out.println(referenceQueue.poll());
}
}
-------------------------------------------------
java.lang.Object@14ae5a5
java.lang.Object@14ae5a5
null
执行GC操作
null
null
java.lang.ref.WeakReference@7f3124
从这里我们能看到,在进行垃圾回收后,我们弱引用对象,也被设置成null,
但是在队列中还能够导出该引用的实例,这就说明在回收之前,该弱引用的实例被放置引用队列中了,我们可以通过引用队列进行一些后置操作
结合GC Roots的理解
- 红色部分在垃圾回收之外,也就是强引用的
- 蓝色部分:属于软引用,在内存不够的时候,才回收
- 虚引用和弱引用:每次垃圾回收的时候,都会被干掉,但是它在干掉之前还会存在引用队列中,我们可以通过引用队列进行一些通知机制
十二、JVM调优和参数配置
JVM参数类型
标配参数 | - -version - -help - java -showversion |
---|---|
X参数(了解) | - -Xint 解释执行 - -Xcomp 第一次使用就编译成本地代码 - -Xmixed 混合模式 |
XX参数(重点) | - Boolean类型 |
--公式:-XX:+或者-某个属性 (+表示开启 -表示关闭) --案例:-XX:+PrintGCDetails 表示开启了GC详情打印功能 - K-V类型 |
|
--公式:-XX:属性key=value |
JPS、Jinfo的使用
在java程序起来后,在Bash窗口中输入jps -l 可以获得系统当前运行的java程序的进程号
然后使用jinfo 针对该进程进行一系列查询
比如现在要查询PrintGCDetails这个功能是否开启
十三、常见OOM
经典错误
JVM中常见的两个错误
- StackoverFlowError :栈溢出
- OutofMemoryError: java heap space:堆溢出
除此之外,还有以下的错误
- java.lang.StackOverflowError
- java.lang.OutOfMemoryError:java heap space
- java.lang.OutOfMemoryError:GC overhead limit exceeeded
- java.lang.OutOfMemoryError:Direct buffer memory
- java.lang.OutOfMemoryError:unable to create new native thread
- java.lang.OutOfMemoryError:Metaspace
StackOverflowError
堆栈溢出,我们有最简单的一个递归调用,就会造成堆栈溢出,也就是深度的方法调用
栈一般是512K,不断的深度调用,直到栈被撑破
public class StackOverflowErrorDemo {
public static void main(String[] args) {
stackOverflowError();
}
/**
* 栈一般是512K,不断的深度调用,直到栈被撑破
* Exception in thread "main" java.lang.StackOverflowError
*/
private static void stackOverflowError() {
stackOverflowError();
}
}
------------------------
Exception in thread "main" java.lang.StackOverflowError
at com.moxi.interview.study.oom.StackOverflowErrorDemo.stackOverflowError(StackOverflowErrorDemo.java:8)
OutOfMemoryError
Java heap space
创建了很多对象,导致堆空间不够存储
public class JavaHeapSpaceDemo {
public static void main(String[] args) {
// 堆空间的大小 -Xms10m -Xmx10m
// 创建一个 80M的字节数组
byte [] bytes = new byte[80 * 1024 * 1024];
}
}
//我们创建一个80M的数组,会直接出现Java heap space
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
GC overhead limit exceeded
GC超出开销限制
GC回收时间过长时会抛出OutOfMemoryError,过长的定义是,超过了98%的时间用来做GC,并且回收了不到2%的堆内存
连续多次GC都只回收了不到2%的极端情况下,才会抛出。假设不抛出GC overhead limit 错误会造成什么情况呢?
那就是GC清理的这点内存很快会再次被填满,迫使GC再次执行,这样就形成了恶性循环,CPU的使用率一直都是100%,而GC却没有任何成果。
为了更快的达到效果,我们首先需要设置JVM启动参数
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
public class GCOverheadLimitDemo {
public static void main(String[] args) {
int i = 0;
List<String> list = new ArrayList<>();
try {
while(true) {
list.add(String.valueOf(++i).intern());
}
} catch (Exception e) {
System.out.println("***************i:" + i);
e.printStackTrace();
throw e;
} finally {
}
}
}
-----------------------------
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7106K->7106K(7168K)] 9154K->9154K(9728K), [Metaspace: 3504K->3504K(1056768K)], 0.0311093 secs] [Times: user=0.13 sys=0.00, real=0.03 secs]
[Full GC (Ergonomics) [PSYoungGen: 2047K->0K(2560K)] [ParOldGen: 7136K->667K(7168K)] 9184K->667K(9728K), [Metaspace: 3540K->3540K(1056768K)], 0.0058093 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 2560K, used 114K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 5% used [0x00000000ffd00000,0x00000000ffd1c878,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 7168K, used 667K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 9% used [0x00000000ff600000,0x00000000ff6a6ff8,0x00000000ffd00000)
Metaspace used 3605K, capacity 4540K, committed 4864K, reserved 1056768K
class space used 399K, capacity 428K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.lang.Integer.toString(Integer.java:403)
at java.lang.String.valueOf(String.java:3099)
at com.moxi.interview.study.oom.GCOverheadLimitDemo.main(GCOverheadLimitDemo.java:9)
!!! 我们能够看到 多次Full GC,并没有清理出空间,在多次执行GC操作后,就抛出异常 GC overhead limit
Direct buffer memory
Netty + NIO:这是由于NIO引起的
写NIO程序的时候经常会使用ByteBuffer来读取或写入数据,这是一种基于通道(Channel) 与 缓冲区(Buffer)的I/O方式,它可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
ByteBuffer.allocate(capability):第一种方式是分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢
ByteBuffer.allocteDirect(capability):第二种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存的拷贝,所以速度相对较快
但如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象就不会被回收,这时候怼内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OutOfMemoryError,那么程序就奔溃了。
一句话说:本地内存不足,但是堆内存充足的时候,就会出现这个问题
为了更快的达到效果,我们首先需要设置JVM启动参数
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
// 只设置了5M的物理内存使用,但是却分配 6M的空间
ByteBuffer bb = ByteBuffer.allocateDirect(6 * 1024 * 1024);
--------------------------
配置的maxDirectMemory:5.0MB
[GC (System.gc()) [PSYoungGen: 2030K->488K(2560K)] 2030K->796K(9728K), 0.0008326 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 488K->0K(2560K)] [ParOldGen: 308K->712K(7168K)] 796K->712K(9728K), [Metaspace: 3512K->3512K(1056768K)], 0.0052052 secs] [Times: user=0.09 sys=0.00, real=0.00 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:693)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at com.moxi.interview.study.oom.DIrectBufferMemoryDemo.main(DIrectBufferMemoryDemo.java:19)
unable to create new native thread
不能够创建更多的新的线程了,也就是说创建线程的上限达到了
在高并发场景的时候,会应用到
高并发请求服务器时,经常会出现如下异常java.lang.OutOfMemoryError:unable to create new native thread
,准确说该native thread异常与对应的平台有关
导致原因:
- 应用创建了太多线程,一个应用进程创建多个线程,超过系统承载极限
- 服务器并不允许你的应用程序创建这么多线程,linux系统默认运行单个进程可以创建的线程为1024个(除去系统占用,实际在900左右,当然root用户没有上限),如果应用创建超过这个数量,就会报
java.lang.OutOfMemoryError:unable to create new native thread
解决方法:
- 想办法降低你应用程序创建线程的数量,分析应用是否真的需要创建这么多线程,如果不是,改代码将线程数降到最低
- 对于有的应用,确实需要创建很多线程,远超过linux系统默认1024个线程限制,可以通过修改linux服务器配置,扩大linux默认限制
```java public class UnableCreateNewThreadDemo { public static void main(String[] args) { for (int i = 0; ; i++) { System.out.println("************** i = " + i); new Thread(() -> { try { TimeUnit.SECONDS.sleep(Integer.MAX_VALUE); } catch (InterruptedException e) { e.printStackTrace(); } }, String.valueOf(i)).start(); } } }
这个时候,就会出现下列的错误,线程数大概在 900多个
Exception in thread "main" java.lang.OutOfMemoryError: unable to cerate new native thread
如何查看线程数
```shell
ulimit -u
MetaSpace
元空间内存不足,Matespace元空间应用的是本地内存-XX:MetaspaceSize
的处理化大小为20M
#### 元空间是什么
元空间就是我们的方法区的实现,存放的是类模板,类信息,常量池等
Metaspace是方法区HotSpot中的实现,它与持久代最大的区别在于:Metaspace并不在虚拟内存中,而是使用本地内存,
也即在java8中,class metadata(the virtual machines internal presentation of Java class),
被存储在叫做Matespace的native memory
永久代(java8后背元空间Metaspace取代了)存放了以下信息:
- 虚拟机加载的类信息
- 常量池
- 静态变量
- 即时编译后的代码
模拟Metaspace空间溢出,我们不断生成类 往元空间里灌输,类占据的空间总会超过Metaspace指定的空间大小
为了更快的达到效果,我们首先需要设置JVM启动参数
-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m
public class MetaspaceOutOfMemoryDemo {
// 静态类
static class OOMTest {
}
public static void main(final String[] args) {
// 模拟计数多少次以后发生异常
int i =0;
try {
while (true) {
i++;
// 使用Spring的动态字节码技术
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMTest.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o, args);
}
});
}
} catch (Exception e) {
System.out.println("发生异常的次数:" + i);
e.printStackTrace();
} finally {
}
}
}
------------------
报错:
发生异常的次数: 201
java.lang.OutOfMemoryError:Metaspace
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 电商平台中订单未支付过期如何实现自动关单?
· 用 .NET NativeAOT 构建完全 distroless 的静态链接应用
· 为什么构造函数需要尽可能的简单
· 探秘 MySQL 索引底层原理,解锁数据库优化的关键密码(下)
· 大模型 Token 究竟是啥:图解大模型Token
· 如何开发 MCP 服务?保姆级教程!
· 1.net core 工作流WorkFlow流程(介绍)
· 瞧瞧别人家的限流,那叫一个优雅!
· C# 工业视觉开发必刷20道 Halcon 面试题
· 从零散笔记到结构化知识库:我的文档网站建设之路