JVM知识点整理
2021-01-28 11:20:04 1 举报
AI智能生成
JVM 2篇
作者其他创作
大纲/内容
性能调用与监控篇
面试篇
大厂面试题
蚂蚁金服
百度
天猫
京东
字节跳动
- 你知道哪几种垃圾回收器,各自的优缺点,重点讲一下cms和G1?
- JVM GC算法有哪些,目前的JDK版本采用什么回收算法?
- G1回收器讲下回收过程GC是什么?为什么要有GC?
- GC的两种判定方法?CMS收集器与G1收集器的特点
百度
- 说一下GC算法,分代回收说下
- 垃圾收集策略和算法
天猫
- JVM GC原理,JVM怎么回收内存
- CMS特点,垃圾回收算法有哪些?各自的优缺点,他们共同的缺点是什么?
- Java的垃圾回收器都有哪些,说下g1的应用场景,平时你是如何搭配使用垃圾回收器的
京东
- 你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms和G1,
- 包括原理,流程,优缺点。垃圾回收算法的实现原理
- 讲一讲垃圾回收算法。
- 什么情况下触发垃圾回收?
- 如何选择合适的垃圾收集算法?
- JVM有哪三种垃圾回收器?
字节跳动
- 常见的垃圾回收器算法有哪些,各有什么优劣?
- System.gc()和Runtime.gc()会做什么事情?
- Java GC机制?GC Roots有哪些?
- Java对象的回收方式,回收算法。
- CMS和G1了解么,CMS解决什么问题,说一下回收的过程。
- CMS回收停顿了几次,为什么要停顿两次?
Jvm内存与垃圾回收篇
转:https://gitee.com/moxi159753/LearningNotes/tree/master/JVM
JVM与java体系结构
推荐书籍
深入理解Java虚拟机(第三版)
Java虚拟机规范
Java虚拟机规范
Java虚拟机定义简述
Java虚拟机是一台执行Java字节码的虚拟计算机,它拥有独立的运行机制,其运行的Java字节码也未必由Java语言编译而成。
JVM平台的各种语言可以共享Java虚拟机带来的跨平台性、优秀的垃圾回器,以及可靠的即时编译器。
Java技术的核心就是Java虚拟机(JVM,Java Virtual Machine),因为所有的Java程序都运行在Java虚拟机内部。
Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行。每一条Java指令,Java虚拟机规范中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里。
特点:
一次编译,到处运行
自动内存管理
自动垃圾回收功能
JVM平台的各种语言可以共享Java虚拟机带来的跨平台性、优秀的垃圾回器,以及可靠的即时编译器。
Java技术的核心就是Java虚拟机(JVM,Java Virtual Machine),因为所有的Java程序都运行在Java虚拟机内部。
Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行。每一条Java指令,Java虚拟机规范中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里。
特点:
一次编译,到处运行
自动内存管理
自动垃圾回收功能
Java整体架构
HotSpot VM是目前市面上高性能虚拟机的代表作之一。
它采用解释器与即时编译器并存的架构。
在今天,Java程序的运行性能早已脱胎换骨,已经达到了可以和C/C++程序一较高下的地步。
它采用解释器与即时编译器并存的架构。
在今天,Java程序的运行性能早已脱胎换骨,已经达到了可以和C/C++程序一较高下的地步。
jJVM整体架构,注 jdk8后使用元空间(mate space)代替了方法区
JVM的架构模型
基于栈式架构的特点
设计和实现更简单,适用于资源受限的系统;
避开了寄存器的分配难题:使用零地址指令方式分配。
指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小,编译器容易实现。
不需要硬件支持,可移植性更好,更好实现跨平台
设计和实现更简单,适用于资源受限的系统;
避开了寄存器的分配难题:使用零地址指令方式分配。
指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小,编译器容易实现。
不需要硬件支持,可移植性更好,更好实现跨平台
基于寄存器架构的特点
典型的应用是x86的二进制指令集:比如传统的PC以及Android的Davlik虚拟机。
指令集架构则完全依赖硬件,可移植性差
性能优秀和执行更高效
花费更少的指令去完成一项操作。
在大部分情况下,基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主,而基于栈式架构的指令集却是以零地址指令为主方水洋
典型的应用是x86的二进制指令集:比如传统的PC以及Android的Davlik虚拟机。
指令集架构则完全依赖硬件,可移植性差
性能优秀和执行更高效
花费更少的指令去完成一项操作。
在大部分情况下,基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主,而基于栈式架构的指令集却是以零地址指令为主方水洋
JVM生命周期
虚拟机的启动
Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的。
虚拟机的执行
一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序。
程序开始执行时他才运行,程序结束时他就停止。
执行一个所谓的Java程序的时候,真真正正在执行的是一个叫做Java虚拟机的进程。
程序开始执行时他才运行,程序结束时他就停止。
执行一个所谓的Java程序的时候,真真正正在执行的是一个叫做Java虚拟机的进程。
虚拟机的退出
1、程序正常执行结束
2、程序在执行过程中遇到了异常或错误而异常终止
3、由于操作系统用现错误而导致Java虚拟机进程终止
4、某线程调用Runtime类或system类的exit方法,或Runtime类的halt方法,并且Java安全管理器也允许这次exit或halt操作。
5、除此之外,JNI(Java Native Interface)规范描述了用JNI Invocation API来加载或卸载 Java虚拟机时,Java虚拟机的退出情况。
2、程序在执行过程中遇到了异常或错误而异常终止
3、由于操作系统用现错误而导致Java虚拟机进程终止
4、某线程调用Runtime类或system类的exit方法,或Runtime类的halt方法,并且Java安全管理器也允许这次exit或halt操作。
5、除此之外,JNI(Java Native Interface)规范描述了用JNI Invocation API来加载或卸载 Java虚拟机时,Java虚拟机的退出情况。
JVM发展历程
Sun Classic VM
Exact VM
HotSpot VM
JRockit
IBM的J9
KVM和CDC / CLDC Hotspot
Azul VM
Liquid VM
Apache Marmony
Micorsoft JVM
Taobao JVM
Dalvik VM
Graal VM
Exact VM
HotSpot VM
JRockit
IBM的J9
KVM和CDC / CLDC Hotspot
Azul VM
Liquid VM
Apache Marmony
Micorsoft JVM
Taobao JVM
Dalvik VM
Graal VM
类加载子系统
加载流程
类加载流程
加载阶段 loadding:
通过一个类的全限定名获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
通过一个类的全限定名获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
链接阶段 linking
验证 verify:
目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
准备 Prepare:
为类变量分配内存并且设置该类变量的默认初始值,即零值。(使用static修饰的成员变量是类变量,属于该类本身;没有使用static修饰的成员变量是实例变量,属于该类的实例。)
这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
为类变量分配内存并且设置该类变量的默认初始值,即零值。(使用static修饰的成员变量是类变量,属于该类本身;没有使用static修饰的成员变量是实例变量,属于该类的实例。)
这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
解析 Resolve:
将常量池内的符号引用转换为直接引用的过程。事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等
将常量池内的符号引用转换为直接引用的过程。事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等
初始化阶段 init:
初始化阶段就是执行类构造器法()的过程。
此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
也就是说,当我们代码中包含static变量的时候,就会有clinit方法
构造器方法中指令按语句在源文件中出现的顺序执行。
初始化阶段就是执行类构造器法()的过程。
此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
也就是说,当我们代码中包含static变量的时候,就会有clinit方法
构造器方法中指令按语句在源文件中出现的顺序执行。
加载器分类
JVM支持两种类型的类加载器 。
分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,
但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。
上图所示。
图中这里的四者之间是包含关系,不是上层和下层,也不是子系统的继承关系。
分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,
但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。
上图所示。
图中这里的四者之间是包含关系,不是上层和下层,也不是子系统的继承关系。
启动类加载器(引导类加载器,Bootstrap ClassLoader)
这个类加载使用C/C++语言实现的,嵌套在JVM内部。
它用来加载Java的核心库(JAVAHOME/jre/1ib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
并不继承自ava.lang.ClassLoader,没有父加载器。
加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
这个类加载使用C/C++语言实现的,嵌套在JVM内部。
它用来加载Java的核心库(JAVAHOME/jre/1ib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
并不继承自ava.lang.ClassLoader,没有父加载器。
加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
扩展类加载器(Extension ClassLoader)
Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
派生于ClassLoader类
父类加载器为启动类加载器
从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/1ib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
派生于ClassLoader类
父类加载器为启动类加载器
从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/1ib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
应用程序类加载器(系统类加载器,AppClassLoader)
java语言编写,由sun.misc.LaunchersAppClassLoader实现
派生于ClassLoader类
父类加载器为扩展类加载器
它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
通过classLoader#getSystemclassLoader()方法可以获取到该类加载器
java语言编写,由sun.misc.LaunchersAppClassLoader实现
派生于ClassLoader类
父类加载器为扩展类加载器
它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
通过classLoader#getSystemclassLoader()方法可以获取到该类加载器
用户自定义类加载器
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,
我们还可以自定义类加载器,来定制类的加载方式。 为什么要自定义类加载器?
隔离加载类
修改类加载的方式
扩展加载源
防止源码泄漏
用户自定义类加载器实现步骤:
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,
我们还可以自定义类加载器,来定制类的加载方式。 为什么要自定义类加载器?
隔离加载类
修改类加载的方式
扩展加载源
防止源码泄漏
用户自定义类加载器实现步骤:
- 开发人员可以通过继承抽象类ava.1ang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
- 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写1oadClass()方法,从而实现自定义的类加载类,
但是在JDK1.2之后已不再建议用户去覆盖1oadclass()方法,而是建议把自定义的类加载逻辑写在findclass()方法中 - 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URIClassLoader类,这样就可以避免自己去编写findclass()
方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。
双亲委派机制
双亲委派流程
工作原理
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,
子加载器才会尝试自己去加载,这就是双亲委派模式。
沙箱安全机制:
自定义string类,但是在加载自定义String类的时候会率先使用引导类加载器加载,
而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),
报错信息说没有main方法,就是因为加载的是rt.jar包中的string类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
自定义string类,但是在加载自定义String类的时候会率先使用引导类加载器加载,
而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),
报错信息说没有main方法,就是因为加载的是rt.jar包中的string类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
如何判断两个class对象是否相同
两个必要条件:
- 类的完整类名必须一致,包括包名。
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
类的主动使用与被动使用
主动使用
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法I
- 反射(比如:Class.forName("com.atguigu.Test"))
- 初始化一个类的子类
- Java虚拟机启动时被标明为启动类的类
- JDK7开始提供的动态语言支持:
- java.lang.invoke.MethodHandle实例的解析结果REF getStatic、REF putStatic、REF invokeStatic句柄对应的类没有初始化,则初始化
被动使用
其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。
运行时数据区概述与线程
运行时数据区
JVM的内存结构,其实取决于其实现,不同厂商的JVM,或者同一厂商发布的不同版本,都有可能存在一定差异。主要以oracle HotSpot VM为默认虚拟机。
JVM系统线程
这些主要的后台系统线程在Hotspot JVM里主要是以下几个:
- 虚拟机线程:这种线程的操作是需要JVM达到安全点才会出现。这些操作必须在不同的线程中发生的
原因是他们都需要JVM达到安全点,这样堆才不会变化。这种线程的执行类型包括"stop-the-world"的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销。 - 周期任务线程:这种线程是时间周期事件的体现(比如中断),他们一般用于周期性操作的调度执行。
- GC线程:这种线程对在JVM里不同种类的垃圾收集行为提供了支持。
- 编译线程:这种线程在运行时会将字节码编译成到本地代码。
- 信号调度线程:这种线程接收信号并发送给JVM,在它内部通过调用适当的方法进行处理。
程序计数器
介绍:
- 程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。
- 它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域。
- 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;
或者,如果是在执行native方法,则是未指定值(undefned)。 - 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。 - 它是唯一一个在Java虚拟机规范中没有规定任何outotMemoryError情况的区域。
作用:
可以看做是当前线程所执行字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条要执行的字节码指令。
因为线程是一个个的顺序执行流,CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行
可以看做是当前线程所执行字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条要执行的字节码指令。
因为线程是一个个的顺序执行流,CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行
图示
PC寄存器为什么被设定为私有的?
- 所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,PC寄存器保证分毫无差。
- 为了准确记录各个线程正在执行的当前字节码指令地址,最好的办法是为每个线程都分配一个PC寄存器,确保各个线程之间独立计算。
- 由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。
虚拟机栈
定义:
Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,
其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。
生命周期:
和线程一致,线程结束,该虚拟机栈也就销毁了。
作用:
主管Java程序的运行,他保存方法的局部变量、部分结果,并参与方法的调用和返回。
特点:
栈是一种快速有效的分配存储方式,访问速度仅次于罹序计数器。JVM直接对Java栈的操作只有两个:
Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,
其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。
生命周期:
和线程一致,线程结束,该虚拟机栈也就销毁了。
作用:
主管Java程序的运行,他保存方法的局部变量、部分结果,并参与方法的调用和返回。
特点:
栈是一种快速有效的分配存储方式,访问速度仅次于罹序计数器。JVM直接对Java栈的操作只有两个:
- 每个方法执行,伴随着进栈(入栈、压栈)
- 执行结束后的出栈工作
图示
异常情况
1:如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。
如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackoverflowError 异常。
如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackoverflowError 异常。
/** * 演示栈中的异常:StackOverflowError */
public class StackErrorTest {
private static int count = 1;
public static void main(String[] args) {
System.out.println(count++);
main(args);
}
}
public class StackErrorTest {
private static int count = 1;
public static void main(String[] args) {
System.out.println(count++);
main(args);
}
}
2:如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有
足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个 OutofMemoryError 异常。
足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个 OutofMemoryError 异常。
/**
* VM Args:-Xss2M
*启动多个线程,实现 OutOfMemoryError异常;
*通过不断的创建线程的方式来产生内存溢出异常。
*/
public class JavaVMStackOOM {
private void dontStop() {
while (true) {
}
}
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) throws Throwable {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
* VM Args:-Xss2M
*启动多个线程,实现 OutOfMemoryError异常;
*通过不断的创建线程的方式来产生内存溢出异常。
*/
public class JavaVMStackOOM {
private void dontStop() {
while (true) {
}
}
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) throws Throwable {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
栈运行原理:
不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
栈帧的内部结构
局部变量表(Local Variables)
介绍:
- 局部变量表:Local Variables,被称之为局部变量数组或本地变量表
- 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。
- 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
- 局部变量表所需的容量大小是在编译期确定下来,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
- 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。
- 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
关于:slot:
- 局部变量表,最基本的存储单元是Slot(变量槽)
- 参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。
- 局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。
- 在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(1ong和double)占用两个slot。
- byte、short、char 在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true。 1ong和double则占据两个slot。
- JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
- 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上
- 如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问1ong或doub1e类型变量)
- 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的s1ot处,其余的参数按照参数表顺序继续排列。
图示
Slot的重复利用:
栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
图示
静态变量与局部变量的对比:
变量的分类:
我们知道类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,
赋予程序员在代码中定义的初始值。
和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。
在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
变量的分类:
- 按数据类型分:基本数据类型、引用数据类型
- 按类中声明的位置分:成员变量(类变量-static修饰,实例变量)、局部变量
- 类变量:linking的paper阶段,给类变量默认赋值,init阶段给类变量显示赋值即静态代码块
- 实例变量:随着对象创建,会在堆空间中分配实例变量空间,并进行默认赋值
- 局部变量:在使用前必须进行显式赋值,不然编译不通过。
参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配。
我们知道类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,
赋予程序员在代码中定义的初始值。
和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。
在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
操作数栈(operand Stack)(或表达式栈)
概念:
- 每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last - In - First -Out)的 操作数栈,也可以称之为 表达式栈(Expression Stack)
- 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)
- 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈
- 比如:执行复制、交换、求和等操作
图示
代码举例
byte、short、char、boolean 内部都是使用int型来进行保存的;
从上面的代码我们可以知道,我们都是通过bipush对操作数 15 和 8进行入栈操作;
同时使用的是 iadd方法进行相加操作,i -> 代表的就是 int,也就是int类型的加法操作。
byte、short、char、boolean 内部都是使用int型来进行保存的;
从上面的代码我们可以知道,我们都是通过bipush对操作数 15 和 8进行入栈操作;
同时使用的是 iadd方法进行相加操作,i -> 代表的就是 int,也就是int类型的加法操作。
- 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
- 操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
- 这个时候数组是有长度的,因为数组一旦创建,那么就是不可变的。
- 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为maxstack的值。
- 栈中的任何一个元素都是可以任意的Java数据类型
- 32bit的类型占用一个栈单位深度
- 64bit的类型占用两个栈单位深度
- 操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问。
- 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
- 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。|
- 另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
代码流程剖析:
public void testAddOperation() {
byte i = 15;
int j = 8;
int k = i + j;
}
public void testAddOperation() {
byte i = 15;
int j = 8;
int k = i + j;
}
使用javap 命令反编译class文件: javap -v 类名.class
step1:首先执行第一条语句,PC寄存器指向的是0,
也就是指令地址为0,然后使用bipush让操作数15入栈。
也就是指令地址为0,然后使用bipush让操作数15入栈。
2:PC + 1,指向下一行代码,下一行代码就是将操作数栈的元素存储
到局部变量表1的位置,我们可以看到局部变量表的已经增加了一个元素
(为什么局部变量表不是从0开始的呢?其实局部变量表也是从0开始的,
但是因为0号位置存储的是this指针,所以说就直接省略了~)
到局部变量表1的位置,我们可以看到局部变量表的已经增加了一个元素
(为什么局部变量表不是从0开始的呢?其实局部变量表也是从0开始的,
但是因为0号位置存储的是this指针,所以说就直接省略了~)
step3:PC+1,指向的是下一行。让操作数8也入栈,
同时执行store操作,存入局部变量表中
同时执行store操作,存入局部变量表中
4、
5、然后从局部变量表中,依次将数据放在操作数栈中
6
7、将操作数栈中的两个元素执行相加操作,并存储在局部变量表3的位置
8。PC寄存器的位置指向10,也就是return方法,则直接退出方法
栈顶缓存技术:Top Of Stack Cashing
- 基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数。
- 由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存(Tos,Top-of-Stack Cashing)技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
- 寄存器:指令更少,执行速度快
动态链接(DynamicLinking)(或指向运行时常量池的方法引用)
概述:
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,
包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如:invokedynamic指令
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Reference)保存在class文件的常量池里。
比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用
就是为了将这些符号引用转换为调用方法的直接引用。
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,
包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如:invokedynamic指令
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Reference)保存在class文件的常量池里。
比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用
就是为了将这些符号引用转换为调用方法的直接引用。
为什么需要运行时常量池?
因为在不同的方法,都可能调用常量或者方法,所以只需要存储一份即可,节省了空间。
常量池的作用:就是为了提供一些符号和常量,便于指令的识别
因为在不同的方法,都可能调用常量或者方法,所以只需要存储一份即可,节省了空间。
常量池的作用:就是为了提供一些符号和常量,便于指令的识别
方法调用:解析与分配
* 在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关
* 在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关
链接
静态链接:
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期克制,且运行期保持不变时,
这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接(比如 静态方法、私有方法)
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期克制,且运行期保持不变时,
这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接(比如 静态方法、私有方法)
动态链接:
如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用的方法的符号转换为直接引用,
由于这种引用转换过程具备动态性,因此也被称之为动态链接。
如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用的方法的符号转换为直接引用,
由于这种引用转换过程具备动态性,因此也被称之为动态链接。
绑定机制:
绑定是一个字段、方法或者类在符号引用
被替换为直接引用的过程,这仅仅发生一次。
绑定是一个字段、方法或者类在符号引用
被替换为直接引用的过程,这仅仅发生一次。
早期绑定:
早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,
这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,
这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
晚期绑定:
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,
这种绑定方式也就被称之为晚期绑定。
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,
这种绑定方式也就被称之为晚期绑定。
虚方法和非虚方法
- 如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。
- 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。
- 其他方法称为虚方法。
- 类的继承关系
- 方法的重写
普通调用指令:
- invokestatic:调用静态方法,解析阶段确定唯一方法版本
- invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本
- invokevirtual:调用所有虚方法
- invokeinterface:调用接口方法
动态调用指令:
- invokedynamic:动态解析出需要调用的方法,然后执行
Java 语言中方法重写的本质:
- 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C。
- 如果在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则
返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.1ang.I1legalAccessError 异常。 - 否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
- 如果始终没有找到合适的方法,则抛出java.1ang.AbstractMethodsrror异常
方法的调用:虚方法表
在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,为了提高性能,JVM采用在类的方法区建立一个虚方法表 (virtual method table)(非虚方法不会出现在表中)来实现。使用索引表来代替查找。
每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
虚方法表是什么时候被创建的呢?
虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。
图示
方法返回地址(Return Address)
概述:
存放调用该方法的pc寄存器的值。一个方法的结束,有两种方式:
存放调用该方法的pc寄存器的值。一个方法的结束,有两种方式:
- 正常执行完成
- 出现未处理的异常,非正常退出
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,
调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。
而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
一些附加信息
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如:对程序调试提供支持的信息。
栈的相关面试题
1、举例栈溢出的情况?(栈大小固定,请求栈的容量超过栈的最大容量 -- StackOverflowError)
通过 -Xss设置栈的大小
2、调整栈大小,就能保证不出现溢出么?
不能保证不溢出
3、分配的栈内存越大越好么?
不是,一定时间内降低了OOM概率,但是会挤占其它的线程空间,因为整个空间是有限的。
4、垃圾回收是否涉及到虚拟机栈?
不会
5、方法中定义的局部变量是否线程安全?
具体问题具体分析
何为线程安全?
* 如果只有一个线程才可以操作此数据,则必是线程安全的
* 如果有多个线程操作,则此数据是共享数据,如果不考虑共享机制,则为线程不安全
通过 -Xss设置栈的大小
2、调整栈大小,就能保证不出现溢出么?
不能保证不溢出
3、分配的栈内存越大越好么?
不是,一定时间内降低了OOM概率,但是会挤占其它的线程空间,因为整个空间是有限的。
4、垃圾回收是否涉及到虚拟机栈?
不会
5、方法中定义的局部变量是否线程安全?
具体问题具体分析
何为线程安全?
* 如果只有一个线程才可以操作此数据,则必是线程安全的
* 如果有多个线程操作,则此数据是共享数据,如果不考虑共享机制,则为线程不安全
public class StringBuilderTest {
// s1的声明方式是线程安全的
public static void method01() {
// 线程内部创建的,属于局部变量
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
}
// 这个也是线程不安全的,因为有返回值,有可能被其它的程序所调用
public static StringBuilder method04() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("a");
stringBuilder.append("b");
return stringBuilder;
}
// stringBuilder 是线程不安全的,操作的是共享数据
public static void method02(StringBuilder stringBuilder) {
stringBuilder.append("a");
stringBuilder.append("b");
}
/**
* 同时并发的执行,会出现线程不安全的问题
*/
public static void method03() {
StringBuilder stringBuilder = new StringBuilder();
new Thread(() -> {
stringBuilder.append("a");
stringBuilder.append("b");
}, "t1").start();
method02(stringBuilder);
}
// StringBuilder是线程安全的,但是String也可能线程不安全的
public static String method05() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("a");
stringBuilder.append("b");
return stringBuilder.toString();
}
}
// s1的声明方式是线程安全的
public static void method01() {
// 线程内部创建的,属于局部变量
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
}
// 这个也是线程不安全的,因为有返回值,有可能被其它的程序所调用
public static StringBuilder method04() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("a");
stringBuilder.append("b");
return stringBuilder;
}
// stringBuilder 是线程不安全的,操作的是共享数据
public static void method02(StringBuilder stringBuilder) {
stringBuilder.append("a");
stringBuilder.append("b");
}
/**
* 同时并发的执行,会出现线程不安全的问题
*/
public static void method03() {
StringBuilder stringBuilder = new StringBuilder();
new Thread(() -> {
stringBuilder.append("a");
stringBuilder.append("b");
}, "t1").start();
method02(stringBuilder);
}
// StringBuilder是线程安全的,但是String也可能线程不安全的
public static String method05() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("a");
stringBuilder.append("b");
return stringBuilder.toString();
}
}
本地方法接口(非运行时数据区内)
什么是本地方法
简单地讲,一个Native Methodt是一个Java调用非Java代码的接囗。一个Native Method是这样一个Java方法:
该方法的实现由非Java语言实现,比如C。这个特征并非Java所特有,很多其它的编程语言都有这一机制,
比如在C++中,你可以用extern "c" 告知c++编译器去调用一个c的函数。
"A native method is a Java method whose implementation is provided by non-java code."
(本地方法是一个非Java的方法,它的具体实现是非Java代码的实现)
在定义一个native method时,并不提供实现体(有些像定义一个Java interface),因为其实现体是由非java语言在外面实现的。
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序。
该方法的实现由非Java语言实现,比如C。这个特征并非Java所特有,很多其它的编程语言都有这一机制,
比如在C++中,你可以用extern "c" 告知c++编译器去调用一个c的函数。
"A native method is a Java method whose implementation is provided by non-java code."
(本地方法是一个非Java的方法,它的具体实现是非Java代码的实现)
在定义一个native method时,并不提供实现体(有些像定义一个Java interface),因为其实现体是由非java语言在外面实现的。
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序。
为什么要用本地方法
与Java环境的交互:
有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因。你可以想想Java需要与一些底层系统,
如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,
而且我们无需去了解Java应用之外的繁琐的细节。
有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因。你可以想想Java需要与一些底层系统,
如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,
而且我们无需去了解Java应用之外的繁琐的细节。
与操作系统的交互:
JVM支持着Java语言本身和运行时库,它是Java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。
然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一底层系统的支持。这些底层系统常常是强大的操作系统。
通过使用本地方法,我们得以用Java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用c写的。还有,
如果我们要使用一些Java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。
JVM支持着Java语言本身和运行时库,它是Java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。
然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一底层系统的支持。这些底层系统常常是强大的操作系统。
通过使用本地方法,我们得以用Java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用c写的。还有,
如果我们要使用一些Java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。
Sun's Java:
Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。jre大部分是用Java实现的,它也通过一些本地方法与外界交互。
例如:类java.lang.Thread的setpriority()方法是用Java实现的,但是它实现调用的是该类里的本地方法setpriorityo()。
这个本地方法是用C实现的,并被植入JVM内部,在Windows 95的平台上,这个本地方法最终将调用Win32 setpriority()ApI。
这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库(external dynamic link library)提供,然后被JVw调用。
Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。jre大部分是用Java实现的,它也通过一些本地方法与外界交互。
例如:类java.lang.Thread的setpriority()方法是用Java实现的,但是它实现调用的是该类里的本地方法setpriorityo()。
这个本地方法是用C实现的,并被植入JVM内部,在Windows 95的平台上,这个本地方法最终将调用Win32 setpriority()ApI。
这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库(external dynamic link library)提供,然后被JVw调用。
现状
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。
因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,不多做介绍。
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。
因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,不多做介绍。
本地方法栈
图示
Java虚拟机栈于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
本地方法栈,也是线程私有的。
允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)
如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个stackoverflowError 异常。
如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的
内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个outofMemoryError异常。
本地方法是使用C语言实现的。
它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。
本地方法栈,也是线程私有的。
允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)
如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个stackoverflowError 异常。
如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的
内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个outofMemoryError异常。
本地方法是使用C语言实现的。
它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。
堆
概述:
一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)。
《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。
几乎”所有的对象实例都在这里分配内存。—从实际使用角度看的。
因为还有一些对象是在栈上分配的
数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
也就是触发了GC的时候,才会进行回收
如果堆中对象马上被回收,那么用户线程就会收到影响,因为有stop the word
堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。
一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)。
《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。
几乎”所有的对象实例都在这里分配内存。—从实际使用角度看的。
因为还有一些对象是在栈上分配的
数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
也就是触发了GC的时候,才会进行回收
如果堆中对象马上被回收,那么用户线程就会收到影响,因为有stop the word
堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。
堆内存细分
Java 7及之前堆内存逻辑上分为三部分:新生区+养老区+永久区
Young Generation Space 新生区 Young/New 又被划分为Eden区和Survivor区
Tenure generation space 养老区 Old/Tenure
Permanent Space永久区 Perm
Young Generation Space 新生区 Young/New 又被划分为Eden区和Survivor区
Tenure generation space 养老区 Old/Tenure
Permanent Space永久区 Perm
Java 8及之后堆内存逻辑上分为三部分:新生区+养老区+元空间
Young Generation Space新生区 Young/New 又被划分为Eden区和Survivor区
Tenure generation space 养老区 Old/Tenure
Meta Space 元空间 Meta
Young Generation Space新生区 Young/New 又被划分为Eden区和Survivor区
Tenure generation space 养老区 Old/Tenure
Meta Space 元空间 Meta
设置大小与OOM
“-Xms" 用于表示堆区的起始内存,等价于-xx:InitialHeapSize
“-Xmx" 则用于表示堆区的最大内存,等价于-XX:MaxHeapSize
例:
-Xms10m
-Xmx10m
一旦堆区中的内存大小超过“-xmx"所指定的最大内存时,将会抛出outofMemoryError异常。
通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在Java垃圾回收机制清理
完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
默认情况下
初始内存大小:物理电脑内存大小/64
最大内存大小:物理电脑内存大小/4
“-Xmx" 则用于表示堆区的最大内存,等价于-XX:MaxHeapSize
例:
-Xms10m
-Xmx10m
一旦堆区中的内存大小超过“-xmx"所指定的最大内存时,将会抛出outofMemoryError异常。
通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在Java垃圾回收机制清理
完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
默认情况下
初始内存大小:物理电脑内存大小/64
最大内存大小:物理电脑内存大小/4
查看堆内存分配:
jps
jstat -gc 进程id
jps
jstat -gc 进程id
OOM 测试
// -Xms10m -Xmx10m
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
while(true) {
list.add(999999999);
}
}
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
while(true) {
list.add(999999999);
}
}
年轻代与老年代
Java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(oldGen)
其中年轻代又可以划分为Eden空间、Survivor0空间和Survivor1空间(有时也叫做from区、to区)
Eden:From:to -> 8:1:1
新生代:老年代 - > 1 : 2
配置新生代与老年代在堆结构的占比。
默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
几乎所有的Java对象都是在Eden区被new出来的。绝大部分的Java对象的销毁都在新生代进行了。
(有些大的对象在Eden区无法存储时候,将直接进入老年代)
其中年轻代又可以划分为Eden空间、Survivor0空间和Survivor1空间(有时也叫做from区、to区)
Eden:From:to -> 8:1:1
新生代:老年代 - > 1 : 2
配置新生代与老年代在堆结构的占比。
默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
几乎所有的Java对象都是在Eden区被new出来的。绝大部分的Java对象的销毁都在新生代进行了。
(有些大的对象在Eden区无法存储时候,将直接进入老年代)
图示
对象分配过程
- new的对象先放伊甸园区。此区有大小限制。
- 当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(MinorGC),
将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区 - 然后将伊甸园中的剩余对象移动到幸存者0区。
- 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
- 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
- 啥时候能去养老区呢?可以设置次数。默认是15次。(可以设置参数:-Xx:MaxTenuringThreshold= N进行设置)
- 在养老区,相对悠闲。当养老区内存不足时,再次触发GC:Major GC,进行养老区的内存清理
- 若养老区执行了Major GC之后,发现依然无法进行对象的保存,就会产生OOM异常。
注意:在Eden区满了的时候,才会触发MinorGC,而幸存者区满了后,不会触发MinorGC操作
如果Survivor区满了后,将会触发一些特殊的规则,也就是可能直接晋升老年代。
图示
常用的调优工具
- JDK命令行
- Eclipse:Memory Analyzer Tool
- Jconsole
- Visual VM(实时监控 推荐~)
- Jprofiler(推荐~)
- Java Flight Recorder(实时监控)
- GCViewer
- GCEasy
常用参数
* -XX:+PrintFlagsInitial : 查看所有的参数的默认初始值
* -XX:+PrintFlagsFinal :查看所有的参数的最终值(可能会存在修改,不再是初始值)
* 具体查看某个参数的指令: jps:查看当前运行中的进程
* jinfo -flag SurvivorRatio 进程id
*
* -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:HandlePromotionFailure:是否设置空间分配担保
Minor GC,MajorGC、Full GC
Minor GC
新生代的GC
新生代的GC
MajorGC
老年代的GC
老年代的GC
指发生在老年代的GC,对象从老年代消失时,我们说 “Major Gc” 或 “Full GC” 发生了
出现了MajorGc,经常会伴随至少一次的Minor GC
(但非绝对的,在Paralle1 Scavenge收集器的收集策略里就有直接进行MajorGC的策略选择过程)
也就是在老年代空间不足时,会先尝试触发MinorGc。如果之后空间还不足,则触发Major GC
Major GC的速度一般会比MinorGc慢10倍以上,STW的时间更长,如果Major GC后,内存还不足,就报OOM了
出现了MajorGc,经常会伴随至少一次的Minor GC
(但非绝对的,在Paralle1 Scavenge收集器的收集策略里就有直接进行MajorGC的策略选择过程)
也就是在老年代空间不足时,会先尝试触发MinorGc。如果之后空间还不足,则触发Major GC
Major GC的速度一般会比MinorGc慢10倍以上,STW的时间更长,如果Major GC后,内存还不足,就报OOM了
Full GC
整堆收集,收集整个Java堆和方法区的垃圾收集
整堆收集,收集整个Java堆和方法区的垃圾收集
触发FullGC执行的情况有如下五种:
- 调用System.gc()时,系统建议执行FullGC,但是不必然执行
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
- 由Eden区、survivor spacee(From Space)区向survivor spacel(To Space)区复制时,
对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
内存分配策略
不同年龄段的对象分配原则如下所示:
- 优先分配到Eden
- 开发中比较长的字符串或者数组,会直接存在老年代,但是因为新创建的对象 都是 朝生夕死的,
所以这个大对象可能也很快被回收,但是因为老年代触发Major GC的次 数比 Minor GC要更少,
因此可能回收起来就会比较慢 - 大对象直接分配到老年代,尽量避免程序中出现过多的大对象
- 长期存活的对象分配到老年代
- 动态对象年龄判断
- 如果survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直
接进入老年代,无须等到MaxTenuringThreshold 中要求的年龄。空间分配担保: -Xx:HandlePromotionFailure - 也就是经过Minor GC后,所有的对象都存活,因为Survivor比较小,所以就需要将Survivor无法容纳的对象,存放到老年代中。
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。
如果大于,则此次Minor GC是安全的
如果小于,则虚拟机会查看-xx:HandlePromotionFailure设置值是否允担保失败。
如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。
如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;
如果小于,则改为进行一次FullGC。
如果HandlePromotionFailure=false,则改为进行一次Full Gc。
在JDK6 Update24之后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,
观察openJDK中的源码变化,虽然源码中还定义了HandlePromotionFailure参数,但是在代码中已经
不会再使用它。JDK6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次
晋升的平均大小就会进行Minor GC,否则将进行FullGC。
如果大于,则此次Minor GC是安全的
如果小于,则虚拟机会查看-xx:HandlePromotionFailure设置值是否允担保失败。
如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。
如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;
如果小于,则改为进行一次FullGC。
如果HandlePromotionFailure=false,则改为进行一次Full Gc。
在JDK6 Update24之后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,
观察openJDK中的源码变化,虽然源码中还定义了HandlePromotionFailure参数,但是在代码中已经
不会再使用它。JDK6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次
晋升的平均大小就会进行Minor GC,否则将进行FullGC。
TLAB
概念:
从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了
一个私有缓存区域,它包含在Eden空间内。
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内
存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了
一个私有缓存区域,它包含在Eden空间内。
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内
存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
问题:堆空间都是共享的么?
不一定,因为还有TLAB这个概念,在堆中划分出一块区域,为每个线程所独占
TLAB:Thread Local Allocation Buffer,也就是为每个线程单独分配了一个缓冲区,
堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据,
由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的,
为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据,
由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的,
为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
堆是分配对象的唯一选择么?
逃逸分析
随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些
微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。
有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,
一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。
逃逸分析的基本行为就是分析对象动态作用域:
在JDK 1.7 版本之后,HotSpot中默认就已经开启了逃逸分析
如果使用的是较早的版本,开发人员则可以通过:
微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。
有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,
一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。
逃逸分析的基本行为就是分析对象动态作用域:
- 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
在JDK 1.7 版本之后,HotSpot中默认就已经开启了逃逸分析
如果使用的是较早的版本,开发人员则可以通过:
- 选项“-xx:+DoEscapeAnalysis"显式开启逃逸分析
- 通过选项“-xx:+PrintEscapeAnalysis"查看逃逸分析的筛选结果
public class EscapeAnalysis {
public EscapeAnalysis obj;
/**
* 方法返回EscapeAnalysis对象,发生逃逸
* @return
*/
public EscapeAnalysis getInstance() {
return obj == null ? new EscapeAnalysis():obj;
}
/**
* 为成员属性赋值,发生逃逸
*/
public void setObj() {
this.obj = new EscapeAnalysis();
}
/**
* 对象的作用于仅在当前方法中有效,没有发生逃逸
*/
public void useEscapeAnalysis() {
EscapeAnalysis e = new EscapeAnalysis();
}
/**
* 引用成员变量的值,发生逃逸
*/
public void useEscapeAnalysis2() {
EscapeAnalysis e = getInstance();
// getInstance().XXX 发生逃逸
}
}
public EscapeAnalysis obj;
/**
* 方法返回EscapeAnalysis对象,发生逃逸
* @return
*/
public EscapeAnalysis getInstance() {
return obj == null ? new EscapeAnalysis():obj;
}
/**
* 为成员属性赋值,发生逃逸
*/
public void setObj() {
this.obj = new EscapeAnalysis();
}
/**
* 对象的作用于仅在当前方法中有效,没有发生逃逸
*/
public void useEscapeAnalysis() {
EscapeAnalysis e = new EscapeAnalysis();
}
/**
* 引用成员变量的值,发生逃逸
*/
public void useEscapeAnalysis2() {
EscapeAnalysis e = getInstance();
// getInstance().XXX 发生逃逸
}
}
栈上分配
JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,
就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,
局部变量对象也被回收。这样就无须进行垃圾回收了。
常见的栈上分配的场景
在逃逸分析中,已经说明了。分别是给成员变量赋值、方法返回值、实例引用传递。
就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,
局部变量对象也被回收。这样就无须进行垃圾回收了。
常见的栈上分配的场景
在逃逸分析中,已经说明了。分别是给成员变量赋值、方法返回值、实例引用传递。
同步省略
在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够
被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会
取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。
被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会
取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。
public void f() {
Object hellis = new Object();
synchronized(hellis) {
System.out.println(hellis);
}
}
会优化成:
public void f() {
Object hellis = new Object();
System.out.println(hellis);
}
Object hellis = new Object();
synchronized(hellis) {
System.out.println(hellis);
}
}
会优化成:
public void f() {
Object hellis = new Object();
System.out.println(hellis);
}
分离对象和标量替换
标量(scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。
相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以
分解成其他聚合量和标量。
在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象
拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。
相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以
分解成其他聚合量和标量。
在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象
拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。
方法区
Person:存放在元空间,也可以说方法区
person:存放在Java栈的局部变量表中
new Person():存放在Java堆中
person:存放在Java栈的局部变量表中
new Person():存放在Java堆中
方法区主要存放的是 Class,而堆中主要存放的是 实例化的对象。
- 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。
- 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
- 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,
虚拟机同样会抛出内存溢出错误:Java.lang.OutofMemoryError:PermGen space
或者java.lang.OutOfMemoryError:Metaspace
- 加载大量的第三方的jar包
- Tomcat部署的工程过多(30~50个)
- 大量动态的生成反射类
- 关闭JVM就会释放这个区域的内存。
JDK1.6及以前 有永久代,静态变量存储在永久代上
JDK1.7 有永久代,但已经逐步 “去永久代”,字符串常量池,静态变量移除,保存在堆中
JDK1.8 无永久代,类型信息,字段,方法,常量保存在本地内存的元空间,但字符串常量池、静态变量仍然在堆中。
JDK1.7 有永久代,但已经逐步 “去永久代”,字符串常量池,静态变量移除,保存在堆中
JDK1.8 无永久代,类型信息,字段,方法,常量保存在本地内存的元空间,但字符串常量池、静态变量仍然在堆中。
为什么永久代要被元空间替代?
JRockit是和HotSpot融合后的结果,因为JRockit没有永久代,所以他们不需要配置永久代。
由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间,这项改动是很有必要的,原因有:
在某些场景下,如果动态加载类过多,容易产生Perm区的oom。比如某个实际Web工 程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,
经常出现致命错误。“Exception in thread‘dubbo client x.x connector'java.lang.OutOfMemoryError:PermGen space”
而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。 因此,默认情况下,元空间的大小仅受本地内存限制。
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不在使用的类型。
由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间,这项改动是很有必要的,原因有:
- 为永久代设置空间大小是很难确定的。
在某些场景下,如果动态加载类过多,容易产生Perm区的oom。比如某个实际Web工 程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,
经常出现致命错误。“Exception in thread‘dubbo client x.x connector'java.lang.OutOfMemoryError:PermGen space”
而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。 因此,默认情况下,元空间的大小仅受本地内存限制。
- 对永久代进行调优是很困难的。
- 主要是为了降低Full GC
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不在使用的类型。
StringTable为什么要调整位置
jdk7中将StringTable放到了堆空间中。因为永久代的回收效率很低,在full gc的时候才会触发。
而fullgc是老年代的空间不足、永久代不足时才会触发。
这就导致StringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,
导致永久代内存不足。放到堆里,能及时回收内存。
而fullgc是老年代的空间不足、永久代不足时才会触发。
这就导致StringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,
导致永久代内存不足。放到堆里,能及时回收内存。
方法区大小与OOM
jdk7及以前:
- 通过-XX:Permsize来设置永久代初始分配空间。默认值是20.75M
- -XX:MaxPermsize来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M
- 当JVM加载的类信息容量超过了这个值,会报异常OutofMemoryError:PermGen space。
JDK8以后:
元数据区大小可以使用参数 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize指定。
默认值依赖于平台。windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1,即没有限制。
与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,|
虚拟机一样会抛出异常OutOfMemoryError:Metaspace。
对于一个64位的服务器端JVM来说,其默认的-xx:MetaspaceSize值为21MB。这就是初始的高水位线,
一旦触及这个水位线,Ful1GC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活)然后这个高水位线将会重置。
新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,
适当提高该值。如果释放空间过多,则适当降低该值。
如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Ful1GC多次调用。
为了避免频繁地GC,建议将-XX:MetaspaceSize设置为一个相对较高的值。
元数据区大小可以使用参数 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize指定。
默认值依赖于平台。windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1,即没有限制。
与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,|
虚拟机一样会抛出异常OutOfMemoryError:Metaspace。
对于一个64位的服务器端JVM来说,其默认的-xx:MetaspaceSize值为21MB。这就是初始的高水位线,
一旦触及这个水位线,Ful1GC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活)然后这个高水位线将会重置。
新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,
适当提高该值。如果释放空间过多,则适当降低该值。
如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Ful1GC多次调用。
为了避免频繁地GC,建议将-XX:MetaspaceSize设置为一个相对较高的值。
如何解决这些OOM:
- 要解决ooM异常或heap space的异常,一般的手段是首先通过内存映像分析工具(如Ec1ipse Memory Analyzer)
- 对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了
- 内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)
- 内存泄漏就是 有大量的引用指向某些对象,但是这些对象以后不会使用了,但是因为它们还和GC ROOT有关联,
- 所以导致以后这些对象也不会被回收,这就是内存泄漏的问题。
- 如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象是通过怎样的
- 路径与GCRoots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GCRoots引用链
- 的信息,就可以比较准确地定位出泄漏代码的位置。
- 如果不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),
- 与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,
- 尝试减少程序运行期的内存消耗。
方法区的内部结构
类型信息
对每个加载的类型(类class、接口interface、枚举enum、注解annotation),
JVm必须在方法区中存储以下类型信息:
JVm必须在方法区中存储以下类型信息:
- 这个类型的完整有效名称(全名=包名.类名)
- 这个类型直接父类的完整有效名(对于interface或是java.lang.object,都没有父类)
- 这个类型的修饰符(public,abstract,final的某个子集)
- 这个类型直接接口的一个有序列表
运行时常量池
- 方法区,内部包含了运行时常量池
- 字节码文件,内部包含了常量池
- 要弄清楚方法区,需要理解清楚C1assFile,因为加载类的信息都在方法区。
- 要弄清楚方法区的运行时常量池,需要理解清楚classFile中的常量池。
常量池
一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述符信息外,
还包含一项信息就是常量池表(Constant Pool Table),包括各种字面量和对类型、域和方法的符号引用
还包含一项信息就是常量池表(Constant Pool Table),包括各种字面量和对类型、域和方法的符号引用
为什么需要常量池:
一个java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到
字节码里,换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。r在动态链接的时候会用到运行时常量池,之前有介绍。
一个java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到
字节码里,换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。r在动态链接的时候会用到运行时常量池,之前有介绍。
运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。
常量池表(Constant Pool Table)是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,
这部分内容将在类加载后存放到方法区的运行时常量池中。
运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池。
JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。
运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。
此时不再是常量池中的符号地址了,这里换为真实地址。
运行时常量池,相对于Class文件常量池的另一重要特征是:具备动态性。
运行时常量池类似于传统编程语言中的符号表(symboltable),但是它所包含的数据却比符号表要更加丰富一些。
当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛outofMemoryError异常。
常量池表(Constant Pool Table)是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,
这部分内容将在类加载后存放到方法区的运行时常量池中。
运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池。
JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。
运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。
此时不再是常量池中的符号地址了,这里换为真实地址。
运行时常量池,相对于Class文件常量池的另一重要特征是:具备动态性。
运行时常量池类似于传统编程语言中的符号表(symboltable),但是它所包含的数据却比符号表要更加丰富一些。
当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛outofMemoryError异常。
静态变量(1.7 及以后移到堆中)
静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分
全局常量就是使用 static final 进行修饰,
被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。
被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。
JIT 代码缓存
域信息
JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
域的相关信息包括:域名称、域类型、域修饰符(public,private,protected,
static,final,volatile,transient的某个子集)
域的相关信息包括:域名称、域类型、域修饰符(public,private,protected,
static,final,volatile,transient的某个子集)
方法信息
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
- 方法名称
- 方法的返回类型(或void)
- 方法参数的数量和类型(按顺序)
- 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
- 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
- 异常表(abstract和native方法除外)
- 每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
常见面试题
百度 三面:说一下JVM内存模型吧,有哪些区?分别干什么的?
蚂蚁金服: Java8的内存分代改进 JVM内存分哪几个区,每个区的作用是什么? 一面:JVM内存分布/内存结构?栈和堆的区别?堆的结构?为什么两个survivor区? 二面:Eden和survior的比例分配
小米: jvm内存分区,为什么要有新生代和老年代
字节跳动: 二面:Java的内存分区 二面:讲讲vm运行时数据库区 什么时候对象会进入老年代?
京东: JVM的内存结构,Eden和Survivor比例。 JVM内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为Eden和survivor。
天猫: 一面:Jvm内存模型以及分区,需要详细到每个区放什么。 一面:JVM的内存模型,Java8做了什么改
拼多多: JVM内存分哪几个区,每个区的作用是什么?
美团: java内存分配 jvm的永久代中会发生垃圾回收吗? 一面:jvm内存分区,为什么要有新生代和老年代?
对象实例化内存布局与访问定位
面试题
对象在JVM中是怎么存储的?
对象头信息里面有哪些东西?
Java对象头有什么?
对象创建方式
- new:最常见的方式、单例类中调用getInstance的静态类方法,XXXFactory的静态方法
- Class的newInstance方法:在JDK9里面被标记为过时的方法,因为只能调用空参构造器
- Constructor的newInstance(XXX):反射的方式,可以调用空参的,或者带参的构造器
- 使用clone():不调用任何的构造器,要求当前的类需要实现Cloneable接口中的clone接口
- 使用序列化:序列化一般用于Socket的网络传输
- 第三方库 Objenesis
创建对象的步骤
加载类元信息
虚拟机遇到一条new指令,首先去检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用,
并且检查这个符号引用代表的类是否已经被加载,解析和初始化。(即判断类元信息是否存在)。如果没有,
那么在双亲委派模式下,使用当前类加载器以ClassLoader + 包名 + 类名为key进行查找对应的 .class文件,
如果没有找到文件,则抛出ClassNotFoundException异常,如果找到,则进行类加载,并生成对应的Class对象
并且检查这个符号引用代表的类是否已经被加载,解析和初始化。(即判断类元信息是否存在)。如果没有,
那么在双亲委派模式下,使用当前类加载器以ClassLoader + 包名 + 类名为key进行查找对应的 .class文件,
如果没有找到文件,则抛出ClassNotFoundException异常,如果找到,则进行类加载,并生成对应的Class对象
为对象分配内存
首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象。
如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小
如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小
- 如果内存规整:指针碰撞
- 如果内存不规整,
- 虚拟表需要维护一个列表,
- 空闲列表分配
指针碰撞:
如果内存规整,采用此方法,意思是所有用过的内存在一边,空闲的内存放另外一边,中间放着一个指针
作为分界点的指示器,分配内存就仅仅是把指针指向空闲那边挪动一段与对象大小相等的距离罢了。如果
垃圾收集器选择的是Serial ,ParNew这种基于压缩算法的,虚拟机采用这种分配方式。一般使用带
Compact(整理)过程的收集器时,使用指针碰撞。
如果内存规整,采用此方法,意思是所有用过的内存在一边,空闲的内存放另外一边,中间放着一个指针
作为分界点的指示器,分配内存就仅仅是把指针指向空闲那边挪动一段与对象大小相等的距离罢了。如果
垃圾收集器选择的是Serial ,ParNew这种基于压缩算法的,虚拟机采用这种分配方式。一般使用带
Compact(整理)过程的收集器时,使用指针碰撞。
空闲列表:
如果内存不规整。虚拟机维护了一个列表,记录上那些内存块是可用的,再分配的时候从列表中找到
一块足够大的空间划分给对象实例,并更新列表上的内容。这种分配方式成为了 “空闲列表(Free List)”
如果内存不规整。虚拟机维护了一个列表,记录上那些内存块是可用的,再分配的时候从列表中找到
一块足够大的空间划分给对象实例,并更新列表上的内容。这种分配方式成为了 “空闲列表(Free List)”
处理并发安全问题
- 采用CAS配上失败重试保证更新的原子性
- 每个线程预先分配TLAB (在Eden区给每个线程分配一块区域) -
通过设置 -XX:+UseTLAB参数来设置(区域加锁机制)
属性的默认初始化(零值初始化)
给对象属性赋值的操作:
- 属性的默认初始化
- 显示初始化
- 代码块中的初始化
- 构造器初始化
- 所有属性设置默认值,保证对象实例字段在不赋值可以直接使用
设置对象头信息
将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、
锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。
锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。
执行init方法进行初始化
在Java程序的视角看来,初始化才正式开始。初始化成员变量,执行实例化代码块,
调用类的构造方法,并把堆内对象的首地址赋值给引用变量
因此一般来说(由字节码中跟随invokespecial指令所决定),new指令之后会接着就是执行方法,
把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完成创建出来。
调用类的构造方法,并把堆内对象的首地址赋值给引用变量
因此一般来说(由字节码中跟随invokespecial指令所决定),new指令之后会接着就是执行方法,
把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完成创建出来。
对象内存布局
图示
对象头
对象头包含了两部分数据:运行时元数据 (Mark Word)和 类型指针;
* 如果是数组,还需要记录数组的长度。
* 如果是数组,还需要记录数组的长度。
运行时元数据:
- 哈希值(HashCode)
- GC分代年龄
- 锁状态标志
- 线程持有的锁
- 偏向线程ID
- 翩向时间戳
类型指针:
指向类元数据InstanceKlass,确定该对象所属的类型。
指向的其实是方法区中存放的类元信息
指向类元数据InstanceKlass,确定该对象所属的类型。
指向的其实是方法区中存放的类元信息
实例数据
是对象真正储存的有效信息,包括代码中定义的各种类型的字段(包括从父类继承的字段和本身的字段)
- 相同宽度的字段总跟被分配在一起;
- 父类中出现的变量会出现在子类之前;
- 如果CompactFileds参数为true(默认false),子类的窄变量可能插入到父类变量的空隙。
对齐填充
不是必须的,也没有特别的含义,仅仅起到占位符的作用
对象的访问定位
句柄访问
图示
句柄访问就是说栈的局部变量表中,记录的对象的引用,然后在堆空间中开辟了一块空间,也就是句柄池
优点:reference中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只会改变句柄中
实例数据指针即可,reference本身不需要被修改
优点:reference中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只会改变句柄中
实例数据指针即可,reference本身不需要被修改
直接指针(HotSpot采用)
图示
直接指针是局部变量表中的引用,直接指向堆中的实例,在对象实例中有类型指针,指向的是方法区中的对象类型数据
直接内存(Direct Memory)
概述:
- 不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
- 直接内存是在Java堆外的、直接向系统申请的内存区间。
- 来源于NIO,通过存在堆中的DirectByteBuffer操作Native内存
- 通常,访问直接内存的速度会优于Java堆。即读写性能高。
- 因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存。
- Java的NIO库允许Java程序使用直接内存,用于数据缓冲区
// 使用下列代码,直接分配本地内存空间
int BUFFER = 1024*1024*1024; // 1GB
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
存在的问题:
也可能导致outofMemoryError异常。
由于直接内存在Java堆外,因此它的大小不会直接受限于-xmx指定的最大堆大小,
但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。
缺点:
如果不指定,默认与堆的最大值-xmx参数值一致。
也可能导致outofMemoryError异常。
由于直接内存在Java堆外,因此它的大小不会直接受限于-xmx指定的最大堆大小,
但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。
缺点:
- 分配回收成本较高
- 不受JVM内存回收管理
如果不指定,默认与堆的最大值-xmx参数值一致。
执行引擎(Execution Engine)
概述
执行引擎属于JVM的下层,里面包括 解释器、及时编译器、垃圾回收器。
执行引擎的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。
简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。
执行引擎的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。
简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。
图示
执行引擎的工作流程
- 执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于PC寄存器。
- 每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址。
- 当然方法在执行的过程中,执行引擎有可能会通过存储在局部变量表中的对象引用
准确定位到存储在Java堆区中的对象实例信息, - 以及通过对象头中的元数据指针定位到目标对象的类型信息。
Java代码编译和执行过程
图示
总图
Java代码的执行分类:
第一种是将源代码编译成字节码文件,然后在运行时通过解释器将字节码文件转为机器码执行
第二种是编译执行(直接编译成机器码)。现代虚拟机为了提高执行效率,会使用即时编译技术
(JIT,Just In Time)将方法编译成机器码后再执行
第一种是将源代码编译成字节码文件,然后在运行时通过解释器将字节码文件转为机器码执行
第二种是编译执行(直接编译成机器码)。现代虚拟机为了提高执行效率,会使用即时编译技术
(JIT,Just In Time)将方法编译成机器码后再执行
解释器(Interpreter):
当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,
将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
解释器真正意义上所承担的角色就是一个运行时“翻译者”,将字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
当一条字节码指令被解释执行完成后,接着再根据PC寄存器中记录的下一条需要被执行的字节码指令执行解释操作。
当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,
将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
解释器真正意义上所承担的角色就是一个运行时“翻译者”,将字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
当一条字节码指令被解释执行完成后,接着再根据PC寄存器中记录的下一条需要被执行的字节码指令执行解释操作。
JIT(Just In Time Compiler)编译器:
虚拟机将源代码直接编译成和本地机器平台相关的机器语言。
虚拟机将源代码直接编译成和本地机器平台相关的机器语言。
概念解释:
|前端编译器:Sun的Javac、Eclipse JDT中的增量式编译器(ECJ)。
JIT编译器:HotSpot VM的C1、C2编译器。
AOT 编译器:GNU Compiler for the Java(GCJ)、Excelsior JET。
- Java 语言的“编译期”其实是一段“不确定”的操作过程,因为它可能是指一个前端编译器
(其实叫“编译器的前端”更准确一些)把.java文件转变成.class文件的过程;也可能是指
虚拟机的后端运行期编译器(JIT编译器,Just In Time Compiler) - 把字节码转变成机器码的过程。
- 还可能是指使用静态提前编译器(AOT编译器,Ahead of Time Compiler)直接把.java文件
编译成本地机器代码的过程。
|前端编译器:Sun的Javac、Eclipse JDT中的增量式编译器(ECJ)。
JIT编译器:HotSpot VM的C1、C2编译器。
AOT 编译器:GNU Compiler for the Java(GCJ)、Excelsior JET。
热点探测技术
一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称之为“热点代码”,
因此都可以通过JIT编译器编译为本地机器指令。由于这种编译方式发生在方法的执行过程中,
因此被称之为栈上替换,或简称为OSR(On Stack Replacement)编译。
目前HotSpot VM所采用的热点探测方式是基于计数器的热点探测。采用基于计数器的热点探测,
HotSpot V将会为每一个方法都建立2个不同类型的计数器,分别为方法调用计数器
(Invocation Counter)和回边计数器(Back Edge Counter)。
因此都可以通过JIT编译器编译为本地机器指令。由于这种编译方式发生在方法的执行过程中,
因此被称之为栈上替换,或简称为OSR(On Stack Replacement)编译。
目前HotSpot VM所采用的热点探测方式是基于计数器的热点探测。采用基于计数器的热点探测,
HotSpot V将会为每一个方法都建立2个不同类型的计数器,分别为方法调用计数器
(Invocation Counter)和回边计数器(Back Edge Counter)。
- 方法调用计数器用于统计方法的调用次数
- 回边计数器则用于统计循环体执行的循环次数
方法调用计数器
图示
这个计数器就用于统计方法被调用的次数,它的默认阀值在Client模式下是1500次,
在Server模式下是10000次。超过这个阈值,就会触发JIT编译。
这个阀值可以通过虚拟机参数 -XX:CompileThreshold 来人为设定。
热点衰减:
如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,
即一段时间之内方法被调用的次数。当超过一定的时间限度,如果方法的调用次数仍然不足以让它
提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器热度
的衰减(Counter Decay),而这段时间就称为此方法统计的半衰周期(Counter Half Life Time)
可以使用虚拟机参数 -XX:-UseCounterDecay 来关闭热度衰减,让方法计数器统计方法调用的绝对次数,
这样,只要系统运行时间足够长,绝大部分方法都会被编译成本地代码。
另外,可以使用-XX:CounterHalfLifeTime参数设置半衰周期的时间,单位是秒。
如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,
即一段时间之内方法被调用的次数。当超过一定的时间限度,如果方法的调用次数仍然不足以让它
提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器热度
的衰减(Counter Decay),而这段时间就称为此方法统计的半衰周期(Counter Half Life Time)
- 半衰周期是化学中的概念,比如出土的文物通过查看C60来获得文物的年龄
可以使用虚拟机参数 -XX:-UseCounterDecay 来关闭热度衰减,让方法计数器统计方法调用的绝对次数,
这样,只要系统运行时间足够长,绝大部分方法都会被编译成本地代码。
另外,可以使用-XX:CounterHalfLifeTime参数设置半衰周期的时间,单位是秒。
回边计数器
图示
它的作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令
称为“回边”(Back Edge)。显然,建立回边计数器统计的目的就是为了触发OSR编译。
称为“回边”(Back Edge)。显然,建立回边计数器统计的目的就是为了触发OSR编译。
设置程序执行方法
缺省情况下HotSpot VM是采用解释器与即时编译器并存的架构,当然开发人员可以根据具体的应用场景,
通过命令显式地为Java虚拟机指定在运行时到底是完全采用解释器执行,还是完全采用即时编译器执行。如下所示:
-Xint:完全采用解释器模式执行程序;
-Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行
-Xmixed:采用解释器+即时编译器的混合模式共同执行程序。
通过命令显式地为Java虚拟机指定在运行时到底是完全采用解释器执行,还是完全采用即时编译器执行。如下所示:
-Xint:完全采用解释器模式执行程序;
-Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行
-Xmixed:采用解释器+即时编译器的混合模式共同执行程序。
HotSpotVM中 JIT 分类
JIT的编译器还分为了两种,分别是C1和C2,在HotSpot VM中内嵌有两个JIT编译器,
分别为Client Compiler和Server Compiler,但大多数情况下我们简称为C1编译器 和 C2编译器。
开发人员可以通过如下命令显式指定Java虚拟机在运行时到底使用哪一种即时编译器,如下所示:
-client:指定Java虚拟机运行在Client模式下,并使用C1编译器;
C1编译器会对字节码进行简单和可靠的优化,耗时短。以达到更快的编译速度。
-server:指定Java虚拟机运行在server模式下,并使用C2编译器。
C2进行耗时较长的优化,以及激进优化。但优化的代码执行效率更高。(使用C++)
分别为Client Compiler和Server Compiler,但大多数情况下我们简称为C1编译器 和 C2编译器。
开发人员可以通过如下命令显式指定Java虚拟机在运行时到底使用哪一种即时编译器,如下所示:
-client:指定Java虚拟机运行在Client模式下,并使用C1编译器;
C1编译器会对字节码进行简单和可靠的优化,耗时短。以达到更快的编译速度。
-server:指定Java虚拟机运行在server模式下,并使用C2编译器。
C2进行耗时较长的优化,以及激进优化。但优化的代码执行效率更高。(使用C++)
C1优化策略:
- 方法内联:将引用的函数代码编译到引用点处,
|这样可以减少栈帧的生成,减少参数传递以及跳转过程。 - 去虚拟化:对唯一的实现樊进行内联。
- 冗余消除:在运行期间把一些不会执行的代码折叠掉。
C2优化策略:
- 标量替换:用标量值代替聚合对象的属性值。
- 栈上分配:对于未逃逸的对象分配对象在栈而不是堆。
- 同步消除:清除同步操作,通常指synchronized。
StringTable
String的基本特性
String:字符串,使用一对 ”” 引起来表示
String s1 = "mogublog" ; // 字面量的定义方式
String s2 = new String("moxi");
string声明为final的,不可被继承
String实现了Serializable接口:表示字符串是支持序列化的。实现了Comparable接口:表示string可以比较大小
string在jdk8及以前内部定义了final char[] value用于存储字符串数据。JDK9时改为byte[]
String s1 = "mogublog" ; // 字面量的定义方式
String s2 = new String("moxi");
string声明为final的,不可被继承
String实现了Serializable接口:表示字符串是支持序列化的。实现了Comparable接口:表示string可以比较大小
string在jdk8及以前内部定义了final char[] value用于存储字符串数据。JDK9时改为byte[]
为什么JDK9改变了结构
String类的当前实现将字符存储在char数组中,每个字符使用两个字节(16位)。从许多不同的应用程序收集的数据表明,
字符串是堆使用的主要组成部分,而且,大多数字符串对象只包含拉丁字符。这些字符只需要一个字节的存储空间,
因此这些字符串对象的内部char数组中有一半的空间将不会使用。
我们建议改变字符串的内部表示clasš从utf - 16字符数组到字节数组+一个encoding-flag字段。
新的String类将根据字符串的内容存储编码为ISO-8859-1/Latin-1(每个字符一个字节)或UTF-16(每个字符两个字节)的字符。
编码标志将指示使用哪种编码。
字符串是堆使用的主要组成部分,而且,大多数字符串对象只包含拉丁字符。这些字符只需要一个字节的存储空间,
因此这些字符串对象的内部char数组中有一半的空间将不会使用。
我们建议改变字符串的内部表示clasš从utf - 16字符数组到字节数组+一个encoding-flag字段。
新的String类将根据字符串的内容存储编码为ISO-8859-1/Latin-1(每个字符一个字节)或UTF-16(每个字符两个字节)的字符。
编码标志将指示使用哪种编码。
String的不可变性
当对字符串重新赋值时,需要重新指定内存区域赋值,不能使用原有的value进行赋值。
当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
当调用string的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,
不能使用原有的value进行赋值。 通过字面量的方式(区别于new)给一个字符串赋值,
此时的字符串值声明在字符串常量池中。
字符串常量池是不会存储相同内容的字符串的。
String的string Pool是一个固定大小的Hashtable,默认值大小长度是1009。
如果放进string Pool的string非常多,就会造成Hash冲突严重,从而导致链表会很长,
而链表长了后直接会造成的影响就是当调用string.intern时性能会大幅下降。
使用-XX:StringTablesize可设置stringTab1e的长度。
在jdk6中stringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。
stringTablesize设置没有要求。
在jdk7中,stringTable的长度默认值是60013,
在JDK8中,StringTable可以设置的最小值为1009
当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
当调用string的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,
不能使用原有的value进行赋值。 通过字面量的方式(区别于new)给一个字符串赋值,
此时的字符串值声明在字符串常量池中。
字符串常量池是不会存储相同内容的字符串的。
String的string Pool是一个固定大小的Hashtable,默认值大小长度是1009。
如果放进string Pool的string非常多,就会造成Hash冲突严重,从而导致链表会很长,
而链表长了后直接会造成的影响就是当调用string.intern时性能会大幅下降。
使用-XX:StringTablesize可设置stringTab1e的长度。
在jdk6中stringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。
stringTablesize设置没有要求。
在jdk7中,stringTable的长度默认值是60013,
在JDK8中,StringTable可以设置的最小值为1009
String的内存分配
在Java语言中有8种基本数据类型和一种比较特殊的类型string。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。
常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,string类型的常量池比较特殊。它的主要使用方法有两种。
直接使用双引号声明出来的String对象会直接存储在常量池中。
比如:string info="atguigu.com";
使用string提供的intern()方法声明的对象也会存储在常量池(已存在的字符串自己返回池中的引用)
字符串常量池概念原本使用得比较多,但是这个改动使得我们有足够的理由让我们重新考虑在Java 7中使用string.intern()。
常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,string类型的常量池比较特殊。它的主要使用方法有两种。
直接使用双引号声明出来的String对象会直接存储在常量池中。
比如:string info="atguigu.com";
使用string提供的intern()方法声明的对象也会存储在常量池(已存在的字符串自己返回池中的引用)
- Java 6及以前,字符串常量池存放在永久代
- Java 7中 oracle的工程师对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到Java堆内
字符串常量池概念原本使用得比较多,但是这个改动使得我们有足够的理由让我们重新考虑在Java 7中使用string.intern()。
- Java8元空间,字符串常量在堆
垃圾回收
垃圾回收概述
垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
- 频繁收集Young区
- 较少收集Old区
- 基本不收集Perm区(元空间)
垃圾回收相关概念
System.gc()
通过system.gc()者Runtime.getRuntime().gc() 的调用,会显式触发FullGC。
但是无法保证对垃圾收集器的调用。(不能确保立即生效)
但是无法保证对垃圾收集器的调用。(不能确保立即生效)
内存溢出
系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。
内存泄漏
也称作“存储渗漏”。严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。
但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致00M,
也可以叫做宽泛意义上的“内存泄漏”。
举例
那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。
否则是不能被回收的。
但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致00M,
也可以叫做宽泛意义上的“内存泄漏”。
举例
- 单例模式
那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。
- 一些提供close的资源未关闭导致内存泄漏
否则是不能被回收的。
Stop The World
stop-the-world,简称STw,指的是GC事件发生过程中,会产生应用程序的停顿。
停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。
停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。
安全点与安全区域
安全点
程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点(Safepoint)”。
Safe Point的选择很重要,如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题。大部分指令的执行时间都非常短暂,
通常会根据“是否具有让程序长时间执行的特征”为标准。比如:选择一些执行时间较长的指令作为Safe Point,如方法调用、循环跳转和异常跳转等。
如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?
Safe Point的选择很重要,如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题。大部分指令的执行时间都非常短暂,
通常会根据“是否具有让程序长时间执行的特征”为标准。比如:选择一些执行时间较长的指令作为Safe Point,如方法调用、循环跳转和异常跳转等。
如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?
- 抢先式中断:(目前没有虚拟机采用了)首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。
- 主动式中断:设置一个中断标志,各个线程运行到Safe Point的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。(有轮询的机制)
安全区域
Safepoint 机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。但是,程序“不执行”的时候呢?例如线程处于sleep-状态或Blocked 状态,这时候线程无法响应JVM的中断请求,“走”到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况,就需要安全区域(Safe Region)来解决。
安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始Gc都是安全的。我们也可以把Safe Region看做是被扩展了的Safepoint。
执行流程:
安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始Gc都是安全的。我们也可以把Safe Region看做是被扩展了的Safepoint。
执行流程:
- 当线程运行到Safe Region的代码时,首先标识已经进入了Safe Region,如果这段时间内发生GC,JVM会忽略标识为Safe Region状态的线程
- 当线程即将离开Safe Region时,会检查JVM是否已经完成GC,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开Safe Region的信号为止;
引用
强引用(Strong Reference)
强引用,强引用具备以下特点:
- 程序中最常见的引用类型是强引用(普通系统99%以上都是强引用),也就是我们最常见的普通对象引用,
- 当在Java语言中使用new操作符创建一个新的对象,并将其赋值给一个变量的时候,
强引用,强引用具备以下特点:
- 强引用可以直接访问目标对象。
- 强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出OOM异常,也不会回收强引用所指向对象。
- 强引用可能导致内存泄漏。
软引用(Soft Reference)
软引用是用来描述一些还有用,但非必需的对象。在JDK1.2版之后提供了SoftReference类来实现软引用。
只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,
如果这次回收还没有足够的内存,才会抛出内存溢出异常。
软引用通常用来实现内存敏感的缓存。比如:高速缓存就有用到软引用。如果还有空闲内存,就可以暂时保留缓存,
当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
一句话概括:当内存足够时,不会回收软引用可达的对象。内存不够时,会回收软引用的可达对象
Object obj = new Object(); // 声明强引用
SoftReference<Object> sf = new SoftReference<>(obj); // 创建一个软引用
obj = null; //销毁强引用,这是必须的,不然会存在强引用和软引用
只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,
如果这次回收还没有足够的内存,才会抛出内存溢出异常。
软引用通常用来实现内存敏感的缓存。比如:高速缓存就有用到软引用。如果还有空闲内存,就可以暂时保留缓存,
当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
一句话概括:当内存足够时,不会回收软引用可达的对象。内存不够时,会回收软引用的可达对象
Object obj = new Object(); // 声明强引用
SoftReference<Object> sf = new SoftReference<>(obj); // 创建一个软引用
obj = null; //销毁强引用,这是必须的,不然会存在强引用和软引用
弱引用(Weak Reference)
发现即回收
弱引用也是用来描述那些非必需对象,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。
在系统GC时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉只被弱引用关联的对象。
Object obj = new Object(); // 声明强引用
WeakReference<Object> sf = new WeakReference<>(obj); // 创建一个弱引用
obj = null; //销毁强引用,这是必须的,不然会存在强引用和弱引用
弱引用也是用来描述那些非必需对象,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。
在系统GC时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉只被弱引用关联的对象。
Object obj = new Object(); // 声明强引用
WeakReference<Object> sf = new WeakReference<>(obj); // 创建一个弱引用
obj = null; //销毁强引用,这是必须的,不然会存在强引用和弱引用
虚应用(Phantom Reference)
Object obj = new Object(); // 声明强引用
ReferenceQueue phantomQueue = new ReferenceQueue(); // 声明引用队列
PhantomReference<Object> sf = new PhantomReference<>(obj, phantomQueue); // 声明虚引用(还需要传入引用队列)
obj = null;
- 也称为“幽灵引用”或者“幻影引用”,是所有引用类型中最弱的一个。
- 为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知。
- 虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,
Object obj = new Object(); // 声明强引用
ReferenceQueue phantomQueue = new ReferenceQueue(); // 声明引用队列
PhantomReference<Object> sf = new PhantomReference<>(obj, phantomQueue); // 声明虚引用(还需要传入引用队列)
obj = null;
垃圾回收相关算法
标记阶段
引用计数算法
概述
引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型的引用计数器属性。
用于记录对象被引用的情况。
即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
用于记录对象被引用的情况。
- 对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
- 优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
- 缺点:它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
可达性分析算法
概述
该算法的基本思路就是通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,
搜索走过的路径被称为(Reference Chain),当一个对象到GC Roots没有任何引用链相连时
(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。
搜索走过的路径被称为(Reference Chain),当一个对象到GC Roots没有任何引用链相连时
(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。
GC ROOTS
- 虚拟机栈中引用的对象
- 本地方法栈内JNI(通常说的本地方法)引用的对象方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁synchronized持有的对象
- Java虚拟机内部的引用。
- 基本数据类型对应的Class对象,一些常驻的异常对象(如:Nu11PointerException、outofMemoryError),系统类加载器。
- 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
对象的finalization机制
Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法。
永远不要主动调用某个对象的finalize()方法I应该交给垃圾回收机制调用。理由包括下面三点:
当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法。
永远不要主动调用某个对象的finalize()方法I应该交给垃圾回收机制调用。理由包括下面三点:
- 在finalize()时可能会导致对象复活。
- finalize()方法的执行时间是没有保障的,它完全由Gc线程决定,极端情况下,若不发生GC,
则finalize()方法将没有执行机会。因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收
- 一个糟糕的finalize()会严重影响Gc的性能。
标记过程
判定一个对象objA是否可回收,至少要经历两次标记过程:
如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize() 方法执行。
finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记。如果objA在finalize()方法中与引用链上的任何一个对象建立了联 系,那么在第二次标记时,objA会被移出“即将回收”集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize方法不会被再次调用,对象会直接 变成不可触及的状态,也就是说,一个对象的finalize方法只会被调用一次。
- 可触及的:从根节点开始,可以到达这个对象。
- 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。
- 不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次。
判定一个对象objA是否可回收,至少要经历两次标记过程:
- 如果对象objA到GC Roots没有引用链,则进行第一次标记。
- 进行筛选,判断此对象是否有必要执行finalize()方法
如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize() 方法执行。
finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记。如果objA在finalize()方法中与引用链上的任何一个对象建立了联 系,那么在第二次标记时,objA会被移出“即将回收”集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize方法不会被再次调用,对象会直接 变成不可触及的状态,也就是说,一个对象的finalize方法只会被调用一次。
清除阶段
标记-清除
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,
判断垃圾的位置空间是否够,如果够,就存放覆盖原有的地址。
关于空闲列表是在为对象分配内存的时候 提,过:
图示
- 标记:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
- 清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,
判断垃圾的位置空间是否够,如果够,就存放覆盖原有的地址。
关于空闲列表是在为对象分配内存的时候 提,过:- 如果内存规整,采用指针碰撞的方式进行内存分配。
- 如果内存不规整,虚拟机需要维护一个列表,空闲列表分配。
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,
判断垃圾的位置空间是否够,如果够,就存放覆盖原有的地址。
关于空闲列表是在为对象分配内存的时候 提,过:
- 如果内存规整,采用指针碰撞的方式进行内存分配。
- 如果内存不规整,虚拟机需要维护一个列表,空闲列表分配。
复制算法
优点
图示
- 将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,
- 把可达的对象,直接复制到另外一个区域中复制完成后,A区就没有用了,里面的对象可以直接清除掉,
优点
- 没有标记和清除过程,实现简单,运行高效
- 复制过去以后保证空间的连续性,不会出现“碎片”问题。
- 此算法的缺点也是很明显的,就是需要两倍的内存空间。
- 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小
标记整理
图示
- 第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象
- 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。
标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,
也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法。
分代收集算法
前面所有这些算法中,并没有一种算法可以完全替代其他算法,它们都具有自己独特的优势和特点。分代收集算法应运而生。
分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,
以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,
以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
增量收集算法
上述现有的算法,在垃圾回收过程中,应用软件将处于一种stop the World的状态。
在stop the World状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,
等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,
将严重影响用户体验或者系统的稳定性。为了解决这个问题,即对实时垃圾收集算法
的研究直接导致了增量收集(Incremental Collecting)算法的诞生。
如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程
和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到
应用程序线程。依次反复,直到垃圾收集完成。
总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间
冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作
缺点
使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,
所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,
会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
在stop the World状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,
等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,
将严重影响用户体验或者系统的稳定性。为了解决这个问题,即对实时垃圾收集算法
的研究直接导致了增量收集(Incremental Collecting)算法的诞生。
如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程
和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到
应用程序线程。依次反复,直到垃圾收集完成。
总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间
冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作
缺点
使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,
所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,
会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
分区算法
图示
一般来说,在相同条件下,堆空间越大,一次Gc时所需要的时间就越长,有关GC产生的停顿也越长。
为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,
每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。
每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。
为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,
每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。
每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。
垃圾回收器
GC分类与性能指标
垃圾收集器分类
按线程数分
按线程数分(垃圾回收线程数),可以分为串行垃圾回收器和并行垃圾回收器。
串行回收指的是在同一时间段内只允许有一个CPU用于执行垃圾回收操作,
此时工作线程被暂停,直至垃圾收集工作结束。
默认被应用在客户端的Client模式下的JVM中
不过并行回收仍然与串行回收一样,采用独占式,使用了“stop-the-world”机制。
串行回收指的是在同一时间段内只允许有一个CPU用于执行垃圾回收操作,
此时工作线程被暂停,直至垃圾收集工作结束。
- 在诸如单CPU处理器或者较小的应用内存等硬件平台不是特别优越的场合,
默认被应用在客户端的Client模式下的JVM中
- 在并发能力比较强的CPU上,并行回收器产生的停顿时间要短于串行回收器。
不过并行回收仍然与串行回收一样,采用独占式,使用了“stop-the-world”机制。
按工作模式分
可以分为并发式垃圾回收器和独占式垃圾回收器。
- 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。
- 独占式垃圾回收器(Stop the world)一旦运行,就停止应用程序中的所有用户线程,
直到垃圾回收过程完全结束。
按碎片处理方式分
按碎片处理方式分,可分为压缩武垃圾回收器和非压缩式垃圾回收器。
- 压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片。
- 非压缩式的垃圾回收器不进行这步操作。
评估GC的性能指标
- 吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间)
- 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。
- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
- 收集频率:相对于应用程序的执行,收集操作发生的频率。
- 内存占用:Java堆区所占的内存大小。
- 快速:一个对象从诞生到被回收所经历的时间。
组合关系
- 两个收集器间有连线,表明它们可以搭配使用:Serial/Serial old、Serial/CMS、ParNew/Serial old、
ParNew/CMS、Parallel Scavenge/Serial 0ld、Parallel Scavenge/Parallel 01d、G1; - 其中Serial o1d作为CMs出现"Concurrent Mode Failure"失败的后备预案。
- (红色虚线)由于维护和兼容性测试的成本,在JDK 8时将Serial+CMS、ParNew+Serial old
这两个组合声明为废弃(JEP173),并在JDK9中完全取消了这些组合的支持(JEP214),即:移除。 - (绿色虚线)JDK14中:弃用Paralle1 Scavenge和Serialold GC组合(JEP366)
- (青色虚线)JDK14中:删除CMs垃圾回收器(JEP363)
新生代收集器:Serial、ParNew、Parallel Scavenge;
老年代收集器:Serial old、Parallel old、CMS;
整堆收集器:G1;
图
如何查看默认垃圾收集器
- -XX:+PrintcommandLineFlags:查看命令行相关参数(包含使用的垃圾收集器)
- 使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程ID
Serial (串行回收)
图示
Serial收集器作为HotSpot中client模式下的默认新生代垃圾收集器。
Serial收集器采用复制算法、串行回收和"stop-the-World"机制的方式执行内存回收。
除了年轻代之外,Serial收集器还提供用于执行老年代垃圾收集的Serial old收集器。
单线程收集
Serial收集器采用复制算法、串行回收和"stop-the-World"机制的方式执行内存回收。
除了年轻代之外,Serial收集器还提供用于执行老年代垃圾收集的Serial old收集器。
单线程收集
ParNew(并行回收)
图示
如果说serialGC是年轻代中的单线程垃圾收集器,那么ParNew收集器则是serial收集器的多线程版本。
Par是Parallel的缩写,New:只能处理的是新生代
ParNew 收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。
ParNew 是很多JVM运行在Server模式下新生代的默认垃圾收集器。
-XX:ParallelGCThreads限制线程数量,默认开启和CPU数据相同的线程数。
Par是Parallel的缩写,New:只能处理的是新生代
ParNew 收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。
ParNew 是很多JVM运行在Server模式下新生代的默认垃圾收集器。
-XX:ParallelGCThreads限制线程数量,默认开启和CPU数据相同的线程数。
Parallel(吞吐量优先)
图示
HotSpot的年轻代中除了拥有ParNew收集器是基于并行回收的以外,Parallel Scavenge收集器同样也采用了复制算法、
并行回收和"Stop the World"机制。
在程序吞吐量优先的应用场景中,IParalle1收集器和Parallel old收集器的组合,在server模式下的内存回收性能很不错。
在Java8中,默认是此垃圾收集器。
并行回收和"Stop the World"机制。
- 和ParNew收集器不同,ParallelScavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),
它也被称为吞吐量优先的垃圾收集器。 - 自适应调节策略也是Parallel Scavenge与ParNew一个重要区别。
在程序吞吐量优先的应用场景中,IParalle1收集器和Parallel old收集器的组合,在server模式下的内存回收性能很不错。
在Java8中,默认是此垃圾收集器。
-XX:+UseParallelGC 手动指定年轻代使用Paralle1并行收集器执行内存回收任务。
-XX:+UseParallelOldGC 手动指定老年代都是使用并行回收收集器。
-XX:MaxGCPauseMillis 设置垃圾收集器最大停顿时间(即STW的时间)。单位是毫秒。
-XX:GCTimeRatio垃圾收集时间占总时间的比例(=1/(N+1))。用于衡量吞吐量的大小。
-XX:+UseParallelOldGC 手动指定老年代都是使用并行回收收集器。
- 分别适用于新生代和老年代。默认jdk8是开启的。
- 上面两个参数,默认开启一个,另一个也会被开启。(互相激活)
- 在默认情况下,当CPU数量小于8个,ParallelGcThreads的值等于CPU数量。
- 当CPU数量大于8个,ParallelGCThreads的值等于3+[5*CPU Count]/8]
-XX:MaxGCPauseMillis 设置垃圾收集器最大停顿时间(即STW的时间)。单位是毫秒。
- 为了尽可能地把停顿时间控制在MaxGCPauseMi11s以内,收集器在工作时会调整Java堆大小或者其他一些参数。
对于用户来讲,停顿时间越短体验越好。但是在服务器端,我们注重高并发,整体的吞吐量。
所以服务器端适合Parallel,进行控制。该参数使用需谨慎。
-XX:GCTimeRatio垃圾收集时间占总时间的比例(=1/(N+1))。用于衡量吞吐量的大小。
- 取值范围(0,100)。默认值99,也就是垃圾回收时间不超过1。
- 与前一个-xx:MaxGCPauseMillis参数有一定矛盾性。暂停时间越长,Radio参数就容易超过设定的比例。
- 在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,
已达到在堆大小、吞吐量和停顿时间之间的平衡点。
CMS(低延迟)
图示
CMS(Concurrent-Mark-Sweep)
- 初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“stop-the-world”
机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GCRoots能直接关联到的对象。
一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。 - 并发标记(Concurrent-Mark)阶段:从Gc Roots的直接关联对象开始遍历整个对象图的过程,
这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。 - 重新标记(Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,
因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,
这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。 - 并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。
由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
-XX:+UseConcMarkSweepGC手动指定使用CMS收集器执行内存回收任务。
-XX:CMSInitiatingoccupanyFraction 设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。
-XX:+UseCMSCompactAtFullCollection用于指定在执行完Full
-XX:CMSFullGCsBeforecompaction 设置在执行多少次FullGC后对内存空间进行压缩整理。
-XX:ParallelcMSThreads 设置cMs的线程数量。
- 开启该参数后会自动将-xx:+UseParNewGC打开。即:ParNew(Young区用)+CMS(old区用)+Serial old的组合。
-XX:CMSInitiatingoccupanyFraction 设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。
- JDK5及以前版本的默认值为68%,即当老年代的空间使用率达到68%时,会执行一次CMS回收。
- JDK6及以上版本默认值为92%。
- 如果内存增长缓慢,则可以设置一个稍大的值,大的阀值可以有效降低CMS的触发频率,
减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,
则应该降低这个阈值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低FullGc的执行次数。
-XX:+UseCMSCompactAtFullCollection用于指定在执行完Full
- GC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了。
-XX:CMSFullGCsBeforecompaction 设置在执行多少次FullGC后对内存空间进行压缩整理。
-XX:ParallelcMSThreads 设置cMs的线程数量。
- CMs默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads是年轻代并行收集器的线程数。
当CPU资源比较紧张时,受到CMS收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕。
G1(区域化分代)
图示
图示
概述
每次根据允许的收集时间,优先回收价值最大的Region。
- G1是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)(物理上不连续的)。
- G1 GC有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆
每次根据允许的收集时间,优先回收价值最大的Region。
- 是JDK9以后的默认垃圾回收器,CMS已经在JDK9中被标记为废弃(deprecated)。
优点
优点:
1、并行与并发
2、分代收集
两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到
连续内存空间而提前触发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显。
4、可预测的停顿时间模型(即:软实时soft real-time):这是G1相对于CMS的另一大优势,G1除了追求低停顿外,
还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
1、并行与并发
- 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。
- 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,
2、分代收集
- 从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。
但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。 - 将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。
- 和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代;
- CMS:“标记-清除”算法、内存碎片、若干次GC后进行一次碎片整理。
- G1将内存划分为一个个的region。内存的回收是以region作为基本单位的。
两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到
连续内存空间而提前触发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显。
4、可预测的停顿时间模型(即:软实时soft real-time):这是G1相对于CMS的另一大优势,G1除了追求低停顿外,
还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
- 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
- G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,
每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。 - 相比于CMSGC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。
G1参数设置
- -XX:+UseG1GC:手动指定使用G1垃圾收集器执行内存回收任务
- -XX:G1HeapRegionSize设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,
目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000。 - -XX:MaxGCPauseMillis 设置期望达到的最大Gc停顿时间指标(JVM会尽力实现,但不保证达到)。默认值是200ms
- -XX:+ParallelGcThread 设置STW工作线程数的值。最多设置为8
- -XX:ConcGCThreads 设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGcThreads)的1/4左右。
- -XX:InitiatingHeapoccupancyPercent 设置触发并发Gc周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45。
分区Region:化整为零
一个region有可能属于Eden,Survivor或者old/Tenured内存区域。但是一个region只可能属于一个角色。
右图中的E表示该region属于Eden内存区域,s表示属于survivor内存区域,o表示属于0ld内存区域。图中空白的表示未使用的内存空间。
G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。主要用于存储大对象,如果超过1.5个region,就放到H。
**设置H的原因:**对于堆中的对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象就会对垃圾收集器造成负面影响。
为了解决这个问题,G1划分了一个Humongous区,它用来专门存放大对象。如果一个H区装不下一个大对象,
那么G1会寻找连续的H区来存储。为了能找到连续的H区,有时候不得不启动Fu11Gc。G1的大多数行为都把H区作为老年代的一部分来看待。
每个Region都是通过指针碰撞来分配空间
- 使用G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块,
- XX:G1HeapRegionsize设定。所有的Region大小相同,且在JVM生命周期内不会被改变。
一个region有可能属于Eden,Survivor或者old/Tenured内存区域。但是一个region只可能属于一个角色。
右图中的E表示该region属于Eden内存区域,s表示属于survivor内存区域,o表示属于0ld内存区域。图中空白的表示未使用的内存空间。
G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。主要用于存储大对象,如果超过1.5个region,就放到H。
**设置H的原因:**对于堆中的对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象就会对垃圾收集器造成负面影响。
为了解决这个问题,G1划分了一个Humongous区,它用来专门存放大对象。如果一个H区装不下一个大对象,
那么G1会寻找连续的H区来存储。为了能找到连续的H区,有时候不得不启动Fu11Gc。G1的大多数行为都把H区作为老年代的一部分来看待。
每个Region都是通过指针碰撞来分配空间
图示
Remembered Set(记忆集)
一个对象被不同区域引用的问题。
一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,
判断对象存活时,是否需要扫描整个Java堆才能保证准确?
无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描:
每个Region都有一个对应的Remembered Set;每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作;
然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象);
如果不同,通过cardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;当进行垃圾收集时,
在GC根节点的枚举范围加入Remembered Set;就可以保证不进行全局扫描,也不会有遗漏。
一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,
判断对象存活时,是否需要扫描整个Java堆才能保证准确?
无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描:
每个Region都有一个对应的Remembered Set;每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作;
然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象);
如果不同,通过cardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;当进行垃圾收集时,
在GC根节点的枚举范围加入Remembered Set;就可以保证不进行全局扫描,也不会有遗漏。
图示
回收过程
年轻代GC(Young GC)
JVM启动时,G1先准备好Eden区,程序在运行过程中不断创建对象到Eden区,当Eden空间耗尽时,G1会启动一次年轻代垃圾回收过程。
YGC时,首先G1停止应用程序的执行(stop-The-World),G1创建回收集(Collection Set),
回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代Eden区和Survivor区所有的内存分段。
然后开始如下回收过程:
年龄会加1,达到阀值会被会被复制到old区中空的内存分段。如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。
没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。
YGC时,首先G1停止应用程序的执行(stop-The-World),G1创建回收集(Collection Set),
回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代Eden区和Survivor区所有的内存分段。
然后开始如下回收过程:
- 第一阶段,扫描根
- 第二阶段,更新RSet
- 第三阶段,处理RSet
- 第四阶段,复制对象。
年龄会加1,达到阀值会被会被复制到old区中空的内存分段。如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。
- 第五阶段,处理引用
没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。
老年代并发标记过程(Concurrent Marking)
- 初始标记阶段:标记从根节点直接可达的对象。这个阶段是STW的,并且会触发一次年轻代GC。
- 根区域扫描(Root Region Scanning):G1 Gc扫描survivor区直接可达的老年代区域对象,并标记被引用的对象。
这一过程必须在youngGC之前完成。 - 并发标记(Concurrent Marking):在整个堆中进行并发标记(和应用程序并发执行),此过程可能被youngGC中断。
在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。同时,并发标记过程中,
会计算每个区域的对象活性(区域中存活对象的比例)。 - 再次标记(Remark):由于应用程序持续进行,需要修正上一次的标记结果。是STW的。G1中采用了比CMS更快
的初始快照算法:snapshot-at-the-beginning(SATB)。 - 独占清理(cleanup,STW):计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。
为下阶段做铺垫。是sTw的。这个阶段并不会实际上去做垃圾的收集 - 并发清理阶段:识别并清理完全空闲的区域。
混合回收(Mixed GC)
当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,
即Mixed GC,该算法并不是一个old GC,除了回收整个Young Region,还会回收一部分的old Region。
这里需要注意:是一部分老年代,而不是全部老年代。可以选择哪些old Region进行收集,从而可以对垃圾回收
的耗时时间进行控制。也要注意的是Mixed GC并不是Full GC。
并发标记结束以后,老年代中百分百为垃圾的内存分段被回收了,部分为垃圾的内存分段被计算了出来。
默认情况下,这些老年代的内存分段会分8次(可以通过-XX:G1MixedGCCountTarget设置)被回收
混合回收的回收集(Collection Set)包括八分之一的老年代内存分段,Eden区内存分段,Survivor区内存分段。
混合回收的算法和年轻代回收的算法完全一样,只是回收集多了老年代的内存分段。具体过程请参考上面的年轻代回收过程。
由于老年代中的内存分段默认分8次回收,G1会优先回收垃圾多的内存分段。垃圾占内存分段比例越高的,越会被先回收。
并且有一个阈值会决定内存分段是否被回收,
XX:G1MixedGCLiveThresholdPercent,默认为65%,意思是垃圾占内存分段比例要达到65%才会被回收。如果垃圾占比太低,
意味着存活的对象占比高,在复制的时候会花费更多的时间。
混合回收并不一定要进行8次。有一个阈值-XX:G1HeapWastePercent,默认值为10%,意思是允许整个堆内存中
有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收。
因为GC会花费很多的时间但是回收到的内存却很少。
即Mixed GC,该算法并不是一个old GC,除了回收整个Young Region,还会回收一部分的old Region。
这里需要注意:是一部分老年代,而不是全部老年代。可以选择哪些old Region进行收集,从而可以对垃圾回收
的耗时时间进行控制。也要注意的是Mixed GC并不是Full GC。
并发标记结束以后,老年代中百分百为垃圾的内存分段被回收了,部分为垃圾的内存分段被计算了出来。
默认情况下,这些老年代的内存分段会分8次(可以通过-XX:G1MixedGCCountTarget设置)被回收
混合回收的回收集(Collection Set)包括八分之一的老年代内存分段,Eden区内存分段,Survivor区内存分段。
混合回收的算法和年轻代回收的算法完全一样,只是回收集多了老年代的内存分段。具体过程请参考上面的年轻代回收过程。
由于老年代中的内存分段默认分8次回收,G1会优先回收垃圾多的内存分段。垃圾占内存分段比例越高的,越会被先回收。
并且有一个阈值会决定内存分段是否被回收,
XX:G1MixedGCLiveThresholdPercent,默认为65%,意思是垃圾占内存分段比例要达到65%才会被回收。如果垃圾占比太低,
意味着存活的对象占比高,在复制的时候会花费更多的时间。
混合回收并不一定要进行8次。有一个阈值-XX:G1HeapWastePercent,默认值为10%,意思是允许整个堆内存中
有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收。
因为GC会花费很多的时间但是回收到的内存却很少。
G1回收可选的过程4 - Full GC
G1的初衷就是要避免Fu11GC的出现。但是如果上述方式不能正常工作,G1会停止应用程序的执行(stop-The-world),使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。
要避免Fu11GC的发生,一旦发生需要进行调整。什么时候会发生Ful1GC呢?比如堆内存太小,当G1在复制存活对象的时候没有空的内存分段可用,则会回退到ful1gc,这种情况可以通过增大内存解决。 导致61Fu11GC的原因可能有两个:
要避免Fu11GC的发生,一旦发生需要进行调整。什么时候会发生Ful1GC呢?比如堆内存太小,当G1在复制存活对象的时候没有空的内存分段可用,则会回退到ful1gc,这种情况可以通过增大内存解决。 导致61Fu11GC的原因可能有两个:
- EVacuation的时候没有足够的to-space来存放晋升的对象;
- 并发处理过程完成之前空间耗尽。
G1回收的优化建议
年轻代大小
避免使用-Xmn或-XX:NewRatio等相关选项显式设置年轻代大小
固定年轻代的大小会覆盖
暂停时间目标暂停时间目标不要太过严苛
G1 GC的吞吐量目标是90%的应用程序时间和10%的垃圾回收时间
评估G1GC的吞吐量时,暂停时间目标不要太严苛。目标太过严苛表示你愿意承受更多的垃圾回收开销,而这些会直接影响到吞吐量。
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日志文件的输出路径
Young GC图片
Full GC图片
字节码与类的加载篇
0 条评论
下一页