JVM 学习笔记

1、JVM的生命周期
① 虚拟机的启动
java虚拟机的启动是通过引导类加载器(Bootstrap class loader) 创建一个初始化类(initial class)来完成的,这个类是由虚拟机的具体实现指定的。

②虚拟机的执行
1、一个运行中的java虚拟机有着一个清晰的任务:执行java程序。

2、程序开始执行时他才运行,程序结束时它才停止。

3、执行一个所谓的java程序的时候,真真正正在执行的是一个叫做java虚拟机的进程。

③ 虚拟机的退出
有如下几种情况:

1、程序正常执行结束。

2、程序在执行过程中遇到了异常或错误而异常终止。

3、由于操作系统出现错误而导致java虚拟机进程终止。

4、某线程调用Runtime 类 或 System 类的exit 方法,或Runtime 类的halt方法,并且java安全管理器也允许这次exit 或 halt操作。

2、 类加载器子系统作用

① 类加载器的作用
1、类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。

2、ClassLoader 只负责class文件的加载,至于它是否可以运行,则由Execution Engline 决定。

3、 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class 文件中常量池部分的内存映射)。

②类加载器ClassLoader角色

1、class file 存在于本地磁盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候要加载到jvm 当中来 根据这个文件实例化出n 个一模一样的实例。

2、class file 加载到JVM 中,被称为DNA源数据模板,放在方法区。

3、在.class 文件 -->JVM --> 最终称为元数据模板,此过程就要一个运输工具(类加载器 Class Loader),扮演一个快递员的角色。

③ 加载的过程

加载:

1、通过一个类的权限定名获取定义此类的二进制字节流。

2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3、在内存中生成一个代表这个类的java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

④链接的过程
链接主要包含3个部分: 验证、准备、解析。

1、验证(Verification):

		目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性不会危害虚拟机自身安全。

		主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证。

2、 准备(Prepare):

		为类变量分配内存并且设置该类变量的默认初始值,即 0 值。

		这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化。

		这里不会为实例变量分配初始化,类变量会分配在方法区,而实例变量是会随着对象一起分配到java堆中。

3、 解析(Resolve):

		将常量池内的符号引用转换为直接引用的过程。

		事实上,解析操作往往会伴随着JVM 在执行完初始化之后再执行。

		符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中,直接引用就是直接指向目标的指针,相对偏移量或一个间接定位到目标的句柄。

		解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info 等。

⑤初始化
1、初始化阶段就是执行类构造器方法() 的过程。

2、此方法不需定义,是javac 编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来的。

3、构造器方法中指令按语句在源文件中出现的顺序执行。

4、<clinit>() 不同于类的构造器。(关联:构造器是虚拟机视角下的<init>()).

5、若该类具有父类,JVM 会保证子类的<clinit>() 执行前,父类的<clinit>() 已经执行完毕。

6、 虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁。

⑥ 类加载器的分类
JVM 支持两种分类的类加载器,分别为引导类加载器(Bootstrap ClassLoader) 和自定义类加载器(User-Defined ClassLoader)。

从概念上来说,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java 虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader 的类加载器都划分为自定义类加载器。

无论类加载器的类型如何划分,在程序中 我们最常见的类加载器始终只有3个:



1、启动类加载器(引导类加载器,Bootstrap ClassLoader)

	这个类加载使用C/C++ 语言实现的,嵌套在jvm内部。

	它用来加载Java 的核心库(JAVA_HOME/jre/lib/rt.jar、resource.jar 或 sun.boot.class.path 路径下的内容),用于提供JVM自身需要的类。、

	并不继承自java.lang.ClassLoader,没有父加载器。

	加载扩展类和应用程序类加载器,并指定为他们的父类加载器。

	处于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类。

2、扩展类加载器(Extension ClassLoader)

	Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。

	派生于ClassLoader类。

	父类加载器为启动类加载器,

	从java.ext.dirs 系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext 子目录(扩展目录)下加载类库,如果用户创建的 JAR 放在此目录下,也会自动由扩展类加载器加载的。

3、应用程序类加载器(系统类加载器,APPClassLoader)

	java 语言编写,由sun.misc.Launcher$AppClassLoader 实现

	派生于ClassLoader类

	父类加载器为扩展类加载器

	它负责加载环境变量classpath 或系统属性 java.class.path 指定路径下的类库。

	该类加载时程序中默认的类加载器,一般来说,java应用的类都是由它来完成加载的。

	通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。

public class ClassLoaderTest {

public static void main(String[] args) {
System.out.println(“启动类加载器");
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL urL : urLs) {
System.out.println(urL.toExternalForm());
}


System.out.println("扩展类加载器
*”);
String extDirs = System.getProperty(“java.ext.dirs”);
for (String path : extDirs.split(";")) {
System.out.println(path);
}


}
}

输出:
启动类加载器
file:/C:/Program%20Files/Java/jdk1.8.0_171/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_171/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_171/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_171/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_171/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_171/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_171/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_171/jre/classes
扩展类加载器
*
C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext
C:\windows\Sun\Java\lib\ext
3、双亲委派机制
Java 虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,java虚拟机采用的是双亲委派模型,即把请求交由父类处理,它是一种任务委派模式。

工作原理

1、如果一个类加载器收到了类加载请求,它并不会先去加载,而是把这个请求委托给父类的加载器去执行;

2、如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。

3、如果父类加载器可以完成加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试直接去加载,这就是双亲委派模型。

package lang;

/** 我们先在 java.lang 包目录下键一个 String类

  • @author HouYC

  • @create 2020-07-18-14:14
    /
    public class String {

    static {
    System.out.println(“如果我被加载,我会在 扩展类加载器被加载!!”);
    }
    }


    /
    *

  • @author HouYC

  • @create 2020-07-18-14:13
    */
    public class StringTest {

    public static void main(String[] args) {
    String string = new String();
    System.out.println(“我执行啦!!!”);
    }
    }

    //输出结果:
    我执行啦!!!
    Process finished with exit code 0


    我们在main方法中new 一个String对象,如果他实例化我们创建的String,应该将static静态代码块里面信息输出来到。很显然是没有输出来的,因为我们实例化这个String对象,他需要加载这个String类,所以他会先通过去系统类加载器中去加载,发现他还有父级加载器,就由交由扩展类加载器去加载,发现扩展类加载器由有父类加载器,就交由 引导类加载器去加载, 最后发现自己可以加载这个类,所以就加载了jre/lib/lang 下的,没有用到我们自己定义的。

    显然,如果引导类加载器无法加载,就向下委派,–> 扩展类加载器 —> 系统类加载器

    双亲委派机制可以理解为:

    1、假设有一个苹果,这个小孩拿着一个苹果准备吃的时候,发现妈妈在旁边就先给他妈妈吃。

    2、小孩的妈妈接过苹果,发现小孩的奶奶也在旁边,就给小孩的奶奶吃。

    3、 小孩奶奶看到苹果,感觉还不错,就直接吃了。

还有一种可能是:

1、假设有一个苹果,这个小孩拿着一个苹果准备吃的时候,发现妈妈在旁边就先给他妈妈吃。

2、小孩的妈妈接过苹果,发现小孩的奶奶也在旁边,就给小孩的奶奶吃。

3、 小孩奶奶看到苹果,拿到苹果感觉有点硬,牙不好,就不吃,于是给了小孩的妈妈。

4、 小孩妈妈拿到苹果,就吃了。

优点
1、避免类的重复加载。

2、保护程序安全,防止核心API被随意篡改。

		比如自定义类:java.lang.String

沙箱安全机制
自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk 自带的文件(rt.jar 包中java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar 包中的String类,这样可以保证对java核心源代码的保护,这就是沙箱安全机制。

对类加载器的引用
JVM 必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM 会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM 需要保证这两个类型的类加载器是相同的。

类的主动使用和被动使用
java程序对类的使用方式分为:主动使用和被动使用.

主动使用,又分为七种情况:

	1、创建类的实例。

	2、访问某个类或接口的静态变量,或者对该静态变量赋值。

	3、调用类的静态方法。

	4、反射(比如:Class.forName("cn.yachao.Test"))。

	5、初始化一个类的子类。

	6、 Java虚拟机启动时被表明为启动类的类。

	7、JDK7 开始提供动态语言支持:java.lang.invoke.MethodHandle 实例的解析结果。

除了以上七种情况,其他使用java类的方式都被看做是对类的被动调用,都不会导致类的初始化。

4、运行时数据区

Java 虚拟机定义了若干程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而启动,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。

灰色的为单线程私有的,红色的为多线程共享的,即:

	1、每个线程:独立包含程序计数器、栈、本地栈。

	2、线程间共享:堆、堆外内存(永久代或元空间、代码缓存)。

① PC Reqister寄存器 (程序计数器)
JVM 中的程序计数寄存器(Program Counter Register)中,Register 的命名源于CPU 的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。

这里,并非是广义所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。

② PC寄存器的作用

作用: PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码,由执行引擎读取下一条指令。

它是一块很小的内存空间,几乎可以忽略不计,也是运行速度最快的存储区域。

在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。

任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会储存当前线程正在执行的Java方法的JVM指令地址。或者,如果是在执行native方法,则是未指定值(undefined)。

它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。

字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

它是唯一一个在Java 虚拟机规范中没有规定任何OutOtMemoryError情况的区域。

③PC寄存器举例

④PC寄存器有关的面试题
1、使用PC寄存器存储字节码指令地址有什么用呢?或者说,为什么使用PC寄存器记录当前线程的执行地址呢?

因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。

JVM 的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。

2、PC寄存器为什么会被设定为线程私有呢?

我们都知道所谓的多线程在一个特定的时间段内只会执行其中一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或回复,如果保证分毫不差呢?  为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。

由于CPU时间片轮限制,众多线程再并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。

这样必然导致经常中断或恢复,如何保证分毫不差呢?  每个线程在创建后,都会产生自己的程序计数器和栈贞,程序计数器在各个线程之间互不影响、

CPU 时间片即 CPU 分配给各个程序的时间,每个程序被分配了一个时间段,称作它的时间片。

在宏观上:我们可以同时打开多个应用程序,每个程序并行不饽,同时运行。

但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。

并行:有几个线程一起被执行。

串行:有个线程排成一队,这个时间片 只能有一个线程被执行。

并发:有个线程被交替执行。

⑤ 栈
1、栈的介绍
栈是运行时的单位,而堆是存储的单位。

即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放,放在哪里。

JVM虚拟机是什么?

java虚拟机栈(java Virtual Machine Stack),早期也叫java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈贞(Stack Frame),对应着一次次的java方法调用。

栈的生命周期?

生命周期和线程一致。

作用:

主管java程序的运行,它保存方法的局部变量(8种基本数据类型,对象的引用地址),部分结果,并参与方法的调用和返回。

局部变量VS成员变量(或属性)

基本数据变量 VS 引用类型变量(类,数组,接口)

栈的特点(优点):

栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。

JVM 直接对Java栈的操作只有两个:

	每个方法执行,伴随着进栈(入栈,压栈)。

	执行结束后的出栈工作。

对于栈来说不存在垃圾回收问题。但是会出现OOM,如果超出栈的值,就挂了 

设置栈内存大小: 我们可以使用参数-Xss 选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。

2、栈的存储单位
① 栈中存储什么?

每个线程都有自己的栈,栈中的数据都是以栈贞(stack Frame)的格式存在。

在这个线程上执行的每个方法都各自对应一个栈贞(stack Frame)。

栈贞是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

②栈运行原理

JVM 直接对java栈的操作只有两个,就是对栈贞的压栈和出栈,遵循“先进后出” \ “先进先出”原则。

在一条活动线程中,一个时间点上,只会有一个活动的栈贞。即只有当前正在执行的方法的栈贞(栈顶栈贞)是有效的,这个栈贞被称为当前栈贞(Current Frame),与当前栈贞相对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)。

执行引擎运行的所有字节码指令只针对当前栈贞进行操作。

如果在该方法中调用了其他方法,对应的新的栈贞会被创建出来,放在栈的顶端,成为新的当贞。



不同线程中所包含的栈贞是不允许存在相互引用的,即不可能在一个栈贞之中引用另外一个线程的栈贞。

如果当前方法调用了其他方法,方法返回之际,当前栈贞会传回此方法的执行结果,给前一个栈贞,接着,虚拟机会丢弃当前栈贞,使得前一个栈贞重新成为当前栈贞。

java方法有两种返回函数的方式,一种是正常的函数返回,使得return 指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈贞被弹出。

3、栈贞的内部结构

每个栈贞中存储着:

①局部变量表(Local Variable)

②操作数栈(Operand Stack)(或表达式栈)

③动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)

④方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)

⑤一些附加信息

3.1、 栈贞里面局部变量表
1、局部变量表也被称为局部变量数组或本地变量表。

2、定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型,对象引用(reference),以及returnAddress类型。

3、由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。

4、局部变量表所需要的容量大小是编译期确定下来的,并保存在方法的Code属性的maximum local variables 数据项中。在方法运行期间是不会改变局部变量表的大小的。

5、方法嵌套调用的次数由栈大小决定。 一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,他的参数和局部变量越多,使得局部变量表膨胀,它的栈贞就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。

6、局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈贞的销毁,局部变量表也会随之销毁。

3.2 关于slot的理解
7、参数值的存放总是在局部变量数组的index0 开始,到数组长度-1 的索引结束。

8、局部变量表,最基本的存储单元是Slot(变量槽)。

9、局部变量表中存放编译器可知的各种基本数据类型(8种)。引用类型(reference),returnAddress类型。

10、在局部变量表里,32位以内的类型只占用一个slot(包含returnAddress类型),64位的类型(long 和 double)占用两个slot。 		byte, short , char 在存储前辈转换为int,boolean 也被转为int,0-表示false,非0 表示true。  long 和double 则占据两个slot。

11、JVM 会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。

12、当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个Slot上。

13、如果需要访问局部变量表中一个64Bit 的局部变量值时,只需要使用前一个索引即可。(比如:访问long或double类型变量)

14、如果当前帧是由构造方法或实例方法创建的,那么该对象引用this 将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。

为什么在静态方法中不能使用this, 因为this变量不存在于当前方法的局部变量表中。

15、slot的重复利用,栈贞中的局部变量表中的槽位是可以重复的,如果一个局部变量过了其作用域,那么在其作用域之后申请的新的局部变量就很有可能会重用过期的局部变量的槽位,从而达到节省资源的目的。

code length: 是这个方法编译成字节码总行数。

Maximum Local variables : 这个方法 有多少个变量。

Start PC: 是字节码所对应的行数。

Line Number: 是所对应的代码的行数。

Length: 所占的长度。

index:为变量开辟空间的索引 (即代码变量的顺序)。

name: 变量的名字。

因为方法为静态方法,所以在局部变量表中 没有对应的this 生成,即不能在静态(static修饰的方法)方法中所使用this。

举例:静态变量与局部变量的对比

①参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配。

②我们知道类变量表有两次初始化的机会,第一次是在 “准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在 “初始化”阶段,赋予程序员在代码中定义的初始化值。

③和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义局部变量则必须人为的初始化,否则无法使用。

在栈贞中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。

局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。即root能直达的 都不会被回收。

3.3、操作数栈
①每一个独立的栈贞中除了包含局部变量表以外,还包含一个后进先出 的操作数栈,也可以称之为 表达式栈。

②操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/ 出栈(pop)。

	某些字节码指令将值压入操作数栈,其余的字节码指令将从操作数取出栈。使用它们后再把结果压入栈。

	比如:执行复制,交换,求和等操作。

③如果被调用的方法带有返回值的话,其返回值将会被压入当前栈贞的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。

④操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析段要再次验证。

⑤另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。

⑥操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。

⑦操作数栈就是JVM 执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈贞也会随之被创建出来,这个方法的操作数栈是空的。

⑧每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_stack 的值。

⑨栈中的任何一个元素都是可以任意的java数据类型。

		32bit 的类型占用一个栈单位深度。

		64bit 的类型占用两个栈单位深度。

⑩操作数栈并非采用访问索引的方式来进行数据访问的,而是只是通过标准的入栈(push)和出栈(pop)操作来完成一次数据访问。

i++ 和 ++i 的区别?

根据生成的字节码分析:

1、先将常量10 压入操作栈中。

2、并保存到局部变量表中的istore_0 中。

3、(先将iload-0加载到栈顶)把局部变量表的istore_0 元素(int i3)压入操作栈顶,因为只有压入栈顶,才能进行操作。

4、iinc 对数值进行 + 1,

5、对 + 1 的结果 赋给 istore_1中,并保存在局部变量表中。

6、 将常量10 压入操作找中,

7、并保存到局部变量表中的istore_2中。

8、对istore_2进行 + 1,

9、并  + 1 操作后的数值压入栈顶

10、 最后赋给变量istore_3 存在局部变量表中

3.4 动态链接
① 每一个栈贞内部都包含一个指向运行时常量池中该栈贞所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如:invokedynamic 指令。

②在java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class 文件的常量池里。比如: 描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

③ 为什么需要常量池呢?  常量池的作用就是为了提供一些符号合常量,便于指令的识别。

上图大概可以解释为:

1、每一个线程 都包含了栈 和 本地方法栈,还有就是PC寄存器。

2、而栈中的栈贞又包含了  操作数,局部变量表,动态链接,返回值,一些附加信息。

3、而在动态链接中 链接Method Area方法常量池中的 方法引用。(因为在类生成 .class 文件的时候,所有的类里面的信息都会保存到方法区中,其他线程中如果要引用类里面的信息,可以通过栈贞中的动态链接去关联)

3.5 方法返回地址(return address)
①存放调用该方法的PC寄存器的值

②一个方法的结束,有两种方式:  正常执行完成。   出现未处理的异常,非正常退出。

③无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈贞中一般不会保存这部分信息。

④本质上,方法的退出就是当前栈贞出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈贞的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。

⑤正常完成出口和异常完成出口的区别在于: 通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。

⑥ 当一个方法开始执行后,只有两种方式可以退出这个方法: 

		1、执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称 正常完成出口。  一个方法在正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的时机数据类型而定。   在字节码指令中,返回指令包含ireturn (当返回值是Boolean、byte、char、short、和 int类型时使用)、lreturn、freturn、dreturn、以及areturn、另外还有一个return 指令供声明为void的方法,实例初始化方法、类和接口的初始化方法使用。

		2、在方法执行的过程中遇到了异常(exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出简称异常完成出口。   方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码。

4、方法的调用
①非虚方法:如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。 静态方法,私有方法,final 方法,实例构造器,父类方法都是非虚方法。

②除了上面几种情况,其他的都是虚方法。 子类对象的多态性的使用前提:类的继承关系,方法的重写。

③虚拟机中提供了以下几条方法调用指令:

	普通调用指令:	

		1、invokestatic:调用静态方法,解析阶段确定唯一方法版本。

		2、invokespecial:调用<init>方法,私有及父类方法,解析阶段确定唯一方法版本。

		3、invokevirtual:调用所有虚方法

		4、invokeinterface:调用接口方法。

	动态调用指令:

		5、invokedynamic:  动态解析出需要调用的方法,然后执行。

前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本。其中 invokestatic 指令和invokespecial 指令调用的方法称为非虚方法,其余的(final 修饰的除外)称为虚方法。

栈管运行,堆管存储。

⑥、堆
① 堆的核心概念
1、一个jvm 实例只存在一个堆内存,堆也是java内存管理的核心区域。

2、Java 堆区在jvm 启动的时候即被创建,其空间大小也就确定了。是JVM 管理的最大一块内存空间。  

		堆内存的大小是可以调节的。

3、堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。

4、所有的线程共享java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer, TLAB)。

5、《Java 虚拟机规范》中 对 Java 堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。

6、数组和对象可能永远不会存储在栈上,因为栈贞中保存引用,这个引用指向对象或数组在堆中的位置。

7、在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。

8、堆,是GC (Garbage Collection , 垃圾收集器)执行垃圾回收的重点区域。

② 堆内存细分
现在垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:

1、java 7 及之前堆内存逻辑上分为 三部分: 新生区 + 养老区 + 永久代

		Young Generation Space    新生区           Young/new    (又被划分为Eden区和Survivor区)

		Tenure  generation space    养老区            Old/Tenure

		Permanent   Space                 永久区           Perm

2、Java  8 及之后堆内存 逻辑 上分为三部分:  新生区 + 养老区 + 元空间

		Young Generation   Space     新生区          Young/New     (又被划分为Eden 区 和Survivor 区)

		Tenure  Generation Space    养老区           Old/Tenure

		Meta   Space                            元空间            Meta

约定: 新生区  <--> 新生代  <--> 年轻代        养老区 <-->  老年区  <--> 老年代    永久区 <--> 永久代

③ 设置堆内存大小与OOM
1、java堆区用于存储java对象实例,那么堆的大小在jvm 启动时就已经设定好了,可以通过选项 “-Xmx” 和 “-Xms” 来进行设置。

	“-Xms” 用于表示堆区的起始内存,等价于 -XX: InitialHeapSize.。   用来设置堆空间(年轻代 + 老年代)的初始内存大小。     -X 是 jvm 的运行参数。  ms 是memory start。

   “-Xmx”  则用于表示堆区的最大内存,等价于 -XX:MaxHeapSize。用来设置堆空间(年轻代 + 老年代)的最大内存大小

2、一旦堆区中的内存大小超过 “-Xmx” 所指定的最大内存时,将会抛出OutOfMemoryError 异常。

3、通常会将 -Xms 和 -Xmx 两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分割计算堆区的大小,从而提高性能。

4、默认情况下,初始内存大小: 物理电脑内存大小 / 64

							最大内存大小:  物理电脑内存大小 / 4

5、 查看设置的参数: 

		方式一: jps      / jstat -gc  进程id

		方式二: -XX:  +PrintGCDetails      在Tomcat运行的时候添加该参数	

④年轻代与老年代
1、存储在JVM 中的java对象可以被划分为两类:

		一类是声明周期较短的瞬时对象,这类对象的创建和消亡都非常迅速。

		另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的声明周期保持一致。

2、Java堆区进一步细分的话,可以划分为年轻代(YoungGen) 和 老年代(OldGen).

3、其中年轻代又可以划分为Eden空间Survivor0 空间和 Survivor1 空间(有时也叫作from区、to 区)。

4、在HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1.

5、当然开发人员可以通过选项“-XX:SurvivorRatio” 调整这个空间比例。 比如-XX:SurvivorRatio = 8.

6、几乎所有的Java对象都是在Eden区被new出来的。

7、绝大部分的Java对象的销毁都在新生代进行了。

8、—XX:NewRatio: 设置新生代与老年代的比例。默认值为1:2

⑤年轻代对象分配的过程
年轻代对象回收 会触发:YGC/ minor GC

1 、new 的对象先放伊甸园区(Eden)。此区有大小限制。

2、当伊甸园区(Eden)的空间填满时,程序又需要创建对象,JVM的垃圾回收器将伊甸园区进行垃圾回收(MinorGC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区。

3、然后将伊甸园区中的剩余对象移动到幸存者0区。

4、如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区;如果没有回收,就会放到幸存者1区。并且存活的对象age会 +1,当达到次数15时,会将对象放入到老年代。

5、如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。

6、什么时候才去养老区呢? 可以设置次数,默认是15次。

		-XX: MaxTenuringThreshold = <N> 进行设置。

伊甸园区(Eden)空间满的时候会触发YGC/MinorGC 回收算法,但是S0 S1满的时候不会触发YGC/MinorGC回收算法。

针对幸存区S0,S1区的总结:复制之后有交换,谁空谁就是to.另一个为from

关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集、

⑥Minor GC、Major GC、Full GC 的区别
1、JVM 在进行GC时,并非每次都对上面三个内存(新生代,老年代,方法区)区域一起回收的,大部分时候回收的都是指新生代。

2、针对HotSpot VM 的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)。

部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:

	① 新生代收集(Minor GC / Young GC):只是新生代的垃圾收集。

	② 老年代收集(Major  GC / Old GC):  只是老年代的垃圾收集。

		目前,只有CMS GC 会有单独收集老年代的行为。

		很多时候Major GC 会和 Full GC 混淆使用,需要具体分辨是老年代回收还是整堆回收。

	③ 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。

		目前,只有G1 GC 会有这种行为。

整堆收集(Full GC):收集整个java堆和方法区的垃圾收集。

3、年轻代GC (Minor GC)触发机制:

	① 当年轻代空间不足时,就会触发Minor GC,这里的年轻代指的是 Eden代满,Survivor 满不会触发GC。(每次 Minor GC 会清理年轻代的内存)。

	② 因为Java对象大多都具备朝生夕灭的特性,所以Minor GC 非常频繁,一般回收速度也比较快。

	③ Minor GC 会引发STW,暂停其他用户的线程,等待垃圾回收结束,用户线程才回复运行。(暂停只是毫秒级别的)

4、老年代GC (Major GC/ Full GC)触发机制:	

	① 指发生老年代的GC,对象从老年代消失时,我们说 “Major GC” 或 “Full GC”发生了。

	②出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在Parallel Scavenge 收集器的手机策略里就有直接进行Major GC 的策略选择过程)。  也就是在老年代空间不足时,会先尝试触发 Minor GC。如果之后空间还不足,则触发Major GC。

	③ Major GC 的速度一般会比 Minor GC 慢10倍以上,STW 的时间更长。

	④ 如果Major GC 后,内存还不足,就报OOM了。

	⑤Major GC 的速度一般会比 Minor GC 慢10倍以上。

5、Full GC 触发机制:

触发Full GC 执行的情况有如下五中情况:

①调用System.gc() 时,系统建议执行Full GC,但是不必然执行。

②老年代空间不足。

③方法区空间不足。

④通过Minor GC 后进入老年代的平均大小大于老年代的可用内存。

⑤ 由Eden区,survivor space0 (From Space)区向survivor space1 (To Space) 区复制时,对象大小大于 To Space可用内存,则把对象转存到老年代,且老年代的可以内存小于该对象大小。

说明:full GC 是开发或调优中尽量要避免的,这样暂时时间会短一些。

⑦ 堆空间分代思想
其实不分代完全可以,分代的唯一理由就是优化GC 性能。如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室。GC 的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一个地方,当GC 的时候先把这块存储 “朝生夕死” 对象的区域进行回收,这样就会腾出很大的空间出来。

⑧ 内存分配策略
1、如果对象在Eden 出生并且经过第一次Minor GC 后仍然存活,并且能被Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为1。 对象在Survivor 区中每熬过一次Minor GC ,年龄就增加1 岁,当他的年龄增加到一定程度 (默认为15岁,其实每个JVM,每个GC 都有所不同)时,就会被晋升到老年代中。

2、针对不同年龄段的对象分配原则如下所示:

	① 优先分配到Eden。

	②大对象直接分配到老年代、尽量避免程序中出现过多的大对象。

	③ 长期存活的对象分配到老年代。

	④动态对象年龄判端, 如果Survivor 区中相同年龄的所有对象大小的总和大于Survivor 空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold  中要求的年龄。

	⑤ 空间分配担保 :  -XX:  HandlePromotionFailure

/**
-Xms60m -XX:NewRatio = 2 -XX:SurvivorRatio = 8 -XX:+PrintGCDetails
**/
⑨为对象分配内存:TLAB (堆空间线程私有)
为什么有 TLAB(Thread Local Allocation Buffer)?

1、堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据。

2、由于对象实例的创建在JVM 中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。

3、为避免多个线程操作统一地址,需要使用加锁等机制,进而影响分配速度。

什么是 TLAB?

1、从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。

2、多线程同时分配内存时,使用TLAB 可以避免一系列的非线程安全问题,同时还能提升内存分配的吞吐量,因此我们可以将内存分配方式称之为快速分配策略。

上图大概意思为:

一个进程可能有多个线程,每个线程在Eden区都有属于自己一块私有空间,在自己的私有空间可以存储对象,如果自己的私有空间存储满了,会去在共享堆中去存储对象。

3、尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM 确实是将TLAB 作为内存分配的首选。

4、在程序中,可以通过  “-XX:UserTLAB” 设置是否开启TLAB空间。

5、默认情况下,TLAB 空间的内存非常小,仅占整个Eden空间的 1%,当然我们可以通过选项 “-XX:TLABWasteTargetPercent” 设置 TLAB 空间所占用 Eden空间的百分比大小。

6、一旦对象在TLAB 空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。

⑩ 堆空间常用的JVM 参数:

1、—XX:+PrintFlagsInitial 查看所有的参数的默认初始值
2、-XX:+PrintFlagsFinal 查看所有的参数的最终值(可能会存在修改,不再是初始值)
具体查看某个参数指令: jps 查看当前运行中的进程
jinfo -flag SurvivorRation 进程Id
3、-Xms:初始堆空间内存(默认为物理内存的1/64)
4、-Xmx:最大堆空间内存 (默认为物理内存的1/4)
5、-Xmn:设置新生代的大小 (初始值及最大值)
6、-XX:NewRation 配置新生代与老年代在堆结构的占比
7、-XX:SurvivorRatio 设置新生代中Eden 和 S0/s1空间的比例
8、-XX:MaxTenuringThreshold 设置新生代垃圾的最大年龄
9、-XX:+PrintGCDetails 输出详细的GC处理日志
打印gc 简要信息 -XX:+PrintGC -verbose:gc
10、-XX:HandlePromotionFailure 是否设置空间分配担保
HandlePromotionFailure

在发生Minor GC 之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。

如果大于,则此Minor GC 是安全的。

如果小于,则虚拟机会查看 -XX: HandlePromtionFailure 设置值 是否允许担保失败。

	如果  HandlePromotionFailure = true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。

	如果大于,则尝试进行一次Minor GC,但这次Minor GC 依然是有风险的。

	如果小于,则改为进行一次Full GC、

如果 HandlePromotionFailure = false,则改为进行一次Full GC。

在JDK6 以后,HandlePromotionFailure 参数不会再影响到虚拟机的空间分配担保策略.。JDK6 更新以后,规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行FullGC

⑦、堆是分配对象存储的唯一选择吗?
1、在《深入理解Java虚拟机》中关于Java堆内存有这样描述: 随着 JIT 编译期 的发展与逃逸分析技术逐渐成熟,栈上分配,标量替换优化技术 将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么 “绝对”了。

2、在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是 如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也就是最常见的堆外存储技术。

3、此外,前面提到的基于OpenJDK 深度定制的TaoBaoVM,其中创新的GCIH(GC invisible heap)技术实现off-heap,将生命周期较长的Java对象从heap 中移至heap 外,并且GC不能管理GCIH 内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的、

4、如何将堆上的对象分配到栈,需要使用逃逸分析手段。

5、这是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。

6、通过逃逸分析,Java Hotspot 编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。

7、逃逸分析的基本行为就是分析对象动态作用域:

当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。

当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如 作为调用参数传递到其他地方中。

public void myMethod() {
V v = new V();
//
//
v = null;
}
没有发生逃逸的对象,则可以分配到栈上,随着方法执行的结束,栈空间就被移除。

//发生逃逸
public static StringBuffer createStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb;
}
//不发生逃逸
public static String createStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}

/**
逃逸分析
如何快速的判断是否发生了逃逸分析,大家就看new的对象是否有可能在方法外被调用
**/
public class EscapeAnalysis {

public EscapeAnalysis obj;

/**
    方法返回EscapeAnalysis 对象,发生逃逸
**/
public EscapeAnalysis getInstance() {
    return obj == null ? new EscapeAnalysis() : obj;
}

/**
    为成员属性赋值,发生逃逸
**/
public void setObj() {
    this.obj = new EscapeAnalysis();
}

//如果当前的obj 引用声明为static 的?  仍然会发生逃逸

/**
    对象的作用域仅在当前方法中有效,没有发生逃逸
**/
public void userEscapeAnalysis() {
    EscapeAnalysis e = new EscapeAnalysis();
}

/**
    引用成员变量的值,发生逃逸
**/
public void userEscapeAnalysis() {
    EscapeAnalysis e = getIntance();
}


}
8、逃逸分析参数设置: 在jdk6 以后,HotSpot 中默认就已经开启了逃逸分析。

如果使用的较早版本,可以通过 “-XX: +DoEscapeAnalysis” 显示开启逃逸分析。

通过选项 “-XX: +PrintEscapeAnalysis” 查看逃逸分析的筛选结果。

9、结论:开发中能使用局部变量的,就不要使用在方法外定义。

10、使用逃逸分析,编译器可以对代码做如下优化:

① 栈上分配。将堆分配转为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。

②同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。在动态编译同步块的时候,JIT 编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。 如果没有,那么JIT 编译器在编译这个同步快的时候就会取消对这部分代码的同步。这样就能大大提过并发性和性能,这个取消同步的过程就叫同步省略,也叫锁消除。

public void f() {
Object hollis = new Object();
synchronized(hollis) {
System.out.println(hollis);
}
}
//代码中对hollis 这个对象进行加锁,但是hollis 对象的生命周期只在f()方法中,并不会被其他线程锁访问到,所以在JIT 编译阶段就会被优化掉。优化成:
public void f() {
Object hollis = new Object();
System.out.println(hollis);
}
//在编译成字节码文件的时候,还是会加锁显示在字节码文件中的,在程序运行的时候,是优化把锁给去掉了、
③分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU 寄存器中。 标量(Scalar) 是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。 相对的,那些还可以分解的数据叫做 聚合量(Aggregate), Java 中的对象就是聚合量,因为他可以分解成其他聚合量和标量。 在JIT 阶段,如果经过逃逸分析,发小一个对象不会被外界访问的话,那么经过JIT 优化,就会把这个对象拆解成若干个其他包含的若干个成员变量来代替,这个过程就是标量替换。

public static void main(String[] args) {
alloc();
}
private static void alloc() {
Point point = new Point(1,2);
System.out.println("point.x = " + point.x + ";point.y= " + point.y);
}
class Point {
private int x;
private int y;
}

//以上代码,经过标量替换后,就会变成:
private static void alloc() {
int x = 1;
int y =2;
System.out.println("point.x = " + point.x + ";point.y= " + point.y);
}
可以看到,Point 这个聚合量经过逃逸分析后,发现他并没有逃逸,就被替换成两个聚合量了,那么标量替换有什么好处呢? 就是可以大大减少堆内存的占用。因为一旦不需要创建对象了,那么就不再需要分配内存了。 标量替换为栈上分配提供了很好的基础。

标量替换参数设置: -XX:+EliminateAllocations 开启了标量替换(默认打开),允许将对象打散分配在栈上。

总结
1、逃逸分析还不是和成熟,其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也就是一个相对耗时的过程。

2、一个极端的例子,就是经过逃逸分析之后,发现没有一个对象时不逃逸的。那这个逃逸分析的过程就白白浪费掉了。

3、虽然这项技术并不十分成熟,但是它也是及时编译器优化技术中一个十分重要的手段。

4、JVM 会在栈上分配那些不会逃逸的对象,这个理论是可行的,但是取决于JVM设计者的选择。Oracle Hotspot JVM 中并未这么做,这一点在逃逸分析相关的文档里已经说明,所以可以明确所有的对象实例都是创建在堆上的。

5、目前很多书籍还是基于JDK 7 以前的版本,JDK 已经发生了很大变化,intern 字符串的缓存和静态变量曾经都被分配在永久代上,而永久代已经被元数据区取代,但是,intern 字符串缓存和静态变量并不是被转移到元数据区,而是直接在堆上分配,所以这一点同样符合前面一点的结论:对象实例都是分配在堆上。

6、年轻代是对象的诞生,成长,消亡的区域,一个对象在这里产生、应用、、最后被垃圾回收器收集、结束生命。

7、老年代放置长生命周期的对象,通常都是从Survivor 区域筛选拷贝过来的Java对象。当然,也有特殊情况,我们知道普通的对象会被分配在TLAB上,如果对象较大,JVM 会试图直接分配在Eden 其他位置上;如果对象太大,完全无法再新生代找到足够长的连续空闲空间,JVM就会直接分配到老年代。

8、当GC只发生在年轻代中,回收年轻代对象的行为被称为MinorGC。当GC发生在老年代时 则被称为MajorGC 或者FUllGC,一般的,MinorGC的发生频率要比MajorGC 高很多,即老年代中垃圾回收发生的频率将大大低于年轻代。

⑦栈、堆、方法区(元空间)的交互关系

⑧方法区
①方法区介绍
1、《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或进行压缩。”但对于HotSpotJVM 而言,方法区还有一个别名叫做Non-Heap (非堆),目的就是要和堆分开。所以,方法区看作是一块独立于Java堆的内存空间(因为来的加载.class 文件都在方法区中,如果方法区属于堆上,这这些文件都应该在堆上,所以方法区不属于堆)。

2、方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。

3、方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。

4、方法区的大小,跟堆空间一样,可以选择固定大小或可扩展。

5、方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError PermGen space 或者 java.lang.OutOfMemoryError: Metaspace。

6、关闭JVM 就会释放这个区域的内存。

② Hotspot中方法区的演进
1、在JDK 7 及以前,习惯上把方法区称为永久代。 jdk8 开始,使用元空间取代了 永久代。

2、本质上,方法区和永久代并不等价。仅是对Hotspot 而言的。《Java虚拟机规范》对如何实现方法区,不做统一要求。现在来看,当年使用永久代,不是好的idea。导致java程序更容易OOM。

3、而到了JDK8,终于完全废弃了永久代的概念,改用在本地内存中实现的元空间(Metaspace)来代替。

4、元空间的本质和永久代类似,都是对JVM 规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存。

5、永久代、元空间二者并不只是名字变量,内存结构也调整了。

6、根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OOM异常。 

③ 设置方法区大小与OOM
1、方法区的大小不必是固定的,JVM 可以根据应用的需求动态调整。

2、JDK7及以前: 

	通过 -XX:PermSize 来设置永久代初始分配空间。默认值是20.75M、

	-XX:MaxPermSize 来设定永久代最大可分配空间。32为机器默认是64M,  64位机器默认是82M、

3、当JVM 加载的类信息容量超过了这个值,会报异常OutOfMemoryError: PermGen space。

4、JDK 8 及以后:

	①元数据区大小可以使用参数:-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 设置为一个相对较高的值。

④方法区的内部结构

1、《深入理解Java 虚拟机》书中对方法区(Method Area)存储内容描述如下: 它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。



①类信息,  对每个加载的类型(类class,接口interface,枚举enum,注解annotation),jvm 必须在方法区中存储以下类型信息:

	1、这个类型的完整有效名称(全名=包名.类型)。

	2、这个类型直接父类的完整有效名(对于interface 或是 java.lang.Object, 都没有父类)

	3、这个类型的修饰符(public,abstract,final 的某个子集)

	4、这个类型直接接口的一个有序列表

②域(Field)信息

	1、JVM 必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。

	2、域的相关信息包括:域名称、域类型、域修饰符(public,private,protected、static、final、Volatile、transient 的某个子集)

③方法(Method)信息  , JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:

	1、方法名称

	2、方法的返回类型(或void)

	3、方法参数的数量和类型(按顺序)

	4、方法的修饰符(public,private,protected、static、final、synchronized,native,abstract的一个子集)

	5、方法的字节码(bytecodes),操作数栈、局部变量表及大小(abstract和native方法除外)

	6、异常表(abstract和native方法除外)

			每个异常处理的开始位置、结束位置、代码处理在程序计数器中 便宜地址、被捕获的异常类的常量池索引。

④ non-final 的类变量

	1、静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分。

	2、类变量被类的所有实例共享,即使没有类实例时你也可以访问它。

public class MethodAreaTest {
public static void main(String[] args) {
Order order = null;
order.hello();
System.out.println(order.count);
}
}

class Order {
public static int count = 1;
public static void hello() {
System.out.println(“hello”);
}
}

//以上代码并不会报空指针异常
全局常量:static final

被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。

⑤ 运行时常量池 VS 常量池

1、方法区,内部包含了运行时常量池。

2、字节码文件,内部包含了常量池。

3、要弄清楚方法区,需要理解清除ClassFile,因为加载类的信息都在方法区。

4、要弄清楚方法区的运行时常量池,需要理解清除ClassFile中的常量池。

3、一个java源文件中的类,接口,编译后产生一个字节码文件。而java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池。

字节码 后面 #数字 就是引用的常量池,

5、常量池中有什么?

1、常量池存储的数据类型包括:  数量值,字符串值,类引用,字段引用,方法引用。

小结: 常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行党的类名,方法名,参数类型,字面量等类型。 只存在于第一步 ClassFile中。

⑥运行时常量池
1、运行时常量池(Runtime Constant Pool)是方法区的一部分。

2、常量池表(Constant Pool Talbe) 是Class 文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

3、运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池。

4、JVM 为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。

5、运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。

	运行时常量池,相对于Class文件常量池的另一重要特征是: 具备动态性。

6、当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM 会抛OutOfMemoryError 异常。

⑦ 方法区演变细节
1、只有HotSpot 才有永久代。

2、HotSpot中方法区的变化;

	① jdk 1.6及之前 :有永久代(Permanent generation),静态变量存放在永久代上。

	②jdk1.7  : 有永久代,但已经逐步 “去永久代”,字符串常量池,静态变量移除,保存在堆中。

	③jdk1.8 及之后:无永久代,类型信息、字段、方法、常量保存在本地内存的元空间。但字符串常量池,静态变量仍在堆中、







3、永久代为什么要被元空间替换?

①随着Java8 的到来,Hotspot VM 中再也见不到永久代了。但是这并不意味着类的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫做元空间(Metaspace)。

②由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。

③这项改动是很有必要的,原因是:

	1、为永久代设置空间大小是很难确定的。  在某些场景下,如果动态加载类过多,容易产生Perm 区的OOM。比如某个实际Web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制、

	2、对永久代进行调优是很困难的。

4、方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。

静态引用对应的对象实体始终都存在堆空间。

5、StringTable 为什么要调整?

	Jdk7 中将StringTable 放到了堆空间中。因为永久代的回收效率很低,在full GC 的时候才会触发。而full GC 是老年代的空间不足,永久代不足时才会触发。这就导致 StringTable 回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。

⑨安全点
我们知道 HotSpot 虚拟机采取的是可达性分析算法。即通过 GC Roots 枚举判定待回收的对象。

那么,首先要找到哪些是 GC Roots。

有两种查找 GC Roots 的方法:

一种是遍历方法区和栈区查找(保守式 GC)。

一种是通过 OopMap 数据结构来记录 GC Roots 的位置(准确式 GC)。

很明显,保守式 GC 的成本太高。准确式 GC 的优点就是能够让虚拟机快速定位到 GC Roots。

对应 OopMap 的位置即可作为一个安全点(Safe Point)。

在执行 GC 操作时,所有的工作线程必须停顿,这就是所谓的"Stop-The-World"。

为什么呢?

因为可达性分析算法必须是在一个确保一致性的内存快照中进行。如果在分析的过程中对象引用关系还在不断变化,分析结果的准确性就不能保证。

安全点意味着在这个点时,所有工作线程的状态是确定的,JVM 就可以安全地执行 GC 。

如何选定安全点?
安全点太多,GC 过于频繁,增大运行时负荷;安全点太少,GC 等待时间太长。

一般会在如下几个位置选择安全点:

	1、循环的末尾

	2、方法临返回前

	3、调用方法之后

	4、抛异常的位置

为什么选定这些位置作为安全点:

	主要的目的就是避免程序长时间无法进入 Safe Point。比如 JVM 在做 GC 之前要等所有的应用线程进入安全点,如果有一个线程一直没有进入安全点,就会导致 GC 时 JVM 停顿时间延长。比如这里,超大的循环导致执行 GC 等待时间过长。

如何在 GC 发生时,所有线程都跑到最近的 Safe Point 上再停下来?
主要有两种方式:

抢断式中断:在 GC 发生时,首先中断所有线程,如果发现线程未执行到 Safe Point,就恢复线程让其运行到 Safe Point 上。

主动式中断:在 GC 发生时,不直接操作线程中断,而是简单地设置一个标志,让各个线程执行时主动轮询这个标志,发现中断标志为真时就自己中断挂起。

JVM 采取的就是主动式中断。轮询标志的地方和安全点是重合的。

安全区域又是什么?
Safe Point 是对正在执行的线程设定的。

如果一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,再运行到 Safe Point 上。

因此 JVM 引入了 Safe Region。

Safe Region 是指在一段代码片段中,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的。

线程在进入 Safe Region 的时候先标记自己已进入了 Safe Region,等到被唤醒时准备离开 Safe Region 时,先检查能否离开,如果 GC 完成了,那么线程可以离开,否则它必须等待直到收到安全离开的信号为止。

⑩记忆集
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。如果我们不考虑 效率和成本的话,最简单的实现可以用非收集区域中所有含跨代引用的对象数组来实现这个数据结构。

这种记录全部含跨代引用对象的实现方案,无论是空间占用还是维护成本都相当高昂。而在垃圾收集的场景中,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针就可以了,并不需要了解这些跨代指针的全部细节。那设计者在实现记忆集的时候,便可以选择更为粗犷的记录粒度来节省记忆集的存储和维护成本,下面列举了一些可供选择(当然也可以选择这个范围以外的)的记录精度:

字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的32位或64位,这个 精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。

对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。

卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。

其中,第三种“卡精度”所指的是用一种称为“卡表”(Card Table)的方式去实现记忆集,这也是目前最常用的一种记忆集实现形式。

  1. 卡表
    前面定义中提到记忆集其实是一种“抽象”的数据结构,抽象的意思是只定义了记忆集的行为意图,并没有定义其行为的具体实现。卡表就是记忆集的一种具体实现,它定义了记忆集的记录精度、与堆内存的映射关系等。 关于卡表与记忆集的关系,不妨按照Java语言中HashMap与Map的关系来类比理解。

    字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称作“卡页”(Card Page)。一般来说,卡页大小都是以2的N次幂的字节数,通过上面代码可以看出HotSpot中使用的卡页是2的9次幂,即512字节(地址右移9位,相当于用地址除以512)。那如果卡表标识内存区域的起始地址是0x0000的话,数组CARD_TABLE的第0、1、2号元素,分别对应了地址范围为0x0000~0x01FF、0x0200~0x03FF、0x0400~0x05FF的卡页内存块,
    
    一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。
    
  2. 三色标记法
    要找出存活对象,根据可达性分析,从GC Roots开始进行遍历访问,可达的则为存活对象:

白色:尚未访问过。
黑色:本对象已访问过,而且本对象 引用到 的其他对象 也全部访问过了。 (自己标记完了,自己的孩子(包括多个孩子)也标记完了,都不是垃圾)
灰色:本对象已访问过,但是本对象 引用到 的其他对象 尚未全部访问完。全部访问后,会转换为黑色。(自己标记完了,自己的孩子(包括多个孩子)没有标记)

假设现在有白、灰、黑三个集合(表示当前对象的颜色),其遍历访问过程为:

初始时,所有对象都在 【白色集合】中;

将GC Roots 直接引用到的对象 挪到 【灰色集合】中;

从灰色集合中获取对象:
3.1. 将本对象 引用到的 其他对象 全部挪到 【灰色集合】中;
3.2. 将本对象 挪到 【黑色集合】里面。

重复步骤3,直至【灰色集合】为空时结束。

结束后,仍在【白色集合】的对象即为GC Roots 不可达,可以进行回收。

注:如果标记结束后对象仍为白色,意味着已经“找不到”该对象在哪了,不可能会再被重新引用。

当Stop The World (以下简称 STW)时,对象间的引用 是不会发生变化的,可以轻松完成标记。
而当需要支持并发标记时,即标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。

13、写屏障
假如 分代区域中对象引用了本区域对象时,其对应的卡表元素就应该变脏,在Hotspot 虚拟机里 通过写屏障(Write Barrier)技术维护 卡表状态的。写屏障可以看做在虚拟机层面对 “引用类型字段赋值” 这个动作的AOP 切面,在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作,也就是说赋值的前后都在写屏障的覆盖范畴内,在赋值前的部分 的写屏障 叫做写前屏障,在赋值后的则叫做写后屏障。

面试题总结:
1、说一下JVM 内存模型把?有哪些区? 分别是干什么的?

	答:① 首先java文件编译成 .class 文件后,jvm 去加载它。

			②先由类加载子系统去加载,由分为 检验(检查主要是看是否符合jvm 语法,元数据等有没有错误信息),准备(为类的变量附初始值 默认为0,被final修饰的变量不会再准备阶段赋值,在编译后就已经赋值了。),解析(主要是将 常量池的符号引用,转变为直接引用),和初始化(就是执行构造器的过程<clint>)。	

			③初始化完后,就到运行时数据区了,.class文件类信息都会被放到方法区(元空间 也存放运行时字符串常量表), 而 class文件new的对象都会被放到堆空间上,而堆有分为年轻代和老年代,新生代由分为( Eden区,survivor0区,survivor1区),老年代(老年区)。new 的一个对象会先放到Eden区,如果这个Eden区放不下会触发young GC垃圾回收,如果还放不下,就会将这个对象尝试放到survivor0区,如果survivor0区也放不下,就直接放到老年代了,老年代放不下直接报异常 OOM。

		④ 而方法的局部变量都会存放到栈 上面。栈主要是 主管 方法的8中基本数据类型 和对象的引用。栈中存放的又是一个一个的栈贞。栈贞又包括(局部变量表,操作数栈,动态链接,方法返回地址,其他信息)。局部变量表可以成为数组或本地变量表,在编译时就已经确定了,主要存放的是方法中的局部变量,如果局部变量表越大,证明方法嵌套越多。而操作数栈主要用做主要做的是一些局部变量表中的一些值的操作。而动态链接主要动态的去链接方法区中的动态链接池,让程序动态的去往下直接。

2、java8 的内存分代改进?

	答:在堆中,jdk7之前主要是 新生代,老年代,永久代。 而在jdk8及以后 是新生代,老年代,元空间。

		在方法区中,jdk6之前有元空间,静态变量存储在永久代上。jdk7 准备去永久代,字符串常量和静态变量存储在堆中,但是还是有永久代。 jdk 8  无永久代,有元空间,静态变量和字符串常量存储在堆中。

	而元空间是存储在本地内存中的,所以元空间可分配的内存的大小就是系统的最大系统内存。

4、JVM 内存分布/内存结构?  栈和堆的区别? 堆的结构?为什么两个survivor区?

	因为survivor 的存在的意义,就是减少 FUll GC ,因为存在survivor区中的对象,只有经历16次的 MinorGC,才可以进入老年代。两个survivor 区主要就是较少碎片化,假设就一个survivor区,Eden区满了,然后会将对象直接放到survivor区,下一次,如果Eden 区又满了,有放到了Survivor区,这两个对象的地址肯定不是连续的,所以会产生大量的内存碎片的产生。如果是两个Survivor区,这样第一次Eden区满了,会将对象放到Survivor0区,第二次如果Eden区满了会触发一次MinorGC,会将 Eden存活的对象和Survivor0区存活的对象都复制到Survivor1区,这样他们的对象地址一定是连续的,将survivor0区的空间清空。所以每次survivor区总有一个为空的。

5、Eden和Survivor 的比例分配

	Eden和Survivor 区的比例为 8:1:1

6、JVM 内存分区,为什么要有新生代和老年代?

	如果没有新生代和老年代,把所有的区域都放到一个区中,这样在回收和管理上很不方便,会很容易产生GC和产生性能降低问题。

9、什么时候对象会进入老年代?

	对象太大,新生代放不下的对象(数组等)。

	经常被使用的对象,还有age年龄16次了,也会被放到老年代。		

16、jvm 的永久代中会发生垃圾回收吗?

		会发生垃圾回收的。常量池中废弃的常量和不再使用的类型。和对象不再使用的回收,只不过对象回收条件比较苛刻

5、 对象的实例化过程
① 创建对象的步骤
1、判断对象对应的类是否加载,链接,初始化。

	虚拟机遇到一条new 指令,首先去检查这个指令的参数能否在Metaspace 的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。(即判断类元信息是否存在)。如果没有,那么在双亲委派模式下,使用当前类加载器以ClassLoader + 包名 + 类名  为key 进行查找对应的class 文件。 如果没有找到这个文件,则抛出ClassNotFountException 异常, 如果找到,则进行类加载,并生成对应的Class 类对象。

2、为对象分配内存。 首先计算对象占用的空间大小,接着在堆中划分一块内存给新对象。如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小。

	① 如果内存规整,这指针碰撞。   如果内存是规整的,那么虚拟机采用的是指针碰撞法(Bump The Pointer) 来为对象分配内存。  意思是所有用过的内存在一边,空闲的内存在另外一边,中间放着一个指针作为分界点的指示器,分配内存就是仅仅把指针向空闲那边移动一段与对象大小相等的距离罢了,如果垃圾收集器选择的是Serial,ParNew 这种基于压缩算法的,虚拟机采用这样分配方式,一般使用带有compact(整理)过程的收集器时,使用指针碰撞。

	②如果内存不规整,虚拟机需要维护一个列表,空闲列表分配。 如果内存不规整的,已使用的内存和未使用的内存相互交错,那么虚拟机采用的是空闲列表法来为对象分配内存。  意思是 虚拟机维护了一个列表,记录上那些内存块是可用的,再分配的时候从列表找到一块足够大的空间划分给对象实例,并更新列表上的内容,这种分配方式成为 “空闲列表(Free List)”。

选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

3、处理并发安全问题。	方式一:  采用CAS失败重试,区域加锁保证更新的原子性。  方式二:每个线程预先分配一块TLAB,通过-XX: +/-UserTLAB 参数来设定。

4、初始化分配到的空间。   所有属性设置默认值(0),保证对象实例字段在不赋值时可以直接使用。

5、设置对象的对象头。

		将对象的所属类(即类的元数据信息),对象的HashCode 和对象的GC信息,锁信息等数据存储在对象的对象头中,这个过程的具体设置方式取决于JVM 实现。

6、执行init方法进行初始化。

	在Java程序的视角来看,吃时候才正式开始,初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。因此一般来说(由字节码中是否跟随有invokespecial 指令所决定),new 指令之后会接着就是执行方法,把对象按照程序员的意愿进行初始化,这样才算一个对象完全的创建出来。

测试对象实例化的过程:
①加载类元信息
②为对象分配内存
③处理并发问题
④属性的默认初始化(零值初始化)
⑤设置对象头的信息
⑥属性的显式初始化、代码块中的初始化、构造器中的初始化

给对象的属性赋值的操作:
①属性的默认初始化。 ②显式初始化 ③ 代码块中初始化 ④构造器中初始化

public class Customer {
int id = 1001;
String name;
Account acct;

{
    name = "匿名客户";
}
public Customer() {
    acct = new Account();
}

}

class Account {

}
②内存布局

public class Customer {
int id = 1001;
String name;
Account acct;

{
    name = "匿名客户";
}
public Customer() {
    acct = new Account();
}

}

class Account {

}


public class Test {
public static void main(String[] args) {
Customer cust = new Customer();
}
}

上图大概意思为:

① Test类里面有个main方法,方法在栈上存储是以栈贞的方式存储的,一个方法一个栈贞。一个栈贞里面有包含了局部变量表,操作数栈,动态链接,方法返回地址,附加信息。所以,像 args,cust 这样都是存在局部变量表上的。

② 而new Customer(); 实例时在堆空间上的。首先需要先为这个实例分配一个空间,这个对象堆空间中 有运行时元数据包含了(哈希值,主要用于和堆上面对象映射。 GC分代年龄,垃圾回收时用到的,锁状态标志)等,类型指针 指向元空间中需要Class信息,而这两部分属于对象头(Header)信息。 接下来就是实例化数据,字符串信息会存储在字符串常量池中,而字符串常量池在jdk8后 都存在堆空间中。而acct 这个属性有需要new Account()实例,所以又得开辟一个堆空间,步骤和上面一样。

③对象访问定位
对象访问定位两种实现:

1、句柄访问。 需要在堆空间中开辟一块空间,来存储对象实例数据指针和对象类型数据指针。reference 指向句柄池中的指针,垃圾回收的时候,他所指向的地址值不会改变。

2、直接访问(Hotspot采用):栈空间中的对象变量指向堆空间的对象头指针。当垃圾回收时,栈空间的地址会发生变化。

6、执行引擎

① 执行引擎概述
1、执行引擎是Java 虚拟机核心的组成部分之一。

2、“虚拟机” 是一个相对于 “物理机” 的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。

3、JVM 的主要任务时负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被 JVM 所识别的字节码指令,符号表,以及其他辅助信息。

4、如果想要让一个Java 程序运行起来,执行引擎 (Execution Engine) 的任务就是将字节码指令解释/ 编译为对应平台上的本地机器指令才可以。简单来说,JVM 中的执行引擎充当了将高级语言翻译为机器语言的译者。

② 执行引擎的工作过程

1、执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于PC寄存器。

2、每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址。

3、当然方法在执行的过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息。

4、从外观上来看,所有的Java虚拟机的执行引擎输入,输出都是一致的:输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行结果。

③ Java代码编译和执行的过程

1、大部分的程序代码转换成物理机的目标代码或虚拟机能执行的指令集之前,都需要经过上图的各个步骤。黄色的部分是javac 编译成字节码文件的过程。 绿色的部分是解释器编译的过程。蓝色的部分是JIT编译的过程。



2、Java代码编译是由Java源码编译器来完成,流程图如上所示:

3、Java字节码的执行是由JVM执行引擎来完成,流程图如上所示:

字节码是一种中间状态(中间码)的二进制代码(文件),它比机器码更抽象,需要直译器转译后才能更为机器码。

字节码主要为了实现特定软件运行和软件环境,与硬件环境无关。

字节码的实现方式是通过编译器和虚拟机器。编译器将源码编译成字节码,特定平台上的虚拟机器将字节码转译为可以直接执行的指令。

4、什么是解释器(Interpret),什么是JIT 编译器?

解释器:当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容 “翻译”为对应平台的本地机器执行执行。

JIT(Just In Time Compiler) 编译器:就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言。

5、为什么说Java是半编译半解释型语言?

JDK1.0时代,将java语言定位为 “解释执行”还是比较准确的。再后来,Java也发展出可以直接生成本地代码的编译器。

现在JVM在执行java代码的时候,通常都会将解释执行与编译执行二者结合起来进行。

为什么说 Java一处编译,到处可以执行? 因为Java在编译完后,执行引擎执行缓存会存在方法区中,所以下次执行的时候会直接在方法区中获取调用。这样不用再编译了,所以说一处编译到处都可以运行。

6、机器指令,二进制码指令。

④解释器

为了满足Java程序实现跨平台特性,因此避免采用静态编译的方式直接生成本地机器指令,从而诞生了实现解释器在运行时采用逐行解释字节码执行程序的想法。

解释器真正意义上所承担的角色就是一个运行时 “翻译者‘,将字节码文件中的内容 ”翻译“为对应平台的本地机器指令执行。

当一条字节码指令被解释执行完成后,接着再根据PC寄存器记录的下一条需要被执行的字节码指令执行解释操作。

JVM 平台支持一种叫做即时编译的技术。即时编译的目的是避免函数被解释执行,而是将整个函数体编译成为机器码,每次函数执行时,只执行编译后的机器码即可,这种方式可以使执行效率大幅度提升。

不过无论如何,基于解释器的执行模式仍然为中间语言的发展做出了不可磨灭的贡献。

⑤JIT 编译器
1、第一种是将源代码编译成字节码文件,然后在运行时通过解释器将字节码文件转为机器码执行。

2、第二种是编译执行(直接编译成机器码)。现代虚拟机为了提高执行效率,会使用即时编译技术(JIT,Just In Time)将方法编译成机器码后再执行。

3、Hotspot VM 是目前市面上高性能虚拟机的代表作之一。它采用解释器与即时编译器并存的架构。在Java虚拟机运行时,解释器和即时编译器能够互相协作,各自取长补短,尽力去选择最合适的方式来权衡编译本地代码的时间和直接解释执行代码的时间。

4、当程序启动后,解释器可以马上发挥作用,省去编译的时间,立即执行。编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间,但编译为本地代码后,执行效率高。

5、当Java虚拟机启动时,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成后再执行,这样可以省去许多不必要的编译时间。随着时间的推移,编即时译器发挥作用,根据热点探测功能,把越来越多的代码编译成本地代码,获得更高的执行效率。

6、解释器执行在编译器进行激进优化不成立的时候,作为编译器的 “逃生门”、

7、Java语言的 “编译器” 其实是一段 “不确定”的操作过程,因为它可能是指一个 前端编译器(其实叫 “编译器的前端” 更准确一些)把 .java 文件转变成 .class 文件的过程。

8、也可能是虚拟机的 后端运行期编译器(JIT 编译器,Just In TIme Compiler) 把字节码转变成机器码的过程。

9、还可能是指使用 静态提前编译器(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。

10、热点代码及探测方式:当然是否需要启动 JIT 编译器将字节码直接编译为对应平台的本地机器指令,则需要根据代码被调用执行的频率而定。 关于那些需要被编译为本地代码的字节码,也被称为 “热点代码”, JIT 编译器在运行时会针对那些频繁被调用的 “热点代码” 做出 深度优化,将其直接编译为对应平台的本地机器指令,依次提升Java程序的执行性能。

1、一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称之为 “热点代码”,因此都可以通过JIT 编译器编译为本地机器指令。由于这种编译方式发生在方法的执行过程中,因为也被称之为栈上替换,或简称为 OSR (On stack Replacement)编译。

10.2、目前Hotspot VM 采用的热点探测方式是基于计数器的热点探测。

10.3、采用基于计数器的热点探测,Hotspot VM 将会为每一个方法都建立2 个不同类型的计数器,分别为方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)。

	方法调用计数器用于统计方法的调用次数。

	回边计数器则用于统计循环体执行的循环次数。

10.4、方法调用计数器,这个计数器就用于统计方法被调用的次数,他的默认阈值在Client 模式下是 1500次,在server模式下是1000次。超过这个阈值,就会触发 JIT 编译。这个阈值可以通过虚拟机参数

-XX:CompileThreshold 来人为设定。 当一个方法被调用时,会先检查该方法是否存在被JIT 编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本,则将此方法的调用计数器值加1,然后判断方法调用计数器与回边计数器值之和 是否超过方法调用计数器的阈值。 如果已超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求。

10.5、方法调用计数器, 热点衰减。

	如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。 当超过一定的时间限度,如果方法的调用次数仍然不足以让他提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,这个过程称为 方法调用计数器热度的 衰减(Counter Decay),而这段时间就称为此方法统计的半衰周期(Count Half Life Time)。

	进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的,可以使用虚拟机参数 —XX: -UseCounterDecay 来关闭热度衰减,让方法计数器方法调用的绝对次数,这样,只要系统运行时间足够长,绝大部分方法都会被编译成本地代码。 可以使用 -XX: CounterHalfLifeTime 参数设置半衰周期的时间,单位是秒。

10.6、回边计数器

	它的作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为 “回边”(Back Edge),显然,建立回边计数器统计的目的就是为了触发OSR 编译。

⑥Hotspot VM 可以设置程序方式
1、缺省情况下Hotspot VM 是采用 解释器并存的架构,当然我们也可以根据具体的应用场景,通过命令显式地为Java虚拟机指定在运行时到底是完全采用解释器执行,还是完全采用即时编译器执行。

	-Xint: 完全采用解释器模式执行程序。

	-Xcomp: 完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行。

	-Xmixed:采用解释器 + 即时编译器的混合模式共同执行程序。

2、在Hotspot VM 中内嵌有两个JIT 编译器,分别为 Client Compiler 和 Server Compiler,但大多数情况下我们简称为C1 编辑器和 C2编辑器。开发人员可以通过如下命令显式指定 Java虚拟机在运行时到底使用哪一种即时编译器,如下所示:

-client:指定Java虚拟机运行在Client 模式下,并使用C1编译器; C1编译器会对字节码进行简单和可靠的优化,好时短。以达到更快的编译速度。

-server:指定 Java 虚拟机运行在Server 模式下,并使用C2编译器。  C2进行耗时较长的优化,以及激进优化。但优化的代码执行效率更高。

3、C1和C2 编译器不同的优化策略:

①在不同编译器上有不同的优化策略,C1编译器上主要有方法内联,去虚拟化,冗余消除。

	方法内联:将引用的函数代码编译到引用点处,这样可以减少栈贞的生成,减少参数传递以及跳转过程。

	去虚拟化:对唯一的实现类进行内联。

	冗余消除:在运行期间把一些不会执行的代码折叠掉。

②C2的优化主要是在全局层面,逃逸分析是优化的基础。基于逃逸分析在C2上有如下几种优化:

	标量替换:用标量值代替聚合对象的属性值。

	栈上分配:对于未逃逸的对象分配对象在栈而不是堆。

	同步消除:消除同步操作,通常指synchronized。

4、分层编译(Tiered Compilation)策略:程序解释执行(不开启性能监控)可以出发C1 编译,将字节码编译成机器码,可以进行简单优化,也可以加上性能监控,C2编译会根据性能监控信息进行激进优化。在jdk7 后,一旦开发人员在程序中显示指定命令 “-server”时,默认将会开启分层编译策略,由C1编译器和C2编译器相互协作共同执行编译任务。

5、JIT 编译出来的机器码性能比解释器高。 C2编译器启动时长比C1编译器慢,系统稳定执行以后,C2编译器执行速度远远快于C1编译器。

7、String
①String的基本特征
1、String:字符串,使用一对 “ ” 引起来表示。

2、String 声明为 final 的,不可被继承。

3、String 实现了 Serializable 接口:表示字符串是支持序列化的。

				 实现了Comparable 接口:表示String可以比较大小。

4、String 在jdk8 及以前内部定义了 final char[]  value 用于存储字符串数据。jdk9 时改为byte[]。

5、String:代表不可变的字符序列。简称:不可变性。

		当字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value 进行赋值。

		当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value 进行赋值。

		当调用String的 replace() 方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。

6、通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。

7、字符串常量池中是不会存储相同内容的字符串的。

8、String 的String Pool 是一个固定大小的Hashtable  ,默认值大小长度是1009,。如果放进String Pool的String 非常多,就会造成Hash 冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern 时性能会大幅下降。

9、使用 -XX:StringTableSize 可设置StringTable 的长度。

10、在jdk6中StringTable 是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。StringTableSize设置没有要求。

11、在jdk7中,StringTable的长度默认值是60013,1009是可设置的最小值。

12、jdk8开始,设置StringTable的长度的话,1009 是可设置的最小值。

② String 的内存分配
1、在Java语言中有8种基本数据类型和一种比较特殊的类型String。这些类型为了使它们在运行过程中速度更快,更节省内存,都提供了一种常量池的概念。

2、常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种。

	直接使用双引号声明出来的String对象会直接存储在常量池中。比如:String info = "HOUYACHAO";

	如果不是用双引号声明的String对象,可以使用String提供的intern()方法,

3、Java 6 及以前,字符串常量池存放在永久代。

4、Java 7 中 Oracle 的工程师对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到 Java堆内。

		所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样可以让你在进行调优应用时仅需要调整堆大小就可以了。

		字符串常量池概念原本使用得比较多,但是这个改动使我们有足够的理由让我们重新考虑在 Java 7 中使用String.intern().

5、Java8 元空间,字符串常量在堆。

6、StringTable 为什么要调整?  因为PermSize 默认比较小,  还有就是永久代垃圾回收频率低。

③ 字符串拼接
1、常量与常量的拼接结果在常量池,原理是编译器优化。

2、常量池中不会存在相同内容的常量。

3、只有其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder。

4、如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象方法池中,并返回此对象的地址。如果常量池中已经存在了,直接将存在的地址返回。

@Test
public void test3() {
String s1 = “a”;
String s2 = “b”;
String s3 = “ab”;
/**
从字节码分析
如下s1 + s2 的执行细节:
① StringBuilder s = new StringBuilder();
②s.append(“a”)
③s.append(“b”)
④s.toString() —> 约等于 new String(“ab”)
在jdk5 之后使用的是StringBuilder,在jdk5 之前使用的是StringBuffer
**/
String s4 = s1 + s2;
System.out.println(s3 == s4); //false
}

/**
1、字符串拼接操作不一定使用的是StringBuilder
如果拼接符号左右两边都是字符串常量或常量引用,则仍然使用编译期优化,即非StringBuilder 的方式。
2、针对于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
}

/**
体会执行效率: 通过StringBuilder 的 append()的方式添加字符串的效率要远高于使用String的字符串拼接方式!
好处:①StringBuilder 的append()的方式:至始至终只创建一个StringBuilder的对象。
使用String的字符串拼接方式:创建多个StringBuilder 和 String的对象。
② 使用String的字符串拼接方式:内存中由于创建了较多的StringBuilder 和String对象;内存占用更大,如果进行GC,需要花费额外的时间。

改进的空间:在实际开发中,如果基本确定要前前后后添加的字符串长度不高于某个限定值highLevenl 的情况下,建议使用构造器:
	StringBuilder s = new StringBuilder(highLevel);// new char[highLevel]

**/
public class Test{
public static void main(String[] args) {

    test1();
    test2();
}

public void test1() {
    String src = "";
    for (int i = 1; i <= 10000000; i++) {
        src = src + "a";
    }
}
public void test2() {
    StringBuffer sb = new StringBuffer();
    for (int i = 1; i<= 10000000; i++) {
        sb = sb.append("a");
    }
}

}
④ intern()的使用
1、如果不是用双引号声明的String 对象,可以使用String提供的intern 方法:intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。

比如:String myInfo = new String("I Love hyc").intern();

也就是说,如果在任意字符串上调用String.intern 方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。因此,下列表达式值必定是 true:

	("a" + "b" + "c").intern == "abc";

通俗点将, Interned String 就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。这个值会被存放在字符串内存池(String Intern Pool)。

String s2 = new String(“a”)
/**
1、会创建两个对象,先在堆空间new String()对象,然后 在字符串常量池中 存一个 “ab”对象。

**/
String s1 = new String(“ab”);

/**
2、会创建6个对象。 可以根据字节码来查看
对象1: new StringBuilder()
对象2: new String(“a”)
对象3: 常量池的 “a"
对象4: new String(“b”)
对象5: 常量池的 “b”

深入剖析:StringBuilder 的toString();
 对象6: new String("ab")
 强调一下,toString() 的调用,在字符串常量池中,没有生成“ab”

**/
String s2 = new String(“a”) + new String(“b”);

public class StringInternTest{
public static void main(String[] args) {
String s = new String(“1”);
s.intern(); //调用此方法之前,字符串常量池中已经存在了 “1”
String s2 = “1”;
System.out.println(s == s2); //jdk6 false jdk7/8 false

    String s3 = new String("1") + new String("1");  //s3 变量记录的地址为: new String("11");     执行完上一行代码以后,字符串常量池中,是否存在 “11”呢?  答案:不存在、
    s3.intern();  //在字符串常量池中生成 “11”,如何理解:jdk6:创建了一个新的对象“11”,也就有新的地址。    jdk7:此时常量中并没有创建“11”。而是创建了一个指向堆空间中的 new String("11")的地址。
    String s4 = "11";  //s4 变量记录的地址: 使用的是上一行代码执行时,在常量池中生成的“11”的地址
    System.out.println(s3 == s4); //jdk6:false,  //jdk7/8:true
}

}

public class StringInternTest {

public static void main(String[] args) {
String s3 = new String(“1”) + new String(“1”); //new String(“11”)
String s4 = “11”;//字符串常量池中生成对象“11”
s3.intern();
System.out.println(s3 == s4); //false
}
}

public class StringInternTest {

public static void main(String[] args) {
String s3 = new String(“1”) + new String(“1”); //new String(“11”)
String s4 = “11”;//字符串常量池中生成对象“11”
String s5 = s3.intern();
System.out.println(s4 == s5); //true
}
}
总结String 的 intern() 的使用:

①JDK1.6 中,将这个字符串对象尝试放入串池。

	如果串池中有,则并不会放入。返回已有的串池中的对象的地址。

	如果没有,会把 此对象复制一份,放入串池,并返回串池中的对象地址。

②jdk1.7中,将这个字符串对象尝试放入串池。

	如果串池中有,则并不会放入。返回已有的串池中的对象的地址。	

	如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址。

总结

/**
使用intern() 测试执行效率: 空间使用上。
结论:对于程序中大量存在的字符串,尤其其中存在很多重复字符串时,使用intern() 可以节省内存空间、
**/
public class StringTest {
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]));  会new 1000000万个对象,并且字符串常量池中也会存在
        arr[i] = new String(String.valueOf(data[i % data.length])).intern();  //因为使用了intern(),所以在jdk7以后,字符串常量池中 会保存堆空间中 new 的对象的地址,部分在堆空间中创建的对象也会被回收。其实数据的引用是常量池中的字符堆中的引用、
    }
    long end = System.currentTimeMillis();
    System.out.println(end - start);
    
    try {
        Thread.sleep(1000000);
    } catch (Exception e) {
        
    }

}
}
大的网站平台,需要内存中存储大量的字符串。比如社交网站,很多人都存储:北京市、海定区等信息。这时候如果字符串都调用intern() 方法,就会明显降低内存的大小。

8、垃圾回收
① 什么是垃圾
1、垃圾收集,不是Java 语言的伴生产物。

2、垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的对象。

3、如归不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出。

4、GC 除了释放没有用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象。

②Java 的垃圾回收机制
1、自动内存管理,无需开发人员动手参与内存的分配和回收,这样降低内存泄露和内存溢出的风险。

2、自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发。

3、那些地方需要垃圾回收?



4、垃圾回收器可以对年轻代回收,也可以对老年代回收,甚至是全堆和方法区的回收。

		其中,Java 堆是垃圾收集器的工作重点。

5、从次数上讲:

	① 频繁收集Young 区

	② 较少收集old 区

	③基本不动 Perm 区

8.1 标记阶段
③ 垃圾标记阶段的算法之引用计数算法
1、垃圾标记阶段:对象存活判断

① 在堆里存放着几乎所有的Java 对象实例,在GC 执行垃圾回收之前,首先需要区分出内存中那些是存活的对象,哪些是已经死亡的对象。只要被标记为已经死亡的对象,GC 才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。	

②那么在JVM 中究竟是如何标记一个死亡对象呢? 简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判死亡了。

③ 判断对象存活一般有两种方式:  引用计数算法 和 可达性分析算法。

2、引用计数算法:

① 引用计数算法 (Reference Counting)比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。

② 对于一个对象A,只要有任何一个对象引用了A,则A 的引用计数器就加 1 ;当引用失效时,引用计数器就减 1。只要对象A 的 引用计数器的值为0,即表示对象A 不可能再被使用,可进行回收。

③ 优点:实现简单,垃圾对象便于识别;判断效率高,回收没有延迟性。

④ 缺点: 它需要单独的字段存储计数器,这样的做法增加了 存储空间的开销。	

				每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。

				引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java 的垃圾回收器中没有使用这类算法。

/**
-XX:+PrintGCDetails
**/
public class RefCountGC {
//这个成员属性唯一的作用就是占用一点内存
private byte[] bigSize = new byte[5 * 1024 * 1025];

Object reference = null;

public static void main(String[] args) {
    RefCountGC obj1 = new RefCountGC();
    RefCountGC obj2 = new RefCountGC();
    
    obj1.reference = obj2;
    obj2.reference = obj1;
    //显式的执行垃圾回收行为
    //System.gc();
}

}
如果使用引用计数算法,上面两个对象是不会被回收的。

⑥引用计数算法,是很多语言的资源回收选择,Python 使用了引用计数和垃圾收集机制。

⑦Java 并没有选择引用计数,是因为其存在一个基本的难题,也就是很难处理循环引用的关系。

⑧Python 如何解决循环引用?

	1、手动解除:很好理解,就是在合适的时机,解除引用关系。

	2、使用弱引用weakref , weakref 是Python 提供的标准库,解决循环引用。

④垃圾标记阶段的算法之可达性分析算法
1、可达性分析(或根搜索算法、追踪性垃圾收集)

2、相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地 解决在引用计数算法中循环引用的问题,防止内存泄露的发生、

3、相较于引用计数算法,这里的可达性分析就是 Java、C# 选择的。这种类型的垃圾收集通常也叫作 追踪性垃圾收集。

4、所谓 “GC Roots” 根集合就是一组必须活跃的引用。

5、基本思路:

① 可达性分析算法是以根对象集合 (GC Roots)为起始点,按照从上至下的方式 搜索被根对象集合所连接的目标对象是否可达。

② 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为 引用链。

③如果目标对象没有任何引用链项链,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象。

④在可达性分析算法中,只有能够被根对象集合直接或间接连接的对象才是存活对象、

6、在Java语言中,GC Roots 包括以下几类元素:

① 虚拟机栈中引用的对象。 比如:各个线程被调用的方法中使用到的参数,局部变量等。

②本地方法栈中 JNI (通常说的本地方法)引用的对象。

③方法区中类静态属性引用的对象。比如:Java类的引用类型静态变量。

④方法区中常量引用的对象。  比如 : 字符串常量池(String TABLE)里的引用。

⑤所有被同步锁synchronized 持有的对象。

⑥Java虚拟机内部的使用。 基本数据类型对应的Class 对象,一些常驻的异常对象(如:NullPointerException、OutOfMemoryError),系统类加载器。

⑦反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

7、处理这些固定的 GC Roots 集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象 “临时性”地加入,共同构成完整 GC Roots 集合。比如:分代收集和局部回收(Rartial GC)。

如果只针对Java 堆中的某一块区域进行垃圾回收 (比如:典型的只针对新生代),必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的,这个区域的对象完全有可能被其他区域的对象所引用,这时候就需要一并将关联的区域对象也加入 GC Roots集合中去考虑,才能保证可达性分析的准确性。

8、由于Root 采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root。

9、如果要使用可达性分析算法来判断内存是否回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。

10、这点也是导致GC进行时必须 “stop The World” 的一个重要原因。 即使是号称 (几乎)不会发生停顿的CMS 收集器中,枚举跟节点时也是必须要停顿的。

⑤ 对象的finalization 机制
1、Java 语言提供了对象终止(finalization)机制来运行开发人员提供 对象被销毁之前的自定义处理逻辑。

2、当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize() 方法。

3、finaize() 方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库链接等。

4、永远不要主动调用某个对象的finalize() 方法,应该交给垃圾回收机制调用。理由包括下面三点:

①在finalize()时可能会导致对象复活。

②finalize()  方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize() 会严重影响GC的性能。

③一个糟糕的finalize() 会严重影响GC的性能。

5、由于finalize() 方法的存在,虚拟机中的对象一般处于三种可能的状态。

6、如果从所有的跟节点都无法访问到某个对象,说明对象已经不再使用了。一般来说,此对象需要被回收。但事实上,也并非是 “非死不可”的,这时候他们暂时处于 “缓刑”阶段。一个无法触及的对象有可能在某一条件下 “复活”自己,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态、如下:

①可触及的: 从根节点开始,可以到达这个对象。

②可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。

③不可触及的: 对象的finalize()被调用,并且没有哦复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize只会被调用一次。

7、以上3中转改中,是由于finalize()方法的存在,进行 区别。只有在对象不可触及时才可以被回收。

8、判断一个对象的objA 是否可回收,至少要经历两次标记过程:

① 如果对象objA 到GC Roots 没有引用链,则进行第一次标记。

② 进行筛选,判断此对象是否有必要执行finalize()方法。

	1、如果对象objA 没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA 被判定为不可触及的。

	2、如果对象objA 重写了finalize()方法,且还未执行过,那么objA 会被插入到F-Queue 队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法执行。

	3、finalize()方法时对象逃逸死亡的最后机会。稍后GC会对F-Queue 队列中的对象进行第二次标记,。如果objA 在finalize()方法中与引用连上的任何一个对象建立了联系,那么在第二次标记时,objA 会被移除“即将回收”集合。之后,对象会再次出现没有引用的情况。在这个情况下,finalize方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize方法只会被调用一次。

⑥MAT 与 JProfiler 的GC Roots 溯源
1、MAT 是 Memory Analyzer 的简介,它是一款功能强大的Java堆内存分析器。用于查找内存泄露以及查看内存消耗情况。 MAT 是基于 Eclipse 开发的,是一款免费的性能分析工具。

2、可以通过 http://www.eclipse.ort/mat/ 下载并使用MAT。

3、使用JVisualVM 导出, 捕获的 heap dump 文件是一个临时文件,关闭JVisualVM 后自动删除,若要保留,需要将其另存为文件。

	可通过以下方法捕获heap dump:

	在左侧 “Application” (应用程序)子窗口中右击相应的应用程序,选择Heap Dump(堆Dump)。

	在Monitor(监视)子标签页中点击Heap Dump(堆Dump)按钮。

本地应用程序的Heap dumps 作为应用程序标签页的一个子标签页打开。同时,heap dump 在左侧的Application(应用程序)栏中对应一个含有时间戳的节点。右击这个节点选择save as (另存为)即可将heap dump 保存到本地。

8.2 清除阶段
当成功区分出内存中存活对象和死亡对象后,GC 接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可以内存空间为新对象分配内存。

在JVM 中比较常见的三种垃圾收集算法是  标记---清除算法(Mark-Sweep),复制算法(Copying)、标记---压缩算法(Mark--Compact)、

① 标记–清除(Mark–Sweep)算法
1、背景:

标记--清除算法(Mark--Sweep)是一种非常基础和常见的垃圾收集算法。

2、执行过程:

当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。

标记:COllector 从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。

清除:Collector 对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。

3、缺点:

①效率不算高。(因为要遍历两边堆空间的内存,第一遍历要标记存活的对象,第二次遍历是要清除没有被标记的对象)。

②在进行GC的时候,需要停止整个应用程序,导致用户体验差。

③这种方式清理出来的空间内存时不连续的,产生内存碎片。需要维护一个空闲列表。(在空闲列表中存放被清除的对象的地址,下次有新对象来的时候,直接去空闲列表中去查看是否能放的下)。

4、注意:何为清除?

这里所谓的清除并不是真的置空,而是把需要清除对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放。

②复制算法
1、核心思想:

将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交互两个内存的角色,最后完成垃圾回收。	

复制算法实例,在新生代中 S0 和 S1 中使用了复制算法。

2、优点:

①没有标记和清除过程,实现简单,运行效率高。

②复制过去以后保证空间的连续性,不会出现 “碎片”问题。

3、缺点:

①此算法的缺点也很明显的,就是需要两倍的内存空间。

②对于G1 这种分拆称为大量region 的GC,复制而不是移动,意味着GC 需要维护 region 之间对象引用关系,不管是内存占用或者时间开销也不小。

4、特别的:

如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量并不会太大,或者说非常低才行。

5、应用场景:

③标记-压缩(或标记-整理、Mark-Compact)算法
1、背景:

复制算法的高效性是建立在存活对象少,垃圾对象多的前提下。这种情况在新生代经常发生,但是在老年代,更场景的情况时大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法(可以使用标记压缩)。

2、标记–压缩算法的最终效果等同于 标记–清除 算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为 标记–清除–压缩(Mark–Sweep–Compact)算法。

3、二者的本质差异在于标记–清除算法是一种非移动式的回收算法,标记–压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策。

4、可以看到,标记的存活对象将会被整理,按照内存地址一次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,jvm 只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多的开销。

5、优点:

①清除了标记--清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。

②清除了复制算法当中,内存减半的高额代价。

6、缺点;

①从效率上来说,标记--整理算法要低于复制算法。

②移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。

③移动过程中,需要全程暂停用户应用程序。即:STW。

④ 三者算法的对比
标记–清除(Mark-Sweep) 标记整理(Mark-Compact) 复制(Copying)
速度 中等 最慢 最快
空间开销 少(但会堆积碎片) 少(不堆积碎片) 通常需要活对象的2倍大小(不堆积碎片)
移动对象 否 是 是
效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。

而为了尽量兼顾上面提到的三个指标,标记–整理 算法相对来说更平滑一些,但是效率上不尽人意,它比复制算法多了一个标记的阶段,比标记–清除多了一个整理内存的阶段。

⑤分代收集算法(具体问题具体分析)
1、分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采用不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高回收的效率。

2、在java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如 Http请求中的Session 对象,线程,Socket链接,这类对象跟业务直接挂钩,因此声明周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象声明周期会比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。

3、目前几乎所有的GC 都是采用分代收集(Generational Collectiong)算法执行垃圾回收的。

4、在HotSpot 中,基于分代的概念,GC 所使用的内存回收算法必须结合年轻代和老年代各自的特点:

①年轻代(Young Gen) 年轻代特点:区域相对老年代较小,对象生命周期较短,存活效率低,回收频繁。

这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过Hotspot 中的两个survivor 的设计得到缓解。

老年代(Tenured Gen ) 老年代的特点:区域较大,对象生命周期长,存活率高,回收不及年轻代频繁。

这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记–清除 或者是标记–整理的混合实现。

Mark 阶段的开销与存活对象的数量成正比。

Sweep阶段的开销与所管理区域的大小成正相关。

Compact 阶段的开销与存活对象的数据成正比。

5、以Hotspot 中的CMS 回收器为例,CMS 是基于Mark-Sweep 实现的,对于对象的回收效率很高,而对于碎片问题,CMS 采用基于Mark-Compact 算法的Serial Old 回收器作为补偿措施:当内存回收不佳(碎片导致的Concurren Mode Failure 时),将采用Serial Old 执行Full GC 以达到对老年代内存的管理。

6、分代的思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年代。

⑦增量收集算法、分区算法
1、上述现有的算法,在垃圾回收过程中,应用软件将处于一种 Stop the World 的状态。在 Stop the World 状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,将严重影响用户体验或者系统的稳定性。为了解决这个问题,即对实时垃圾收集算法的研究直接导致了增量收集(Incremental Collecting)算法的诞生。

2、基本思想:

如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。一次反复,直到垃圾收集完成。

总的来说,增量收集算法的基础仍是传统的标记--清除 和 复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作。

3、缺点:

使用这种方式,由于在垃圾回收过程中,间断性还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文切换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

⑧ 分区算法
1、一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC产生的停顿也就越长。为了更好地控制GC 产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。

2、分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间region。

3、每个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。

4、这些只是基本的算法思路,实际GC 实现过程要复杂的多,目前还在发展中的前言GC 都是复合算法,并且并行和并发兼备。

9、垃圾回收的相关概念
① System.gc() 的理解
1、在默认情况下,通过System.gc() 或者 Runtime.getRuntime().gc() 的调用,会显式触发Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。

2、然而System.gc() 调用附带一个免责声明,无法保证对垃圾收集器的调用。

3、JVM 实现者可以通过System.gc() 调用来决定 JVM 的GC 行为。而一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。 在一些特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用System.gc()。

public class SystemGCTest {

public static void main(String[] args) {
new SystemGCTest();

//提醒jvm 的垃圾回收器执行GC,但是不确定是否马上执行GC
System.gc();
//与Runtime.getRuntime().gc(); 作用一样。

System.runFinalization();//强制调用者引用的对象的finalize()方法
}

@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println(“SystemGCTest 重写了finalize”);
}
}

启动的时候 Tomcat添加 VM 参数: -XX:+PrintGCDetails

public class LocalVarGC {

public void loaclvarGC1() {
//从下面的 图片显示, GC 对象并没有被回收。
byte[] buffer = new byte[10 * 1024 * 1024];
System.gc();
}
public void loaclvarGC2() {
// GC 对象被回收了
byte[] buffer = new byte[10 * 1024 * 1024];
buffer = null;
System.gc();
}
public void loaclvarGC3() {
// GC 对象没有被回收,因为虽然是局部代码块,但是,它还占据着位置,在栈贞的槽的位置并没有被替代
{
byte[] buffer = new byte[10 * 1024 * 1024];
}
System.gc();
}
public void loaclvarGC4() {
// GC 对象被回收,因为它的槽的位置 被 value 所代替,
{
byte[] buffer = new byte[10 * 1024 * 1024];
}
int value = 10;
System.gc();
}
public void loaclvarGC5() {
// GC1 并没有回收对象, 在System.gc() 时被回收对象了
loaclvarGC1();
System.gc();
}

public static void main(String[] args) {
LocalVarGC localVarGC = new LocalVarGC();
localVarGC.loaclvarGC5();
}
}
方法1:

方法2:

方法3:

方法4:

方法5:

②内存溢出与内存泄露
1、内存溢出(OOM)

①内存溢出相对于内存泄露来说,尽管更容易理解,但是同样的,内存溢出也是引发程序崩溃的罪魁祸首之一。

②由于GC 一直在发展,所有一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现OOM的情况。

③大多数情况下,GC会进行各种年龄段的垃圾回收,实在不行了就放大招,来一次独占式的Full GC 操作,这时候会回收大量的内存,供应用程序继续使用。

④javadoc中对OutOfMemoryError 的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。

⑤首先说没有空闲内存的情况:说明Java虚拟机的堆内存不够。原因有二:

	5.1、Java虚拟机的堆内存设置不够。 比如:可能存在内存泄露问题,也很有可能就是堆的大小不合理,比如我们要处理比较客观的数据量,但是没有显式指定JVM堆大小或指定数值偏小。我们可以通过参数 -Xms、-Xmx 来调整。

	5.2 、代码中创建了大量大对象,并且长时间不能被垃圾收集器手机(存在被引用),对于老版本的Oracle JDK,因为永久代的大小是有限的,并且JVM 对永久代垃圾回收(如:常量池回收,卸载不再需要的类型)非常不积极,所以当我们不断添加新类型的时候,永久代出现OutOfMemoryError 也非常多见,尤其是在运行时存在大量动态类型生成的场合;类型intern 字符串缓存占用太多空间,也会导致OOM问题。对应的异常信息,会标记出来和永久代相关:“java.lang.OutOfMemoryError:  PermGen space”。

随着元数据区的引入,方法区内存已经不再那么窘迫,所有相应的OOM 有所改观,出现OOM,异常信息则变成了:“java.lang.OutOfMemoryError:Metaspace”。直接内存不足,也会导致OOM。

这里蕴含着一层意思是,在抛出OutOfMemoryError 之前,通常垃圾收集器会被触发,尽其所能去清理出空间。

例如:在引用机制分析中,涉及到JVM 会去尝试回收软引用指向的对象向等。

在java.nio.BIts.reserveMemory() 方法中,我们能清楚的看到,System.gc() 会被调用,以清理空间。

当然,也不是在任何情况下垃圾收集器都会被触发的。

比如:我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM 可以判断出垃圾收集并不能解决这个问题,所以就直接抛出OutOfMemoryError。

2、内存泄露(Memory Leak)

①也称为 “内存渗漏”。严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄露。

②但实际很多时候一些不太好的实践(或疏通)会导致对象的生命周期变得很长甚至导致OOM,也可以叫做宽泛意义上的 “内存泄露”。

比如:一个对象可以声明为局部变量,在方法调用完后就应该被清除掉,但是 用静态变量来修饰,设置为全局的,所以在引用完毕后,它一直会存在。这样可能会导致  “内存泄露”。

③尽管内存泄露并不会引起程序崩溃,但是一旦发生内存泄露, 程序中的可用内存就会被逐渐蚕食,直至耗尽所有内存,最终出现OutOfMemory 异常,导致程序崩溃。

④注意,这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。

举例:

1、单例模式:  单例的声明周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄露的产生。

2、一些提供close 的资源未关闭导致内存泄露。   数据库链接(DataSource.getConnection()),网络链接(socket)和 io 链接必须手动close,否则是不能被回收的。

个人理解上图:

图一,最上面三个节点没有被Root 根引用,所以在GC的时候,会直接被清除掉。

图二,最三面三个字节没有被Root 根引用,所以在GC的时候,会直接被清除掉。 而中间几个,应该也被清除的,不想再使用了,但是它被其他对象关联了,所以还是存在。会造成内存泄露的产生。

③Stop The World(STW)
1、Stop-the-World,简称 STW,指的是GC 事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。

①可达性分析算法中枚举跟节点(GCRoots)会导致所有Java执行线程停顿。

	分析工作必须在一个能确保一致性的快照中进行。

	一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上。

	如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证。

2、被STW 中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以我们需要减少STW的发生。

3、STW 事件和采用哪款GC无关,所有的GC都有这个事件。

4、哪怕是G1也不能完全避免Stop-the-word 情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。

5、STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。

6、开发中不要用System.gc() ;会导致Stop-the-world 的发生。

④垃圾回收的并行与并发
1、并发(Concurrent)

①在操作系统中,是指一个时间段中有几个程序都处于已启动到运行完毕之间,且这几个程序都是在同一个处理器上运行。

②并发不是真正意义上的 “同时进行”,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间来回切换,由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行。

2、并行(Parallel)

①当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,我们称之为并行(Parallel)。

②其实决定并行的因素不是CPU的数量,而是CPU的核心数量,比如一个CPU多个核也可以并行。

③适合科学计算,后台处理等弱交互场景。

3、并行 VS 并发

①二者对比

并发,指的是多个事情,在同一时间段内同时发生了。

并行,指的是过个事情,在同一时间点上同时发生了。

并发的多个任务之间是互相抢占资源的。

并行的多个任务之间是不互相抢占资源 的。

只有在多CPU 或者一个 CPU 多核的情况中,才会发生并行。

否则,看似同时发生的事情,其实都是并发执行的。

⑤ 垃圾回收的并发与并行
1、并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。

如:ParNew,Parallel,Scavenge,Parallel Old。

2、串行(Serial):

相较于并行的概念,单线程执行。

如果内存不够,则程序暂停,启动JVM 垃圾回收器进行垃圾回收。回收完,再启动程序的线程。

3、并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行。

①用户程序在继续运行,而垃圾收集程序线程运行与另一个CPU上。

②如:CMS,G1。

⑥ 再谈引用
1、在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。

2、除强引用外,其他3种引用均可以在java.lang.ref 包中找到他们。

3、Reference 子类中只有终结器引用是包内可见的,其他3种引用类型均为public,可以在应用程序中直接使用。 ①强引用(StrongReference):最传统的 “引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似 “Object obj = new Object() ; " 这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

②软引用(SoftReference): 在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。

③弱引用(WeakReference): 被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。

④虚引用(PhantomReference): 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。(对象的跟踪)

6.1、强引用(Strong Reference)-----不回收
①在java程序中,最常见的引用类型是强引用(普通系统99% 以上都是强引用),也就是我们最常见的普通对象引用,也是默认的引用类型。

②当在java语言中使用 new 操作符创建一个新的对象,并将其赋值给一个变量的时候,这个变量就称为指向该对象的一个强引用。

③强引用的对象是可触及的,垃圾收集器就永远不会回收掉被引用的对象。

④对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,就是可以当做垃圾被收集了,当然具体回收时还是要看垃圾收集策略。

⑤相对的,软引用、弱引用和虚引用的对象都是软可触及、弱可触及和许可触及的,在一定条件下,都是可以被回收的。所以,强引用是造成java内存泄露的主要原因之一。

⑥如果从所有的跟节点都无法访问到某个对象,说明对象已经不再使用了。一般来说,此对象需要被回收。但事实上,也并非是 “非死不可”的,这时候他们暂时处于 “缓刑”阶段。一个无法触及的对象有可能在某一条件下 “复活” 自己,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态。如下:

1、可触及的:从跟节点开始,可以到达这个对象。

2、可复活的: 对象的所有引用都被释放,但是对象有可能在 finalize() 中复活。

3、不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为 finalize()只会被调用一次。

⑦以上3种状态中,是由于finalize()方法的存在,进行的区分,,只有在对象不可触及时才可以被回收。

6.2、软引用(Soft Reference)----内存不足即回收
① 只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。

② 软引用通常用来实现内存敏感的缓存。 比如:高速缓存 就有用到软引用。 如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

③ 垃圾回收器在某个时刻决定回收软可达的对象的时候,会清理软引用,并可选地把引用存放到一个引用队列(Reference Queue)。

④ 类似软弱引用,只不过Java 虚拟机会尽量让软引用的存活时间长一些,迫不得已才清理。

⑤ 当内存足够 ----> 不会回收软引用的可达对象。。 当内存不够时 ----> 会回收软引用的可达对象。

Object obj = new Object(); //声明强引用

SoftReference sf = new SoftReference<>(obj);

obj = null; //销毁强引用


/**

  • @author HouYC
  • @create 2020-08-01-13:09
  • 软引用
    -Xms10m -Xmx10m -XX:+PrintGCDetails
    */
    public class SoftReferenceTest {
    public static class User {
    public int id;
    public String name;

    @Override
    public String toString() {
    return “User{” +
    “id=” + id +
    “, name=’” + name + ‘’’ +
    ‘}’;
    }

    public User(int id, String name) {
    this.id = id;
    this.name = name;
    }

    public int getId() {
    return id;
    }

    public void setId(int id) {
    this.id = id;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }
    }

    public static void main(String[] args) {
    //创建对象,建立软引用
    SoftReference sf = new SoftReference<>(new User(1, “侯亚超”));
    System.out.println(sf.get());

    System.gc();
    System.out.println(“After GC”);

    System.out.println(sf.get());

    try {
    //让系统认为内存资源紧张,不够,要回收软引用。
    byte[] b = new byte[1024 * 7168 - 666 * 1024];
    } catch (Exception e) {
    e.printStackTrace();
    } finally {
    //再次获取软引用
    System.out.println(sf.get());
    }
    }
    }

6.3、弱引用(Weak Reference)—发现即回收
① 弱引用也是用来描述那些非必须对象,被弱引用关联的对象只要生存到下一次垃圾收集发生为止。在系统 GC时,只要发现弱引用,不管系统堆空间使用示范法充足,都会回收掉只被弱引用关联的对象。

②但是,由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间。

③弱引用和软引用一样,在构造弱引用时,也可以指定一个弱用队列,当弱引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况。

④ 软引用、弱引用 都非常适合来保存那些可有可无的缓存数据。如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用。

⑤ 在jdk 1.2 版之后提过了java.lang.ref.WeakReference 类 来实现弱引用。

Object obj = new Object(); //声明强引用

WeakReference wr = new WeakReference(obj);

obj = null; //销毁强引用
⑥弱引用对象与软引用对象的最大不同就在于,当GC 在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,GC总是进行回收。弱引用对象更容易、更快被GC回收。


/**

  • @author HouYC
  • @create 2020-08-01-13:45
  • 弱引用

*/
public class WeakReferenceTest {
public static class User {
public int id;
public String name;

@Override
public String toString() {
return “User{” +
“id=” + id +
“, name=’” + name + ‘’’ +
‘}’;
}

public User(int id, String name) {
this.id = id;
this.name = name;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

public static void main(String[] args) {
//构造了弱引用
WeakReference wr = new WeakReference<>(new User(1, “侯亚超”));
//从弱引用中重新获取对象
System.out.println(wr.get());

System.gc();

//不管当前内存足够与否,都会回收它的内存
System.out.println(“after GC”);
System.out.println(wr.get());
}
}

⑧ 你开发中用过 WeakHashMap 吗?

6.4、虚引用(Phantom Reference)–对象回收跟踪
① 虚引用是所有引用类型中最弱的一个。

②一个对象是否有虚引用的存在,完全不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时都可能被垃圾回收器回收。

③它不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的get() 方法获取得对象时,总是null。

④ 为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知。

⑤ 虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。

⑥由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录。

Object obj = new Object();
//引用队列
ReferenceQueue ph = new ReferenceQueue();
PhantomReference pf = new PhantomReference(obj, ph);
obj = null;


/**

  • @author HouYC
  • @create 2020-08-01-16:18
    虚引用
    /
    public class PhantomReferenceTest {
    /
    *
    • 当前类的声明
      /
      public static PhantomReferenceTest obj;
      /
      *
    • 引用队列
      */
      static ReferenceQueue phantomQueue = null;

      public static class CheckRefQueue extends Thread {
      @Override
      public void run() {
      while (true) {
      if (phantomQueue != null) {
      PhantomReference objt = null;
      try {
      objt = (PhantomReference) phantomQueue.remove();
      } catch (Exception e) {
      e.printStackTrace();
      }

      if (objt != null) {
      System.out.println(“追踪垃圾回收过程: PhantomReferenceTest 实例被GC了!”);
      }
      }
      }
      }
      }

      @Override
      public void finalize() throws Throwable {
      super.finalize();
      System.out.println(“调用当前类的finalize() 方法”);
      //第一次调用后,将这个对象复活
      obj = this;
      }

      public static void main(String[] args) {
      Thread t = new CheckRefQueue();
      //设置守护线程 即第一调用完后断掉,不再执行
      t.setDaemon(true);
      t.start();

      phantomQueue = new ReferenceQueue();
      obj = new PhantomReferenceTest();
      //构造了PhantomReferenceTest 对象的虚引用,并指定了引用队列
      PhantomReference phantomReference = new PhantomReference<>(obj, phantomQueue);
      try {
      //不可获取虚引用中的对象
      System.out.println(phantomReference.get());

      //将强引用去除
      obj = null;

      //第一次GC,由于对象可以复活,GC无法回收对象
      System.gc();
      Thread.sleep(1000);
      if (obj == null) {
      System.out.println(“obj 1 是null”);
      } else {
      System.out.println(“obj 1 不是null”);
      }

      System.out.println(“第二次回收”);
      obj = null;

      System.gc();

      Thread.sleep(1000);
      if (obj == null) {
      System.out.println(“obj 2 是null”);
      } else {
      System.out.println(“obj 2 不是null”);
      }

      } catch (Exception e) {
      e.printStackTrace();
      }
      }
      }

      //输出结果
      null
      调用当前类的finalize() 方法
      obj 1 不是null
      第二次回收
      追踪垃圾回收过程: PhantomReferenceTest 实例被GC了!
      obj 2 是null
      10、垃圾回收器
      ①GC分类与性能指标
      1、按线程数分: 可以分为串行垃圾回收器和并行垃圾回收器。

2、串行回收指的是在同一时间段内只允许有一个CPU 用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。

在诸如单CPU 处理器或者较小的应用内存等硬件平台不是特别优越的场合,串行回收器的性能表现可以超过并行回收器和并发回收器。所以,串行回收默认被应用在客户端的Client 模式下的JVM中(Windows 32位 默认为Client ,Windows64位 默认为Servlet)。

在并发能力比较强的CPU上,并行回收器产生的停顿时间要短于串行回收器。

3、和串行回收相反,并行收集可以运用多个CPU 同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了 “Stop-the-world”机制。

4、按工作模式分, 可以分为并发式垃圾回收器和独占式垃圾回收器。

5、按碎片处理方式分, 可分为压缩式垃圾回收器和非压缩式垃圾回收器。

①压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片。  再分配对象空间使用:指针碰撞。

②非压缩式的垃圾回收器不进行这不操作。  再分配对象空间使用: 空闲列表。

6、按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器。

②评估GC的性能指标
1、吞吐量:
运行用户代码的时间占总运行时间的比例。(总运行时间:程序的运行时间 + 内存回收的时间)。

2、暂停时间:
执行垃圾收集时,程序的工作线程被暂停的时间。

① 吞吐量(throughput):

吞吐量就是CPU用于运行用户代码与CPU 总耗费时间的比值。即吞吐量 = 运行用户代码时间/(运行用户代码时间 + 垃圾收集时间)。  比如: 虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的。

吞吐量优先,意味着在单位时间内,STW的时间最短: 0.2 + 0.2 = 0.4;

②暂停时间(pause time):

暂停时间 是指一个时间段内应用程序线程暂停,让GC 线程执行的状态。 例如,GC 期间100毫秒的暂停时间意味着这100毫秒期间内没有应用程序线程是活动的。

暂停时间优先,意味着尽可能让单次STW的时间最短:0.1 + 0.1 + 0.1 + 0.1 + 0.1 = 0.5;

在设计(或使用)GC算法时,我们必须确定我们的目标: 一个GC 算法只可能针对两个目标之一(即只专注于吞吐量或最小暂停时间),或尝试找到一个二者的折中。

现在标准:在最大吞吐量优先的情况下,降低停顿时间。

11、不同垃圾回收器概述
11.1、7中垃圾回收器
①串行回收器
1、Serial

2、Serial Old

②并行回收器
1、ParNew

2、Parallel Scavenge

3、Parallel Old

③ 并行回收器
1、CMS

2、G1

11.2、7款收集器与垃圾分代之间的关系

① 新生代收集器:
Serial 、ParNew、Parallel Scavenge;

②老年代收集器:
Serial Old, Parallel Old 、CMS;

③整堆收集器:
G1。

④垃圾收集器的组合关系

虚线 是 JDK8 (不包括jdk8)之前的代码的组合方式。

⑤ 如何查看默认的垃圾收集器

-XX:+PrintCommandLineFlags : 查看命令行相关参数(包含使用的垃圾收集器)

使用命令行指令: jinfo - flag 相关垃圾回收器参数 进程Id
11.3、Serial回收器:串行回收器
1、Serial 收集器是最基本,历史最悠久的垃圾收集器了。Jdk1.3 之前回收新生代唯一的选择。

2、Serial 收集器作为 Hotspot 中 Client 模式下的默认新生代垃圾收集器。

3、Serial 收集器采用复制算法、串行回收 和 “Stop- the- World” 机制的方式执行内存回收。

4、除了年轻代之外,Serial 收集器还提供了执行老年代垃圾收集器的 Serial Old 收集器。Serial Old 收集器同样也采用了串行回收 和 “Stop-the-World”机制,只不过内存回收算法使用的是 标记–压缩算法。

Serial Old 是运行在Client 模式下默认的老年代的垃圾回收器。

Serial Old 在Server 模式下主要有两个用途:① 与新生代的Parallel Scavenge 配合使用  。 ②作为老年代CMS 收集器的后备垃圾收集方案。



这个收集器时一个单线程的收集器,但它的 “单线程” 的意义并不仅仅说明它只会使用一个CPU 或一个收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World)。

5、优势: 简单与高效(与其他收集器的单线程比)。 对于限定单个CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得更高的单线程收集效率。

	运行在Client 模式下的虚拟机是个不错的选择。

6、适用场景: 在用户的桌面应用场景中,可用内存一般不大(几十MB 至 一两百MB),可以在较短时间内完成垃圾收集(几十ms 至一百对ms),只要不频繁发生,使用串行回收器是可以接受的。

7、在HotSpot 虚拟机中,使用 -XX:+UseSerialGC 参数可以指定年轻代和老年代都使用串行收集器。

	等价于 新生代用SerialGC,且老年代用Serial Old GC。

8、Serial 这种垃圾收集器现在已经都不使用了,而且在限定单核CPU才可以用、现在都不是单核的了。 对于交互较强的应用而言,这种垃圾收集器是不能接受的。一般在Java web 应用程序中是不会采用串行垃圾收集器的。

11.4、ParNew 回收器:并行回收
1、如果说Serial GC 是年轻代中的单线程垃圾收集器,那么ParNew收集器则是Serial 收集器的多线程版本。

		Par 是 Parallel 的缩写,New 只能处理的是新生代。

2、ParNew 收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。ParNew 收集器在年轻代中同样也是采用复制算法、“Stop–the–World”机制。

3、ParNew 是很多JVM 运行在Server 模式下新生代的默认垃圾收集器。

对于新生代,回收次数频繁,使用并行方式高效。

对于老年代,回收次数少,使用串行方法节省资源。(CPU并行需要切换线程,串行可以省去切换线程的资源)。

4、由于ParNew 收集器是基于并行回收,那么是否可以断定ParNew 收集器的回收效率在任何场景下都会比Serial 收集器更高效呢?

①ParNew:  收集器运行在多CPU 的环境下,由于可以充分利用多CPU、多核心等物理硬件资源优势,可以更快地完成垃圾收集,提升程序的吞吐量。

②但是在单个CPU 的环境下,ParNew收集器不比Serial  收集器更高效。虽然Serial 收集器时基于串行回收,但是由于CPU 不需要频繁地做任务切换,因此可以有效避免多线程交互过程中产生的一些额外开销。

5、因为除Serial外,目前只有ParNew GC 能与CMS 收集器配合工作。

6、可以通过 “-XX: +UseParNewGC” 手动指定使用ParNew 收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代。 -XX:ParallelGCThreads 限制线程数量,默认开启和CPU数据相同的线程数。

11.5、Parallel 回收器:吞吐量优先
1、HotSpot 的年轻代中除了拥有ParNew收集器是基于并行回收的以为,Parallel Scavenge 收集器同样也采用了 复制算法、并行回收 和 “Stop-the-world”机制。

2、那么Parallel 收集器的出现是否多次一举??

	和ParNew 收集器不同,Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器。

	自适应调节策略也是 Parallel Scavenge 与 ParNew一个重要区别。

3、高吞吐量则是可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。例如:那些执行批量处理,订单处理,工资支付,科学计算的应用程序。

4、Parallel 收集器在Jdk1.6 时提供了用于执行老年代垃圾收集的 Paraller Old 收集器,用来替代老年代的Serial Old 收集器。

5、Parallel Old 收集器采用了 标记–压缩算法,但同样也是基于 并行回收 和 “Stop-the-world”机制。

6、在程序吞吐量优先的应用场景下,Parallel 收集器和 Parallel Old 收集器的组合,在Server 模式下的内存回收性能很不错。

7、Jdk8中,默认是Parallel Scavenge 垃圾收集器。

8、参数设置:

① -XX:+UseParallelGC : 手动指定年轻代使用Parallel 并行收集器执行内存回收任务。

② -XX:+UseParallelOldGC : 手动指定老年代都是使用并行回收收集器。
1、分别适用于新生代和老年代。默认jdk8是开启的。
2、上面两个参数,默认开启一个,另一个也会被开启。(互相激活)

③ -XX:ParallelGCThreads : 设置年轻代并行收集器的线程数。一般地,最好与CPU 数量相等,以避免过多的线程数影响垃圾收集性能。
1、在默认情况下,当CPU 数量小于8个,ParallelGCThreads 的值等于CPU数量。
2、当CPU数量大于8个,ParallelGCThreads 的值等于 3+[5*CPU_Count]/8

④ -XX:MaxGCPauseMillis :设置垃圾收集器最大停顿时间(即STW的时间)。单位是毫秒。
1、为了尽可能地把停顿时间控制在MaxGCPauseMills以为,收集器在工作时会调整Java堆大小或者其他一些参数。
2、对于用户来说,停顿时间越短体验越好。但是在服务器端,我们注重高并发,整体的吞吐量,所以服务器端适合Parallel,进行控制。
3、该参数使用需谨慎。

⑤-XX:GCTimeRatio : 垃圾收集时间占总时间的比例(= 1/(N+1)). 用于衡量吞吐量的大小。
1、取值范围(0,100)。默认值99,也就是垃圾回收时间不超过1%、
2、与前一个-XX:MaxGCPauseMillis 参数有一定矛盾性,暂停时间越长,Radio 参数就容易超过设定的比例。

⑥ -XX:+UseAdaptiveSizePolicy : 设置Parallel Scavenge 收集器具有自适应调节能力。
1、在这种模式下,年轻代的大小,Eden 和 Survivor 的比例,晋升老年代的对象年龄参数会自动调整,已达到在堆大小,吞吐量和停顿时间之间的平衡点。
2、在手动调优比较困难的场合,可以直接使用这种自适应的方式。仅指定虚拟机的最大堆,目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMills),让虚拟机自己完成调优工作。

11.6、 CMS回收器:低延迟
1、在jDK1.5 时期,HotSpot 推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器:CMS(Concurrent-Mark-Sweep)收集器,这款收集器是HotSpot 虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。

2、CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。

①目前很大一部分的Java应用集中在互联网或者B/S 系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。

3、CMS的垃圾收集算法采用 标记–清除算法,并且也会 “Stop-the-world”。

4、不幸的是,CMS 作为老年代的收集器,却无法与JDK1.4.0 中已经存在的新生代收集器Parallel Scavenge 配合工作,所以在JDK1.5 中使用CMS 来收集老年代的时候,新生代只能选择ParNew 或者Serial 收集器中的一个。

5、在G1出现之前,CMS 使用还是非常广泛的,一直到今天,仍然有很多系统使用CMS GC。。

7、CMS 的工作原理:

CMS 整个过程比之前的收集器要复杂,整个过程分为4 个主要阶段,即 初始标记阶段、并发标记阶段、重新标记阶段 和 并发清除阶段。

① 初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都会被因为 “Stop-the-world”机制而出现短暂的暂停,这个阶段的主要任务 仅仅只是标记出GC Roots 能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。

② 并发标记(Concurrent–Mark)阶段: 从GC Roots 的直接关联对象开始遍历整个对象图的过程。 这个过程耗时较长 但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。

③重新标记(Remark)阶段: 由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。

④并发清除(Concurrent–Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

8、尽管CMS收集采用的是并发回收(非独占式),但是在其初始化标记和再次标记着两个阶段中仍然需要执行 “Stop-the-world” 机制暂停程序中的工作线程,不过暂停时间不会太长,因此可以说明目前所有的垃圾收集器都做不到完全不需要 “Stop-the-world”,只是尽可能地缩短暂停时间。

9、由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收时低停顿的。

10、另外,由于在垃圾收集阶段用户线程没有中断,所以CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率到达某一阈值时,便开始进行回收,以确保应用程序在CMS 工作过程中依然有足够的空间支持应用程序运行。要是CMS运行期间预留的内存无法满足程序需求,就会出现一次 “Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用 Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。

11、CMS 收集器的垃圾收集算法采用的是 标记–清除算法,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一块内存块,不可避免地将会产生一些内存碎片。那么CMS 在为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配。

12、有人会觉得既然Mark Sweep会造成内存碎片,那么为什么不把算法换成 Mark Compact 呢?

因为当并发清除的时候,用Compact 整理内存的话,原来的用户线程使用的内存还怎么用呢?标记整理算法,需要将对象进行移动,需要回收的放到一起,不需要回收的放到一起,所以如果要使用标记压缩算法,会导致对象的地址发生移动变化,给正在使用的线程对象造成报错。要确保用户线程能继续执行,前提的它运行的资源不受影响,Mark Compact 更适合 “Stop-the-world” 这种场景下使用。

13、CMS的优点:

①并发收集。

②低延迟。

14、CMS的弊端:

① 会产生内存碎片,导致并发清除后, 用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发Full GC。

②CMS 收集器堆 CPU 资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。

③CMS收集器无法处理浮动垃圾。可能出现 “Concurrent Mode Failure” 失败而导致另一次FUll GC 的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被即时回收,从而只能在下一次进行GC时释放这些之前未被回收的内存空间。

15、CMS参数:

① -XX:+UseCMSCompactAtFullCollection : 用于指定在执行完Full GC 后堆内存空间进行压缩整理,因此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了。

② -XX:CMSFullGCsBeforeCompaction : 设置在执行多少次FUll GC 后 堆内存空间进行压缩整理。

③ -XX:ParallelCMSThreads : 设置CMS的线程数量。
1、CSM 默认启动的线程数是 (ParallelGCThreads + 3)/4,ParallelGCThreads 是年轻代并行收集器的线程数。当CPU 资源比较紧张时,收到CMS 收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕。

④ -XX:+UseConcMarkSweepGC : 手动指定使用CMS 收集器执行内存回收任务。
1、开启该参数后会自动将 -XX:+UseParNewGC 打开。即:ParNew(Young区用) + CMS(old区用)+ Serial Old的组合。

⑤ -XX:CMSInitiatingOccupanyFraction : 设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。
1、JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68% 时,会执行一次CMS 回收,JDK6及以上版本默认值为92%。
2、如果内存增长缓慢,则可以设置一个稍大的值,大的阈值可以有效降低CMS 的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低Full GC 的执行次数。
16、小结:

Hotspot 有这么多的垃圾回收器,那额如果有人问,Serial GC,Parallel GC,Concurrent Mark Sweep GC 这是三个 GC 有什么不同呢?

① 如果你想要最小化地使用内存和并行开销,请选Serial GC。

②如果你想要最大化应用程序的吞吐量,请选Parallel GC。

③如果你想要最小化GC 的中断或停顿时间,请选CMS GC。

17、JDK9新特性中,CMS 被标记为Deprecate了(就是以后会被废除)。

18、JDK14新特性:删除CMS 垃圾回收器。

11.7 G1(Garbage First)回收器:区域分代化式
①基本概念
1、既然我们已经有了前面几个强大的GC,为什么还要发布Garbage First(G1)GC?

原因就在于应用程序所应对的业务越来越庞大,复杂,用户越来越多,没有GC就不能保证应用程序正常进行,而经常造成STW的GC 又跟不上实际的需求,所以才会不断地尝试对GC进行优化。G1(Garbage--First)垃圾回收器是Java7 UPDATE4 之后引入的一个新的垃圾回收器,是当今收集器技术发展的最前沿成果之一。

与此同时,为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂时时间(pause time),同时兼顾良好的吞吐量。

官方给G1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才担当起 “全功能收集器” 的重任与期望。

2、为什么名字叫做Garbage First (G1)呢?

①因为G1是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)(物理上不连续的)。使用不同的Region 来表示Eden,幸存者0区,幸存者1区,老年代等。

②G1 GC有计划地避免在整个Java堆中进行全区域的垃圾收集。G1 跟踪各个Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。

③由于这种方式的侧重点在于回收垃圾最大量的区域(Region),所以我们给G1一个名字:垃圾优先(Garbage First)。

3、G1 (Garbage–First)是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU 及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征。

4、在jdk7 版本正式启用,移除了 Experimental 的标识,是JDK 9以后的默认垃圾回收器,取代了CMS回收器以及Parallel + parallel Old组合。被Oracle 官方称为 “全功能的垃圾收集器”。

5、与此同时,CMS 已经在 JDK9 中被标记为 废弃(deprecated)。在jdk8中还不是默认的垃圾回收器,需要使用 -XX:+UseG1GC 来启用。

②G1 回收器的特点
1、与其他GC 收集器相比,G1使用了全新的分区算法,其特点如下:

①并行与并发

	并行性:G1在回收期间可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW。

	并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会再整个回收阶段发生阻塞应用程序的情况。

②分代收集

	从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区,但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。

将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。

和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代。

③可预测的停顿时间模型(即:软实时soft real--time )

	这是G1相对于CMS 的另一大优势,G1 除了追求地停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒、

	由于分区的原因,G1 可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。

	G1跟踪各个Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护了一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

	相比于CMS GC,G1 未必能做到CMS 在最好情况下的延时停顿,但是最差情况要好很多。

④空间整合

	CMS: “标记--清除”算法,内存碎片,若干次GC后进行一次碎片整理。

	G1将内存划分为一个个的region,内存的回收是以region 作为基本单位的。Region 之间是复制算法,但整体上实际可以看做是 标记--压缩(Mark--Compact)算法,两种算法都可以避免内存碎片,这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC,尤其是当Java堆非常大的时候,G1的优势更加明显。

2、G1回收器的缺点

①相当于CMS,G1 还不具备全方位,压倒性优势,比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(Overload)都要比CMS要高。

②从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势,平衡点6-8GB之间。

③G1回收器的参数设置
-XX:+UseG1GC : 手动指定使用G1收集器执行内存回收任务。

-XX:G1HeapRegionSize : 设置每个Region的大小。值是2 的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000

-XX:MaxGCPauseMillis : 设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到)。默认值为200ms。

-XX:ParallelGCThread : 设置STW时GC线程数的值,最多设置为8

-XX:ConcGCThreads : 设置并发标记的线程数,将n 设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。

-XX:InitiltingHeapOccupancyPercent : 设置触发并发GC周期的Java堆占用率阈值,超过这个值,就触发GC,默认值为45.
G1的设计原则就是简化JVM 性能调优,开发人员只需要简单的三个步骤就可以完成调优:

第一步:开启G1 垃圾收集器。

第二步:设置堆的最大内存。

第三步:设置最大的停顿时间。

G1中提供了三种垃圾回收模式: YoungGC 、Mixed GC 和 Full GC,在不同的条件下被触发。

④ G1回收器的适用场景
1、面向服务端应用,针对具有大内存,多处理器的机器。(在普通大小的堆里表现并不惊喜)。

2、最主要的应用时需要低GC延迟,并具有大堆的应用程序提供解决方案。

3、如:在堆大小约6GB 或更大时,可预测的暂停时间可以低于0.5秒,(G1 通过每次只清理一部分而不是全部的region 的增量式清理来保证每次GC停顿时间不会过长)。

4、用来替换掉JDK1.5中的CMS 收集器:

在下面的情况时,使用G1可能比CMS好。

①超过50% 的java堆被活动数据占用。

②对象分配频率或年代提升频率变化很大。

③GC停顿时间过长(长于0.5至1秒)。

5、HotSpot 垃圾收集器里,除了G1以外,其他的垃圾收集器使用内置的JVM 线程执行GC的多线程操作,而G1 GC 可以采用应用线程承担后台运行的GC 工作,即当JVM 的GC 线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程。

⑤G1回收器中的Region:化整为零
1、使用G1 收集器时,它将整个Java堆划分成约2048个大小相同的独立Region 块,每个Region 块大小根据堆空间的实际大小而定,整体被控制在1MB 到32MB之间,且为2的N次幂,即 1MB、2MB、4MB、8MB、16MB、32MB。可以通过 -XX:G1HeapRegionSize 设定。所有的Region 大小相同,且在JVM 声明周期内不会被改变。

2、虽然还保留着有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,他们都是一部分Region(不需要连续)的集合,通过Region 的动态分配方式实现逻辑上的连续。

一个Region 有可能属于Eden,Survivor 或者Old\Tenured 内存区域。但是一个Region只可能属于一个角色。图中的 E 表示该region 属于 Eden 内存区域, s 表示属于Survivor 内存区域,o 表示属于old 内存区域,图中空白的表示未使用的内存空间。

G1垃圾收集器还增加了一种新的内存区域,叫做 Humongous 内存区域,如图中的 H 块。主要用于存储大对象,如果超过1.5个region ,就放到H。

设置H的原因:

对于堆中的大对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous 区,它用来专门存放大对象,如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储。为了能找到联系单H区,有时候不得不启动FullGC,G1的大多数行为都把H区作为老年代的一部分来看待。		

⑥G1回收器回收过程
1、G1 GC 的垃圾回收过程主要包括如下3个环节:

① 年轻代GC (Young GC)

②老年代并发标记过程 (Concurrent Marking)

③混合回收(Mixed GC)

④(如果需要、单线程、独占式、高强度 的 Full GC 还是继续存在的。它针对GC 的评估失败提供了一种失败保护机制,即强力回收)。

回收过程:顺时针,young Gc —> young gc + concurren mark —> Mixed GC 顺序,进行垃圾回收。

① 应用程序分配内存,当年轻代的Eden 区用尽时开始年轻代回收过程:G1 的年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期,G1 GC 暂停所有应用程序线程,启动多线程执行年轻代回收。然后 从年轻代区间移动存活对象到Survivor 区间或者老年区间,也有可能是两个区间都会涉及。

② 当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程。

③ 标记完成马上开始混合回收过程。对于一个混合回收期,G1 GC 从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。和年轻代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描\回收一小部分老年代的Region 就可以了。同时,这个老年代Region 是和年轻代一起被回收的。

举个例子: 一个Web服务器,Java进程最大堆内存为4G,每分钟响应1500个请求,每45秒钟会新分配大约2G的内存。G1会每45秒钟进行一次年轻代回收,每31个小时整个堆的使用率会达到45%,会开始老年代并发标记过程,标记完成后开始四到五次的混合回收。

⑦ G1 回收器垃圾回收过程:Remembered Set
回收过程中的问题:

①一个对象被不同区域引用的问题。

②一个Region 不可能是孤立的,一个Region中的对象可能被其他任意Region 中对象引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确?

③在其他的分代收集器,也存在这样的问题(而G1更突出)

④回收新生代也不得不同时扫描老年代?

⑤这样的话会降低Minor GC 的效率。

解决方法:

①无论G1还是其他分代收集器,JVM 都是使用Remembered Set 来避免全局扫描。

②每个Region 都有一个对应的Remembered Set;

③每次Reference 类型数据写操作时,都会产生一个Write Bbarrier 暂时中断操作。

④然后检查将要写入的引用指向的对象是否和该Reference 类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象)。

⑤如果不同,通过CardTable 把相关引用信息记录到引用指向对象的所在Region 对应的Remembered Set中。

⑥当进行垃圾收集时,在GC跟节点的枚举范围加入Remembered Set. 就可以保证不进行全局扫描,也不会有遗漏。

⑧G1 回收器垃圾回收的整个过程

回收过程: 顺时针,young Gc —> young gc + concurren mark —> Mixed GC 顺序,进行垃圾回收。

1、G1 回收过程一:年轻代GC

JVM 启动时,G1先准备好Eden 区,程序在运行过程中不断创建对象到Eden区,当Eden空间耗尽时,G1会启动一次年轻代垃圾回收过程。

年轻代垃圾回收只会回收Eden区 和 Survivor 区。

YGC 时,首先G1 停止应用程序的执行(Stop-the-world),G1创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代 Eden 区和 Survivor 区所有的内存分段。

图一表示年轻代回收之前的图,图二表示年轻代回收之后的图,回收后的每个Region 不会产生碎片问题。

然后开始如下回收过程:

第一阶段,扫描根。 : 根是指static 变量指向的对象,正在执行的方法调用链上的局部变量等。根引用连同RSet 记录的外部引用作为扫描存活对象的入口。

第二阶段,更新RSet: 处理dirty card queue 中的card,更新RSet。此阶段完成后,RSet可以准确的反应老年代对所在内存分段中对象的引用。 dirty card queue(对于应用程序的引用赋值语句Object.field = Object,JVM 会在之前和之后执行特殊的操作以在dirty card queue 中入队一个保存了对象引用信息的card。在年轻代回收的时候,G1 会对Dirty Card Queue 中所有的card 进行处理,以更新RSet,保证RSet 实时准确的反映引用关系。 那为什么不在引用赋值语句处直接更新RSet呢? 这是为了性能的需要,RSet 的处理需要线程同步,开销很大,使用队列性能会好很多。)

第三阶段,处理RSet:识别被老年代对象指向的Eden中的对象,这些被指向的Eden 中的对象被认为是存活的对象,

第四阶段,复制对象: 此阶段,对象树被遍历,Eden 区内存段中存活的对象会被复制到Survivor 区中空的内存分段,Survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阈值会被复制到Old区中空的内存分段。如果Survivor 空间不够,Eden空间的部分数据会直接晋升到老年代空间。

第五阶段,处理引用: 处理Soft,Weak,Phantom,Final,JNI Weak 等引用。最终Eden 空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所有复制过程可以达到内存整理的效果,较少碎片。

2、G1 回收过程二:并发标记过程

① 初始标记阶段:标记从根节点直接可达的对象。这个阶段是STW的,并且会触发一次年轻代GC。

②根区域扫描(Root Region Scanning):G1 GC 扫描Survivor 区直接可达的老年代区域对象,并标记被引用的对象。这一过程必须在young GC 之前完成。

③并发标记(ConcurrentMapCache MarkIng):在整个堆中进行并发标记(和应用程序并发执行),此过程可能被Young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。

④再次标记(Remark):由于应用程序持续进行,需要修正上一次的标记结果。是STW 的,G1中采用了比CMS 更快的初始快照算法:snapshot-at-the-beginning (SATB)。

⑤独占清理(cleanup,STW):计算各个区域的存活对象和 GC回收比例,并进行排序,识别可以混合回收的区域。为下阶段做铺垫。是STW的。 这个阶段并不会实际上去做垃圾的收集。

⑥并发清理阶段:识别并清理完全空闲的区域。

3、G1 回收过程三: 混合回收

当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机触发一个混合的垃圾收集器,即Mixed GC,该算法并不是一个Old GC,除了回收整个Young Region,还会回收一部分的Old Region。这里需要注意:是一部分老年代,而不是全部老年代,可以选择那些Old Region 进行收集,从而可以对垃圾回收的耗时时间进行控制。也要注意的是Mixed GC 并不是FullGC。

① 并发标记结束以后,老年代中百分之百为垃圾的内存分段被回收了,部分为垃圾的内存分段被计算了出来。默认情况下,这些老年代的内存分段会分8次 (可以通过 -XX:G1MixedGCCountTarget 设置)被回收。

②混合回收的回收集(Collection Set)包括八分之一的老年代内存分段,Eden区内存分段,Survivor 区内存分段。混合回收的算法和年轻代回收的算法完全一样,只是回收集多了老年代的内存分段。具体过程请参考上面的年轻代回收过程。

③由于老年代中的内存分段默认分8次回收,G1会优先回收垃圾多的内存分段。垃圾占内存分段比例越高的,越会被先回收。并且有一个阈值会决定内存分段是否被回收, -XX:G1MixedGCLiveThresholdPercent,默认未65%。意思是垃圾占内存分段比例要达到65% 才会被回收。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间。

④混合回收并不一定要进行8次。有一个阈值 -XX:G1HeapWastePercent,默认值为10%,意思是允许整个堆内存中有10% 的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收。因为GC会花费很多的时间但是回收到的却很少。

4、G1 回收可选的过程四:FullGC

①G1的初衷就是要避免Full GC 的出现。但是如果上述方式不能正常工作,G1 会停止应用程序的执行(Stop-the-world),使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。

②要避免Full GC的发生,一旦发生需要进行调整。什么时候会发生Full GC 呢? 比如堆内存太小, 当G1 在复制存活对象的时候没有空的内存分段可用,则会退回到full GC,这种情况可以通过增大内存解决。

③导致 G1Full GC 的原因有两个:

Evacuation 的时候没有足够的to-space 来存放晋升的对象。

并发处理过程完成之前空间耗尽。

⑨G1 回收器优化建议
1、年轻代大小:

	避免使用  -Xmn 或 -XX:NewRatio  等相关选项显示设置年轻代大小。

	固定年轻代的大小会覆盖暂停时间目标。

2、暂停时间目标不要太过苛刻:

	G1 GC 的吞吐量目标是90% 的应用程序时间 和 10% 的垃圾回收时间。

	评估 G1 GC 的吞吐量时,暂停时间目标不要太苛刻,目标太过苛刻表示你愿意承受更多的垃圾回收开销,而这些会直接影响到吞吐量。

11.8、Shenandoah 收集器(低延迟垃圾收集器)
①衡量垃圾收集器的是三个重要指标
1、内存占用、

2、吞吐量

3、延迟

②Shenandoah
Shenandoah 不仅要进行并发的垃圾标记,还要并发地进行对象清理后的整理动作。Shenandoah 虽然使用了Region的布局,同样有着存放大对象的Humongous Region。Shenandoah 默认不使用分代收集的,不会有专门的新生代Region或者老年代Region 的存在,没有实现分代,Shenandoah 摒弃了在G1 中耗费大量内存和计算资源去维护的记忆集,改用了 “连接矩阵”的全局数据结构来记录跨Region 的引用关系,降低了处理跨代指针时的记忆集的消耗,也降低了伪共享问题的发生概率。 连接矩阵 可以简单理解为一张二维表格,如果region n 有对象指向Region N ,就在表格的N 行 M列中打上一个标记。在回收时通过这张表格就可以得出那些Region 之间产生了跨代引用。

③Shenandoah 收集器可以分为9个步骤
1、初始标记(Initial Marking):与G1 一样,首先标记与 GC Roots 直接关联的对象,这个阶段仍是 “Stop The World”的,但停顿时间与堆大小无关,只与 GC Roots 的数量相关。

2、并发标记(Concurrent Marking): 与G1 一样,遍历对象图,标记出全部可达的对象,这个阶段是与用户线程一起并发的,时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度。

3、最终标记(Final Marking): 与 G1一样,处理剩下的SATB 扫描,并在这个阶段统计出回收价值最高的Region,将这些Region 构成一组回收集 (Collection Set)。最终标记阶段也会有一小段短暂的停顿。

4、并发清理(Concurrent Cleanup): 这个阶段用于清理那些整个区域内连一个存活对象都没有找到的Region(这类Region被称为Immediate Garbage Region)。

5、并发回收(Concurrent Evacuation): 并发回收阶段是 Shenandoah 与之前Hotspot 中其他收集器的核心差异。在这个阶段,Shenandoah 要把回收集里面的存活对象先复制一份到其他未被使用的Region 之中。 复制对象这件事情如果将用户线程冻结起来,再做那是相当简单的,但如果两者必须要同时并发进行的话,就变得复杂了。其困难点是在于移动对象的同时,用户线程仍然可能不停对被移动的对象进行读写访问,移动对象是一次性的行为,但移动之后整个内存中所有指向该对象的引用都还是旧对象的地址,这是很难一瞬间全部改变过来的。对于并发回收阶段遇到这个困难,Shenandoah  将会 通过读屏障和被称为 “Brooks Pointers ” 的转发指针来解决的。并发回收阶段运行的时间长短取决于回收集的大小。

6、初始引用更新(Initial Update Reference): 并发回收阶段复制对象结束后,还需要把堆中所有指向旧对象的引用修正到复制后的新地址,这个操作称为引用更新。引用更新的初始化阶段实际上并未做什么具体的处理,设立这个阶段只是为了建立一个线程集合点,确保所有并发回收阶段中进行的收集器线程都已完成分配给它们的对象移动任务而已。初始引用更新时间很短,会产生一个非常短暂的停顿。

7、并发引用更新(Concurrent Update Reference): 真正开始进行引用更新操作,这个阶段是与用户线程一起并发的,时间长短取决于内存中涉及的引用数量的多少。并发引用更新与并发标记不同,它不再需要沿着对象图搜索,只需要按照内存物理地址的顺序,线性地 搜索出引用类型,把旧值改为新值即可。

8、最终引用更新(Final Update Reference): 解决了堆中引用更新后,还要正存在于GC Roots 中的引用,这个阶段是Shenandoah  的最后一次停顿,停顿时间只与 GC Roots 的数量相关。

9、并发清理(Concurrent Cleanup):解决并发回收和 引用更新之后,整个回收集中所有的Region 已再无存活对象,这些Region 都变成 Immediate Garbage Region 了,最后再调用一次并发清理过程来回收这些Region 的内存空间,供以后新对象分配使用。

④ Brooks Pointer (转发指针)
转发指针(Brooks Pointer) 来实现对象移动与用户程序并发的一种解决方案,通常是在被移动对象原有的内存上设置 保护陷阱(Memory Protection Trap),一旦用户程序访问到归属于旧对象的内存空间就会产生自陷中断,进入预设好的异常处理器中,再由其中的代码逻辑把访问转发到复制后的新对象上。

而这个保护陷阱,是在原有对象布局结构的最前面统一增加一个新的引用字段,在正常不处于并发移动的情况下,该引用指向对象自己。而句柄需要统一存储在专门的句柄池中,而转发指针是分散存放在每一个对象头前面。



转发指针加入后带来的收益自然是当对象拥有一份新的副本时,只需要修改一处指针的值,即旧对象上转发指针的引用位置,使其指向新对象,便可将所有对该对象的访问转发到新的副本上。这样只要旧对象的内存仍然存在,未被清理掉,虚拟机内存中所有通过旧引用地址访问的代码便仍然可用,都会被自动转发到新对象上继续工作。

Brooks形式的转发指针在设计上决定了它是必然会出现多线程竞争问题的,如果收集器线程与用户线程发生的只是并发读取,那无论读到旧对象还是新对象上的字段,返回的结果都应该是一样的,这个场景还可以有一些“偷懒”的处理余地;但如果发生的是并发写入,就一定必须保证写操作只能发生在新复制的对象上,而不是写入旧对象的内存中。

不妨设想以下三件事情并发进行时的场景:

1)收集器线程复制了新的对象副本;

2)用户线程更新对象的某个字段;

3)收集器线程更新转发指针的引用值为新副本地址。

解决方案:

如果不做任何保护措施,让事件2在事件1、事件3之间发生的话,将导致的结果就是用户线程对对象的变更发生在旧对象上,所以这里必须针对转发指针的访问操作采取同步措施,让收集器线程或者用户线程对转发指针的访问只有其中之一能够成功,另外一个必须等待,避免两者交替进行。实际上Shenandoah收集器是通过比较并交换(Compare And Swap,CAS)操作来保证并发时对象的访问正确性的。

转发指针另一点必须注意的是执行频率的问题,尽管通过对象头上的Brooks Pointer来保证并发时原对象与复制对象的访问一致性,这件事情只从原理上看是不复杂的,但是“对象访问”这四个字的分量是非常重的,对于一门面向对象的编程语言来说,对象的读取、写入,对象的比较,为对象哈希值计算,用对象加锁等,这些操作都属于对象访问的范畴,它们在代码中比比皆是,要覆盖全部对象访问操作,Shenandoah不得不同时设置读、写屏障去拦截。

之前介绍其他收集器时,或者是用于维护卡表,或者是用于实现并发标记,写屏障已被使用多次,累积了不少的处理任务了,这些写屏障有相当一部分在Shenandoah收集器中依然要被使用到。除此以外,为了实现Brooks Pointer,Shenandoah在读、写屏障中都加入了额外的转发处理,尤其是使用读屏障的代价,这是比写屏障更大的。代码里对象读取的出现频率要比对象写入的频率高出很多,读屏障数量自然也要比写屏障多得多,所以读屏障的使用必须更加谨慎,不允许任何的重量级操作。

⑤ 优点
低延迟

⑥缺点
高运行负担使得吞吐量下降,使用大量的读写屏障,尤其是读屏障,增大了系统的性能开销。

11.9、 ZGC(收集器)
① 介绍
ZGC 和 Shenandoah,都希望在尽可能对吞吐量影响不太大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。

ZGC 收集器是一款基于Region 内存分布的,(暂时)不设分代的,使用了读屏障,染色指针 和 内存多重映射等技术来实现可并发的标记---整理算法的,以低延迟为首要目标的一款垃圾收集器。

ZGC 采用了Region堆内存布局,但与之前不同的是 ,ZGC 的Region 具有动态性---动态创建和销毁,以及动态的区域容量大小。

小型Region(Small Region):容量固定为2MB,用于放置小于256KB的小对象。

中型Region(Medium Region):容量固定为32MB,用于放置大于等于256KB但小于4MB的对象。·

大型Region(Large Region):容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置4MB或以上的大对象。每个大型Region中只会存放一个大对象,最小容量可低至4MB,所有大型Region可能小于中型Region。大型Region在ZGC的实现中是不会被重分配的,因为复制一个大对象的代价非常高昂。

②染色指针技术
HotSpot虚拟机的标记实现方案有如下几种:

把标记直接记录在对象头上(如Serial收集器);

把标记记录在与对象相互独立的数据结构上(如G1、Shenandoah使用了一种相当于堆内存的1/64大小的,称为BitMap的结构来记录标记信息);

直接把标记信息记在引用对象的指针上(如ZGC)

染色指针是一种直接将少量额外的信息存储在指针上的技术。目前在Linux下64位的操作系统中高18位是不能用来寻址的,但是剩余的46为却可以支持64T的空间,到目前为止我们几乎还用不到这么多内存。于是ZGC将46位中的高4位取出,用来存储4个标志位,剩余的42位可以支持4T的内存,如图所示

Linux下64位指针的高18位不能用来寻址,所有不能使用;

Finalizable:表示是否只能通过finalize()方法才能被访问到,其他途径不行;

Remapped:表示是否进入了重分配集(即被移动过);

Marked1、Marked0:表示对象的三色标记状态;

最后42用来存对象地址,最大支持4T;

染色指针的三大优势:

① 染色指针可以使得一旦某个Region 的存活对象被移走之后,这个Region 立即就能够被释放和 重用掉,而不必等待整个堆中所有指向该Region 的引用都被修正后才能清理。

② 染色指针可以大幅度减少在垃圾收集过程中内存屏障的使用数量,设置内存屏障,尤其是写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作。

③ 染色指针 可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据;譬如存储一些追踪信息来让垃圾手偶机器在移动对象时能低频次使用的对象移动到不常访问的内存区域。

染色指针中的标志位看做是地址的分段符,那只要将这些不同的地址段都映射到同一个物理内存空间,经过多重映射转换后,就可以使用染色指针正常进行寻址了。

③ZGC 回收的过程
1、并发标记(Concurrent Mark): 与 G1 、Shenandoah 一样,并发标记变量对象图做可达性分析阶段,而ZGC 的标记时指针上而不是在对象上进行的,标记阶段会更新染色指针中的Marked 0, Marked1 标志位。

2、并发预备重分配(Concurrent Prepare for Relocate): 这个阶段需要根据特定的查询条件统计得出本次收集过程要清理那些Region,将这些Region 组成重分配集(Relocation Set),ZGC 每次回收都会扫描所有的 Region ,用范围更大的扫描成本换取省去G1 中国记忆集的维护成本。因此,ZGC 的重分配集只是决定了里面的存活对象会被重新复制到其他的Region 中,里面的Region 会被释放,而并不能说回收行为就只是针对这个集合里面的Region 进行,因为标记过程是针对全堆的。

3、并发重分配(Concurrent Relocate): 重分配是ZGC 执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region中,并未重分配集中的每个Region 维护一个转发表(Forward TABLE),记录从旧对象到新对象的转向关系。得益于染色指针的支持,ZGC 收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region 上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC 将这种行为成为指针的 “自愈” 能力,这样做的好处是只有第一次访问旧对象会陷入转发,也就是只慢一次,。另外还有一个直接好处,由于染色指针的存在,一旦重分配集中某个Region 的存活对象都复制完毕后,这个Region 就可以立即释放用于新对象的分配(但是转发表还得留着不能释放掉),哪怕堆中还有很多指向这个对象的未更新指针也没有关系,这些旧指针一旦被使用,它们都是可以自愈的。

11.10、垃圾回收器总结
截止JDK1.8 ,一共有7款不同的垃圾收集器。每一款不同的垃圾收集器都有不同的特点,在具体使用的时候,需要根据具体的情况选用不同的垃圾收集器。

GC 发展阶段:

Serial ==》 Parallel (并行) ==》 CMS (并发) ==》G1 ==》ZGC

怎么选择垃圾收集器?

① 优先调整堆的大小让JVM 自适应完成。

②如果内存小于100M,使用串行收集器。

③如果是单核,单机程序,并且没有停顿时间的要求,串行收集器。

④如果是多CPU,需要高吞吐量,允许停顿时间超过1,秒,选择并行或者JVM自己选择。

⑤如果是多CPU,追求停顿时间,需快速响应(比如延迟不能超过1秒,如互联网应用),使用并发收集器。

官方推荐G1,性能高。现在互联网的项目,基本都是使用G1 。

12、GC日志分析
1、GC日志分析
内存分配与垃圾回收的参数列表:

1、-XX:+PrintGC 输出GC日志,类似:-verbose:gc
2、-XX:+PrintGCDetails 输出GC的详细日志
3、-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
4、-XX:+PrintGCDateStamps 输出GC 的时间戳(以日期的形式,如2013-05-01T21:53:57.234+0800)
5、-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
6、-Xloggc:…/logs/gc.log 日志文件的输出路径。

日志补充说明:

1、GC 和 FUll GC 说明了这次垃圾收集的停顿类型,如果有“Full”则说明GC发生了 “Stop-the-world”。
2、使用Serial 收集器在新生代的名字是 Default New Generation ,因此显示的是 “DefNew”。
3、使用ParNew 收集器在新生代的名字会变成 “ParNew”,意思是 parallel New Generation。
4、使用Parallel Scavenge 收集器在新生代的名字是 “PSYoungGen”。
5、老年代的收集和新生代道理一样,名字也是收集器决定的。
6、使用G1收集器的话,会显示为 “garbage-first heap”.
7、Allocation Failure: 表明本次引用GC的原因是因为在年轻代中没有足够的空间能够存储新的数据了。
8、[PSYoungGen:5986K -》 696K (8704K)] 5986K ->704K(9216K)
中括号内:GC回收前年轻代大小,回收后大小,(年轻代总大小)
括号外:GC回收前年轻代和老年代大小,回收后大小,(年轻代和老年代总大小)
9、user 代表用户态回收耗时,sys 内核态回收耗时,rea 实际耗时。由于多核的原因,时间总和可能会超过real 时间。

/**

  • @author HouYC

  • @create 2020-08-04-20:56

  • -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
    */
    public class GCLogTest {

    public static void main(String[] args) {
    byte[] allocation1,allocation2,allocation3,allocation4;
    allocation1 = new byte[2 * 1024 * 1024];
    allocation2 = new byte[2 * 1024 * 1024];
    allocation3 = new byte[2 * 1024 * 1024];
    allocation4 = new byte[4 * 1024 * 1024];

    }
    }

GC 日志分析工具:

可以用一些工具去分析这些GC 日志。

常用的日志分析工具有:GCViewer、GCEasy、GCHisto、GCLogViewer、Hpjmter、garbagecat 等。

二、JVM 中篇
1、Class 文件的结构

① Class 文件结构简介

1.Class 类的本质
任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,Class文件实际上它并不一定以磁盘文件的形式存在。Class文件时一组以8位字节为基础单位的二进制流。

2.Class文件格式
Class 的结构不像XML 等描述语言,由于它没有任何分隔符号。所以在其中的数据项,无论是字节顺序还是数量,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。

Class 文件格式采用一种类似C 语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数和表。

无符号数属于基本的数据类型,以U1,U2,U4,U8 来分别代表 1个字节,2个字节,4个字节,8个字节 的无符号数,无符号数可以用来描述数字,索引引用,数量值或者按照UTF-8 编码构成字符串值。

表时由多个无符号数或其他表作为数据项构成的复合数据类型,所有表都习惯性地以 “_info” 结尾。表用于描述有层次关系的复合结构的数据,整个Class 文件本质上就是一张表。由于表没有固定长度,所以通常会在其前面加上个数说明。

4.class 文件结构概述
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html

Class文件的结构并不是一成不变的,随着Java虚拟机的不断发展,总是不可避免地会对CLass文件结构做出一些调整,但是其基本结构和框架是非常稳定的。

Class 文件的总体结构如下:

① 魔数

②Class 文件版本

③常量池

④访问标志

⑤类索引,父类索引,接口索引集合

⑥字段表集合

⑦方法表集合

⑧属性表集合

5.class字节码文件结构
类型 名称 说明 长度 数量
u4 magic 魔数,识别Class文件格式 4个字节 1
u2 minor_version 副版本号(小版本) 2个字节 1
u2 major_version 主版本号(大版本) 2个字节 1
u2 constant_pool_count 常量池计数器 2个字节 1
cp_info constant_pool 常量池表 n个字节 constant_pool_count-1
u2 access_flags 访问标识 2个字节 1
u2 this_class 类索引 2个字节 1
u2 super_class 父类索引 2个字节 1
u2 interfaces_count 接口计数器 2个字节 1
u2 interfaces 接口索引集合 2个字节 interfaces_count
u2 fields_count 字段计数器 2个字节 1
field_info fields 字段表 n个字节 fields_count
u2 methods_count 方法计数器 2个字节 1
method_info methods 方法表 n个字节 methods_count
u2 attributes_count 属性计数器 2个字节 1
attribute_info attributes 属性表 n个字节 attributes_count
① 魔数:Class文件的标志
Magic Number(魔数)

1、每个Class 文件开头的4个字节的无符号整数称为魔数(Magic Number)。

2、它的唯一作用是确定这个文件是否为一个能被虚拟机接受的有效合法的Class文件。即:魔数是Class 文件的标识符。

3、魔数值固定为 0xCAFEBABE。不会改变。

4、如果一个Class 文件不以 0xCAFEBABE 开头,虚拟机在进行文件效验的时候就会直接抛出一下错误:

Error:A JNI error has occurred,please check your installation and try again Exception in thread “main” java.lang.ClassFormatError: Incopatible magic value 1885430635 in class file
5、使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。

② Class 文件版本号
1、紧接着魔数的 4 个字节存储的是Class文件的版本号。同样也是4个字节。第5个 和 第6个字节所代表的含义就是编译的副版本号minor_version,而第7个 和 第8个 字节就是编译的主版本号major_version。

2、它们共同构成了class 文件的格式版本号。譬如某个CLass文件 的主版本号为M,服版本号m,那么这个Class 文件的格式版本号就确定为 M.m。



3、版本号和Java 编译器的对应关系如下表:



Java 的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1。

不同版本的Java编译器编译的Class文件对应的版本是不一样的,目前,高版本的Java虚拟机可以执行由低版本编译器生成的Class文件,但是低版本的Java虚拟机不能执行由高版本编译器生成的Class文件。否则JVM 会抛出java.lang.UnsupportedClassVersionError 异常。(向下兼容)

在实际应用中,由于开发环境和生成环境的不同,可能会导致该问题的发生,因此,需要我们在开发时,特别注意开发编译的JDK 版本和生产环境中的JDK版本是否一致。

	虚拟机JDK版本为1.k(k >= 2)时,对应的class 文件格式版本号的范围为45.0 - 44+k.0 (含两端)。

③ 常量池:存放所有常量
1、常量池时Class文件中内容最为丰富的区域之一。常量池对于Class 文件中的字段和方法解析也有着至关重要的作用。

2、随着Java 虚拟机的不断发展,常量池的内容也日渐丰富。可以说,常量池是整个Class文件的基石。

3、在版本号之后,紧跟着的是常量池的数量,以及若干个常量池表项。

4、常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2 类型的无符号数,代表常量池容量计数值(constant_pool_count)。与Java 中语言习惯不一样的是,这个容量计数是从1而不是0开始的。

由上表可见,class文件使用了一个前置的容量计数器(constant_pool_count)加若干个连续的数据项(constant_pool)的形式来描述常量池内容的。我们把这一系列连续常量池数据称为常量池集合。

常量池表项中,用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

3.1 常量池计数器
1、constant_pool_count(常量池计数器)

2、由于常量池的数量不固定,时长时短,所以需要放置两个字节来表示常量池容量计数值。

3、常量池计数值(u2类型):从1开始,表示常量池中有多少项常量,即constant_pool_count=1 表示常量池中有0个常量项。



其值为0x0016,也就是22.

需要注意的是,这实际上只有21项常量。索引为范围是1-21, 通常我们写代码时都是从0开始的,但是这里的常量池却是从1开始,因为它把第0项常量空出来了,这是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可用索引值0来表示。

3.2 常量池表
1、constant_pool [] (常量池)

2、constant_pool 是一种表结构,以 1~constant_pool_count -1 为索引。表明了后面后多少个常量项。

3、常量池主要存放两大类常量:字面量(Literal)和符号引用(Symbolic REFERENCES)。

4、它包含了class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。常量池中的每一项都具备相同的特征。第一个字节作为类型标记,用于确定该项的格式,这个字节称为tag byte(标记字节、标签字节)。

①、字面量和符号引用:
在对这些常量解读前,我们需要搞清楚几个概念。

常量池主要存放两大类常量:字面量(Literal)和 符号引用(Symbolic References)。如下表:

6、全限定名:

com/hycxm/test/Demo 这个就是类的全限定名,仅仅是把包名的  “.”替换成  "/",为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个 “;”表示全限定名结束。

7、简单名称:

简单名称是指没有类型和参数修饰的方法或者字段名称,上面例子中的类的 add() 方法 和 num 字段的简单名称分别是 add 和 num。

8、描述符:

描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量,类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void 类型都用一个大写字符来表示,而对象类型则用符号 L 加对象的全限定名来表示,详见下表:(数据类型:基本数据类型、引用数据类型)



用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号  ”()“ 之内,如方法 java.lang.String toString() 的描述符为()Ljava/lang/String;,方法int abc(int[] x, int y) 的描述符为([II)I。

补充说明:

	虚拟机在加载Class 文件时才会进行动态链接,也就是说,Class文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中解析阶段将其替换为直接引用,并翻译到具体的内存地址中。

这里说明下符号引用和直接引用的区别与关联:

	1、符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。

	2、直接引用:直接引用可以是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。

② 常量类型和结构

根据上图解读字节码:

总结1:

这14种表(或者常量项结构)的共同点是:表开始的第一位是一个u1 类型的标志位(tag),代表当前这个常量项使用的是哪种表结构,即哪种常量类型。

在常量池列表中,CONSTANT_utf8_info 常量项是一种使用改进过的UTF-8 编码格式来存储诸如文字字符串、类或者接口的全限定名,字段或者方法的简单名称以及描述符等常量字符串信息。

这14种常量项结构还有一个特定是,其中13个常量项占用的字节固定,只有CONSTANT_UTF8_info 占用字节不固定,其大小由length 决定。 因为从常量池存放的内容可知,其存放的是字面量和符号引用,最终这些内容都会是一个字符串,这些字符串的大小是在编写程序时才确定,比如你定义一个类,类名可以取长取短,所以在没编译前,大小不固定,编译后,通过utf-8 编码,就可以知道其长度。

总结2:

常量池:可以理解为Class文件之中的资源仓库,它是Class 文件结构中与其他项目关联最多的数据类型 (后面的很多数据类型都会指向此处),也是占用Class 文件空间最大的数据项目之一。

常量池中为什么会包含这些内容:

Java 代码在进行Javac编译的时候,并不像c 和C++ 那样有 “链接”这一步,而是在虚拟机加载Class文件的时候进行动态链接。也就是说,在Class  文件中不会保存各个方法,字段的最终内存布局信息,因此这些字段,方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。在虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析,翻译到具体的内存地址之中。

④ 访问标识
1、访问标识(access_flag、访问标志、访问标记)

2、在常量池后,紧跟着访问标记。该标记使用两个字节表示,用于识别一些类或接口层次的访问信息,包括:这个Class 是类还是接口;是否定义为public 类型;是否定义为 abstract 类型;如果是类的话,是否被声明为final等。各种访问标记如下所示:



3、类的访问权限通常为 ACC_ 开头的常量。

4、每一种类型的表示都是通过设置访问标记的32 位中的特定位来实现的。比如,若是public final 的类,则该标记为ACC_PUBLIC | ACC_FINAL。

5、使用ACC_SUPER 可以让类更准确地定位到父类的方法 super.method(),现代编译器都会设置并且使用这个标记。

补充说明:

6、带有ACC_INTERFACE 标志的class文件表示的是接口而不是类,反之则表示的是类而不是接口。

	如果一个class 文件被设置了ACC_INTERFACE 标志,那么同时也得设置ACC_ABSTRACT 标志。同时它不能再设置 ACC_FINAL、ACC_SUPER 或 ACC_ENUM 标志。

	如果没有设置 ACC_INTERFACE标志,那么这个class 文件可以具有上表中除ACC_ANNOTATION 外的其他所有标志,当然,ACC_FINAL 和 ACC_ABSTRACT 这类互斥的标志除外,这两个标志不得同时设置。

7、ACC_SUPER 标志用于确定类或接口里面的invokespecial 指令使用的是哪一种执行语义。针对Java虚拟机指令集的编译器都应当设置这个标志。对于Java SE 8 及后续版本来说,无论class文件中这个标志的实际值是什么,也不管class 文件的版本号是多少,Java虚拟机都认为每个class 文件均设置了ACC_SUPER 标志。	

ACC_SUPER 标志是为了向后兼容由旧JAVA 编译的代码而设计的,目前的ACC_SUPER 标志在由JDK 1.0.2之前的编译器所生成的access_flages 中是没有确定含义的,如果设置了该标志,那么Oracle 的Java 虚拟机实现会将其忽略。

8、ACC_SYNTHETIC 标志意味着该类或接口是由编译器生成的,而不是由源代码生成的。

9、注解类型必须设置ACC_ANNOTATION 标志。如果设置了 ACC_ANNOTATION 标志,那么也必须设置 ACC_INTERFACE 标志。

10、ACC_ENUM 标志声明该类或其父类为枚举类型。

⑤ 类索引、父类索引、接口索引集合
1、在访问标记后,会指定该类的类别、父类类别以及实现的接口,格式如下:

2、这三项数据来确定这个类的继承关系。

	① 类索引用于确定这个类的全限定名。

	② 父类索引用于确定这个类的父类的全限定名。由于Java 语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object 之外,所有的Java 类都有父类,因此除了java.lang.Object 外,所有等Java类的父类索引都不为0.	

	③ 接口索引集合就用来描述这个类实现了那些接口,这些被实现的接口将按implements 语句(如果这个类本身是一个接口,则应当是extends 语句)后的接口顺序从左到右排列在接口索引集合中。

	1、this_class(类索引):2字节无符号整数,指向常量池的索引。它提供了类的全限定名,如com/hycZZ/java1/Demo。this_class 的值必须是对常量池中某项的一个有效索引值。常量池在这个所引处的成员必须在CONSTANT_Class_info 类型结构体,该结构体表示这个class文件所定义的类或接口。



2、super_class(父类索引):2字节无符号整数,指向常量池的索引。它提供了当前类的父类的全限定名。如果我们没有继承任何类,其默认继承的是java/lang/Object 类。同时,由于java不支持多继承,所以其父类只有一个。   superclass  指向的父类不能是final。

3、interfaces: 指向常量池索引集合,它提供了符号引用到所有已实现的接口。 由于一个类可以实现多个接口,因此需要以数组形式保存多个接口的索引,表示接口的每个索引也是一个指向常量池的CONSTANT_Class(当然这里就必须是接口,而不是类)。

3.1、interfaces_cont(接口计数器): interfaces_count 项的值表示当前类或接口的直接超接口数量。

3.2、interfaces [](接口索引集合):interfaces [] 中每个成员的值必须是对常量池表中某项的有效索引值,它的长度为interfaces_count 。 每个成员interfaces[i] 必须为 CONSTANT_Class_info 结构,其中 0 <= i < interfaces_count。在 interfaces [] 中,各成员所表示的接口顺序和对应的源代码中给定的接口顺序(从左至右)一样,即interfaces[0] 对应的是源代码中最左边的接口。

⑥字段表集合
1、fields: 用于描述接口或类中声明的变量。字段(field)包括类级变量以及实例级变量,但是不包括方法内部、代码块内部声明的局部变量。 字段叫什么名字、字段被定义为 什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。 它指向常量池索引集合,它描述了每个字段的完整信息。比如 字段的标识符、访问修饰符(public、private 或 protected)、是类变量还是实例变量(static 修饰符)、是否是常量(final修饰符)等。

注意事项:

字段表集合中不会列出从父类或者实现的接口中继承而来的字段,但有可能列出原本java代码之中不存在的字段。譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

在java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。

2、字段计数器 fields_count: fields_count 的值表示当前class 文件fields表的成员个数。使用两个字节来表示。 fields 表中每个成员都是一个 field_info 结构,用于表示该类或接口所声明类字段或者实例字段,不包括方法内部声明的变量,也不包括从父类或父接口继承的那些字段。

3、字段表 fields[ ] :

	① fields 表中的每个成员都必须是一个fields_info 结构的数据项,用于表示当前类或接口中某个字段的完整描述。

	② 一个字段的信息包括如下这些信息。这些信息中,各个修饰符都是布尔值,要么有,要么没有。

		作用域(public、private、protected 修饰符)。

		是实例变量还是类变量(static修饰符)。

		可变性(final)。

		并发可见性(Volatile 修饰符,是否强制从主内存读写)

		可否序列化(transient修饰符)

		字段数据类型(基本数据类型、对象、数组)

		字段名称

	③ 字段表结构: 字段表作为一个表,同样有他自己的结构:

6.2.2 字段名索引: 根据字段名索引的值,查询常量池中的指定索引项即可。

⑦ 方法表集合
1、methods:指向常量池索引集合,它完整描述了每个方法的签名。

2、methods [] (方法表):

① methods 表中的每个成员都必须是一个method_info 结构,用于表示当前类或接口中某个方法的完整描述。如果某个method_info 结构的access_flags 项既没有设置ACC_NATIVE 标志也没有设置ACC_ABSTRACT 标志,那么该结构中应包含实现这个方法所用的Java 虚拟机指令。

②method_info 结构可以表示类和接口中定义的所有方法,包括实例方法、类方法、实例初始化方法和类接口初始化方法。

③ 方法表的结构实际跟字段表是一样的,方法表结构如下:



③ 方法表访问标志: 跟字段表一样,方法表也有访问标志,而且他们的标志有部分相同,部分则不同。

⑧属性表集合

2、 字节码指令集与解析
① 加载与存储指令
1、 作用
加载与存储指令用于将数据从栈贞的局部变量表和操作数栈之间来回 传递。

2、常用指令
【局部变量压栈指令】将一个局部变量加载到操作数栈:xload、xload_ (其中x 为 i、l、 f、d、a,n为 0到3)。

【常量入栈指令】将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、inconst_m1、iconst、 lconst、 fconst、dconst

【出栈装入局部变量表指令】将一个数值从操作数栈存储到局部变量表:xstore、xstore_(其中x 为i、l、f、d、a,n 为 0 到 3 );xastore (其中x 为 i 、l、f、d、a、b、c、s)

扩充局部变量表的访问索引的指令:wide。

上面所列举的指令助记符中,有一部分是以尖括号结尾的(例如iload_)。这些指令助记符实际上代表了一组指令(例如 iload__ 代表了iload_0,iload_1,iload_2 和 iload_3 这几个指令)。这几组指令都是某个带有哦一个操作数的通用指令(例如 iload) 的特殊形式,对于这若干组特殊指令来说,他们表面上没有操作数,不需要进行取操作数的动作,但操作数都隐含在指令中。

比如:

iload_0:将局部变量表中索引为0位置上的数据压入操作数栈中。

iload  0:将局部变量表中索引为0位置上的数据压入操作数栈中。

iload 4:   将局部变量表中索引为4位置上的数据压入操作数栈中。

3、使用举例
① 局部变量压入栈指令
public class loadTest {
/**
* 局部变量压入栈指令
*/
public void load(int num, Object obj, long count, boolean flag, short[] arr) {
System.out.println(num);
System.out.println(obj);
System.out.println(count);
System.out.println(flag);
System.out.println(arr);
}
}
因为long,double 占用8个字节,所以占用两个位置

② 常量入栈指令
常量入栈指令的功能是将常数压入操作数栈,根据数据类型和入栈内容的不同,又可以分为const 系列,push 系列和 ldc 指令。

指令const 系列:用于对特定的常量入栈,入栈的常量隐含在指令本身里。指令有:iconst(i 从 -1 到 5)、lconst ( l 从0到1)、fconst (f 从0 到2)、dconst (d 从0到1)、aconst_null。

比如:

iconst_m1 将 -1 压入操作数栈;

iconst_x (x 为 0 到 5)将 x 压入栈;

lconst_0、lconst_1 分别将长整数 0 和 1压入栈。

fconst_0、fconst_1、fconst_2 分别将浮点数0,1,2, 压入栈;

dconst_0 和 dconst_1 分别将double 型0 和1压入栈。

aconst_null 将 null 。

指令助记符的第一个字符总是喜欢表示数据类型,i 表示整数,l 表示长整数,f 表示浮点数,d 表示双精度浮点,习惯上用a 表示对象引用。如果指令隐含操作的参数,会以下划线形式给出。

指令push 系列:主要包括bipush 和 sipush 。他们的区别在于接收数据类型的不同,bipush 接收8位整数作为参数,sipush 接收16位整数,他们都将参数压入栈。

指令ldc系列:如果以上指令都不能满足需求,那么可以使用万能的ldc 指令,它可以接收一个8位的参数,该参数指向常量池中的int, float 或者String 的索引,将指定的内容压入堆栈。 类似的还有 ldc_w, 它接收两个8位参数,能支持的索引范围大于ldc。 如果要压入的元素是long 或者double 类型的,则使用ldc2_w 指令,使用方式都是类似的。

总结如下:

public class loadTest {
/**
* 常量压入栈指令
*/
public void pushConstLdc() {
int i = -1;
int a = 1;
int b = 5;
int c = 127;
int d = 128;
int e = 32767;
int f = 32768;
}

public void store() {
    long a1 = 1;
    long a2 = 2;
    float b1 = 2;
    float b2 = 3;
    double c1 = 1;
    double c2 = 2;
    Date d = null;
}

}

③ 出栈装入局部变量表指令
出栈装入局部变量表指令用于将操作数栈中栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值。

这类指令的主要以store 的形式存在,比如 xstore(x 为 i、l、f、d、a),xstore_n(x 为 i、l、f、d、a,n 为0 到 3)。

其中,指令 istore_n 将从操作数栈中弹出一个整数,并将它赋值给局部变量索引n 位置。

指令xstore 由于没有隐含参数信息,故需要提供一个byte 类型的参数类指定目标局部变量表的位置。

说明:

一般来说,类似像store 这样的命令需要带一个参数,用来指明将弹出的元素放在局部变量表的第几个位置。但是,为了尽肯能压缩指令大小,使用专门的istore_1 指令表示将弹出的元素放置在局部变量表第1 个位置。类似的还有 istore_0,istore_2、istore_3、他们分别表示从操作数栈顶弹出一个元素,存放在局部变量表第0,2,3个位置。

由于局部变量表前几个位置总是非常常用,因此这种做法虽然增加了指令数量,但是可以大大压缩生成的字节码的体积。 如果局部变量表很大,需要存储的槽位大于3,那么可以使用istore 指令,外加一个参数,用来表示需要存放的槽位位置。

public class loadTest {
/**
* 出栈装入局部变量表指令
*/
public void store(int k, double d) {
int m = k + 2;
long l = 12;
String str = “侯亚超”;
float f = 10.0F;
d = 10;
}
}

② 算术指令
所有的算术指令包括:

加法指令: iadd、ladd、fadd、dadd。

减法指令: isub、lsub、fsub、dsub。

乘法指令: imul、lmul、fmul、dmul。

除法指令: idiv、 ldiv、fdiv、ddiv。

求余指令: irem、lrem、frem、drem、 // remainder:余数

取反指令: ineg、lneg、fneg、dneg、 // negation : 取反

自增指令: iinc

位运算指令,又可以分为:

位移指令:ishl、ishr、iushr、lshl、lshr、lushr。

按位或指令: ior、lor。

按位与指令: iand、land。

按位异或指令:ixor、lxor。

比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp。

在每一大类中,都有针对Java 虚拟机具体数据类型的专用算术指令。但没有直接支持byte、short、char 和 Boolean 类型的算术指令,对于这些数据的运算,都使用int类型的指令来处理。此外,在处理Boolean、byte、short 和 char 类型的数组时,也会转换为使用对应的int 类型的字节码指令来处理。

																	Java 虚拟机中的实际类型与运算类型

实际类型 运算类型 分类
Boolean int -------
byte int -------
char int -------
short int -------
int int -------
float float -------
reference reference -------
returnAddress returnAddress -------
long long 二
double double 二
③ 类型转换指令
1、类型转换指令说明
类型转换指令可以将两种不同的数值类型进行相互转换。

这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来处理字节码指令集中数据类型相关指令,无法与数据类型一一对应的问题。

2、宽化类型转换(Widening Numeric Conversions)
①、转换规则:

Java 虚拟机直接支持以下数值的宽化类型转换(Widening Numeric Conversions,小范围类型向大范围类型的安全转换)、也就是说,并不需要指令执行,包括:

从int 类型到long,float 或者double 类型。对应的指令为:i2l、i2f、i2d。

从long 类型到 float、double 类型。对应的指令为:l2f、l2d。

从float 类型到double类型。对应的指令为:f2d。

简化为: int----> long —> float----->double。

② 精度损失问题:
宽化类型转换是不会因为超过目标类型最大值而丢失信息的,例如,从int 转换到long,或者从int 转换到double,都不会丢失任何信息,转换前后的值是精确相等的。

从int、long类型数值转换到float,或者long类型数值转换到double时,将可能发生精度丢失-----可能丢失掉几个最低有效位上的值,转换后的浮点数值是根据IEEE754 最接近舍入模式所得到的正确整数值。

尽管宽化类型转换实际上是可能发生精度丢失的,但是这种转换永远不会导致Java 虚拟机抛出运行时异常。

③补充说明:

3、窄化类型转换(Narrowing Numeric Conversion)
① 转换规则

② 精度损失问题

③ 补充说明

④ 对象的创建与访问指令
1、创建指令
虽然类实例和数组都是对象,但Java 虚拟机对类实例和数组的创建与操作使用了不同的字节码指令;

① 创建类实例的指令:它接收一个操作数,为指向常量池的索引,表示要创建的类型,执行完成后,将对象的引用压入栈。

② 创建数组的指令:

创建数组的指令:newarray、anewarray、multianewarray。

newarray:创建基本类型数值。

anewarray: 创建引用类型数组。

multianewarray:创建多维数组。

2、字段访问指令
对象创建后,就可以通过对象访问指令获取对象实例或数组实例中的字段或者数组元素。

访问类字段(static 字段,或者称为类变量)的指令:getstatic、putstatic。

访问类实例字段(非static字段,或者称为实例变量)的指令:getfield、putfield。

3、数组操作指令
数组操作指令主要有:xastore 和 xaload 指令。具体为:

把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload。

将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、lastore、fasotre、dastore、aastore。

取数组长度的指令:arraylength。 该指令弹出栈顶的数组元素,获取数组的长度,将长度压入栈。

2、说明:

指令 xaload 表示将数组的元素压栈,比如saload、caload 分别表示压入 short 数组和 char 数组。指令xaload 在执行时,要求操作数中栈顶元素为数组索引 i,栈顶顺位第 2 个元素为数组引用 a,该指令会弹出栈顶这两个元素,并将 a[i] 重新压入堆栈。

xastore 则专门针对数组操作,以 iastore 为例,它用于给一个int 数组的给定索引赋值。 在iastore 执行前,操作数栈顶需要以此准备3个元素:值、索引、数组引用,iastore 会弹出这3个值,并将值赋给数组中指定索引的位置。

4、类型检查指令
检查类实例或数组类型的指令:instanceof 、checkcast。

指令checkcast 用于检查类型强制转换是否可以进行。如果可以进行,那么checkcast 指令不会改变操作数栈,否则它会抛出ClassCastException 异常。

指令instanceOf 用于判断给定对象是否是某一个类的实例,它会将判断结构压入操作数栈。

⑤方法调用与返回指令
1、方法调用指令
invokevirtual:指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),支持多态。这也是java语言中最常见的方法分派方式。

invokeinterface: 指令用于调用接口方法,它会在运行时搜索由特定对象所实现的这个接口方法,并找出适合的方法进行调用。

invokespecial: 指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法(构造器)、私有方法 和 父类方法。这些方法都是静态类型绑定的,不会再调用时进行动态派发。

invokestatic: 指令用于调用命名类中的类方法(static 方法)。这是静态绑定的。、

invokedynamic:调用动态绑定的方法,这个是JDK1.7后新加入的指令。用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。前面4条调用指令的分派逻辑都固化在java虚拟机内部,而invokedynamic 指令的分派逻辑是由用户所设定的引导方法决定的。

2、方法返回指令:
方法调用结束前,需要进行返回。方法返回指令是 根据返回值的类型区分的。

包括 ireturn (当返回值是 Boolean,byte,char,short 和 int 类型时使用),lreturn、freturn、dreturn、和 areturn。

另外还有一条 return 指令供声明为 void 的方法、实例初始化方法以及类和接口的类初始化方法使用。

⑥操作数栈管理命令
如同操作一个普通数据结构中的堆栈那样,JVM 提供的操作数栈管理指令,可以用于直接操作 操作数栈的指令。

这类指令包括如下内容:

将一个或两个元素从栈顶弹出,并且直接废弃:pop,pop2。

复制栈顶一个或两个数值并将复制值或两份的复制值重新压入栈顶:dup, dup2,dup_x1,dup_x1,dup_x2,dup2_x2;

将栈最顶端的两个Slot的数值位置交换:swap。 Java 虚拟机没有提供交换两个64位数据类型(long,double)数值的指令。

指令nop,是一个非常特殊的指令,它的字节码为 0x00。和汇编语言中的nop 一样,它表示什么都不做。这条指令一般可用于调试、占位等。

这些指令属于通用型,对栈的压入或者弹出无需指明数据类型。

说明:

不带_x 的指令是复制栈顶数据并压入栈顶。包括两个指令,dup 和 dup2,。dup 的系数代表要复制的Slot 个数。

dup 开头的指令用于复制 1 个 slot 的数据。例如 1 个 int 或 1个 reference 类型数据。

dup2 开头的指令用于复制 2 个slot 的数据。例如 1 个 long , 或 2个 int,或1 个 int+1个float类型数据。

带x 的指令是复制栈顶数据并插入栈顶以下的某个位置。共有4个指令,dup_x1,dup2_x1,dup_x2, dup2_x2。对于带 _x 的复制插入指令,只要即将指令的dup 和 x 的系数相加,结果即为需要插入的位置。因此:

dup_x1 插入位置: 1+ 1 = 2,即栈顶2个slot下面。

dup_x2 插入位置: 1+ 2 = 3, 即栈顶3 个slot下面。

dup2_x1 插入位置: 2 + 1 = 3,即栈顶3个slot下面。

dup2_x2 插入位置: 2 + 2 = 4,即栈顶4个slot 下面。

pop: 将栈顶的1个 slot 数值出栈。例如1个short 类型数值

pop2: 将栈顶的2个slot数值出栈。例如1个double 类型数值,或者2个int类型数值。

⑦ 控制转移指令
1、比较指令的说明
比较指令的作用是比较栈顶两个元素的大小,并将比较结果入栈。

比较指令有: dcmpg,dcmpl,fcmpg,fcmpl, lcmp。

与前面讲解的指令类型,首字符 d 表示double 类型,f 表示 float,l 表示long。

对于double 和 float 类型的数字,由于NaN 的存在,各有两个版本的比较指令,以float 为例,有fcmpg 和 fcmpl 两个指令,他们的区别在于数字表示时,若 遇到NaN值,处理结果不同。

指令dcmpl 和dcmpg 也是类似的,根据其命名可以推测其含义,在此不再獒述。

指令lcmp 针对 long 型整数,由于long 型整数没有NaN 值,故无需准备两套指令。

举例:

指令fcmpg 和 fcmpl 都从栈中弹出两个操作数,并将他们做比较,设栈顶的元素为v2,栈顶顺位第2位的元素为v1,若v1 = v2,则压入0,;若v1 > v2 则压入1;若 v1 < v2 则压入 -1。

两个指令的不同之处在于,如果遇到NaN 值,fcmpg 会压入1,而fcmpl 会压入-1。

2、条件跳转指令
条件跳转指令通常和比较指令结合使用。在条件跳转指令执行前,一般可以先用比较指令进行栈顶元素的准备,然后进行条件跳转。

条件跳转指令有: ifeq, iflt,ifle,ifne,ifgt,ifge,ifnull,ifnonnull。这些指令都接收两个字节的操作数,用于计算跳转的位置(16位符号整数作为当前位置的offset)。

它们的统一含义为:弹出栈顶元素,测试它是否满足某一条件,如果满足条件,则跳转到给定位置。

具体说明:

注意:

与前面运算规则一致:

对于Boolean、byte、char、short、类型的条件分支比较操作,都是使用int 类型的比较指令完成。

对于long,float,double 类型的条件分支比较操作,则会先执行相应类型的比较运算指令,运算指令会返回一个整型值到操作数栈中,随后再执行int 类型的条件分支比较操作来完成整个分支跳转。

由于各类型的比较最终都会转为int类型的比较操作,所以Java 虚拟机提供的int类型的条件分支指令是最为丰富和强大的。

2、比较条件跳转指令
比较条件跳转指令类似于比较指令和条件跳转指令的结合体,它将比较和跳转两个步骤合二为一。

这类指令有: if_icmpeq 、 if_icmpne、 if_icmplt、 if_icmpgt、if_icmple、if_icmpge、if_acmpeq 和 if_acmpne 、其中指令助记符加上 “if_” 后,以字符 “i” 开头的指令针对 int 型整数操作(也包括short 和 byte类型),以字符 “a” 开头的指令表示对象引用的比较。

具体说明:

这些指令都接收两个字节的操作数作为参数,用于计算跳转的位置。同时在执行指令时,栈顶需要准备两个元素进行比较。指令执行完成后,栈顶的这两个元素被清空,且没有任何数据入栈。如果预设条件成立,则执行跳转,否则,继续执行下一跳语句。

3、多条件分支跳转
多条件分支跳转指令是专为 switch-case 语句设计的,主要有tableswitch 和 lookupswitch。

指令名称 描述
tableswitch 用于switch条件跳转,case值连续
lookupswitch 用于switch 条件跳转,case值不连续
从助记符上看,两者都是switch 语句的实现,它们的区别:

tableswitch 要求多个条件分支是连续的,它内部只存放起始值 和 终止值,以及若干个跳转偏移量, 通过给定的操作数index,可以立即定位到跳转偏移量位置,因此效率比较高。

指令 lookupswitch 内部 存放着 各个离散的 case-offset 对,每次执行都要搜索全部的case-offset 对,找到匹配的case 值,并根据对应的offset 计算跳转地址,因此效率较低。

指令tableswitch 的示意图如下图所示。由于tableswitch 的case 值是连续的,因此只需要记录最低值 和 最高值,以及每一项对应的offset 偏移量,根据给定的index 值 通过简单的计算即可直接定位到 offset。

指令 lookupswitch 处理的是离散的case 值,但是出于效率考虑,将case-offset 对按照case 值大小排序,给定index 时,需要查找与index 相等的case,获得其offset,如果找不到则跳转到default。指令lookupswitch 如下图所示。

4、无条件跳转指令
目前主要的无条件跳转指令为 goto。 指令goto 接收两个字节的操作数,共同组成一个带符号的整数,用于指定指令的偏移量,指令执行的目的就是跳转到偏移量给定的位置处。

如果指令偏移量太大,超过双字节的带符号整数的范围,则可以使用指令 goto_w,它和goto 有相同的作用,但是它接收4个字节的操作数,可以表示更大的地址范围。

指令 jsr,jsr_w,ret 虽然也是无条件跳转的,但主要用于 try-finally 语句,且已经被虚拟机逐渐废弃,故不再这里介绍这两个指令。

⑧异常处理指令
1、抛出异常指令:
① athrow 指令: 在Java 程序中显示抛出异常的操作(throw 语句)都是由athrow 指令来实现。除了使用 throw 语句显示抛出异常情况之外,JvM 规范还规定了许多运行时异常会在其他 Java 虚拟机指令检测到异常状况时自动抛出。例如,在之前介绍的整数运算时,当除数为零时,虚拟机会在 idiv 或 ldiv 指令中抛出ArithmeticException。

② 注意: 正常情况下,操作数栈的压入弹出都是一条条指令完成的。唯一的例外情况是 在抛出异常时,Java 虚拟机会清除操作数上的所有内容,而后将异常实例压入调用者操作数栈上。

##############
异常及异常的处理:

过程一: 异常对象的生成过程 -------> throw(手动\ 自动) ------> 指令: athrow

过程二: 异常的处理:抓抛模型。try-cathch-finally       	------> 使用异常表

2、异常处理与异常
① 处理异常:

在Java 虚拟机中,处理异常(catch语句)不是由字节码指令来实现的(早期使用 jsr,ret 指令),而是采用异常表来完成的。

②异常表:

如果一个方法定义了一个 try-catch 或者 try-finally 的异常 处理,就会创建一个异常表。它包含了每个异常处理或者finally 块的信息。异常表保存了每个异常处理信息。比如:

起始位置

结束位置

程序计数器记录的代码处理的偏移地址

被捕获的异常类在常量池中的索引

当一个异常被抛出时,JVM 会在当前的方法里寻找一个匹配的处理,如果没有找到,这个方法会强制结束并弹出当前栈帧,并且异常会重新抛给上层调用的方法(在调用方法栈帧)。如果在所有栈帧弹出前仍然没有找到合适的异常处理,这个线程将终止。如果这个异常在最后一个非守护线程里抛出,将会导致JVM 自己终止,比如 这个线程是个main 线程。

不管什么时候抛出异常,如果异常处理最终匹配了所有异常类型,代码就会继续执行。在这种情况下,如果方法结束后没有抛出异常,仍然执行finally块,在return 前,它直接跳到 finally块来完成目标。

⑨ 同步控制指令
1、方法级别的同步
方法级别的同步:是隐式的,即必须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池的方法表结构中的ACC_SYNcHRONIZED 访问标志得知一个方法是否声明为同步方法;

当调用方法时,调用指令将会检查方法的ACC_SYNCHRONIZED 访问标志是否设置。

如果设置了,执行线程将先持有同步锁,然后执行方法。最后在方法完成(无论是正常完成还是非正常完成)时释放同步锁。

在方法执行期间,执行线程持有了同步锁,其他任何线程都无法再获得用一个锁。

如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的锁将在异常抛到同步方法之外时自动释放。

2、方法内指定指令序列的同步
同步一段指令集序列:通常是由java中的synchronized 语句块来表示的。jvm 的指令集有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义。

当一个线程进入同步代码块时,它使用 monitorenter 指令请求进入。如果当前对象的监视器计数器为0,则它会被准许进入,若为1,则判断持有当前监视器的线程是否为自己,如果是,则进入,否则进入等待,直到对象的监视器计数器为0,才会被允许进入同步块。

当线程退出同步块时,需要使用monitorexit 声明退出。在java 虚拟机中,任何对象都有一个监视器与之关联,用来判断对象是否被锁定,当监视器被持有后,对象处于锁定状态。

指令monitorenter 和monitorexit 在执行时,都需要在操作数栈顶压入对象,之后 monitorenter 和 monitorexit 的锁定和释放都是针对这个对象的监视器进行的。

下图展示了监视器如何保护进阶区代码不同时被多个线程访问,只有当线程4离开临界区后,线程1,2,3才有可能进入。

编译器必须确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都必须执行其对应的monitorexit 指令,而无论这个方法时正常结束还是异常结束。

为了确保在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。

3、类的加载过程详解
① 过程一: Loading(加载)阶段
1、加载完成的操作
加载的理解:

所谓加载,简而言之就是将Java类的字节码文件加载到机器内存中,并在内存中构建出Java 类的原型---类模板对象。 所谓类模板对象,其实就是Java 类在JVM 内存中的一个快照,JVM 将从字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中,这样JVM 在运行期便能通过类模板而获取Java 类中的任意信息,能够堆Java类 的成员变量进行遍历,也能进行Java方法的调用。。

加载完成的操作:

加载阶段,简言之,查找并加载类的二进制数据,生成Class 的实例。

在加载类时,Java 虚拟机必须完成以下3件事情:

通过类的全名,获取类的二进制数据流。

解析类的二进制数据流为方法区内的数据结构(java类模型)

创建java.lang.Class 类的实例,表示该类型。作为方法区这个类的各个数据的访问入口。

2、二进制流的获取方式
对于类的二进制数据流,虚拟机可以通过多种途径产生或获得。(只有所读取的字节码符合JVM规范即可)

虚拟机可能通过文件系统读入一个class 后缀的文件

读入jar、zip 等归档数据包,提取类文件

事先存放在数据库中的类的二进制数据

使用类似于HTTP 之类的协议通过网络进行加载

在运行时生成一段CLass 的二进制信息等。

在获取到类的二进制信息后,Java 虚拟机就会处理这些数据,并最终转为一个java.lang.Class 的实例。

如果输入数据不是ClassFile 的结构,则会抛出ClassFormatError。

3、类模型与Class 实例的位置
① 类模型的位置
加载的类在JVM 中创建相应的类结构,类结构会存储在方法区(jDK1.8之前:永久代;JDK1.8及之后:元空间)。

② Class 实例的位置
类 将 .class 文件加载至元空间后,会在堆中创建一个Java.lang.Class 对象,用来封装位于方法区内的数据结构,该Class 对象是在加载类的过程中创建的,每个类都对应有一个Class 类型的对象。

外部可以通过访问代表Order类的Class 对象来获取Order 的类数据结构。

4、再说明
Class 类的构造方法时私有的,只有JVM 能够创建。

java.lang.Class 实例时访问类型元数据的接口,也是实现反射的关键数据、入口。通过Class 类提供的接口,可以获得目标类所关联的 .class 文件中具体的数据结构: 方法、字段等信息。

4、数组类的加载
创建数组类的情况稍微有些特殊,因为数组类本身并不是类加载器负责创建,而是由JVM 在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建。创建数组类的过程:

1、如果数组的元素类型是引用类型,那么就遵循定义的加载过程递归加载和创建数组A 的元素类型。

2、JVM 使用指定的元素类型和数组维度来创建新的数组类。

如果数组的元素类型是引用类型,数组类的可访问性就由元素类型的可访问性决定。否则数组类的可访问性将缺省定义为public。

② 过程二:Linking(链接)阶段
1、环节1:链接阶段之Verification(验证)
当类加载到系统后,就开始链接操作,验证是链接操作的第一步。

它的目的是保证加载的字节码是合法、合理并符合规范 。

验证的步骤比较复杂,实际要验证的项目也很繁多,大体上Java 虚拟机需要做以下检查,如图所示。

整体说明:

验证的内容则涵盖了类数据信息的格式验证、语义检查、字节码验证,以及符号引用验证等。

其中格式验证会和加载阶段一起执行。验证通过之后,类加载器才会成功将类的二进制数据信息加载到方法区中。

格式验证之外的验证操作将会在方法区中进行。

链接阶段的验证虽然拖慢了加载速度,但是它会避免了在字节码运行时还需要进行各种检查。

具体说明:

1、格式验证: 是否以魔数 0xCaFEBABE 开头,主版本和副版本号是否在当前Java 虚拟机的支持范围内,数据中每一个项是否都拥有正确的长度等。

2、Java 虚拟机会进行字节码的语义检查,但凡在语义上不符合规范的,虚拟机也不会给予验证通过。比如:

是否所有的类都有父类的存在(在Java里,除了Object外,其他类都应该有父类)。

是否一些定义为final 的方法或者类被重写或继承了。

非抽象类是否实现了所有抽象方法或者接口方法。

是否存在不兼容的方法(比如方法的签名除了返回值不同,其他都一样,这种方法会让虚拟机无从下手调度;abstract 情况下的方法,就不能是final 的了)。

3、Java 虚拟机还会进行 字节码验证, 字节码验证也是验证过程中最为复杂的一个过程。它视图通过对字节码流的分析,判断字节码是否可以被正确地执行。比如:

在字节码的执行过程中,是否会跳转到一条不存在的指令

函数的调用是否传递了正确类型的参数

变量的赋值是不是给了正确的数据类型等。

栈映射帧(StackMapTable)就是在这个阶段,用于检测在特定的字节码处,其局部变量表和操作数栈是否有着正确的数据类型。但遗憾的是,100% 准确地判断一段字节码是否可以被安全执行时无法实现的,因此,该过程只是尽可能地检查出可以预知的明显的问题。如果在这个阶段无法通过检查,虚拟机也不会正确装载这个类。但是,如果通过了这个阶段的检查,也不能说明这个类是完全没有问题的。

2、环节2:链接阶段之Preparetion(准备)
准备阶段(Preparetion),简言之,为类的静态变量分配内存,并将其初始化为默认值。

当一个类验证通过时,虚拟机就会进入准备阶段。在这个阶段,虚拟机就会为这个类分配相应的内存空间,并设置默认初始化。Java虚拟机为各类型变量默认的初始值如表所示。

注意:Java 并不支持Boolean 类型,对于Boolean 类型,内部实现是int,由于int 的默认值为0,故对应的,Boolean的默认值就是false。

这里不包括基本数据类型的字段用static final 修饰的情况,因为 final在编译的时候就会分配了,准备阶段会显式赋值。

注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。

在这个阶段并不会像初始化阶段中那样会有初始化或者代码被执行。

3、环节3:链接阶段之 resolution (解析)
在准备阶段完成后,就进入了解析阶段。

解析阶段(Resolution),简言之,将类、接口、字段和方法的符号引用转为直接引用。

以方法为例,Java 虚拟机为每个类都准备了一张方法表,将其所有的方法都列在表中,当需要调用一个类的方法的时候,只要知道这个方法在方发表中的偏移量就可以直接调用该方法。通过解析操作,符号引用就可以转变为目标方法在类中方法表中的位置,从而使得方法被成功调用。

由于字符串在程序开发中非常重要,当在Java代码中直接使用字符串常量时,就会在类中出现CONSTANT_String,它表示字符串常量,并且会引用一个CONSTANT_UTF8 的常量项。在Java 虚拟机内部运行中的常量池中,会维护一张字符串拘留表(intern),它会保存所有出现过的字符串常量,并且没有重复项。只有以CONSTANT_String 形式出现的字符串也都会在这张表中。使用String.intern() 方法得到一个字符串在拘留表中的引用,因为该表中没有重复项,所以任何字面相同的字符串的String.intern() 方法返回总是相等的。

③ 过程三:Initialization(初始化)阶段
1、初始化过程
初始化阶段,简言之,为类的静态变量赋予正确的初始值。

类的初始化是类装载的最后一个阶段。如果前面的步骤都没有问题,那么表示类可以顺利装载到系统中。此时,类才会开始执行Java 字节码。(即:到了初始化阶段,才真正开始执行类中定义的Java 程序代码)。

初始化阶段的重要工作时执行类的初始化方法:<clinit>() 方法。

该方法仅能由Java 编译器生成并由JVM 调用,程序开发者无法自定义一个同名的方法,更无法直接在Java 程序中调用方法,虽然该方法也是由字节码指令所组成。

它是由类静态成员的赋值语句以及 static 语句块合并产生的。

说明:

在加载一个类之前,虚拟机总是会试图加载该类的父类,因此父类的 <clinit> 总是在子类 <clinit> 之前被调用。 也就是说, 父类的static 块优先级高于子类。

口诀:由父及子,静态先行。

Java 编译器并不会为所有的类都产生 ()初始化方法。那些类在编译为字节码后,字节码文件将不会包含() 方法?

一个类中并没有声明任何的类变量,也没有静态代码块时。

一个类中声明类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时。

一个类中包含 static final 修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式。

结论:

在链接阶段的准备环节赋值的情况:

1、对于基本数据类型的字段来说,如果使用static final 修饰,则显式赋值(直接赋值常量,而非调用方法)通常是在链接阶段的准备环节进行。

2、对于String 来说,如果使用字面量的方式赋值,使用 static final 修饰的话,则 显式赋值通常是在链接阶段的准备环节进行。

在初始化阶段 () 中赋值的情况: 排除上述的在准备环节赋值的情况之外的情况。

最终结论: 使用static + final 修饰,且显式赋值中不涉及到方法或构造器调用的基本数据类型或String 类型的显式赋值,是 在链接阶段的准备环节进行。

2、类的初始化情况,主动使用VS被动使用
Java 程序对类的使用分为两种:主动使用 和 被动使用。

① 主动使用

Class 只有在必须要首次使用的时候才会被装载,Java 虚拟机不会无条件地装载Class 类型。Java 虚拟机规定,一个类或接口在初次使用前,必须要进行初始化。 这里指的 “使用”,是指主动使用,主动使用只有下列几种情况:(即:如果出现如下的情况,则会对类进行初始化操作。而初始化操作之前的加载、验证、准备已经完成)。

1、当创建一个类的实例时,比如使用new 关键字,或者通过反射、克隆、反序列化。

2、当调用类的静态方法时,即当使用了字节码 invokestatic 指令。

3、当使用类、接口的静态字段时(final 修饰特殊考虑),比如,使用 getstatic 或者 putstatic 指令。(对应访问变量、赋值变量操作)。

4、当使用java.lang.reflect 包中的方法反射类的方法时。比如:Class.forName(“com.hyc.java.Test”).

5、当初始化子类时,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。

6、如果一个接口定义了default 方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化。

7、当虚拟机启动时,用户需要指定一个要执行的主类(包含main() 方法的那个类),虚拟机会先初始化这个主类。

8、当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。

针对5,补充说明:

当Java 虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口。

在初始化一个类时,并不会先初始化它所实现的接口。

在初始化一个接口时,并不会先初始化它的父接口。

因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态字段时,才会导致该接口的初始化。

针对7,说明:

JVM 启动的时候通过引导类加载器加载一个初始化类。这个类在调用 public  static  void  main (String[] ) 方法之前被链接和初始化。这个 方法的执行将以此引导所需的类的加载,链接和初始化。

② 被动使用

除了以上的情况属于主动使用,其他的情况均属于被动使用。被动使用不会引起类的初始化。也就是说:并不是在代码中出现的类,就一定会被加载或者初始化。如果不符合主动使用的条件,类就不会初始化。

1、当访问一个静态字段时,只有真正声明这个字段的类才会被初始化。当通过子类引用父类的静态变量,不会导致子类初始化。

2、通过数组定义类引用,不会触发此类的初始化。

3、引用常量不会触发此类或接口的初始化。因为常量在链接阶段就已经被显式赋值了。

4、调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用了,不会导致类的初始化。

④ 过程四: 类的卸载
1、类的生命周期
当Sample 类被加载、链接和初始化后,它的生命周期就开始了。当代表Sample 类的Class 对象不再被引用,即不可触及时,Class 对象就会结束生命周期,Sample 类在方法区内的数据也会被卸载,从而结束Sample 类的生命周期。

一个类何时结束生命周期,取决于代表它的Class 对象何时结束生命周期。



loader1 变量和obj 变量间接应用代表Sample 类的Class 对象,而 objClass 变量则直接引用它。

如果程序运行过程中,将上图左侧三个引用变量都置为null,此时Sample对象结束生命周期,MyClassLoader 对象结束生命周期,代表Sample 类的Class 对象也结束生命周期,Sample 类在方法区内的二进制数据被卸载。

当再次有需要时,会检查Sample 类的Class对象是否存在,如果存在会直接使用,不再重新加载;如果不存在Sample 类 会被重新加载,在Java 虚拟机的堆区会生成一个新的代表Sample 类的Class 实例(可以通过哈希码查看是否是同一个实例)。

2、类的卸载
① 启动类加载器加载的类型在整个运行期间是不可能被卸载的(jvm 和 jls 规范)。

② 被系统类加载器和扩展类加载的类型在运行期间不可能被卸载,因为系统类加载器实例或者扩展类的实例基本上在整个运行期间总能直接或间接的访问到,其达到unreachable 可能性极小。

③ 被开发者自定义的类加载器实例加载的类型只有在很简单的上下文环境中才能被卸载,而且一般还要借助于强制调用虚拟机的垃圾收集功能才可以做到。可以预想,稍微复杂点的应用场景中(比如:很多时候用户在开发自定义类加载器实例的时候采用缓存的策略以提高系统性能),被加载的类型在运行期间也是几乎不太可能被加载的(至少卸载的时间是不确定的)。

4、类的加载器
1、何为类的唯一性?
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确认其Java 虚拟机中的唯一性。每一个类加载器,都拥有一个独立的类名称空间:比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类源自同一个Class 文件,被同一个虚拟机加载,只有加载他们的类加载器不同,那这两个类就必定不等。

2、命名空间
每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成。

在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。

在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。

3、类加载机制的基本特征
双亲委派模型。但不是所有类加载器都遵守这个模型,有的时候,启动类加载器所加载的类型,是可能要加载用户代码的,比如JDK内部的ServiceProvider/ ServiceLoader 机制,用户可以在标准API 框架上,提供自己的实现,JDK 也需要提供默认的参考实现。例如,Java 中 JNDI,JDBC ,文件系统、Cipher 等很多方面,都是利用的这种机制,这种情况就不会用双亲委派模型去加载,而是利用所谓的上下文加载器。

可见性,子类加载器可以访问父加载器加载的类型,但是反过来是不允许的。不然,因为缺少必要的隔离,我们就没有办法利用类加载器去实现容器的逻辑。

单一性,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,就不会再子加载器中重复加载。但是注意,类加载器 "邻居"间,同一类型仍然可以被加载多次,因为互相并不可见。

4、引导类加载器
启动类加载器(引导类加载器,Bootstrap ClassLoader)。

这个类加载使用C\C++ 语言实现的,嵌套在JVM 内部。

它用来加载 Java 的核心库(JAVA_HOME/jre/lib/rt.jar 或 sun.boot.class.paht 路径下的内容)。用于提供JVM 自身需要的类。

并不继承自 java.lang.ClassLoader ,没有父加载器。

出于安全考虑,Bootstrap 启动类 加载器只加载包名 为 java、javax、sun 等开头的类。

加载扩展类和应用程序类加载器,并指定为他们的父类加载器。

5、扩展类加载器
扩展类加载器(Extension ClassLoader)

Java语言编写,由sun.misc.Launcher$ExtClassLoader 实现。

继承于ClassLoader 类。

父类加载器为启动类加载器

从java.ext.dirs 系统属性所指定的目录中加载类库,或从JDK 的安装目录的 jre/lib/ext 子目录下加载类库。如果用户创建的JAR 放在此目录下,也会自动由扩展类加载器加载。

6、系统类加载器
应用程序类加载器(系统类加载器,APPClassLoader)

java 语言编写,由 sun.misc.Launcher$AppClassLoader 实现

继承于ClassLoader类

父类加载器为扩展类加载器

它负责加载环境变量classpath 或系统属性 java.class.path 指定路径下的类库。

应用程序中的类加载器默认是系统类加载器

它是用户自定义类加载器的默认父加载器。

通过ClassLoader 的 getSystemClassLoader() 方法可以获取到该类加载器。

7、用户自定义类加载器

8、ClassLoader 源码

ClassLoader 类中 最核心方法

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 首先,在缓存中判断是否已经加载了同名的类
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 获取当前类加载的父类加载器
if (parent != null) {
// 如果存在父类加载器,则调用父类加载器进行类的加载,,这个地方就是双亲委派模式。递归调用
c = parent.loadClass(name, false);
} else {
// parent 为null,父类加载器是引导类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

            if (c == null) {
            // 当前类的加载器的父类加载器未加载此类 or 当前类的加载器为加载到此类
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

5、双亲委派
① 定义
如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回。只有父类加载器无法完成此加载任务时,才自己去加载。

② 本质
规定了类加载的顺序是:引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由系统类加载器或自定义的类加载器进行加载。

2、双亲委派机制优势
避免类的重复加载,确保一个类的全局唯一性。Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级可以避免类的重复加载,当父类已经加载了该类时,就没有必要子 ClassLoader 再加载一次。

保护程序安全,防止核心API 被随意篡改。

② 代码支持

③ 举例

④ 思考

⑤ 双亲委派模式的弊端

⑥ 结论
由于Java 虚拟机规范并没有明确要求类加载器的加载机制一定要使用双亲委派模型,只是建议采用这种方式而言。

3、破坏双亲委派机制
① 破坏双亲委派机制1

② 破坏双亲委派机制2

默认上下文加载器就是应用类加载器,这样以上下文加载器为中介,使得启动类加载器中的代码也可以访问应用类加载器中的类。

③ 破坏双亲委派机制3

⑤ 热替换的实现

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值