Java语言可能不是最强大的语言,但是JVM一定是最强大的虚拟机。
Java代码执行流程
JVM的架构模型
虚拟机的启动
Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的。
虚拟机的执行
一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序。程序开始执行时他才运行,程序结束时他就停止。执行一个所谓的Java程序的时候,真真正正在执行的是一个叫做Java虚拟机的进程。
虚拟机的退出
有如下的几种情况:
1、类加载器子系统负责从文件系统或者网络中加载class文件,class文件在文件开头有特定的文件标识。
2、classLoader只负责class文件的加载,至于它是否可以运行,则由ExecutionEngine决定。
3、加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串常量和数字常量(这部分常量信息是class文件中常量池部分的内存映射)。
因此总结一下方法区都存放了哪些内容?
来一张经典的JVM内存结构图:其中类加载器的工作范围只限于下图的左半部分,不包含调用构造器实例化对象
4、如果调用构造器实例化对象,则其实例存放在堆区
加载过程一、加载
1.通过一个类的全限定明获取定义此类的二进制字节流;
2.将这个字节流所代表的的静态存储结构转化为方法区的运行时数据;
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
加载过程二、链接(验证、准备、解析)
加载过程三、初始化阶段(执行类构造器方法 clinit() 的过程)
1.clinit()即“class or interface initialization method”,注意他并不是指构造器init()
2.此方法不需要定义,是javac编译器自动收集类中的所有静态变量的赋值动作和静态代码块中的语句合并而来。
虚拟机必须保证一个类的clinit()方法在多线程下被同步加锁即一个类只需被clinit一次,之后该类的内部信息就被存储在方法区。
1.JVM支持两种类型的加载器,分别为引导类加载器C/C++实现(BootStrap ClassLoader)和自定义类加载器由Java实现
2.从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。
3.按照这样的加载器的类型划分,在程序中我们最常见的类加载器是:引导类加载器BootStrapClassLoader、自定义类加载器(Extension Class Loader、System Class Loader、User-Defined ClassLoader)
1.用户自定义类JVM是使用系统类加载器System Class Loader中的AppClassLoader进行加载。
2.java核心类库都是使用引导类加载器BootStrapClassLoader加载的。
/**
* ClassLoader加载
*/
public class ClassLoaderTest {
public static void main(String[] args) {
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//获取其上层 扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@610455d6
//获取其上层 获取不到引导类加载器
ClassLoader bootStrapClassLoader = extClassLoader.getParent();
System.out.println(bootStrapClassLoader);//null
//对于用户自定义类来说:使用系统类加载器进行加载
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//String 类使用引导类加载器进行加载的 -->java核心类库都是使用引导类加载器加载的
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1);//null获取不到间接证明了String 类使用引导类加载器进行加载的
}
}
为什么使用用户自定义类加载器
1.隔离加载类
2.修改类加载的方式
3.拓展加载源
4.防止源码泄漏
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将她的class文件加载到内存生成的class对象。而且加载某个类的class文件时,java虚拟机采用的是双亲委派机制,即把请求交由父类处理,它是一种任务委派 模式
举个例子
内存是非常重要的系统资源,是硬盘和cpu的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了JAVA在运行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行。不同的JVM对于内存的划分方式和管理机制存在着部分差异(对于Hotspot主要指方法区)
java虚拟机定了了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。如图,灰色的区域为单独线程私有的,红色的为多个线程共享的,即
每个线程:独立包括程序计数器、栈、本地栈
线程间共享:堆、堆外内存(方法区、永久代或元空间、代码缓存)
一般来说,jvm优化95%是优化堆区,5%优化的是方法区,至于栈区无非出入栈操作优化较少
PC寄存器是用来存储指向下一条指令的地址,也即将要执行的指令的地址。由执行引擎读取下一条指令。
1.它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域
2.在jvm规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
3.任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的java方法的JVM指令地址;或者,如果实在执行native方法,则是未指定值(undefined),因为程序计数器不负责本地方法栈。
4.它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
5.字节码解释器工作时就是通过改变这个计数器的值来选取下一跳需要执行的字节码指令
6.它是唯一一个在java虚拟机规范中没有规定任何OOM(Out Of Memery)情况的区域,而且没有GC垃圾回收
使用PC寄存器存储字节码指令地址有什么用呢?(为什么使用PC寄存器记录当前线程的执行地址呢)
多线程情况下,宏观上是并行的(多个线程同时执行),实际上是并发交替的执行的。线程之间会存在频繁地来回切换。
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
因此JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行哪一条字节码指令。
所以,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。
PC寄存器为什么会设定为线程私有?
CPU会不停滴做任务切换,这样必然会导致经常中断或恢复,如何保证分毫无差呢?
为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是**为每一个线程都分配一个PC寄存器,**这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
由于跨平台性的设计,java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。
根据栈设计的优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
==栈是运行时的单位,而堆是存储的单位。==栈管运行,堆管存储。
1.栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储问题,即数据怎么放、放在哪儿。
2.一般来讲,对象主要都是放在堆空间的,是运行时数据区比较大的一块。
3.栈空间存放 基本数据类型的局部变量,以及引用数据类型的对象的引用
栈生命周期和线程一致
栈是一种快速有效的分配存储方式,访问速度仅次于PC寄存器。
作用:主管java程序的运行,它保存方法的局部变量、8种基本数据类型、对象的引用地址、程序运算的中间结果,形参与方法的调用和返回。
JVM直接对java栈的操作只有两个
(1)每个方法执行,伴随着进栈(入栈,压栈)
(2)执行结束后的出栈工作
因此对于栈来说不存在垃圾回收问题,因为它处理完就出栈了,但是肯定存在OOM异常。
java虚拟机规范允许Java栈的大小是动态的或者是固定不变的
如果采用固定大小的Java虚拟机栈,那每一个线程的java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量,java虚拟机将会抛出一个 StackOverFlowError异常
如果java虚拟机栈可以动态拓展,并且在尝试拓展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那java虚拟机将会抛出一个 OutOfMemoryError异常
我们可以使用参数-Xss
选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。 (IDEA设置方法:Run-EditConfigurations-VM options 填入指定栈的大小-Xss256k等)
1.每个线程都有自己的栈,栈中的数据都是以**栈帧(Stack Frame)**的格式存在。
2.在这个线程上正在执行的每个方法都对应各自的一个栈帧。
3.栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
4.JVM直接对java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循先进后出/后进先出的和原则。
5.在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧对应的方法就是当前方法(Current Frame),拥有该方法的类叫当前类。
6.执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
7.如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前栈帧。
8.不同线程中所包含的栈帧是不允许相互引用的,即不可能在另一个栈帧中引用另外一个线程的栈帧
9.如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
10.Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
1.举例栈溢出的情况?(StackOverflowError)
2.调整栈的大小,就能保证不出现溢出么?
3.分配的栈内存越大越好么?
4.垃圾回收是否会涉及到虚拟机栈?
关于Error我们再多说一点,上面的讨论不涉及Exception
首先Exception和Error都是继承于Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。
Exception和Error体现了JAVA这门语言对于异常处理的两种方式。
Exception是java程序运行中可预料的异常情况,咱们可以获取到这种异常,并且对这种异常进行业务外的处理。
Error是java程序运行中不可预料的异常情况,这种异常发生以后,会直接导致JVM不可处理或者不可恢复的情况。所以这种异常不可能抓取到,比如OutOfMemoryError、NoClassDefFoundError等。
其中的Exception又分为检查性异常和非检查性异常。两个根本的区别在于,检查性异常 必须在编写代码时,使用try catch捕获(比如:IOException异常)。非检查性异常 在代码编写使,可以忽略捕获操作(比如:ArrayIndexOutOfBoundsException),这种异常是在代码编写或者使用过程中通过规范可以避免发生的。
5.方法中定义的局部变量是否线程安全?
/**
* 面试题:
* 方法中定义的局部变量是否线程安全?具体情况具体分析
*
* 何为线程安全?
* 如果只有一个线程可以操作此数据,则必定是线程安全的。
* 如果有多个线程操作此数据,则此数据是共享数据。如果不考虑同步机制的话,会存在线程安全问题
*
* 我们知道StringBuffer是线程安全的源码中实现synchronized,StringBuilder源码未实现synchronized,在多线程情况下是不安全的 * 二者均继承自AbstractStringBuilder * */
public class StringBuilderTest {
//s1的声明方式是线程安全的,s1在方法method1内部消亡了
public static void method1(){
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
}
//stringBuilder的操作过程:是不安全的,因为method2可以被多个线程调用
public static void method2(StringBuilder stringBuilder){
stringBuilder.append("a");
stringBuilder.append("b");
}
//s1的操作:是线程不安全的 有返回值,可能被其他线程共享
public static StringBuilder method3(){
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
return s1;
}
//s1的操作:是线程安全的 ,StringBuilder的toString方法是创建了一个新的String,s1在内部消亡了
public static String method4(){
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
return s1.toString();
}
public static void main(String[] args) {
StringBuilder s = new StringBuilder();
new Thread(()->{
s.append("a");
s.append("b");
}).start();
method2(s);
}
}
每个栈帧中存储着
1.局部变量表(Local Variables)
2.操作数栈(Operand Stack)(或表达式栈)
3.动态链接(Dynamic Linking)(或执行 “运行时常量池” 的方法引用)----深入理解Java多态特性必读!!
4.方法返回地址(Return Adress)(或方法正常退出或者异常退出的定义)
5.一些附加信息
其中部分参考书目上,称方法返回地址、动态链接、附加信息为帧数据区
局部变量表也被称之为局部变量数组或本地变量表
==定义为一个数字数组,主要用于存储方法参数、局部变量==这些数据包括各类基本数据类型、对象引用,以及返回地址类型。
由于局部变量表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题。
局部变量表所需的容量大小是在编译期确定下来的。并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套次数越多。对一个函数而言,他的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间。
局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。
局部变量表,最基本的存储单位是slot(变量槽)
局部变量表中存放编译期可知的各种基本数据类型,引用类型,returnAddress类型的变量。
在局部变量表里,32位以内的类型只占用一个slot,64位的类型(long和double)占用两个slot。
byte、short、char、float在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true。
long和double则占据两个slot。
JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。
当一个实例方法被调用的时候,它的参数和局部变量将会按声明顺序被复制到局部变量表中的每一个slot上。
如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问long或者double类型变量)
如果当前栈是由构造方法或者实例方法创建的(意思是当前帧所对应的方法是构造器方法或者是普通的实例方法),那么该对象引用this将会存放在index为0的slot处,其余的参数都按照参数表顺序排列。
静态方法中不能引用this,是因为静态方法所对应的栈帧中的局部变量表中不存在this。
栈帧中的局部变量表中的槽位是可以重复利用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
上述代码对应的栈帧中局部变量表中一共有多少个slot,或者说局部变量表的长度是几?答案是3:
变量b的作用域是
{
int b = 0;
b = a+1;
}
this占0号、a单独占1个槽号、c重复使用了b的槽号
静态变量与局部变量的对比及小结
变量的分类:
1.栈 :可以使用数组或者链表来实现
2.每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出的操作数栈,也可以成为表达式栈
3.操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)或出栈(pop)
某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用他们后再把结果压入栈。(如字节码指令bipush操作)
1.运行时常量池位于方法区(注意: JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。)
字节码中的常量池结构如下:
为什么需要常量池呢?
常量池的作用,就是为了提供一些符号和常量,便于指令的识别。下面提供一张测试类的运行时字节码文件格式。
2**.每一个栈帧内部都包含一个指向运行时常量池Constant pool或该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。比如invokedynamic**指令 **。
3.在Java源文件被编译成字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Refenrence)保存在class字节码文件(javap反编译查看)的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么==动态链接的作用就是为了将这些符号引用最终转换为调用方法的直接引用。==
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。
静态链接
当一个 字节码文件被加载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。
动态链接
如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。
对应的方法的绑定机制为:早起绑定(Early Binding)和晚期绑定(Late Bingding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
早期绑定
早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
晚期绑定
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。
随着高级语言的横空出世,类似于java一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差别,但是它们彼此之间始终保持着一个共性,那就是都支持封装,继承和多态等面向对象特性,既然这一类的编程语言具备多态特性,那么自然也就具备早期绑定和晚期绑定两种绑定方式。
Java中任何一个普通的方法其实都具备虚函数的特征,它们相当于C++语言中的虚函数(C++中则需要使用关键字virtual来显式定义)。如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。
虚方法就是可以被子类重写的方法,如果子类重写了虚方法,那么运行时将使用重写后的逻辑,如果没有重写,则使用父类中虚方法的逻辑。
子类对象的多态性使用前提:
①类的继承关系(父类的声明)②方法的重写(子类的实现)
实际开发编写代码中用的接口,实际执行是导入的的三方jar包已经实现的功能
非虚方法
其他所有体现多态特性的方法称为虚方法
普通调用指令:
1.invokestatic:调用静态方法,解析阶段确定唯一方法版本;
2.invokespecial:调用方法、私有、及父类方法,解析阶段确定唯一方法版本;
3.invokevirtual调用所有虚方法;
4.invokeinterface:调用接口方法;
动态调用指令(Java7新增):
5.invokedynamic:动态解析出需要调用的方法,然后执行 .
前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本。
其中invokestatic
指令和invokespecial
指令调用的方法称为非虚方法
其中invokevirtual
(final修饰的除外,JVM会把final方法调用也归为invokevirtual指令,但要注意final方法调用不是虚方法)、invokeinterface
指令调用的方法称称为虚方法。
/**
* 解析调用中非虚方法、虚方法的测试
*/
class Father {
public Father(){
System.out.println("Father默认构造器");
}
public static void showStatic(String s){
System.out.println("Father show static"+s);
}
public final void showFinal(){
System.out.println("Father show final");
}
public void showCommon(){
System.out.println("Father show common");
}
}
public class Son extends Father{
public Son(){
super();
}
public Son(int age){
this();
}
public static void main(String[] args) {
Son son = new Son();
son.show();
}
//不是重写的父类方法,因为静态方法不能被重写
public static void showStatic(String s){
System.out.println("Son show static"+s);
}
private void showPrivate(String s){
System.out.println("Son show private"+s);
}
public void show(){
//invokestatic
showStatic(" 大头儿子");
//invokestatic
super.showStatic(" 大头儿子");
//invokespecial
showPrivate(" hello!");
//invokespecial
super.showCommon();
//invokevirtual 因为此方法声明有final 不能被子类重写,所以也认为该方法是非虚方法
showFinal();
//虚方法如下
//invokevirtual
showCommon();//没有显式加super,被认为是虚方法,因为子类可能重写showCommon
info();
MethodInterface in = null;
//invokeinterface 不确定接口实现类是哪一个 需要重写
in.methodA();
}
public void info(){}
}
interface MethodInterface {
void methodA();
}
Java:String info = "硅谷";//静态语言
JS:var name = "硅谷“;var name = 10;//动态语言
Python: info = 130;//更加彻底的动态语言
当一个方法开始执行后,只有两种方式可以退出这个方法
1.执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口;
2.在方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜素到匹配的异常处理器,就会导致方法退出,简称异常完成出口
方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码。
一个进程对应一个jvm实例一个运行时数据区,而进程又包含多个线程,这些线程共享了方法区和堆,每个线程各自拥有自己的程序计数器、本地方法栈和虚拟机栈。
1.一个jvm实例只存在一个堆内存,堆也是java内存管理的核心区域。
2.Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间(堆内存的大小是可以调节的)。
3.《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
4.所有的线程共享java堆,在这里还可以划分线程私有的缓冲区(TLAB:Thread Local Allocation Buffer).(面试问题:堆空间一定是所有线程共享的么?不是,TLAB线程在堆中独有的)。
5.《Java虚拟机规范》中对java堆的描述是:几乎所有的对象实例以及数组都应当在运行时分配在堆上。从实际使用的角度看,“几乎”所有的对象的实例都在这里分配内存 (‘几乎’是因为可能存储在栈上,另见逃逸分析)。
6。数组或对象永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
7.在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
8.堆,是GC(Garbage Collection垃圾收集器) 执行垃圾回收的重点区域。
1.Java堆区用于存储java对象实例,堆的大小在jvm启动时就已经设定好了,可以通过 -Xms
和 -Xmx
来进行设置
2.一旦堆区中的内存大小超过 -Xmx所指定的最大内存时,将会抛出OOM异常。
3.通常会将-Xms和-Xmx两个参数配置相同的值,其目的就是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
比如说:
默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。
因此服务器一般设置-Xms、-Xmx相等以避免在每次GC 后调整堆的大小
4.查看设置的堆内存参数:
jps
, 然后jstat -gc
+进程id
。java.lang.OutOfMemoryError: Java heap space。代码示例:
年轻代与老年代
1.存储在JVM中的java对象可以被划分为两类:
2.Java堆区进一步细分可以分为年轻代(YoungGen)和老年代(OldGen)。其中年轻代可以分为Eden空间、Survivor0空间和Survivor1空间(有时也叫frmo区,to区)
3.配置新生代与老年代在堆结构的占比
4.在hotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1(测试的时候是6:1:1,因为存在自适应),开发人员可以通过选项 -XX:SurvivorRatio 调整空间比例,如-XX:SurvivorRatio=8
5.几乎所有的Java对象都是在Eden区被new出来的。
6.绝大部分的Java对象都销毁在新生代了(IBM公司的专门研究表明,新生代80%的对象都是“朝生夕死”的)
7.可以使用选项-Xmn设置新生代最大内存大小(这个参数一般使用默认值就好了)
为新对象分配内存是件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配、在哪里分配的问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。
1.new的对象先放伊甸园区。此区有大小限制。
2.当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区
3.然后将伊甸园中的剩余的幸存对象移动到幸存者0区。
4.如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
5.如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
6.啥时候能去养老区呢?可以设置次数。默认是15次。·可以设置参数:-XX:MaxTenuringThreshold=
进行设置。
7.在养老区,相对悠闲。当老年区内存不足时,再次触发GC:Major GC,进行养老区的内存清理。
8.若养老区执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常。
总结:针对幸存者s0,s1区:复制之后有交换,谁空谁是to。
关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不再永久区/元空间收集。
注意:只有伊甸园满了才会触minorGC/youngGC,而幸存者区满了是绝对不会触发minorGC的。
1.JDK命令行
2.Eclipse:Memory Analyzer Tool
3.Jconsole
4**.VisualVM**
5.Jprofiler
6.Java Flight Recorder
7.GCViewer
8.GC Easy
JVM在进行GC时,并非每次都针对上面三个内存区域(新生代、老年代、方法区)一起回收的,大部分时候回收都是指新生代。
针对hotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)
1.部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:
2.整堆收集(Full GC):收集整个java堆和方法区的垃圾收集。
年轻代GC(Minor GC)触发机制:
当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC.(每次Minor GC会清理年轻代的内存,Survivor是被动GC,不会主动GC)
因为Java队形大多都具备朝生夕灭的特性,所以Minor GC 非常频繁,一般回收速度也比较快,这一定义既清晰又利于理解。
Minor GC 会引发STW(Stop the World),暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行。
老年代GC(Major GC/Full GC)触发机制:
Full GC触发机制:
触发Full GC执行的情况有以下五种
说明:Full GC 是开发或调优中尽量要避免的,这样暂停时间会短一些
为什么要把Java堆分代?不分代就不能正常工作了么
如果对象在Eden出生并经过第一次Minor GC后依然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,把那个将对象年龄设为1.对象在Survivor区中每熬过一次MinorGC,年龄就增加一岁,当它的年龄增加到一定程度(默认15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代中
-XX:MaxTenuringThreshold
来设置针对不同年龄段的对象分配原则如下:
分配60m堆空间,新生代 20m ,Eden 16m, s0 2m, s1 2m,创建一个buffer对象20m,Eden 区无法存放buffer, 直接晋升老年代。
简单解释一下为什么会出现这种情况: 因为给 allocation2 分配内存的时候 eden 区内存几乎已经被分配完了,我们刚刚讲了当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC.GC 期间虚拟机又发现 allocation1 无法存入 Survivor 空间,所以只好通过 **分配担保机制:**把新生代的对象提前转移到老年代中去,老年代上的空间足够存放 allocation1,所以不会出现 Full GC。执行 Minor GC 后,后面分配的对象如果能够存在 eden 区的话,还是会在 eden 区分配内存。可以执行如下代码验证:
尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM明确还是将TLAB作为内存分配的首选。
在程序中,开发人员可以通过选项-XX:UseTLAB
设置是够开启TLAB空间。
默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项 -XX:TLABWasteTargetPercent
设置TLAB空间所占用Eden空间的百分比大小。
一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存
在发生Minor Gc之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。
注意:在JDK6 Update24之后(JDK7),HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,观察openJDK中的源码变化,虽然源码中还定义了HandlePromotionFailure参数,但是在代码中已经不会再使用它。
JDK6 Update24之后的规则变为:只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。
在《深入理解Java虚拟机》中关于Java堆内存有这样一段描述:随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。
在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,==有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。==这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。
此外,前面提到的基于OpenJDK深度定制的TaoBaoVM,其中创新的GCIH(GCinvisible heap)技术实现off-heap,将生命周期较长的Java对象从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。
栗子分析:
没有发生逃逸的对象,则可以分配到栈上:原因如下:
1.随着方法执行的结束,栈空间就被移除。
2.虚拟机栈空间是线程私有的,不会被共享。
-XX:DoEscapeAnalysis
显式开启逃逸分析-XX:+PrintEscapeAnalysis
查看逃逸分析的筛选结果结论:开发中能使用局部变量的,就不要使用在方法外定义
使用逃逸分析,编译器可以对代码做如下优化:
同步代码需要加锁,为不同线程的同步共享。但是要求所加的锁是同一个锁才有意义,上述同步代码块每次的锁对象hollos都是新new出来的,这并不符合加锁的同步效果。可以说是一个错误的代码。但能说明问题!
可以看到,Point这个聚合量经过逃逸分析后,发现他并没有逃逸,就被替换成两个标量了。那么标量替换有什么好处呢?就是可以大大减少堆内存的占用。因为一旦不需要创建对象了,那么就不再需要分配堆内存了。
标量替换为栈上分配提供了很好的基础。
配置参数:-XX:+EliminateAllocations
,开启标量替换。
可以测试对比一下,如果开启标量替换的性能提升,还是很巨大的。
方法区的大小不必是固定的,jvm可以根据应用的需要动态调整。
jdk7及以前(永久代):
PermSize
来设置永久代初始分配空间。默认值是20.75MMaxPermSize
来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82Mjdk8及以后(元空间):
元数据区大小可以使用参数-XX:MetaspaceSize
和-XX :MaxMetaspaceSize
指定,替代上述原有的两个参数。
默认值依赖于平台。windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1, 即没有限制。
与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。 如果元数据区发生溢出,虚拟机一样会拋出异常OutOfMemoryError: Metaspace。
-XX:MetaspaceSize: 设置初始的元空间大小。对于一个64位的服务器端JVM来说, 其默认的-XX :MetaspaceSize值为21MB.这就是初始的高水位线,一旦触及这个水位线,Full GC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。
如果初始化的高水位线设置过低,.上 述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Full GC多次调用。为了避免频繁地GC,建议将- XX :MetaspaceSize设置为一个相对较高的值。
以下代码在JDK8环境下会报 Exception in thread “main” java.lang.OutOfMemoryError: Compressed class space 错误:
《深入理解Java虚拟机》书中对方法区存储内容描述如下:它用于存储已被虚拟机加载的 类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
对每个加载的类型( 类class、接口interface、枚举enum、注解annotation),JVM必 .须在方法区中存储以下类型信息:
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就被分配了。
代码解析
Order.class字节码文件,右键Open in Teminal打开控制台,使用javap -v -p Order.class > tst.txt 将字节码文件反编译并输出为txt文件,可以看到被声明为static final的常量number在编译的时候就被赋值了,这不同于没有被final修饰的static变量count是在类加载的准备阶段才被赋值(还记得clinit吗)。
复习:
1.clinit()即“class or interface initialization method”,注意他并不是指构造器init()
2.此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
3.如果没有静态变量,那么字节码文件就不会有clinit方法。
一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Poo1 Table),包括各种字面量和对类型域和方法的符号引用。
一个 java 源文件中的类、接口,编译后产生一个字节码文件。而 Java 中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池;而这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池.
比如如下代码,虽然只有 194 字节,但是里面却使用了 string、System、Printstream 及 Object 等结构。这里代码量其实已经很小了。如果代码多,引用到的结构会更多!
小结:字节码当中的常量池结构(constant pool),可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型、字面量等信息
首先明确:只有HotSpot才有永久代。 BEA JRockit、IBM J9等来说,是不存在永久代的概念的。原则上如何实现方法区属于虛拟机实现细节,不受《Java虚拟机规范》管束,并不要求统一。
Hotspot中 方法区的变化:
注意:
jdk1.8及之后: 无永久代,类型信息、字段、方法、常量保存在本地内存的元空间。
但字符串常量池、静态变量仍留在堆空间。
除此之外,元空间(或称方法区),不再使用虚拟机内存,而是使用本地内存。
"Exception in thread' dubbo client x.x connector’java.lang.OutOfMemoryError: PermGenspace"
而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。静态变量随着Test的类型信息存放在方法区,但jdk7以后就挪到了堆中
成员变量随着Test的对象实例存放在Java堆中,因为实例变量(也就是成员变量)的生命周期是跟随对象的,属于对象的内容。而对象实例化之后,存放在堆中
局部变量则是存放在foo()方法栈帧的局部变量表中。
误区:
只要是对象的引用都存在于栈中。
记着:
存在栈中的变量是方法中定义的局部变量。
对象中定义的实例变量存储在堆中。
只要是对象实例必然会在Java堆中分配。
接着,找到了一个引用该staticObj对象的地方,是在一个java.lang.Class的实例里,并且给出了这个实例的地址。
通过Inspector查看该对象实例,可以清楚看到这确实是一个 java.lang.Class类型的对象实例,里面有一个名为staticObj的实例字段:
java.lang.Class对象,它们从来都是“普通”Java对象,跟其它Java对象一样存在普通的Java堆(GC堆的一部分)里。
类的元数据(元数据并不是类的Class对象!Class对象是加载的最终产品,类的方法代码,变量名,方法名,访问权限,返回值等等都是在方法区的)才是存在方法区的。
有些人认为方法区(如Hotspot,虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java 虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如 JDK11 时期的ZGC 收集器就不支持类卸载)。
一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前 Sun 公司的 Bug 列表中,曾出现过的若干个严重的 Bug 就是由于低版本的 Hotspot 虚拟机对此区域未完全回收而导致内存泄漏。
方法区的垃圾收集主要回收两部分内容:常量池中废奔的常量和不再使用的类型。
常量池中废弃的常量
常量池中不再使用的类型
判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:
Java虛拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot虚拟机提供了一Xnoclassgc 参数进行控制,还可以使用一verbose:class以及一XX: +TraceClass一Loading、一XX:+TraceClassUnLoading查 看类加载和卸载信息
在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及oSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。
百度
三面:说一下JVM内存模型吧,有哪些区?分别干什么的?
蚂蚁金服:
Java8的内存分代改进
JVM内存分哪几个区,每个区的作用是什么?
一面: JVM内存分布/内存结构?栈和堆的区别?堆的结构?为什么两个survivor区?
二面: Eden和Survior的比例分配
小米:
jvm内存分区,为什么要有新生代和老年代
字节跳动:
二面: Java的内存分区
二面:讲讲jvm运行时数据区
什么时候对象会进入老年代?
京东:
JVM的内存结构,Eden和Survivor比例 。
JVM内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为Eden和Survivor。
天猫:
一面: Jvm内存模型以及分区,需要详细到每个区放什么。
一面: JVM的内存模型,Java8做了什么修改
拼多多:
JVM内存分哪几个区,每个区的作用是什么?
美团:
java内存分配
jvm的永久代中会发生垃圾回收吗?
一面: jvm内存分区,为什么要有新生代和老年代?
1.new:最常见的方式(本质是构造器)
变形1 : Xxx的静态方法
变形2 : XxBuilder/XxoxFactory的静态方法
2.Class的newInstance():反射的方式,只能调用空参的构造器,权限必须是public
3.Constructor的newInstance(Xxx):反射的方式,位于java.lang.reflect.Constructor 可以调用空参、带参的构造器,权限没有要求
4.使用clone() :不调用任何构造器,当前类需要实现Cloneable接口,实现clone()
5.使用反序列化:从文件中、从网络中获取一个对象的二进制流
6.第三方库Objenesis
判断对象对应的类是否加载、链接、初始化
虚拟机遇到一条new指令,首先去检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。( 即判断类元信息是否存在)。
如果没有,那么在双亲委派模式下,使用当前类加载器以ClassLoader+包名+类名为Key进行查找对应的.class文件。如果没有找到文件,则抛出ClassNotFoundException异常,如果找到,则进行类加载,并生成对应的java.lang.Class类对象
为对象分配内存
首先计算对象占用空间大小,接着在堆中划分一块内存给新对象。 如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小。
**说明:**选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
处理并发安全问题
在分配内存空间时,另外一个问题是及时保证new对象时候的线程安全性:创建对象是非常频繁的操作,虚拟机需要解决并发问题。虚拟机采用 了两种方式解决并发问题:
初始化分配到的空间
Java给对象的属性赋值的操作有如下四种:
① 属性的默认初始化
② 显式初始化
③ 代码块中初始化
④ 构造器中初始化
**这里提到的是第①种:初始化分配到的空间指的是①:**所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用。
内存分配结束,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。这一步保证了对象的实例字段在Java代码中可以不用赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值。
设置对象的对象头
将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。
执行init方法进行初始化
在Java程序的视角看来,初始化才正式开始。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。 因此一般来说(由字节码中是否跟随有invokespecial指令所决定),new指令之 后会接着就是执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全创建出来。
对象头:
包含两部分
说明:如果是数组,还需记录数组的长度
实例数据:
说明:它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)
规则:
对齐填充
不是必须的,也没特别含义,仅仅起到占位符作用。
1.直接内存不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
2.直接内存是Java堆外的、直接向系统申请的内存区间。
3.简单理解: java process memory = java heap + native memory
/**
* IO NIO (New IO / Non-Blocking IO)
* byte[] / char[] Buffer
* Stream Channel
*
* 查看直接内存的占用与释放
*/
public class BufferTest {
private static final int BUFFER = 1024 * 1024 * 1024;//1GB
public static void main(String[] args){
//直接分配本地内存空间
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
System.out.println("直接内存分配完毕,请求指示!");
Scanner scanner = new Scanner(System.in);
scanner.next();
System.out.println("直接内存开始释放!");
byteBuffer = null;
System.gc();
scanner.next();
}
}
通过存在堆中的DirectByteBuffer操作Native内存。访问直接内存的速度会优于Java堆。即读写性能高。
同样,也可能导致OutOfMemoryError异常:OutOfMemoryError: Direct buffer memory
由于直接内存在Java堆外,因此它的大小不会直接受限于一Xmx指定的最大 堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。
MaxDirectMemorySize
设置1.执行引擎是Java虚拟机的核心组成部分之一
2.虚拟机是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的。
而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。
3.JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM锁识别的字节码指令、符号表和其他辅助信息
4.那么,如果想让一个Java程序运行起来、==执行引擎的任务就是将字节码指令解释/编译为对应平台上的本地机器指令==才可以。简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者.
上图:
JDK1.0时代,将Java语言定位为“解释执行”还是比较准确的(先编译成字节码,再对字节码逐行解释执行interpreter)。
再后来,Java也发展出可以直接生成本地代码的编译器(这里指的是后端编译器JIT,将源代码直接编译成和本地机器平台相关的机器语言)。
现在JVM在执行Java代码的时候,会将解释执行与编译执行二者结合起来进行。如今Java采用的是解释和编译混合的模式:
执行引擎获取到,由javac将源码编译成字节码文件.class之后,
然后在运行的时候通过解释器interpreter转换成最终的机器码。(解释型)
另外JVM平台支持一种叫作即时编译的技术。即时编译的目的是避免函数被解释执行,而是将整个函数体编译成为机器码,这种方式可以使执行效率大幅度提升(直接编译型)
当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
在HotSpot VM中,解释器主要由Interpreter模块和Code模块构成。
当然是否需要启动 JIT 编译器将字节码直接编译为对应平台的本地机器指令,则需要根据代码被调用执行的频率而定。关于那些需要被编译为本地代码的字节码,也被称之为**“热点代码”**。JIT编译器在运行时会针对那些频繁被调用的“热点代码”做出深度优化,将其直接编译为对应平台的本地机器指令,以此提升Java程序的执行性能。
-XX :CompileThreshold
来人为设定。注意:热频代码片段经过即时编译后的产物–机器指令,需要缓存起来Code Cache,存放在方法区(元空间/本地内存)
-XX:-UseCounterDecay
来关闭热度衰减,让方法计数器统计方法调用的绝对次数,这样,只要系统运行时间足够长,绝大部分方法都会被编译成本地代码。-XX:CounterHalfLifeTime
参数设置半衰周期的时间,单位是秒。它的作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边” (Back Edge)。显然,建立回边计数器统计的目的就是为了触发OSR编译。
有些开发人员会感觉到诧异,既然HotSpotVM中已经内置JIT编译器了,那么为什么还需要再使用解释器来“拖累”程序的执行性能呢?比如JRockit VM内部就不包含解释器,字节码全部都依靠即时编译器编译后执行。
首先明确:
当程序启动后,
所以:
尽管JRockitVM中程序的执行性能会非常高效,但程序在启动时必然需要花费更长的时间来进行编译。对于服务端应用来说,启动时间并非是关注重点,但对于那些看中启动时间的应用场景而言,或许就需要采用解释器与即时编译器并存的架构来换取一一个平衡点。在此模式下,当Java虚拟器启动时,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成后再执行,这样可以省去许多不必要的编译时间。随着时间的推移,编译器发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率。
同时,解释执行在编译器进行激进优化不成立的时候,作为编译器的“逃生门”。
缺省情况下HotSpot VM是采用解释器与即时编译器并存的架构,当然开发人员可以根据具体的应用场景,通过命令显式地为Java虚拟机指定在运行时到底是完全采用解释器执行,还是完全采用即时编译器执行。如下所示:
-Xint
: 完全采用解释器模式执行程序;-Xcomp
: 完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行。-Xmixed
:采用解释器+即时编译器的混合模式共同执行程序。测试表明:
在HotSpot VM中内嵌有两个JIT编译器,分别为Client Compiler和Server Compiler,但大多数情况下我们简称为C1编译器和C2编译器。开发人员可以通过如下命.令显式指定Java虚拟机在运行时到底使用哪一种即时编译器,如下所示:
注意:64位操作系统默认使用-server服务器模式,即C2编译器。
在不同的编译器上有不同的优化策略,C1编译器上主要有方法内联,去虚拟化、冗余消除。
C2的优化主要是在全局层面,逃逸分析是优化的基础。基于逃逸分析在C2.上有如下几种优化:(server模式下才会有这些优化,64位系统默认就是server模式)
C2编译器启动时长比C1编译器慢,系统稳定执行以后,C2编译器执行速度远远快于C1编译器。
程序解释执行(不开启性能监控)可以触发C1编译,将字节码编译成机器码,可以进行简单优化,也可以加上性能监控,C2编译会根据性能监控信息进行激进优化。
不过在Java7版本之后,一旦开发人员在程序中显式指定命令“-server"时,默认将会开启分层编译策略,由C1编译器和C2编译器相互协作共同来执行编译任务。
简称:不可变性。
-XX:StringTableSize
可设置StringTable的长度@Test
public void test1(){
String s1 = "a" + "b" + "c";//编译期优化:等同于"abc"
String s2 = "abc"; //"abc"一定是放在字符串常量池中,将此地址赋给s2
/*
* 最终.java编译成.class,再执行.class
* String s1 = "abc";
* String s2 = "abc"
*/
System.out.println(s1 == s2); //true
System.out.println(s1.equals(s2)); //true
}
@Test
public void test2(){
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";
String s4 = "javaEE" + "hadoop";//编译期优化
//如果拼接符号的前后出现了变量,则相当于在堆空间中new String(),具体的内容为拼接的结果:javaEEhadoop
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
//intern():判断字符串常量池中是否存在javaEEhadoop值,如果存在,则返回常量池中javaEEhadoop的地址;
//如果字符串常量池中不存在javaEEhadoop,则在常量池中加载一份javaEEhadoop,并返回次对象的地址。
String s8 = s6.intern();
System.out.println(s3 == s8);//true
}
@Test
public void test3(){
String s1 = "a";
String s2 = "b";
String s3 = "ab";
/*
如下的s1 + s2 的执行细节:(变量s是我临时定义的)
① StringBuilder s = new StringBuilder();
② s.append("a")
③ s.append("b")
④ s.toString() --> 约等于 new String("ab"),并且“ab”不会在常量池中而是作为普通对象在堆中
补充:在jdk5.0之后使用的是StringBuilder,效率高,但线程不安全
在jdk5.0之前使用的是StringBuffer
*/
String s4 = s1 + s2;//
System.out.println(s3 == s4);//false
}
示例代码3:
如果拼接符号左右两边都是字符串常量或常量引用(final修饰的变量也称为–常量),则仍然使用编译期优化,即非StringBuilder的方式。
/*
1. 字符串拼接操作不一定使用的是StringBuilder!
2.如果拼接符号左右两边都是字符串常量或常量引用,则仍然使用编译期优化,即非StringBuilder的方式。
3. 针对于final修饰类、方法、基本数据类型、引用数据类型的量的结构时,能使用上final的时候建议使用上。
*/
@Test
public void test4(){
final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4);//true
}
//练习:
@Test
public void test5(){
String s1 = "javaEEhadoop";
String s2 = "javaEE";
String s3 = s2 + "hadoop";
System.out.println(s1 == s3);//false
final String s4 = "javaEE";//s4:常量
String s5 = s4 + "hadoop";
System.out.println(s1 == s5);//true
}
append效率要比字符串拼接高很多
@Test
public void test6(){
long start = System.currentTimeMillis();
// method1(100000);//4014
method2(100000);//7
long end = System.currentTimeMillis();
System.out.println("花费的时间为:" + (end - start));
}
public void method1(int highLevel){
String src = "";
for(int i = 0;i < highLevel;i++){
src = src + "a";//每次循环都会创建一个StringBuilder、String
}
// System.out.println(src);
}
public void method2(int highLevel){
//只需要创建一个StringBuilder
StringBuilder src = new StringBuilder();
for (int i = 0; i < highLevel; i++) {
src.append("a");
}
// System.out.println(src);
}
体会执行效率:通过StringBuilder的append()的方式添加字符串的效率要远高于使用String的字符串拼接方式!
详情:
① StringBuilder的append()的方式:自始至终中只创建过一个StringBuilder的对象。
② 使用String的字符串拼接方式:创建过多个StringBuilder和String的对象。,内存中由于创建了较多的StringBuilder和String的对象,内存占用更大;如果进行GC,需要花费额外的时间。
改进的空间:
在实际开发中,如果基本确定要前前后后添加的字符串长度不高于某个限定值highLevel的情况下,建议使用构造器实例化,手动指定StringBuilder容器上限大小。
防止超过默认容量大小16而扩容,扩容会将原有的字符串赋值到新的容器当中,原有的容器变成垃圾需要回收。
StringBuilder s = new StringBuilder(highLevel);//new char[highLevel]
看字节码,就知道是两个。
new String(“a”) + new String(“b”)呢?
对象1:new StringBuilder(),变量拼接“+”操作肯定有StringBuilder
对象2: new String(“a”)
对象3: 常量池中的"a"
对象4: new String(“b”)
对象5: 常量池中的"b"
对象6 :new String(“ab”)
深入剖析: StringBuilder的toString():
如果不是用双引号声明的String对象,可以使用String提供的intern()方法: intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。
比如: String myInfo = new String(“I love u”).intern();
也就是说,如果在任意字符串上调用String. intern方法,那么其返回结果所指向的那个类实例,必定和直接以常量形式出现的字符串实例完全相同(字符串常量池当中的对象)。因此,下列表达式的值必定是true: (“a” + “b” + “c”).intern()== “abc”;
通俗点讲,Interned String就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(String Intern Pool)。
/**
* 如何保证变量s指向的是字符串常量池中的数据呢?
* 有两种方式:
* 方式一: String s = "shkstart";//字面量定义的方式
* 方式二: 调用intern()
* String s = new String("shkstart").intern();
* String s = new StringBuilder("shkstart").toString().intern();
*
*/
public class StringIntern {
public static void main(String[] args) {
String s = new String("1");
s.intern();//调用此方法之前,字符串常量池中已经存在了"1".即此方法可理解为无效
String s2 = "1";
//s 指向堆空间"1"的内存地址//s2 指向字符串常量池已存在的"1"的内存地址
System.out.println(s == s2);//jdk6:false jdk7/8:false
String s3 = new String("1") + new String("1");//s3变量记录的地址为堆中"11",此时字符串常量池当中并不存在11
//在字符串常量池中生成"11"。如何理解:jdk6:创建了一个新的对象"11",也就有新的地址。
// jdk7:此时常量中并没有创建"11",因为底层是StringBuilder 进行两次append("1")之后调用了toString(),我们知道toString()相当于new String(),所以是创建一个指向堆空间中new String("11")的地址
s3.intern();
String s4 = "11";//s4变量记录的地址,使用的是上一行代码执行后在常量池生成的,指向11的地址
System.out.println(s3 == s4);//jdk6:false jdk7/8:true
//jdk6:当常量池没有这个字符串时,调用intern()是直接在常量池创建一个新对象
//jdk7/8:如果常量池没有,调用intern()常量池存的是该字符串对象的引用即在堆中的地址。
}
}
面试题拓展:
将String s4 = “11”;提到s3.intern();之前。
public class StringIntern1 {
public static void main(String[] args) {
//StringIntern.java中练习的拓展:
String s3 = new String("1") + new String("1");//new String("11")
//执行完上一行代码以后,字符串常量池中,是否存在"11"呢?答案:不存在!!
String s4 = "11";//在字符串常量池中直接生成对象"11"
String s5 = s3.intern();//此时s3.intern全程OB视角,没作为。判断字符串常量池存在11,将s5指向字符串常量池
System.out.println(s3 == s4);//false
System.out.println(s5 == s4);//true
}
}
判断s==“ab”,因为是字面量的写法,所以判断的是变量s与字符串常量池当中的"ab"
public class StringExer1 {
public static void main(String[] args) {
//String x = "ab";
String s = new String("a") + new String("b");//new String("ab")
//在上一行代码执行完以后,字符串常量池中并没有"ab"
String s2 = s.intern();//jdk6中:在串池中创建一个字符串"ab"
//jdk8中:串池中没有创建字符串"ab",而是创建一个引用,指向new String("ab"),将此引用返回
System.out.println(s2 == "ab");//jdk6:true jdk8:true
System.out.println(s == "ab");//jdk6:false jdk8:true
}
}
public class StringExer2 {
public static void main(String[] args) {
String s1 = new String("ab");//执行完以后,会在字符串常量池中会生成"ab"。但是指向堆aa
// String s1 = new String("a") + new String("b");执行完以后,不会在字符串常量池中会生成"ab"
s1.intern();//检查字符串常量池当中有ab,有了就全程OB视角,不作为
String s2 = "ab";//指向字符串常量池已经存在的ab
System.out.println(s1 == s2); //false
}
}
大的网站平台,需要内存中存储大量的字符串。比如社交网站,很多人都存储:北京市、海淀区等信息。这时候如果字符串都调用 intern()方法,就会明显降低内存的大小。
示例代码:
/**
* 使用intern()测试执行效率:空间使用上
*
* 结论:对于程序中大量存在存在的字符串,尤其其中存在很多重复字符串时,使用intern()可以节省内存空间。
*
*/
public class StringIntern2 {
static final int MAX_COUNT = 1000 * 10000;
static final String[] arr = new String[MAX_COUNT];
public static void main(String[] args) {
Integer[] data = new Integer[]{1,2,3,4,5,6,7,8,9,10};
long start = System.currentTimeMillis();
for (int i = 0; i < MAX_COUNT; i++) {
// arr[i] = new String(String.valueOf(data[i % data.length]));
arr[i] = new String(String.valueOf(data[i % data.length])).intern();
}
long end = System.currentTimeMillis();
System.out.println("花费的时间为:" + (end - start));
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.gc();
}
}
示例代码:
-XX:+PrintStringTableStatistics
展示字符串常量池信息
/**
* String的垃圾回收:
* -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
*
*/
public class StringGCTest {
public static void main(String[] args) {
// for (int j = 0; j < 100; j++) {
// String.valueOf(j).intern();
// }
//发生垃圾回收行为
for (int j = 0; j < 100000; j++) {
String.valueOf(j).intern();
}
}
}
什么是垃圾( Garbage) 呢?
➢垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
➢外文: An object is considered garbage when it can no longer be reached from any pointer in the runningprogram.
如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出。
垃圾收集,不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和垃圾收集技术的Lisp语言诞生。
垃圾收集机制是Java的招牌能力,极大地提高了开发效率。如今,垃圾收集几乎成为现代语言的标配,即使经过如此长时间的发展,Java的垃圾收集机制仍然在不断的演进中,不同大小的设备、不同特征的应用场景,对垃圾收集提出了新的挑战,这当然也是面试的热点。
MibBridge *pBridge = new cmBaseGroupBridge () ;
//如果注册失败,使用Delete释放该对象所占内存区域
if (pBridge->Register(kDestroy)!= NO_ERROR)
delete pBridge;
MibBridge *pBridge = new cmBaseGroupBridge();
pBridge 一> Register(kDestroy);
解释一下这张图,每个Android(或Java)应用程序都有一个起点(GC Roots),从这个点中实例化对象、调用方法。一些对象直接引用GC Root,这些对象有引用了另一些对象。因此,形成了引用链,就像上图一样。因此垃圾收集器从GC Root开始并遍历直接或间接链接到GC Root的对象。在此过程结束时,脱离GC Root的对象/对象链将被回收。
有了上图,理解内存泄漏的概念就很简单,说白了就是:长生命周期对象A持有了短生命周期的对象B,那么只要A不脱离GC Root的链,那么B对象永远没有可能被回收,因此B就泄漏了。
现在,除了Java以外,C#、Python、 Ruby等语言都使用了自动垃圾回收的思想,也是未来发展趋势。可以说,这种自动化的内存分配和垃圾回收的方式己经成为现代开发语言必备的标准。
蚂蚁金服:
百度:
天猫:
滴滴:
京东:
阿里:
字节跳动:
循环引用导致的内存泄漏情况:
也叫根搜索算法或追踪性垃圾收集
相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高 效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生(但并不能避免)。
相较于引用计数算法,这里的可达性分析就是Java、C#选择的。这种类型的垃圾收集通常也叫作追踪性垃圾收集(Tracing GarbageCollection)。
所谓**"GC Roots"根集合就是一组必须活跃的引用。**
➢可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
➢使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为==引用链==(Reference Chain)
➢如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
GC Roots
在Java语言中,GC Roots对象包括以下几类元素:
1.虚拟机栈中引用的对象:➢比如:各个线程当中被调用的方法中使用到的参数、局部变量等。
2.本地方法栈内JNI(通常说的本地方法)引用的对象
3.方法区中类静态属性引用的对象➢比如:Java类的引用类型静态变量
4.方法区中常量引用的对象:➢比如:字符串常量池(string Table) 里的引用
5.所有被同步锁synchroni zed持有的对象
6.Java虚拟机内部的引用:➢基本数据类型对应的Class对象,一些常驻的异常对象(如: NullPointerException、OutOfMemoryError) ,系统类加载器。
7.反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等.
8.除了这些固定的GCRoots集合以外,根据用户所选用的垃圾收集器以及当 前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。比如:分代收集和局部回收(Partial GC)。➢如果只针对Java堆中的某一块区域进行垃圾回收(比如:典型的只针 对新生代),必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的,这个区域的对象完全有可能被其他区域的对象所引用,这时候就需要一.并将关联的区域对象也加入GC Roots集合中去考虑,才能保证可达性分析的准确性。
9.小技巧:由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root
如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在 一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。
➢即使是号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。这点也是导致GC进行时必须“StopTheWorld"的一个重要原因。
Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法。
finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
应该交给垃圾回收机制调用,永远不要主动调用某个对象的finalize ()方法。
理由包括下面三点:
从功能上来说,finalize()方法与C++ 中的析构函数比较相似,但是Java采用的是基于垃圾回收器的自动内存管理机制,所以finalize()方法在本质,上不同于C++ 中的析构函数。
由于finalize ()方法的存在,虚拟机中的对象一般处于三种可能的状
如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用了。一般来说,此对象需要被回收。但事实上,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段。一个无法触及的对象有可能在某一个条件下“复活”自己,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态。如下:
以上3种状态中,是由于finalize()方法的存在,进行的区分。只有在对象不可触及时才可以被回收。
判定是否可以回收具体过程 判定一个对象objA是否可回收,至少要经历两次标记过程:
过程1.如果对象objA到GC Roots没有引用链,则进行第一 次标记。
过程2.进行筛选,判断此对象是否有必要执行finalize()方法
MAT是Memory Analyzer的简称,它是一 款功能强大的Java堆内存分析器。用于查找内存泄漏以及查看内存消耗情况。
MAT是基于Eclipse开发的,是一款免费的性能分析工具。
可以在http://www.eclipse org/mat/下载并使用MAT。
获取dump文件
方式1: 命令行使用jmap
方式2:使用JVisualVM导出
标记一清除算法(Mark一Sweep)是一种非常基础和常见的垃圾收集算法,该算法被J . McCarthy等人在1960年提出并并应用于Lisp语言。
当堆中的有效内存空间(available memory) 被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
注意:何为清除?
为了解决标记一清除算法在垃圾收集效率方面的缺陷,M.L.Minsky于1963年发表了著名的论文,“ 使用双存储区的Li sp语言垃圾收集器CALISP Garbage Collector Algorithm Using SerialSecondary Storage )”。M.L. Minsky在该论文中描述的算法被人们称为复制(Copying)算法,它也被M. L.Minsky本人成功地引入到了Lisp语言的一个实现版本中。
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
堆中S0和S1使用的就是复制算法。
复制算法优缺点:
没有标记和清除过程,实现简单,运行高效
复制过去以后保证空间的连续性,不会出现“碎片”问题。
此算法的缺点也是很明显的,就是需要两倍的内存空间。
对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。
如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量并不会太大。因此在真正需要垃圾回收的时刻,复制算法的效率是很高的。
又由于对象在垃圾回收过程中统一被复制到新的内存空间中,因此,可确保回收后的内存空间是没有碎片的。该算法的缺点是将系统内存折半。
应用场景:(复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生)
在新生代,对常规应用的垃圾回收,一次通常可以回收70%一 90%的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生。
但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。
因此,基于老年代垃圾回收的特性,需要使用其他的算法。标记一清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以JVM的设计者需要在此基础之上进行改进。标记一压缩(Mark一Compact) 算法由此诞生。1970年前后,G. L. Steele 、C. J. Chene和D.S. Wise 等研究者发布标记一压缩算法。在许多现代的垃圾收集器中,人们都使用了标记一压缩算法或其改进版本。
标记压缩算法执行过程:
标记一压缩算法的最终效果等同于标记一清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记一清除一压缩(Mark一 Sweep一Compact)算法。
二者的本质差异在于标记一清除算法是一种非移动式的回收算法,标记一压.缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策。
可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。
如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
如果内存空间以规整和有序的方式分布,即已用和未用的内存都各自一边,彼此之间维系着一个记录下一次分配起始点的标记指针,当为新对象分配内存时,只需要通过修改指针的偏移量将新对象分配在第一个空闲内存位置上,这种分配方式就叫做指针碰撞(Bump the Pointer) 。
难道就没有一种最优的算法么?没有最好的算法,只有更合适的算法。
前面所有这些算法中,并没有一种算法可以完全替代其他算法,它们都具有自己独特的优势和特点。分代收集算法应运而生。
分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接, 这类对象跟业务直接挂钩,因此生命周期比较长。
但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如: String对象, 由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。
目前几乎所有的GC都是采用分代收集(Generational Collecting) 算法执行垃圾回收的。在HotSpot中,基于分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代各自的特点。
年轻代(Young Gen)
老年代(Tenured Gen)
以HotSpot中的CMS回收器为例,CMS是基于Mark一 Sweep实现的,对于对象的回收效率很高。而对于碎片问题,CMS采用基于Mark一Compact算法的Serial 0ld回收器作为补偿措施:当内存回收不佳(碎片导致的Concurrent Mode Failure时),将采用Serial 0ld执行Full GC以达到对老年代内存的整理。
分代的思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年代。
上述现有的算法,在垃圾回收过程中,应用软件将处于一种stop the World的状态。在Stop the World状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。为了解决这个问题,即对实时垃圾收集算法的研究直接导致了增量收集(Incremental Collecting) 算法的诞生。
增量收集算法基本思想
如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
总的来说,增量收集算法的基础仍是传统的标记一清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作。
增量收集算法优缺点:
使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。
但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC产生的停顿也越长。
为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
分代算法将按照对象的生命周期长短划分成两个部分,而分区算法将整个堆空间划分成连续的不同小区间。
每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。
在默认情况下,通过System.gc()或者Runtime.getRuntime().gc()的调用,会显式触发Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。
JVM实现者可以通过system.gc()调用来决定JVM的GC行为。而一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。
然而System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用(无法保证马上触发GC)。
在一些特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用System.gc()。以下示例代码:
System.runFinalization();//强制调用 失去引用的对象的finalize()方法
如果注掉System.runFinalization(); 那么控制台不保证一定打印,
从而证明了System.gc()无法保证GC一定执行.
注意,这里的存储空间并不是指物理内存,而是指Java层面JVM虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。
Forgotten Reference:由于某种原因没有/忘记断开的引用,导致内存泄漏。
举个内存泄露的栗子:
一个单例对象引用了一个对象,但这个对象我只用一次之后就不用了,而由于和这个单例对象还存在一个引用链,导致GC没办法回收它,这造成了内存泄漏。
并发和并行,在谈论垃圾收集器的上下文语境中,它们可以解释如下:
并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
串行(Serial):
并发(Concurrent) :指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行。
Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint
但是,程序“不执行”的时候呢?例如线程处于Sleep 状态或Blocked状态,这时候线程无法响应JVM的中断请求,“走” 到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况,就需要安全区域(Safe Region)来解决。
安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。我们也可以把Safe Region 看做是被扩展了的Safepoint。
我们希望能描述这样一类对象: 当内存空间还足够时,则能保留在内存中;如果内存空间在进行垃圾收集后还是很紧张,则可以抛弃这些对象。 -【既偏门又非常高频的面试题】强引用、软引用、弱引用、虚引用有什么区别?具体使用.场景是什么?
在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference) 、弱引用(Weak Reference) 和虚引用(Phantom Reference) 4种,这4种引用强度依次逐渐减弱。
除强引用外,其他3种引用均可以在java.lang.ref包中找到它们的身影。如下图,显示了这3种引用类型对应的类,开发人员可以在应用程序中直接使用它们。
对于强、软、弱、虚这四种引用对象的垃圾回收特点的描述,都是指的在引用关系还存在的情况下:
在Java程序中,最常见的引用类型是强引用(普通系统99%以上都是强引用),也就是我们最常见的普通对象引用,也是默认的引用类型。
当在Java语言中使用new操作符创建一个新的对象, 并将其赋值给一个变量的时候,这个变量就成为指向该对象的一个强引用。
强引用的对象是可触及的(可达的),垃圾收集器就永远不会回收掉被引用的对象。
对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为null,才可以当做垃圾被收集,当然具体回收时机还是要看垃圾收集策略。
相对的,软引用、 弱引用和虚引用的对象是软可触及、弱可触及和虛可触及的,在一定条件下,都是可以被回收的。
所以,强引用是造成Java内存泄漏的主要原因之一。
示例代码:
StringBuffer str = new StringBuffer ("Hello,尚硅谷");
局部变量str指向StringBuffer实例所在堆空间,通过str可以操作该实例,那么str就是StringBuffer实例的强引用。
对应内存结构:
此时,如果再运行一个赋值语句:StringBuffer str1 = str;
对应内存结构:
本例中的两个引用,都是强引用。
强引用具备以下特点:
Object obj = new object(); //声明强引用
SoftReference<0bject> sf = new SoftReference<0bject>(obj);//创建软引用
obj = null; //然后要销毁强引用
Object obj = new object(); //声明强引用
WeakReference<0bject> sf = new WeakReference<0bject>(obj);//创建弱引用
obj = null; //销毁强引用
面试题:你开发中使用过WeakHashMap吗?
private static class Entry extends WeakReference
object obj = new object();//声明一个强引用
ReferenceQueue phantomQueue = new ReferenceQueue( ) ;//声明一个引用队列
PhantomReference<object> pf = new PhantomReference<object>(obj, phantomQueue); //创建一个虚引用
obj = null;//销毁强引用
垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的 JVM 来实现。
由于JDK的版本处于高速迭代过程中,因此Java发展至今已经衍生了众多的GC版本。
从不同角度分析垃圾收集器,可以将GC分为不同的类型。
可以分为串行垃圾回收器和并行垃圾回收器。
串行回收指的是在同一时间段内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。
可以分为并发式垃圾回收器和独占式垃圾回收器。
可分为压缩式垃圾回收器和非压缩式垃圾回收器
又可分为年轻代垃圾回收器和老年代垃圾回收器
有了虚拟机,就一定需要收集垃圾的机制,这就是Garbage Collection, 对应的产品我们称为Garbage Collector.
垃圾收集器的组合关系
两个收集器间有连线,表明它们可以搭配使用: Serial/Serial Old、Serial/CMS、 ParNew/Serial Old、ParNew/CMS、 Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;
其中Serial Old作为CMS 出现"Concurrent Mode Failure"失败的后备预案。
(红色虚线)由于维护和兼容性测试的成本,在JDK 8时将Serial+CMS、 ParNew+Serial Old这两个组合声明为废弃(JEP 173) ,并在JDK 9中完全取消了这些组合的支持(JEP214),即:移除。
(绿色虚线)JDK 14中:弃用Parallel Scavenge和SerialOld GC组合(JEP366 )
(青色虚线)JDK 14中:删除CMS垃圾回收器 (JEP 363)
串行回收器:Serial & Serial Old
并行回收器:(ParNew | Parallel Scavenge ) & Parallel Old
并发回收器:CMS ; G1(分区算法)
为什么要有很多收集器个不够吗? 因为Java的使用场景很多, 移动端,服务器等。所以就需要针对不同的场景,提供不同的垃圾收集器,提高垃圾收集的性能。
虽然我们会对各个收集器进行比较,但并非为了挑选一个最好的收集器出来。没有一种放之四海皆准、任何场景下都适用的完美收集器存在,更加没有万能的收集器。所以我们选择的只是对具体应用最合适的收集器。
方法1:-xx:+PrintCommandLineFlags
: 查看命令行相关参数(包含使用的垃圾收集器)
方法2:使用命令行指令:jinfo -flag
相关垃圾回收器参数进程ID
/**
* -XX:+PrintCommandLineFlags
*
* -XX:+UseSerialGC:表明新生代使用Serial GC ,同时老年代使用Serial Old GC
*
* -XX:+UseParNewGC:标明新生代使用ParNew GC
*
* -XX:+UseParallelGC:表明新生代使用Parallel GC
* -XX:+UseParallelOldGC : 表明老年代使用 Parallel Old GC
* 说明:二者可以相互激活
* -XX:+UseConcMarkSweepGC:表明老年代使用CMS GC。同时,年轻代会触发对ParNew 的使用
*/
public class GCUseTest {
public static void main(String[] args) {
ArrayList<byte[]> list = new ArrayList<>();
while(true){
byte[] arr = new byte[100];
list.add(arr);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
打印输出:
-XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
命令行方式:
jdk8 使用的是parallel
jdk9 使用的是G1
除了年轻代之外,Serial收集器还提供用于执行老年代垃圾收集的Serial Old收集器。
Serial Old收集器同样也采用了串行回收 和=="Stop the World"机制==。
只不过内存回收算法使用的是标记-压缩 算法。
➢Serial Old是运行在Client模式下默认的老年代的垃圾回收器
➢Serial Old在Server模式下主要有两个用途:①与新生代的ParallelScavenge配合使用; ②作为老年代CMS收集器的后备垃圾收集方案。
-XX: +UseSerialGC
参数可以指定年轻代和老年代都使用串行收集器。
-XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseSerialGC
对于新生代,回收次数频繁,使用并行方式高效。
对于老年代,回收次数少,使用串行方式节省资源。(CPU并行 需要切换线程,串行可以省去切换线程的资源)
由于ParNew收集器是基于并行回收,那么是否可以断定ParNew收集器的回收效率在任何场景下都会比Serial收集器更高效?
除Serial Old外,目前ParNew GC还可以与CMS收集器配合工作
在程序中,开发人员可以通过选项-XX: +UseParNewGC
手动指定使用.ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代。
-XX:ParallelGCThreads
限制线程数量,默认开启和CPU数据相同的线程数。
HotSpot的年轻代中除了拥有ParNew收集器是基于并行回收的以外, **Parallel Scavenge收集器同样也采用了复制算法、并行回收和"Stop the World"机制。**那么Parallel收集器的出现是否多此一举?
高吞吐量则可以高效率地利用CPU 时间,尽快完成程序的运算任务,主 要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。
Parallel收集器在JDK1.6时提供了用于执行老年代垃圾收集的 Parallel Old收集器,用来代替老年代的Serial Old收集器。
Parallel Old收集器采用了标记一压缩算法,但同样也是基于并行回收和==”Stop一the一World"机制==。
在程序吞吐量优先的应用场景中,Parallel 收集器和Parallel Old收集器的组合,在Server模式下的内存回收性能很不错。
在Java8中,默认是此垃圾收集器
在JDK1.5时期, HotSpot推出了一款在强交互应用中几乎可认为有划 时代意义的垃圾收集器: CMS (Concurrent-Mark-Sweep)收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
**CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。**停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段。
1.由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。
2.尽管CMS收集器采用的是并发回收(非独占式),但是在其初始化标记和重新标记这两个阶段中仍然需要执行“Stop一the一World”机制暂停程序中的工作线程,不过暂停时间并不会太长。
3.因此可以说明目前所有的垃圾收集器都做不到完全不需要“Stop一the一World”,只是尽可能地缩短暂停时间。
4.另外,由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial 0ld收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
5.CMS收集器的垃圾收集算法采用的是标记一清除算法,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免地将会产生一些内存碎片。
那么CMS在为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer) 技术,而只能够选择空闲列表(Free List) 执行内存分配。
有人会觉得既然Mark Sweep会造成内存碎片,那么为什么不把算法换成Mark Compact呢?
答案其实很简答,因为当并发清除的时候,用Compact整理内存的话,原来的用户线程使用的内存还怎么用呢?要保证用户线程能继续执行,前提的它运行的资源不受影响嘛。Mark Compact更适合“Stop the World”这种场景”下使用
有人会觉得既然重新标记阶段就包含垃圾修正了,那么为什么还会出现浮动垃圾呢?
重新标记是对已标记的对象进行检测:因为可能之前的垃圾对象又被其他对象引用,就需要重新标记(防止漏标),防止被误以为垃圾被清除掉。
而浮动垃圾一开始并没有被标记,是在并发标记阶段之后可能变成垃圾的对象,指产生的新垃圾。
-XX:+UseConcMarkSweepGC
手动指定使用CMS收集器执行内存回收任务。
-XX:CMS1nitiatingOccupanyFraction
设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。
-XX: +UseCMSCompactAtFullCollection
用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了。-XX:CMSFullGCsBeforeCompaction
设置在执行多少次Full GC后对内存空间进行压缩整理。-XX:ParallelCMSThreads
设置CMS的线程数量。
HotSpot有这么多的垃圾回收器,那么如果有人问,Serial GC、 Parallel GC、Concurrent Mark Sweep GC这三个GC有什么不同呢?
请记住以下口令:
如果你想要最小化地使用内存和并行开销,请选Serial GC;
如果你想要最大化应用程序的吞吐量,请选Parallel GC;
如果你想要最小化GC的中断或停顿时间,请选CMS GC。
原因就在于应用程序所应对的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序正常进行,而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化。G1 (Garbage一First) 垃圾回收器是在Java7 update4之后引入的一个新的垃圾回收器,是当今收集器技术发展的最前沿成果之一。
与此同时,为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time) ,同时兼顾良好的吞吐量。
官方给G1设定的目标是在延迟可控(低暂停)的情况下获得尽可能高的吞吐量(高吞吐),所以才担当起“全功能收集器”的重任与期望。
-XX: +UseG1GC
来启用。与其他GC收集器相比,G1使用了全新的==分区算法==,其特点如下所示:四个特点:
3.空间整合
4.可预测的停顿时间模型(即:软实时soft real一time): 这是G1相对于CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
-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。G1的设计原则就是简化JVM性能调优,开发人员只需要简单的三步即可完成调优:
G1中提供了三种垃圾回收模式: YoungGC、 Mixed GC和Full GC, 在不同的条件下被触发。(后文详细讲)
使用G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂,即1MB, 2MB, 4MB, 8MB, 1 6MB, 32MB。
可以通过 -XX:G1HeapRegionSize
设定。所有的Region大小相同,且在JVM生命周期内不会被改变。
虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region (不需要连续)的集合。通过Region的动态分配方式实现逻辑_上的连续。
G1 GC的垃圾回收过程主要包括如下三个环节:
顺时针, young gc 一> young gc + concurrent mark 一> Mixed GC顺序,进行垃圾回收。
应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程; G1的年轻代收集阶段是一个并行的(多个回收线程)独占式(STW)收集器。在年轻代回收期,G1 GC暂停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到Survivor区间或者老年区间,也有可能是两个区间都会涉及。
当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程。
标记完成马上开始混合回收过程。对于一个混合回收期,G1 GC从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。和年轻代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的Region就可以了。同时,这个老年代Region是和年轻代一起被回收的。
举个例子:一个web服务器,Java进程最大堆内存为4G,每分钟响应1500个请求,每45秒钟会新分配大约2G的内存。G1会每45秒钟进行一次年轻代回收,每31 个小时整个堆的使用率会达到45%,会开始老年代并发标记过程,标记完成后开始四到五次的混合回收。
·解决方法:
然后开始如下回收过程:
处理dirty card queue( 脏卡表)中的card,更新RSet。 此阶段完成后,RSet可 以准确的反映老年代对所在的内存分段中对象的引用(指的新生代)。
第三阶段,处理RSet。
识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象。
第四阶段,复制对象。
此阶段,对象树被遍历,Eden区 内存段中存活的对象会被复制到Survivor区中空的内存分段,Survivor区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到0ld区中空的内存分段。如果Survivor空间不够,Eden空间的 部分数据会直接晋升到老年代空间。
第五阶段,处理引用。
处理Soft,Weak, Phantom, Final, JNI Weak等引用。最终Eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。
当越来越多的对象晋升到老年代Oldregion时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即Mixed GC, 该算法并不是一个OldGC,除了回收整个Young Region,还会回收一部分的0ldRegion。这里需要注意:是一部分老年代, 而不是全部老年代。可以选择哪些OldRegion进行收集,从而可以对垃圾回收的耗时时间进行控制。也要注意的是Mixed GC并不是Full GC。
G1的初衷就是要避免Full GC的出现。但是如果上述方式不能正常工作,G1会停止应用程序的执行(Stop一 The一World),使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。
要避免Full GC的发生,一旦发生需要进行调整。什么时候会发生Full GC呢?比如堆内存太小,当G1在复制存活对象的时候没有空的内存分段可用,则会回退到full gc, 这种情况可以通过增大内存解决。
导致G1Full GC的原因可能有两个:
补充:
从Oracle官方透露出来的信息可获知,回收阶段(Evacuation)其实.本也有想过设计成与用户程序一起并发执行,但这件事情做起来比较复杂,考虑到G1只是回收一部分Region, 停顿时间是用户可控制的,所以并不迫切去实现,而选择把这个特性放到了G1之后出现的低延迟垃圾收集器(即ZGC)中。另外,还考虑到G1不是仅仅面向低延迟,停顿用户线程能够最大幅度提高垃圾收集效率,为了保证吞吐量所以才选择了完全暂停用户线程的实现方案。
截止JDK 1.8,一共有7款不同的垃圾收集器。每一款不同的垃圾收集器都有不同的特点,在具体使用的时候,需要根据具体的情况选用不同的垃圾收集器。
通过阅读GC日志,我们可以了解Java虛拟机内存分配与回收策略。内存分配与垃圾回收的参数列表
-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. log日志文件的输出路径