其他文章链接
Java基础
Java集合
多线程
JVM
MySQL
Redis
docker
计算机网络
操作系统
JVM
- 一.概述
- 二.类加载子系统
- 三.运⾏时数据区
- 四.执行引擎
- 五.字符串常量池(StringTable)
- 1.String有哪些特性
- 2.JDK9时改为byte[]存储String的原因
- 3.字符型常量和字符串常量的区别?
- 4.String,StringBuffer 和 StringBuilder 的区别是什么?
- 5.String为什么要设计成不可变的
- 6.在使用 HashMap 的时候,用 String 做 key 有什么好处
- 7.字符串常量池
- 8.String的内存分配
- 9.字符串常量池存放位置
- 10.为什么 StringTable从永久代调整到堆中
- 11.intern()
- 12.字符串拼接
- 13.字符串拼接的底层原理
- 14.+和append()对比
- 15.字符串拼接改进
- 16.new String(“aaa”)创建了几个字符串对象
- 17.new String("a") + new String("b") 会创建几个对象
- 18.JDK6和JDK7 intern()的区别
- 六.垃圾回收
- 1.标记阶段/判断对象是否存活
- 2.清除阶段
- 3.垃圾回收器
- 4.Java垃圾回收优点
- 5.Minor GC,MajorGC、Full GC
- 6.什么情况下会发生Full GC
- 7.对象的 finalization机制/finalize()
- 8.对象在虚拟机中三种状态
- 9.判定一个对象是否可回收,至少要经历两次标记过程
- 10.内存溢出
- 11.内存泄漏
- 12.STW
- 13.串行、并行、并发
- 14.安全点
- 15.System.gc()
- 其他问题
一.概述
JVM在Java程序运行中所处的位置:
1.JAVA内存结构
执行引擎包含三部分:解释器,及时编译器,垃圾回收器
2.JAVA内存模型
在并发编程中, 线程之间的通信机制有两种: 共享内存和消息机制。在共享内存的并发模型里, 线程之间共享程序的公共状态, 线程之间通过写-读内存中的公共状态来隐式通信. 在消息传递的并发模型里, 线程之间没有公共状态, 线程之间必须通过明确的发送消息来显示进行通信。
Java的并发采用的是共享内存模型
Java内存模型(JMM)定义:是一种规范,规定了JVM如何使用计算机内存。同时,JMM向开发者保证,如果程序是正确同步的,程序的执行将具有顺序一致性(顺序一致性内存模型)
目标:定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从主内存中取出变量这样的底层细节(此处变量与Java编程中的变量有区别,它包括了实例字段、静态字段和构成数组对象的元素,不包括局部变量与方法参数,后者是线程私有)。
从上图来看, 线程A与线程B之间如要通信的话, 必须要经历下面2个步骤:
- 首先, 线程A把本地内存A中更新过的共享变量刷新到主内存中去;
- 然后, 线程B到主内存中去读取线程A之前已更新过的共享变量.
主内存:规定所有的变量都存储到主内存中(可类比物理硬件的主内存,但此处只是虚拟机内存的一部分)
工作内存:每条线程所有(可类比处理器高速缓存),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,不能直接读写内存中的变量。不同线程之间无法访问对方工作内存中的变量。
3.JVM特点
- 一次编译,到处运行
- 自动内存管理
- 自动垃圾回收功能
二.类加载子系统
如果自己想手写一个Java虚拟机的话,主要考虑哪些结构呢?
- 类加载器
- 执行引擎
ClassLoader只负责 class文件的加载,至于它是否可以运行,则由Execution Engine决定。
加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
1.类加载过程
1.1 加载
通过一个类的全限定名获取定义此类的二进制字节流。并将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
1.2 链接
1.2.1 验证 Verify
目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
主要包括四种验证:
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
1.2.2 准备 Prepare
为类变量分配内存并且设置该类变量的默认初始值,即零值。
- 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化。
- 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
public class HelloApp {
private static int a = 1; // 准备阶段为0,在下个阶段,也就是初始化的 时候才是1
public static void main(String[] args) {
System.out.println(a);
}
}
上面的变量a在准备阶段会赋初始值,但不是1,而是0。
1.2.3 解析 Resolve
将常量池内的符号引用转换为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。
1.3初始化
执行类构造器法()的过程。
此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
构造器方法中指令按语句在源文件中出现的顺序执行。
1.3.1 构造方法、成员变量初始化、静态成员变量三者的初始化顺序
静态成员变量、成员变量、构造方法。
详细的先后顺序:
父类静态变量、父类静态代码块、子类静态变量、子类静态代码块
父类非静态变量、父类非静态代码块、父类构造函数
子类非静态变量、子类非静态代码块、子类构造函数。
2.类加载器的分类
这里的四者之间是包含关系,不是上层和下层,也不是子系统的继承关系。
- 启动类加载器(引导类加载器,根加载器,Bootstrap ClassLoader)
这个类加载使用 C/C++语言实现的,嵌套在 JVM内部。用来加载 Java的核心库(java、javax、sun等开头的类),用于提供 JVM自身需要的类。并不继承自Java.lang.ClassLoader,没有父加载器。加载扩展类和应用程序类加载器,并指定为他们的父类加载器。 - 扩展类加载器(Extension ClassLoader)
派生于ClassLoader类,父类加载器为启动类加载器。 - 应用程序类加载器(系统类加载器,AppClassLoader)
派生于ClassLoader类,父类加载器为扩展类加载器。是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载。 - 用户自定义类加载器
隔离加载类(同名类)、修改类加载的方式(Bootstrap ClassLoader必需,其他不必需 )、扩展加载源、防止源码泄漏
ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括Bootstrap加载器)
3.双亲委派机制
3.1 工作原理
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
3.2 沙箱安全机制
自定义string类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的string类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
3.3 双亲委派机制的优势
- 避免类的重复加载
- 保护程序安全,防止核心API被随意篡改
–自定义类:java.lang.String
–自定义类:java.lang.ShkStart(报错:阻止创建java.lang开头的类)
4.如何判断两个class对象是否相同
两个必要条件:
- 类的完整类名必须一致,包括包名。
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
三.运⾏时数据区
线程私有的:
- 程序计数器
- 虚拟机栈:解决程序的运行问题
- 本地⽅法栈
线程共享的:
- 堆:解决数据储存问题
- 方法区(JDK1.7为永久代,1.8为元空间)
- 直接内存(⾮运⾏时数据区的⼀部分)
1.程序计数器
当前线程执行的字节码行号指示器,储存指向下一条指令的地址。
分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
程序计数器是Java虚拟机规范中唯⼀⼀个不会出现OOM(OutOfMemoryError)的内存区域。
1.1 PC寄存器为什么被设定为私有的
为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
2.Java虚拟机栈
描述的是Java⽅法执⾏的内存模型,每次⽅法调⽤对应一个栈帧(Stack Frame)。
不同线程中所包含的栈帧是不允许存在相互引用的。
执行引擎运行的所有字节码指令只针对当前栈帧(栈顶的栈帧)进行操作。
Java栈的操作只有两个:方法执行——入栈,方法结束——出栈。
return/抛出异常都会导致栈帧被弹出。
Java栈可能出现StackoverflowError和OutOfMemoryError
2.1 栈帧
组成:局部变量表、操作数栈、动态链接、⽅法出⼝和一些附加信息
栈帧的大小主要由局部变量表和操作数栈决定。
2.1.1 局部变量表
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,并参与方法的调用与返回。数据类型包括各类基本数据类型、对象引用、返回地址。
局部变量表所需的容量大小是在编译期确定下来的。
局部变量表最基本的存储单元是 Slot(变量槽)。 在局部变量表里,32位以内的类型只占用一个 slot(包括 returnAddress类型), 64位的类型(1ong和 double)占用两个 slot。
byte、short、char 在存储前被转换为 int,boolean也被转换为 int,0表示 false,非0表示 true。 long和 double则占据两个 slot。
slot的重复利用:栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
2.1.2 操作数栈
主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
操作数栈由数组实现。
操作数栈所需的容量大小是在编译期确定下来的,之后不可变。
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和出栈(pop)。
我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
2.1.3 动态链接
指向运行时常量池中该栈帧所属方法的引用。
包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。当一个方法调用了其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
2.1.4 方法返回地址
存放调用该方法的pc寄存器的值。
一个方法的结束,有两种方式:
- 正常执行完成
- 出现未处理的异常,非正常退出
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。
2.2 设置栈内存大小
通过 -Xss 设置栈的大小,栈的大小直接决定了函数调用的最大可达深度。如:
-Xss1m
-Xss1k
2.3 分配的栈内存越大越好么
不是,一定时间内降低了OOM概率,但是会挤占其它的线程空间,因为整个空间是有限的。
2.4 栈顶缓存技术(Top Of Stack Cashing)
基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派次数和内存读/写次数。
由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpotJVM的设计者们提出了栈顶缓存(Tos,Top-ofStackCashing)技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
寄存器:指令更少,执行速度快
2.5 链接(静态链接和动态链接)(早期绑定和晚期绑定)(非虚方法和虚方法)
- 静态链接:当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时,这种情况下降调用方法的符号引用转换为直接引用的过程称之为静态链接。对应早期绑定。对应非虚方法(静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法)。
- 动态链接:如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用的方法的符号转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接。对应晚期绑定。对应虚方法。
2.6 动态类型语言和静态类型语言
动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。
静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。
Java时静态类型语言,会先编译就进行类型检查。
String info = "mogu blog";
JS时动态类型语言,会先编译就进行类型检查。
var name = "shkstart"; var name = 10;
2.7 方法重写的本质
- 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C。
- 如果在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.1ang.I1legalAccessError异常。
- 否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
- 如果始终没有找到合适的方法(如接口,未实现方法),则抛出java.1ang.AbstractMethodsrror异常。
2.8 方法的调用:虚方法表
在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,为了提高性能,JVM采用在类的方法区建立一个虚方法表(非虚方法不会出现在表中)来实现。使用索引表来代替查找。
每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。
如上图所示:如果类中重写了方法,那么调用的时候,就会直接在虚方法表中查找,否则将会直接连接到Object的方法中。
2.9 垃圾回收是否涉及到虚拟机栈
不会
2.10 方法中定义的局部变量是否线程安全
不一定。如果对象是在方法内部生成,并在内部消亡,没有返回到外部,那么他就是线程安全的,反之则是线程不安全的。
3 本地方法栈
登记 native方法,在 Execution Engine 执行时加载本地方法库。
当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
- 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。
- 它甚至可以直接使用本地处理器中的寄存器。
- 直接从本地内存的堆中分配任意数量的内存。
在HotSpot虚拟机中,本地方法栈和Java虚拟机栈合⼆为⼀。
4 堆
存放所有对象实例以及数组
Java虚拟机所管理的内存中最⼤的⼀块,Java堆是所有线程共享的⼀块内存区域。在虚拟机启动时创建,大小随即确定。GC的重点区域。
《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
新对象实例的分配次序:TLAB->Eden->老年代
4.1 堆内存细分
Java 7及之前堆内存逻辑上分为三部分:年轻代+老年代+永久代
Java 8及之后堆内存逻辑上分为三部分:年轻代+老年代+元空间
年轻代又可以划分为 Eden空间、S0(Survivor0)空间和S1( Survivor1)空间(有时也叫做 from区、to区,谁空谁是to)
几乎所有的Java对象都是在Eden区被new出来的。绝大部分的Java对象的销毁都在新生代进行了。(有些大的对象在Eden区无法存储时候,将直接进入老年代)
IBM公司的专门研究表明,新生代中80%的对象都是“朝生夕死”的。
4.2 堆分代的原因
优化GC性能。如果没有分代,那所有的对象都在一块,GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
4.3 堆中各区域内存比例及设置
默认:
- Eden:From:to -> 8:1:1
- 年轻代:老年代 - > 1:2
相关配置 -XX:NewRatio=2、xx:SurvivorRatio=8
配置新生代与老年代在堆结构的占比。
- 默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
- 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整老年代的大小,来进行调优。
在 HotSpot中,Eden空间和另外两个 survivor空间缺省所占的比例是 8:1:1当 然开发人员可以通过选项“-xx:SurvivorRatio”调整这个空间比例。比如xx:SurvivorRatio=8。
4.4 内存分配策略
如果对象在Eden出生并经过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,将被移动到survivor空间中,并将对象年龄设为1。对象在survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到阈值(默认为15岁,每个JVM、每个GC都有所不同)时,就会被晋升到老年代。
对阀值可以通过选项-xx:MaxTenuringThreshold来设置
4.5 不同年龄段的对象分配原则
- 优先分配到 Eden
- 开发中比较长的字符串或者数组,会直接存在老年代,但是因为新创建的对象都是朝生夕死的,所以这个大对象可能也很快被回收,但是因为老年代触发MajorGC的次数比MinorGC要更少,因此可能回收起来就会比较慢。 - 大对象直接分配到老年代
- 尽量避免程序中出现过多的大对象 - 长期存活的对象分配到老年代
- 动态对象年龄判断
- 如果survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
Hotspot遍历所有对象时,按照年龄从⼩到⼤对其所占⽤的⼤⼩进⾏累积,当累积的某个年龄⼤⼩超过了survivor区的⼀半时,取这个年龄和MaxTenuringThreshold中更⼩的⼀个值,作为新的晋升年龄阈值。
- 也就是经过MinorGC后,所有的对象都存活,因为Survivor比较小,所以就需要将Survivor无法容纳的对象,存放到老年代中。
4.6 对象分配过程
- new的对象先放Eden区。此区有大小限制。
- 当Eden区的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对Eden区进行垃圾回收(MinorGC),将Eden区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到Eden区。
- 然后将Eden区中的剩余对象移动到S0区,年龄设为1。
- 如果再次触发垃圾回收,S0区幸存的的放到S1区,年龄加1。
- S1和S0之间转移一次年龄加1,当年龄超过阈值(默认为15,可通过参数-XX:MaxTenuringThreshold来设置)后,进入老年代。
- 当老年代内存不足时,再次触发GC:MajorGC,进行老年代的内存清理。
- 若老年代执行了MajorGC之后,发现依然无法进行对象的保存,就会产生OOM异常。
4.7 Survivor满了之后怎么办
在Eden区满了会触发MinorGC;而幸存者区满了不会触发MinorGC。
操作如果Survivor区满了后,将会触发一些特殊的规则,也就是可能直接晋升老年代。
4.8 TLAB(Thread Local Allocation Buffer)
从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
4.8.1 堆中的所有数据都是线程共享的吗
不一定,因为还有TLAB这个概念,在堆中划分出一块区域,为每个线程所独占。
4.8.2 为什么要有TLAB
- 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
- 避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
4.8.3 TLAB分配过程
对象首先是通过TLAB开辟空间,如果不能放入,那么需要通过Eden来进行分配。
4.8.4 相关参数
-Xx:UseTLAB设置是否开启 TLAB空间。
-Xx:TLABWasteTargetPercent设置 TLAB空间所占用 Eden空间的百 分比大小。默认情况下,TLAB空间的内存非常小,仅占有整个 Eden空间的1%。
4.9 为什么堆存放“几乎”所有的对象实例和数组
随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致⼀些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。
从jdk1.7开始已经默认开启逃逸分析,如果某些⽅法中的对象引⽤没有被返回或者未被外⾯使⽤(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
4.10 逃逸分析
如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。
4.10.1 使用逃逸分析,编译器可以对代码做如下优化
- 栈上分配:将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会发生逃逸,对象可能是栈上分配的候选,而不是堆上分配。
- 同步省略:如果一个对象被发现只有一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
- 分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存(堆),而是存储在CPU寄存器(栈)中。
4.10.2 逃逸分析的不足
无法保证逃逸分析的性能消耗一定能高于他的消耗。比如,经过逃逸分析之后,发现没有一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。
4.10.3 堆中存放所有的对象实例,到底对不对
对。
因为HostSpot JVM使用逃逸分析进行了标量替换,但并未进行栈上分配。
4.10.4 相关参数
-xx: +DoEscapeAnalysis 显式开启逃逸分析
-xx: +PrintEscapeAnalysis 查看逃逸分析的筛选结果
4.11 在方法结束后,堆中的对象是否会马上被移除
不会。
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
- 也就是触发了GC的时候,才会进行回收
- 如果堆中对象马上被回收,那么用户线程就会收到影响,因为有stop the word
4.12 堆空间的参数设置
- -XX:+PrintFlagsInitial:查看所有的参数的默认初始值
- -XX:+PrintFlagsFinal:查看所有的参数的最终值(可能会存在修改,不再是初始值)
- -Xms:初始堆空间内存(默认为物理内存的 1/64)
- -Xmx:最大堆空间内存(默认为物理内存的 1/4)
- -Xmn:设置新生代的大小。(初始值及最大值)
- -XX:NewRatio:配置新生代与老年代在堆结构的占比
- -XX:SurvivorRatio:设置新生代中 Eden和 S0/S1空间的比例
- -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
- -XX:+PrintGCDetails:输出详细的 GC处理日志
- 打印 gc简要信息:①-Xx:+PrintGC ② -verbose:gc
5 方法区
存储已被虚拟机加载的类信息、运行时常量池、方法信息、即时编译器编译后的代码等数据。
5.1 HotSpot中方法区的演进
JDK版本 | 变化 |
---|---|
1.6及之前 | 有永久代,静态变量存储在永久代 |
1.7 | 有永久代,字符串常量池、静态变量保存在堆中 |
1.8 | 无永久代,类型信息、方法信息、运行时常量池保存在本地内存的元空间,但字符串常量池、静态变量依旧在堆中 |
5.2 为什么要将永久代替换为元空间
- 为永久代设置空间大小是很难确定的。而元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
- 对永久代进行调优是很困难的,主要是为了降低Full GC。
5.3 字符串常量池StringTable为什么要调整位置
因为永久代的回收效率很低,在full gc的时候才会触发,导致 stringTable回收效率不高。放到堆里,能及时回收内存。
5.4 ⽅法区和永久代的关系
永久代是HotSpot的概念,⽅法区是Java虚拟机规范中的定义,是⼀种规范,⽽永久代是⼀种实现,⼀个是标准⼀个是实现,其他的虚拟机实现并没有永久代这⼀说法。
5.5 静态变量static和全局常量static final
- 静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分。静态变量保存在堆中。
- 被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。
5.6 运⾏时常量池和常量池
- 常量池是字节码文件(.class)中,存放编译时期生成的各种字面量和符号引用,类加载后进入运行时常量池中存放
- 运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池。
运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。
每个class都有一个运行时常量池,类在解析之后将符号引用替换成直接引用,与全局常量池中的引用值保持一致。
运行时常量池,相对于Class文件常量池的另一重要特征是:具备动态性。
5.7 如何判断⼀个类是⽆⽤的类
⽅法区主要回收的是⽆⽤的类,那么如何判断⼀个类是⽆⽤的类的呢?
判定⼀个常量是否是“废弃常量”⽐较简单,⽽要判定⼀个类是否是“⽆⽤的类”的条件则相对苛刻许多。类需要同时满⾜下⾯3个条件才能算是“⽆⽤的类”:
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
- 加载该类的ClassLoader已经被回收。
- 该类对应的java.lang.Class对象没有在任何地⽅被引⽤,⽆法在任何地⽅通过反射访问该类的⽅法。
虚拟机可以对满⾜上述3个条件的⽆⽤类进⾏回收,这⾥说的仅仅是“可以”,⽽并不是和对象⼀样不使⽤了就会必然被回收。
5.8相关参数(JDK1.8)
-XX:MetaspaceSize:设置初始的元空间大小。
windows下默认值为21MB。-1表示没有限制。这就是初始的高水位线,一旦触及这个水位线,Full GC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活)然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Ful1GC多次调用。为了避免频繁地GC,建议将XX:MetaspaceSize设置为一个相对较高的值。
-XX:MaxMetaspaceSize=N:设置 Metaspace 的最⼤⼤⼩
6.运行时数据区相关问题
8.1 栈、堆、方法区的交互关系
- Person:存放在元空间,也可以说方法区
- person:存放在 Java栈的局部变量表中
- new Person():存放在 Java堆中
8.1 静态变量与局部变量的对比
变量的分类:
- 按数据类型分:基本数据类型、引用数据类型
- 按类中声明的位置分:成员变量(类变量,实例变量)、局部变量
- 类变量:有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。
- 实例变量:随着对象创建,会在堆空间中分配实例变量空间,并进行默认赋值
- 局部变量:在使用前必须进行显式赋值,不然编译不通过。
8.2 为什么需要运行时常量池
因为在不同的方法,都可能调用常量或者方法,所以只需要存储一份即可,节省了空间。
常量池的作用:就是为了提供一些符号和常量,便于指令的识别。
8.3 运行时数据区各部分是否存在Error和GC?
运行时数据区 | 是否存在Error | 是否存在GC |
---|---|---|
程序计数器 | 否 | 否 |
虚拟机栈 | 是 | 否 |
本地方法栈 | 是 | 否 |
方法区 | 是(OOM) | 是 |
堆 | 是 | 是 |
8.4 如何解决OOM
先分清楚到底是出现了内存泄漏还是内存溢出
- 如果是内存泄漏,可进一步通过工具查看泄漏对象到GCRoots的引用链。于是就能找到泄漏对象是通过怎样的路径与GCRoots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GCRoots引用链的信息,就可以比较准确地定位出泄漏代码的位置。
- 如果不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
8.5 对象内存布局
5.9 对象的访问定位的两种⽅式
- 句柄访问:句柄访问就是说栈的局部变量表中,记录的对象的引用,然后在堆空间中开辟了一块空间,也就是句柄池。
优点:reference中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只会改变句柄中实例数据指针即可,reference本身不需要被修改 - 直接指针(HotSpot采用):直接指针是局部变量表中的引用,直接指向堆中的实例,在对象实例中有类型指针,指向的是方法区中的对象类型数据
优点:速度快,节省了⼀次指针定位的时间开销。
四.执行引擎
执行引擎属于JVM的下层,里面包括解释器、及时编译器、垃圾回收器
执行引擎的任务就是将字节码指令解释/编译为对应平台上的本地机器指令。
1.解释器
将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
2.编译器
JIT(Just In Time Compiler)编译器:就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言。
3.为什么Java是半编译半解释型语言
现在JVM在执行Java代码的时候,通常都会将解释执行与编译执行二者结合起来进行。
翻译成本地代码后,就可以做一个缓存操作,存储在方法区中。
4.HotSpot JVM执行方式
当虚拟机启动的时候,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成再执行,这样可以省去许多不必要的编译时间。并且随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率。
5.热点探测技术
一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称之为“热点代码”,因此都可以通过JIT编译器编译为本地机器指令。由于这种编译方式发生在方法的执行过程中,因此被称之为栈上替换,或简称为OSR(On Stack Replacement)编译。
HotSpotVM所采用的热点探测方式是基于计数器的热点探测。HotSpot JVM将会为每一个方法都建立2个不同类型的计数器,分别为方法调用计数器和回边计数器。
- 方法调用计数器用于统计方法的调用次数
- 回边计数器则用于统计循环体执行的循环次数
热点衰减
如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器热度的衰减,而这段时间就称为此方法统计的半衰周期。
可以使用虚拟机参数-XX:-UseCounterDecay来关闭热度衰减,让方法计数器统计方法调用的绝对次数,这样,只要系统运行时间足够长,绝大部分方法都会被编译成本地代码。
另外,可以使用-XX:CounterHalfLifeTime参数设置半衰周期的时间,单位是秒。
五.字符串常量池(StringTable)
String:字符串,使用一对 ”” 引起来表示
String s1 = "mogublog"; //字面量的定义方式
String s2 = new String("moxi");
1.String有哪些特性
- 不变性:只要进行了修改,就会重新创建一个对象,这就是不可变性;
-
- final:使用final来定义String类,表示String类不能被继承,提高了系统的安全性。
- 常量池优化:String对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用;
- 实现了Serializable接口:表示字符串是支持序列化的。
- 实现了Comparable接口:表示 string可以比较大小
-
- string在 jdk8及以前底层用final char[] value存储字符串数据。 JDK9时改为byte[]
2.JDK9时改为byte[]存储String的原因
JDK1.8中,String将字符存储在char数组中,每个字符使用两个字节(16位)。字符串是堆使用的主要组成部分,而且大多数字符串对象只包含拉丁字符,这些字符只需要一个字节的存储空间,因此这些字符串对象的内部char数组中有一半的空间将不会使用。造成了空间浪费。因此,JDK9时改为byte[]。
3.字符型常量和字符串常量的区别?
- 形式上:字符常量是单引号引起的⼀个字符;字符串常量是双引号引起的若⼲个字符
- 含义上:字符常量相当于⼀个整型值(ASCII值),可以参加表达式运算;字符串常量代表⼀个地址值(该字符串在内存中存放位置)
- 占内存⼤⼩:字符常量只占2个字节;字符串常量占若⼲个字节(注意:char在Java中占两个字节。)
4.String,StringBuffer 和 StringBuilder 的区别是什么?
- 可变性
String 类中使⽤ final 关键字修饰字符数组来保存字符串, private final char/byte[] value,所以 String 对象是不可变的。
StringBuilder 与 StringBuffer 都继承⾃ AbstractStringBuilder 类,也是使⽤字符数组保存字符串,但是没有⽤ final 关键字修饰,所以这两种对象都是可 变的。 - 线程安全性
String 中的对象是不可变的,线程安全。
StringBuffer 对⽅法加了同步锁或者对调⽤的⽅法加了同步锁(synchronized),所以是线程安全的。StringBuilder 并没有对⽅法进⾏加同步锁,所以是⾮线程安全的。 - 性能
每次对String类型进⾏改变的时候,都会⽣成⼀个新的String对象,然后将指针指向新的String对象,性能最差。StringBuffer加了锁,效率比StringBuilder差一些。
使用情景:
- 操作少量的数据: 适⽤ String
- 单线程操作字符串缓冲区下操作⼤量数据: 适⽤ StringBuilder
- 多线程操作字符串缓冲区下操作⼤量数据: 适⽤ StringBuffer
5.String为什么要设计成不可变的
- 便于实现字符串池(Stringpool)
如果字符串是可变的,某一个字符串变量改变了其值,那么其指向的变量的值也会改变,Stringpool将不能够实现! - 线程安全
在并发场景下,多个线程同时读一个资源,是安全的,不会引发竞争。但对资源进行写操作时是不安全的,不可变对象不能被写,所以保证了多线程的安全。 - 避免安全问题
在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址URL,文件路径path,反射机制所需要的String参数。其不可变性可以保证连接的安全性。如果字符串是可变的,黑客就有可能改变字符串指向对象的值,那么会引起很严重的安全问题。 - 加快字符串处理速度
由于String是不可变的,保证了hashcode的唯一性,于是在创建对象时其hashcode就可以放心的缓存了,不需要重新计算。这也就是Map喜欢将String作为Key的原因,处理速度要快过其它的键对象。所以HashMap中的键往往都使用String。
6.在使用 HashMap 的时候,用 String 做 key 有什么好处
HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。
7.字符串常量池
java中常量池的概念主要有三个:全局字符串常量池,class文件常量池,运行时常量池。我们现在所说的就是全局字符串常量池。
常量池中不会存在相同内容的变量。
String的string Pool是一个固定大小的Hashtable,默认值大小长度是 1009,1009也是JDK1.8可以设置的最小值。如果放进 string Pool的 string非常多,就会造成 Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用 string.intern时性能会大幅下降。
-XX:StringTablesize可设置 stringTable的长度
8.String的内存分配
常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,string类型的常量池比较特殊。它的主要使用方法有两种。
- 直接使用双引号声明出来的 String对象会直接存储在常量池中。比如:
string info="atguigu.com";
- 如果不是用双引号声明的string对象,可以使用 string提供的 intern()方法。
9.字符串常量池存放位置
Java 6及以前,在永久代。
Java 7,在堆。
Java 8,在堆。
10.为什么 StringTable从永久代调整到堆中
- 永久代的默认比较小
- 永久代垃圾回收频率低
11.intern()
intern是一个 native方法,调用的是底层C的方法。
如果调用intern()方法,则会从字符串常量池中查询当前字符串是否存在。如果存在,则返回池中的字符串。否则,该字符串对象将被添加到池中,并返回对该字符串对象的引用。
12.字符串拼接
- 常量与常量的拼接结果在常量池,原理是编译期优化。
- 只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder。
public static void test1() {
String s1 = "a" + "b" + "c"; // 得到 abc的常量池
String s2 = "abc"; // abc存放在常量池,直接将常量池的地址返回
/** *
*最终 java编译成.class,再执行.class
*/
System.out.println(s1 == s2); // true,因为存放在字符串常量池
System.out.println(s1.equals(s2)); // true
}
public static void test2() {
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";
String s4 = "javaEE" + "hadoop";
String s5 = s1 + "hadoop";
String s6 = "javaEE" + s2;
String s7 = s1 + s2;
System.out.println(s3 == s4); // true
System.out.println(s3 == s5); // false
System.out.println(s3 == s6); // false
System.out.println(s3 == s7); // false
System.out.println(s5 == s6); // false
System.out.println(s5 == s7); // false
System.out.println(s6 == s7); // false
String s8 = s6.intern();
System.out.println(s3 == s8); // true
}
从上述的结果我们可以知道:
如果拼接符号的前后出现了变量,则相当于在堆空间中new String(),具体的内容为拼接的结果。
而调用intern方法,则会判断字符串常量池中是否存在JavaEEhadoop值,如果存在则返回常量池中的值,否者就在常量池中创建。
public static void test4() {
final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4);//true
}
从上述的结果我们可以知道:
即使左右两边如果存在变量,但是如果使用的是final修饰,则是从常量池中获取。所以说拼接符号左右两边都是字符串常量或常量引用则仍然使用编译器优化。
在开发中,能够使用final的时候,建议使用上。
13.字符串拼接的底层原理
拼接操作的底层其实使用了StringBuilder。
s1 + s2的执行细节:
- StringBuilder s = new StringBuilder();
- s.append(s1);
- s.append(s2);
- s.toString(); //类似于 new String(“ab”);
14.+和append()对比
通过StringBuilder的 append()方式添加字符串的效率,要远远高于String的字符串拼接方法。
- StringBuilder的 append的方式,自始至终只创建一个 StringBuilder的对象。
- 对于字符串拼接的方式,还需要创建很多 StringBuilder对象和调用toString()时候创建的String对象。
- 内存中由于创建了较多的StringBuilder和String对象,内存占用过大,如果进行GC那么将会耗费更多的时间。
15.字符串拼接改进
- 我们使用的是StringBuilder的空参构造器,默认的字符串容量是16,然后将原来的字符串拷贝到新的字符串中,我们也可以默认初始化更大的长度,减少扩容的次数。
- 因此在实际开发中,我们能够确定,前前后后需要添加的字符串不高于某个限定值,那么建议使用构造器创建一个阈值的长度。
16.new String(“aaa”)创建了几个字符串对象
两个对象:
- 一个对象:new关键字在堆空间中创建
- 另一个对象:字符串常量池中的对象
17.new String(“a”) + new String(“b”) 会创建几个对象
6个对象:
- 对象 1:new StringBuilder()
- 对象 2:new String(“a”)
- 对象 3:常量池的a
- 对象 4:new String(“b”)
- 对象 5:常量池的b
- 对象 6:toString中会创建一个new String(“ab”)
- 调用toString方法,不会在常量池中生成ab
18.JDK6和JDK7 intern()的区别
JDK1.6中,将这个字符串对象尝试放入串池。
- 如果串池中有,则并不会放入。返回已有的串池中的对象的地址。
- 如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址。
JDK1.7起,将这个字符串对象尝试放入串池。
- 如果串池中有,则并不会放入。返回已有的串池中的对象的地址。
- 如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址。
public static void main(String[] args) {
String s = new String("2");
s.intern();
String s2 = "2";
System.out.println(s == s2);//JDK6 false,JDK7 false
String s3 = new String("3") + new String("3");
s3.intern();
String s4 = "33";
System.out.println(s3 == s4);//JDK6 false,JDK7 true
}
JDK 1.6
Strings=new String(“2”);创建了两个对象,一个在堆中的StringObject对象,一个是在常量池中的“2”对象。
s.intern();在常量池中寻找与s变量内容相同的对象,发现已经存在内容相同对象“2”,返回对象2的地址。
Strings2=“2”;使用字面量创建,在常量池寻找是否有相同内容的对象,发现有,返回对象"2"的地址。
System.out.println(s==s2);从上面可以分析出,s变量和s2变量地址指向的是不同的对象,所以返回false
String s3 = new String(“3”) + new String(“3”);创建了两个对象,一个在堆中的StringObject 对象,一个是在常量池中的“3”对象。中间还有2个匿名的new String(“3”)我们不去讨论它们。
s3.intern();在常量池中寻找与s3变量内容相同的对象,没有发现“33”对象,在常量池中创建“33”对 象,返回“33”对象的地址。
String s4 = “33”;使用字面量创建,在常量池寻找是否有相同内容的对象,发现有,返回对象"33"的 地址。
System.out.println(s3 == s4);从上面可以分析出,s3变量和s4变量地址指向的是不同的对象,所 以返回false
JDK 1.7
Strings=new String(“2”);创建了两个对象,一个在堆中的StringObject对象,一个是在堆中的“2”对象,并在常量池中保存“2”对象的引用地址。
s.intern();在常量池中寻找与s变量内容相同的对象,发现已经存在内容相同对象“2”,返回对象“2”的引用地址。
Strings2=“2”;使用字面量创建,在常量池寻找是否有相同内容的对象,发现有,返回对象“2”的引用地址。
System.out.println(s==s2);从上面可以分析出,s变量和s2变量地址指向的是不同的对象,所以返回false
Strings3=newString(“3”)+newString(“3”);创建了两个对象,一个在堆中的StringObject对象,一个是在堆中的“3”对象,并在常量池中保存“3”对象的引用地址。中间还有2个匿名的new String(“3”)我们不去讨论它们。
s3.intern();在常量池中寻找与s3变量内容相同的对象,没有发现“33”对象,将s3对应的StringObject对象的地址保存到常量池中,返回StringObject对象的地址。
Strings4=“33”;使用字面量创建,在常量池寻找是否有相同内容的对象,发现有,返回其地址,也就是StringObject对象的引用地址。
System.out.println(s3 == s4);从上面可以分析出,s3变量和s4变量地址指向的是相同的对象,所以返回true。
六.垃圾回收
堆是垃圾回收发生的最主要区域,方法区也会发生垃圾回收。
1.标记阶段/判断对象是否存活
1.1 引用计数算法
对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
缺点:1)需要单独的字段存储计数器,这样的做法增加了存储空间的开销。2)无法处理循环引用的情况,造成内存泄漏。
1.2 可达性分析算法(Java使用)
也叫根搜索算法、追踪性垃圾收集。
通过一系列的GC Roots对象为起始点,向下搜索,能被根直接或者间接连接的对象为存活对象,否者,为垃圾对象。
1.2.1 GC Roots对象种类
堆空间外的结构,比如虚拟机栈、本地方法栈、方法区、字符串常量池等地方对堆空间进行引用的,都可以作为GCRoots进行可达性分析。
如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root。
- 虚拟机栈中引用的对象,如被调用的方法中使用到的参数、局部变量等。
- 方法区中类静态属性引用的对象,如Java类的引用类型静态变量。
- 方法区中常量引用的对象,如字符串常量池里的引用。
- 所有被同步锁 synchronized持有的对象。
- 本地方法栈内引用的对象。
- Java虚拟机内部的引用,如基本数据类型对应的 Class对象,一些常驻的异常对象(如: NullPointerException、OOM),系统类加载器。
2.清除阶段
JVM中常见的垃圾收集算法有三种:标记-清除、复制算法和标记-整理算法。
2.1 标记-清除算法
当堆中的有效内存空间被耗尽时,停止整个程序(stop the world, STW),进行标记和清除。
- 标记:从引用根节点开始遍历,标记所有被引用的对象。
- 清除:对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。
清除并不是真的置空,而是把需要清除的对象地址保存在空闲列表里。如果有新对象需要加载时,而且垃圾的空间能够存放新对象,则对原有信息进行覆盖。
缺点:
- 标记清除算法的效率不算高。
- STW,用户体验差。
- 产生碎片,需要维护一个空闲列表。
2.2 复制算法
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色。
优点:
- 没有标记和清除过程,实现简单,运行高效。
- 复制过去以后保证空间的连续性,不会出现“碎片”问题。
缺点:
- 需要两倍的内存空间。
- 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。
复制算法需要复制的存活对象数量并不会太大,或者说非常低才行。比如新生代。
2.3 标记-整理算法
- 标记:从引用根节点开始遍历,标记所有被引用的对象。
- 整理:将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。
优点:
- 相比于标记-清除算法,不会产生碎片。我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
- 相比于复制算法,没有内存减半的高额代价。
缺点:
- 效率低于复制算法。
- 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。
- 移动过程中,STW。
2.4 三种垃圾回收算法对比
- | 标记-清楚 | 复制算法 | 标记-整理 |
---|---|---|---|
速率 | 中等 | 最快 | 最慢 |
空间开销 | 少(但会堆积碎片) | 通常需要活对象的2倍空间(不堆积碎片) | 少(不堆积碎片) |
移动对象 | 否 | 是 | 是 |
2.5 分代收集
不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
年轻代 - 区域相对老年代较小,对象生命周期短、存活率低,回收频繁 - 复制算法
老年代 - 区域较大,对象生命周期长、存活率高,回收不及年轻代频繁 - 标记-清除/标记-清除与标记-整理的混合实现
- Mark阶段的开销与存活对象的数量成正比。
- Sweep阶段的开销与所管理区域的大小成正相关。
- compact阶段的开销与存活对象的数据成正比。
2.6 增量收集算法
垃圾收集线程和应用程序线程交替执行,每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。可以缓解STW。
2.7 分区算法
将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
3.垃圾回收器
7种经典的垃圾收集器
- 串行回收器:Serial、Serial old
- 并行回收器:ParNew、Parallel Scavenge、Parallel old
- 并发回收器:CMS、G1
- 新生代收集器:Serial、ParNew、Paralle1 Scavenge;
- 老年代收集器:Serial old、Parallel old、CMS
- 整堆收集器:G1
组合关系:
红色虚线在JDK 8废弃,绿色虚线和CMS在JDK 14废弃
3.1 Serial回收器
复制算法、串行回收和STW
HotSpot中client模式下的默认新生代垃圾收集器。
3.2 Serial old回收器
标记-压缩、串行回收和STW
Serial old是运行在Client模式下默认的老年代的垃圾回收器
Serial old在 Server模式下主要有两个用途:
- 与新生代的 Parallel scavenge配合使用
- 作为老年代 CMS收集器的后备垃圾收集方案
Serial是一个单线程收集器,收集时必须暂停其他所有的工作线程。
优点:简单高效,在client模型下的模拟机是不错的选择
3.3 ParNew回收器
复制算法、并行回收和STW
serial收集器的多线程版本
除过Serial old收集器(8废除),只能与CMS搭配
对于新生代,回收次数频繁,使用并行高效,对于老年代,回收次数少,使用串行节省资源(CPU并行需要切换资源)
ParNew在单CPU下不如Serial收集器
3.4 Parallel scavenge回收器
复制算法、并行回收和STW
- 与ParNew相比,Parallel Scavenge收集器的目标是达到一个可控的吞吐量(Throughput),也称为吞吐量优先的垃圾回收器。
- 自适应调节策略是Parallel Scavenge 与ParNew的重要区别:动态调整Eden : S0 : S1。
3.5 Parallel old回收器
标记-压缩、并行回收和STW
在程序吞吐量优先的应用场景中,Parallel Old与Parallel Scavenge收集器的组合,在Server模式下的内存回收性能不错。
高吞吐量可以高效的利用CPU资源,尽快完成程序的运算任务,主要适用于后台计算而不需要太多交互:批量处理,科学计算。
Parallel关注于吞吐量,Java8中默认的垃圾收集器
3.6 CMS(Concurrent Mark Sweep)
标记-清除、并发回收、低STW
低延迟,第一次实现了让垃圾收集线程与用户线程同时工作
作为老年代收集器,只能与Serial与ParNew搭配
- CMS并不是没有内存才进行回收,到达阈值后进行回收(6以后 92%)。
- CMS运行期间预留的内存无法满足程序需要,出现"Concorrent Mode Failure",虚拟机临时启用Serial Old进行老年代的回收。
- CMS采用标记清除,会产生内存碎片,在为新对象分配内存时,无法采用指针碰撞,只能选择空闲列表执行内存分配。
3.6.1 CMS收集的4个阶段
- 初始标记(STW):所有用户线程暂停,只标记GC Roots直接关联的对象。
- 并发标记:从GC Roots的直接关联对象开始遍历整个对象,不需要暂停用户线程,可以与垃圾回收线程一起并发运行。
- 重新标记(STW):修正并发标记期间,用户程序继续运作导致变动的部分对象的标记记录。
- 并发清除:清除删掉标记阶段判定以及死亡的对象,释放内存空间,不需要移动对象
由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。
3.6.2 CMS为什么不使用标记整理算法
并发清除的时候,整理移动内存的话,原来的用户线程使用的内存无法使用。因此,为了并发清除时保证用户线程可以正常运行,使用标记清除。
3.6.3 CMS优点
- 并发收集
- 低延迟
3.6.4 CMS缺点
- 会产生内存碎片。
- 对CPU资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
- 可能出现“Concurrent Mode Failure"失败而导致Full GC的产生。
- 无法处理浮动垃圾,并发标记阶段如果产生新的垃圾对象,只能在下一次执行GC时释放这些之前未被回收的内存空间。
3.7 G1回收器
标记压缩+复制算法、并发+并行、STW
region——复制算法
整体——压缩
并行——多个GC线程同时工作——STW
并发——GC和应用程序交替或同时进行
在延迟可控的情况下,提高吞吐量,将堆内存分割成不相关的域(Region),使用不同的域表示Eden、 S0、 S1、老年代、H区。每个Region都是通过指针碰撞来分配空间。
3.7.1 G1收集的4个阶段
除过并发标记,其余阶段也是要完全暂停用户线程的,并非纯粹的追求低延迟,在延迟可控的情况下提高吞吐量。
- 初始标记:仅标记GCRoots能够直接关联到的对象,并且修改TAMS指针的值,让下一阶段的用户并发运行时,能够正确在可用的Region中分配新对象,需要短暂的停顿线程
- 并发标记:从GCRoot开始对堆中对象进行可达性分析,递归扫描整个堆的对象图,找出要回收的对象,耗时较长,可以与用户线程并发执行。当对象扫描完成后,需要重新处理SATB记录下在并发时有引用变动的对象
- 最终标记:对用户线程做一个短暂的暂停,用于处理并发阶段结束后遗留下来的少量SATB记录
- 筛选回收:更新Region的统计数据,对各个Region的回收价值进行排序,根据用户停顿时间来制定回收计划,可以选择多个Region构成回收集,然后把决定复制的Region中的对象复制到Region中,再清理掉整个旧Region的全部空间,涉及对象的移动,应该要暂停用户线程,由多条线程收集器并行完成。
Snapshot-At-The-Beginning(SATB): SATB是在G1 GC在并发标记阶段使用的增量式的标记算法。SATB可以理解成在GC开始之前对堆内存里的对象做一次快照,此时活的对象就认为是活的,从而形成一个对象图
3.7.2 G1收集器的适用场景
面向服务端应用,针对具有大内存、多处理器的机器。(在普通大小的堆里表现并不惊喜)
最主要的应用是需要低GC延迟,并具有大堆的应用程序提供解决方案。
3.7.3 可预测的停顿时间模型
这是G1相对于CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
- 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
- G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
- 相比于CMSGC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。
3.7.4 G1收集器设置H区的原因
为了解决短期存在的对象进入老年代,H区专门存放大对象,G1认为大小超过Region一半的对象就是大对象,可以N个连续的Region存放。
3.7.5 如何解决跨Region引用对象:Remembered Set(记忆集)
一个Region不可能时孤立的,一个Region中的对象可能被其他任意Region引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确?在其他分代收集器中,也存在这样的问题(G1更加突出),回收新生代是否需要扫描老年代?
解决方案:无论是G1还是其他分代收集器,JVM都采用Remembered Set来避免全局扫描
每个Region都有一个对应的Remembered Set,每次Reference类型数据写操作时,都会产生一个Writer Barrier暂停中断操作:检查要写入的引用对象是否和该Reference数据在同一个Region中(其他收集器,检查老年代对象是否引用新生代对象);如果不同,把相关引用信息记录到引用指向对象所在Region对应的Remembered Set中,进行GC时,在GC根节点中加入Remembered Set;保证不进行全局扫描
3.7.6 G1垃圾收集器的缺点优点
- 并行:G1回收期间,多个GC同时工作,利用多核计算,用户线程STW,G1可以与应用程序交替执行
- 空间整合: 标记压缩算法
- 可预测的停顿时间
3.7.7 G1垃圾收集器的缺点
在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比CMS要高。
从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间。
3.7.8 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。
3.8 垃圾回收器对比
GC发展阶段:Serial=> Parallel(并行)=> CMS(并发)=> G1 => ZGC
3.9 如何选择垃圾回收器
- 如果你想要最小化地使用内存和并行开销,请选Serial GC;
- 如果你想要最大化应用程序的吞吐量,请选Parallel GC;
- 如果你想要最小化GC的中断或停顿时间,请选CMS GC
3.10 CMS和G1的区别
- 追求目标:CMS追求低延迟, G1追求延迟可控的情况下,提高吞吐量
- 算法: CMS是标记清除,G1整体看是基于标记-整理的,局部(Region)看是基于标记-复制的,没有内存碎片
- 内存占用:G1和CMS都使用卡表处理跨代指针,但G1的卡表更为复杂,对于每个Region都得一份卡表,导致G1的记忆集可能占整个堆的20%或者更多的空间,相比CMS的卡表更简单,只有一份,老年代对新生代的引用
- 并发标记阶段:CMS采用增量更新, G1采用的是原始快照
- 从经验来说,小内存应用上CMS的表现大概率会优于G1, G1在大内存上发挥作用,平衡点在6-8G
3.11 增量更新与原始快照
三色标记,把遍历对象图遇到的对象,按照是否访问过,标记三种颜色:
- 白色:对象尚未被垃圾回收器访问过
- 黑色:对象已经被访问过,且这个对象的所有引用都已经扫描过
- 灰色: 表示对象已经被GC访问过,但这个对象至少存在一个引用没有被扫描
并发标记可能存在对象消失问题,即原本应该是黑色的对象被误标为白色,有两种解决方案,增量更新与原始快照
- 增量更新:黑色对象一旦插入指向白色对象的引用之后,就变为灰色
- 原始快照:无论引用关系删除与否,都会按照刚开始扫描那一刻的对象图快照进行搜索
4.Java垃圾回收优点
自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险。
自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发。
5.Minor GC,MajorGC、Full GC
- Minor GC:新生代的 GC
- Major GC:老年代的 GC
- Full GC:整堆收集,收集整个 Java堆和方法区的垃圾收集
JVM的调优的一个环节,也就是垃圾收集,我们需要尽量的避免垃圾回收,因为在垃圾回收的过程中,容易出现STW的问题。
而MajorGC和FullGC出现STW的时间,是MinorGC的10倍以上
在堆中,GC频繁在新生区收集,很少在老年代收集,几乎不再永久代和元空间进行收集。
6.什么情况下会发生Full GC
- 调用System.gc()时,系统建议执行Full GC,但是不必然执行
- 老年代空间不足
- 方法区空间不足
- 通过MinorGC后进入老年代的平均大小大于老年代的连续可用内存(小于则进行MinorGC即可)
- 由Eden区、From区向To区复制时,对象大小大于To区可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
Full GC是开发或调优中尽量要避免的。这样暂时时间会短一些
7.对象的 finalization机制/finalize()
- 允许开发人员提供对象被销毁之前的自定义处理逻辑。
- 垃圾回收此对象之前,总会先调用这个对象的finalize()方法。
- finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。
- finalize()只能被调用一次
永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用。
- 在finalize()时可能会导致对象复活。
- finalize()方法的执行时间是没有保障的,它完全由Gc线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会。
- 一个糟糕的finalize()会严重影响Gc的性能。
8.对象在虚拟机中三种状态
- 可触及的:从根节点开始,可以到达这个对象。
- 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。
- 不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次。
9.判定一个对象是否可回收,至少要经历两次标记过程
- (第一次标记)如果对象A到GCRoots没有引用链,则进行第一次标记。
- 进行筛选,判断A是否有必要执行finalize()方法
- 如果A没有重写finalize()方法,或者finalize()方法被调用过。A被判定为不可触及的,回收A。
- 如果A重写了finalize()方法,且还未执行过,那么A会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法执行。
- (第二次标记)如果A在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,对象会被移出“即将回收”集合。
之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize()不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize()只会被调用一次。
10.内存溢出
没有空闲内存,并且垃圾收集器也无法提供更多内存。
原因:
- Java虚拟机的堆内存设置不够。
- 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)
在抛出OOM之前,通常垃圾收集器会被触发,尽其所能去清理出空间。也不是在任何情况下垃圾收集器都会被触发的,比如,我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接抛出OOM。
11.内存泄漏
对象不会再被程序用到了,但是GC又不能回收他们的情况。
举例:
- 单例模式
单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。 - 一些提供 close的资源未关闭导致内存泄漏
数据库连接,网络连接(socket)和 io连接必须手动 close,否则是不能被回收的。
12.STW
stop-the-world,指的是GC事件发生过程中,整个应用程序线程都会被暂停,没有任何响应。
13.串行、并行、并发
- 串行(Serial):单线程执行。
- 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等 待状态。( ParNew、Parallel Scavenge、Parallel old)
- 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行)
14.安全点
并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点”。
如何在cc发生时,检查所有线程都跑到最近的安全点停顿下来呢?
- 抢先式中断:首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。(目前没有虚拟机采用了)
- 主动式中断:设置一个中断标志,各个线程运行到SafePoint的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。(有轮询的机制)
15.System.gc()
显式触发 FullGC,同时对老年代和新生代进行回收。但附带一个免责声明,无法保证对垃圾收集器的调用。(不能确保立即生效)
其他问题
1.常用的调优工具
- JDK命令行
- Eclipse:Memory Analyzer Tool
- Jconsole
- Visual VM(实时监控 推荐)
- Jprofiler(推荐)
- Java Flight Recorder(实时监控)
- GCView
2.对象的创建方式
3.对象实例化步骤/创建对象步骤
4.内存规整/不规整给对象分配内存的方式
如果内存规整
- 采用指针碰撞的方式进行内存分配
如果内存不规整
- 虚拟机需要维护一个列表
- 空闲列表分配
5.强引⽤,软引⽤,弱引⽤,虚引⽤
5.1 强引用(Strong)
常见的普通对象引用,默认的引用类型; new操作符创建一个对象,并赋值给一个变量时,变量就成为指向该对象的强引用。
只要强引用关系存在,GC就不会回收引用的对象,可能导致内存泄漏。
5.2 软引用(Soft)
用来描述一些有用,但非必需的对象,通常用来实现内存敏感的缓存,比如:高速缓存有用到软引用,如果有空闲内存,就可以暂时保存缓存,当内存不足时清理。
当系统即将发生内存溢出之前,会把Soft Reference列入回收范围进行二次回收,如果回收后依然没有足够内存,抛出内存溢出。
5.3 弱引用(Weak)
用来描述非必需对象,用来保存可有可无的缓存数据。
被弱引用关联的对象只能生存到下一次垃圾回收之前,当垃圾回收器工作时,无论内存是否足够,都会回收弱引用关联的对象。
5.4 虚引用(Phantom)
跟踪垃圾回收的过程,对象被收集时收到一个系统通知。
一个对象是否有虚引用的存在,不会对其生存时间构成影响,也无法通过虚引用获得一个对象的实例。
特别注意,在程序设计中⼀般很少使⽤弱引⽤与虚引⽤,使⽤软引⽤的情况较多,这是因为软引⽤可以加速JVM对垃圾内存的回收速度,可以维护系统的运⾏安全,防⽌内存溢出(OutOfMemory)等问题的产⽣。