ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

JVM笔记(黑马+尚硅谷+张龙整合笔记)

2021-05-18 20:59:18  阅读:162  来源: 互联网

标签:String 引用 笔记 内存 JVM 张龙 方法 public 加载


JVM笔记(黑马+尚硅谷+张龙整合笔记)

 

hancoder 2020-03-30 22:11:43   2254   收藏 33

分类专栏: Java JVM 文章标签: JVM 笔记 黑马尚硅谷 张龙

版权

前要

本身整合了如下视频的笔记,并进行了整理:尚硅谷周阳、张龙、黑马程序员

  • 黑马ppt非常好:https://download.csdn.net/download/hancoder/12834607
  • 本文及JVM系列笔记地址:https://blog.csdn.net/hancoder/category_10345348.html
  • 多线程并发、JMM等笔记:https://blog.csdn.net/hancoder/article/details/105740321

内容算是笔记充分了,张龙的代码附在文尾,文字部分整合到了正文中

 

文章目录

 

1_介绍

1.1_什么是JVM

定义:java virtual meachine -java运行时环境(java二进制字节码的运行环境)。JVM是运行在操作系统之上的,它与硬件没有直接的交互。Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。
好处:

  1. 一次编写到处运行
  2. 自动内存管理,垃圾回收
  3. 数组下标越界检查
  4. 多态

Java程序运行过程

我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。

JDK 是 Java Development Kit,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。

JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。

如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些 Java 编程方面的工作,那么你就需要安装 JDK 了。但是,这不是绝对的。有时,即使您不打算在计算机上进行任何 Java 开发,仍然需要安装 JDK。例如,如果要使用 JSP 部署 Web 应用程序,那么从技术上讲,您只是在应用程序服务器中运行 Java 程序。那你为什么需要 JDK 呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。

1.3_常见的JVM

image.png

Oracle JDK 和 OpenJDK 的对比:

对于 Java 7,没什么关键的地方。OpenJDK 项目主要基于 Sun 捐赠的 HotSpot 源代码。此外,OpenJDK 被选为 Java 7 的参考实现,由 Oracle 工程师维护。

关于 JVM,JDK,JRE 和 OpenJDK 之间的区别,Oracle 博客帖子在 2012 年有一个更详细的答案:

问:OpenJDK 存储库中的源代码与用于构建 Oracle JDK 的代码之间有什么区别?

答:非常接近 - 我们的 Oracle JDK 版本构建过程基于 OpenJDK 7 构建,只添加了几个部分,例如部署代码,其中包括 Oracle 的 Java 插件和 Java WebStart 的实现,以及一些封闭的源代码派对组件,如图形光栅化器,一些开源的第三方组件,如 Rhino,以及一些零碎的东西,如附加文档或第三方字体。展望未来,我们的目的是开源 Oracle JDK 的所有部分,除了我们考虑商业功能的部分。

总结:

  1. Oracle JDK 大概每 6 个月发一次主要版本,而 OpenJDK 版本大概每三个月发布一次。但这不是固定的,我觉得了解这个没啥用处。详情参见:https://blogs.oracle.com/java-platform-group/update-and-faq-on-the-java-se-release-cadence 。
  2. OpenJDK 是一个参考模型并且是完全开源的,而 Oracle JDK 是 OpenJDK 的一个实现,并不是完全开源的;
  3. Oracle JDK 比 OpenJDK 更稳定。OpenJDK 和 Oracle JDK 的代码几乎相同,但 Oracle JDK 有更多的类和一些错误修复。因此,如果您想开发企业/商业软件,我建议您选择 Oracle JDK,因为它经过了彻底的测试和稳定。某些情况下,有些人提到在使用 OpenJDK 可能会遇到了许多应用程序崩溃的问题,但是,只需切换到 Oracle JDK 就可以解决问题;
  4. 在响应性和 JVM 性能方面,Oracle JDK 与 OpenJDK 相比提供了更好的性能;
  5. Oracle JDK 不会为即将发布的版本提供长期支持,用户每次都必须通过更新到最新版本获得支持来获取最新版本;
  6. Oracle JDK 根据二进制代码许可协议获得许可,而 OpenJDK 根据 GPL v2 许可获得许可。

字节码文件class以CA FE BACE开头

查看二进制码的软件是Binary Viewer

2_类装载子系统

2.0_概述

类的使用流程:

  • 是否加载了该类
    • 没有加载:使用类加载器加载该类
    • 加载了:链接–初始化—调用main方法

类加载归纳为有三个阶段:

1、加载:

从文件系统或者网络中查找并加载类的二进制数据class到java虚拟机中

2、连接:

2.1、验证 : 确保被加载的类的正确性

2.2、准备:为类的static静态变量分配内存,并将其初始化为默认值,但是到达初始化之前类变量都没有初始化为真正的初始值。(这里不包含final修饰的static,因为final在编译时候就会分配了,准备阶段会显示初始化。)(这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量会随着对象一起分配到java堆中)这些内存都将在方法区中进行分配。

  • 类变量在方法区
  • 实例在堆区
  • 实例属性在堆区
  • 局部变量在JVM栈中的局部变量表中

2.3、解析:把类中的符号引用转换为直接引用,就是在类的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用的过程。符号引用就是一组符号来描述所引用的目标,符号引用的字面量明确规定在<<JAVA规范>>的class文件格式中。直接引用就是直接指向目标的指针、相对偏移或一个间接定位到目标的句柄。(符号引用是字符串常量池的阶段,直接引用指向指针)

常量池

常量池中保存的是一个 Java 类引用的一些常量信息,包含一些字符串常量及对于类的符号引用信息等。Java 代码编译生成的类文件中的常量池是静态常量池,当类被载入到虚拟机内部的时候,在内存中产生类的常量池叫运行时常量池。

常量池在逻辑上可以分成多个表,每个表包含一类的常量信息

  • CONSTANT_Utf8_info
    字符串常量表,该表包含该类所使用的所有字符串常量,比如代码中的字符串引用、引用的类名、方法的名字、其他引用的类与方法的字符串描述等等。其余常量池表中所涉及到的任何常量字符串都被索引至该表。
  • CONSTANT_Class_info
    类信息表,包含任何被引用的类或接口的符号引用,每一个条目主要包含一个索引,指向 CONSTANT_Utf8_info 表,表示该类或接口的全限定名。
  • CONSTANT_NameAndType_info
    名字类型表,包含引用的任意方法或字段的名称和描述符信息在字符串常量表中的索引。
  • CONSTANT_InterfaceMethodref_info
    接口方法引用表,包含引用的任何接口方法的描述信息,主要包括类信息索引和名字类型索引。
  • CONSTANT_Methodref_info
    类方法引用表,包含引用的任何类型方法的描述信息,主要包括类信息索引和名字类型索引

图 2. 常量池各表的关系

2.3.1、解析阶段是虚拟机将量池内的符号引用替换为值接引用的过程,符号引用在 Class文件中它以 CONSTANT_Class_info, CONSTANT_Fieldref_info、 CONSTANT_Methodref_info等类型的常量出现,那解析阶段中所说的直接引用与符号引用又有什么关联呢?

  • 符号引用( Symbolic References):符号引用以一组符号来描述所引用的目标,符号引用可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,用的目标并不一定已经加载到内存中。
  • 直接引用 (Direct Referenc):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。

虚拟机规范之中并未规定解析阶段发生的具体时间,只要求了在执行anewarray、checkcast, getfield, getstatic, instanceof, invokeinterface, invokespecial, invokestatic、invokevirtual, multianewarray、new、 putfield和 putstatIc这13个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。所以虚拟机实现会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。

对同一个符号引用进行多次解析请求是很常见的事情,虚拟机实现可能会对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标识为已解析状态)从而避免解析动作重复进行。无论是否真正执行了多次解析动作,虚拟机需要保证的都是在同一个实体中,如果一个符号引用之前已经被成功解析过,那么后续的引用解析请求就应当一直成功;同样地,如果第一次解析失败了,其他指令对这个符号的解析请求也应该收到相同的异常。

解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行,分别对应于常量池的 CONSTANT_Class_info、 CONSTANT_Fieldref_info、 CONSTANT_Methodref_info及 CONSTANT_InterfaceMethodref_info四种常量类型。下面将讲解这四种引用的解析过程。

  • 1.类或接口的解析:

假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,那虚拟机完成整个解析的过程需要包括以下3个步骤:

1)如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。在加载过程中,由于无数据验证、字节码验证的需要,又将可能触发其他相关类的加载动作,例如加载这个类的父类或实现的接口。一旦这个加载过程出现了任何异常,解析过程就将宣告失败

2)如果C是一个数组类型,并且数组的元素类型为对象,也就是N的描述符会是类似“[ Ljava. ang Integer”的形式,那将会按照第1点的规则加载数组元素类型。如果N的描述符如前面所假设的形式,需要加载的元素类型就是“ java. lang Integer”,接着由虚拟机生成一个代表此数组维度和元素的数组对象

3)如果上面的步骤没有出现任何异常,那么C在虚拟机中实际上已经成为一个有效的类或接口了,但在解析完成之前还要进行符号引用验证,确认C是否具备对D的访问权限。如果发现不具备访问权限,将抛出 java.lang.IllegalAccessError异常

  • 2.字段解析:

要解析一个未被解析过的字段符号引用,首先将会对字段表内 class index项中索引的CONSTANT_Class_info符号引用进行解解析,也就是字段所属的类成接口的符号引用。如果在解析这个类或接口符号引用的过程中出现了任何异常,都会导致字段符号引用解析的失败。如果解析成功完成,那将这个字段所属的类或接口用C表示,虚拟机规范要求按照如下步骤对C进行后续字段的搜索

1)如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返这个字段的直接引用,查找结束
2)否则,如果在C中实现了接口,将会按照继承关系从上往下递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
3)否则,如果C不是 java.lang.Object的话,将会按照继承关系从上往下递归搜索其父类)如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
4)否则,查找失败,抛出 java.lang.NoSuchFieldError异常

如果查找过程成功返回了引用,将会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将抛出 java.lang.IllegalAccess Error异常

在实际应用中,虚拟机的编译器实现可能会比上述规范要求得更加严格一些,如果有一个同名字段同时出现在C的接口和父类中,或者同时在自己或父类的多个接口中出现,那编译器将可能拒绝编译。在代码清单7-4中,如果注释了Sub类中的“ public static int A=4;”,接口与父类同时存在字段A,那编译器将提示“ The field Sub.A is ambiguous" ,并且会拒绝编译这段代码

  • 3.类方法解析

类方法解析的第一个步骤与字段解析一样,也是需要先解析出类方法表的 class index项中索引的方法所属的类或接口的符号引用,如果解析成功,我们依然用C表示这个类,接下来虚拟机将会按照如下步骤进行后续的类方法搜索:

1)类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现 class_index中索引的C是个接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。

2)如果通过了第(1)步,在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。

3)否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。

4)否则,在类C实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,说明类C是一个抽象类,这时候查找结束,抛出java.lang.AbstractMethodError异常

5)否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError

最后,如果查找过程成功返回了直接引用,将会对这个方法进行权限验证;如果发现不具备对此方法的访问权限,将抛出 java.lang.IllegalAccessError异常

  • 4.接口方法解析

接口方法也是需要先解析出接口方法表的 class index项中索引的方法所属的类或接口的符号引用,如果解析成功,依然用C表示这个接口,接下来虚拟机将会按照如下步骤进行后续的接口方法搜索:

1)与类方法解析相反,如果在接口方法表中发现 class index中的索引C是个类而不是接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。

2)否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。

3)否则,在接口C的父接口中递归查找,直到 java.lang.Object类(查找范围会包括 Object类)为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。

4)否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError异常

由于接口中的所有方法都默认是 public的,所以不存在访问权限的问题,因此接口方法的符号解析应当不会抛出java.lang.IllegalAccess Error异常。

3、初始化:

这个初始化是类的初始化,不是实例的初始化。

为类的静态变量赋予正确的初始值。为新的对象分配内存,为实例变量赋默认值,为实例变量赋正确的初始值。初始化阶段就是指向类构造器方法<clinit>()【意思是class init】的过程,此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。<clinit>()不同于类的构造器,若该类具有父类,JVM会保证子类的<clinit>执行前,父类的<clinit>已经执行完毕。JVM必须保证一个类的<clinit>()方法在多线程下被同步加锁。

代码执行顺序

3.1、<clinit>()是由编译器自动收集类中的所有类变量的赋值动作(static变量)和静态语句块(static代码块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能访问。

3.2、 <clinit>()方法与类的构造函数(或者说实例构造器<init>方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object

3.3、由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语块要优先于子类的变量赋值操作,如代码清单7-5中,字段B的值将会是2而不是1

static class Parent{
    public static int A=1;
    static{
        A=2;
    }
}
public class Sub extends Parent{
    public static int B=A;
}
public static void main(String[] args){
    Sub.B.sout;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

3.4、 <clinit>()方法对于类和接口来说并不是必须的,如果一个类中没有静态代码块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法方法

3.5、 接口中不能使用静态代码块,带仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有父接口定义的变量被使用时,父接口才回被初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法

3.6、 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果一个类的<clinit>()方法有耗时很长的操作,那就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

(1)使用到静态加载时,静态又分为: 静态变量, 静态代码块,其中加载顺序是按照类中书写的先后顺序加载的
(2)非静态加载顺序: 按照非静态书写顺序加载 /()执行
(3)静态方法,实例方法只有在调用的时候才会去执行
(4)当静态加载中遇到需要加载非静态的情况: 先加载非静态再加载静态(因为非静态可以访问静态,而静态不能访问非静态)

public static Text t1 = new Text("t1");  
// 当加载静态变量是需要先加载构造器, 那就转为先加载所有非静态属性
  • 1
  • 2

静态变量声明 一定 放在使用前面

public static int count;
static {count++;}
  • 1
  • 2

实例执行顺序

实例的构造器<init>

时机:在非静态成员全部赋值完成,才会继续执行自己构造内,剩余代码。

非静态成员的赋值,是在自己的构造调用之后,并且是在自己的构造调用完父类的构造super之后。实例初始化是在实例的构造函数中,而他相应的父类是调用super()完成的,如果没有显示写super(),那么将加在第一句。当父类没有无参构造函数时,在子类构造方法中必须显示指定,如super("hello")

有父类有无参构造时,super可以省略,此时子类可以使用this调用构造方法,this在构造方法的作用是调用子类的其他构造方法;父类没有无参构造时,子类不能使用this调用构造方法。

普通代码块在每次创建对象时都会调用一次。

顺序是写的顺序,并且优先于构造函数执行。

总结:

1、父类的静态变量和静态块赋值(按照声明顺序)
2、自身的静态变量和静态块赋值(按照声明顺序)
3、main方法
4、父类的成员变量和块赋值(按照声明顺序)
5、父类构造器赋值
6、自身成员变量和块赋值(按照声明顺序)
7、自身构造器赋值
8、静态方法,实例方法只有在调用的时候才会去执行

以final关键字为例先体会一下类加载流程

// 常量都是用final来修饰的,所以只要在包含它类实例化对象完成之前初始化就行了,什么都不影响。但是如果前面加个static表明类装载时这个常量必须是有个状态的(被赋予了值,初始化了),所以如果用static就必须类加载时初始化。

// 只被final关键字修饰的常量,实例化的构造方法完成之前有值就可
//可以在其类加载时就初始化,也可以到类的构造方法里面再对它进行初始化:例如
class A{
    final int i;//或者final int i=10; // 有没有值无所谓,实例化的构造方法完成之前有值就可
    public A(){
        i=10;
    }
}

//用static和final关键字同时修饰的常量就必须得在定义时初始化,例如:
class A{
    static final int i=10;//编译时候就赋值了
}

// 基本类型,是值不能被改变  //引用类型,是地址值不能被改变,对象中的属性可以改变
public static void method(final int x) { //此处的final修饰的 x随着方法使用完毕后回收,当再次调用时,重新分配空间
	System.out.println(x);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

2.1_类加载

类加载定义:将类的.class文件中的二进制数据(字节流)读入到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个java.lang.Class对象(规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在方法区中)用来封装内在方法区内的数据结构。

注:

  • class文件在文件开头有特定的文件标示

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

  • 加载.class文件的方式
    (1)从本地系统中直接加载
    (2)通过网络下载.class文件
    (3)从zip,jar等归档文件中加载.class文件
    (4)从专用数据库中提取.class文件
    (5)将java源文件动态编译为.class文件

2.1.1_类装载器的分类:

  • ① 启动类加载器/根加载器/引导类加载器(Bootstrap):
    • C++编写。默认加载路径$JAVAHOME/$jre/lib/rt.jar。里面有如rt.jar/sun/misc /Launcher.class,Object.class等。该加载器没有父加载器,它负责加载虚拟机中的核心类库。根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库。类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,它并没有集成java.lang.ClassLoader类。出于安全考虑,根加载器只加载包名为java,javax,sun等开头的类。
  • ② 扩展类加载器(Extension):
    • Java编写 ,由sun.misc.Launcher$ExtClassLoader实现。
    • 默认加载路径JDK安装目录/jre/lib/ext/*.jar(可通过Djava.ext.dirs系统属性重新指定)如果用户创建的JAR放在此目录下,也会由拓展类加载器自动加载
    • 扩展类加载器是java.lang.ClassLoader的子类。
  • ③ 应用程序类加载器(AppClassLoader,也叫系统类加载器):
    • Java编写,它的父加载器为扩展类加载器,他是用户自定义的类加载器的默认父加载器。
    • 加载当前应用的classpath的所有类。默认加载路径为:环境变量$classpath(可以通过Djava.class.path重新指定)。
    • 可以通过ClassLoader.getSystemClassLoader()获取到应用类加载器。
  • ④用户自定义加载器:Java.lang.ClassLoader的子类,用户可以定制类的加载方式。默认加载路径\$CLASSPATH
    • 为什么要使用自定义类加载器:隔离加载类、修改类加载的方式、拓展加载源、防止源码泄漏
    • 实现步骤:继承抽象类Java.lang.ClassLoader,重写findClass()方法

层次为:根类加载器–>扩展类加载器–>系统应用类加载器–>自定义类加载器

获取类加载器方法:

//通过java对象.object.getClass().getClassLoader();获取该类的载器
Object object=new Object();
object.getClass().getClassLoader();//null。Bootstrap根加载器是c++写的,java查不出来
object.getClass().getClassLoader().getParent();//报错,根加载器是最初级的了

//-----------------------------------------
Object object2=new MyTest();//自己写的类
System.out.println(object2.getClass().getClassLoader(););
//自定义类默认的加载器是应用加载器AppClassloader//sun.misc.launcher$AppClassLoader$18b4aac2。位于rt.jar包中

ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();//获取系统加载器
System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(systemClassLoader.getParent());//sun.misc.Launcher$ExtClassLoader@1b6d3586
System.out.println(systemClassLoader.getParent().getParent());//null


//获取根加载器所能加载的路径
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for ( URL element :urLs) {
    System.out.println(element.toExternalForm());
}
/*
file:/E:/Java/jdk1.8.0_231/jre/lib/resources.jar
file:/E:/Java/jdk1.8.0_231/jre/lib/rt.jar
file:/E:/Java/jdk1.8.0_231/jre/lib/sunrsasign.jar
file:/E:/Java/jdk1.8.0_231/jre/lib/jsse.jar
file:/E:/Java/jdk1.8.0_231/jre/lib/jce.jar
file:/E:/Java/jdk1.8.0_231/jre/lib/charsets.jar
file:/E:/Java/jdk1.8.0_231/jre/lib/jfr.jar
file:/E:/Java/jdk1.8.0_231/jre/classes
*/

System.out.println(System.getProperty("sun.boot.class.path"));//获取根加载器路径
System.out.println(System.getProperty("java.ext.dirs"));//获取扩展类加载器路径
// E:\Java\jdk1.8.0_231\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
System.out.println(System.getProperty("java.class.path"));//获取应用类加载器路径
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

类加载器的加载时机:类加载器并不需要等到某个类被“首次主动使用”时再加载它:JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类才报告错误(LinkageError错误),如果这个类没有被程序主动使用,那么类加载器就不会报告错误。

2.1.2_自定义加载器

为什么需要自定义类加载器:

  • 隔离加载类
  • 修改类加载的方式
  • 扩展加载源
  • 防止源码泄漏

使用方法:

  • 继承抽象了java.lang.ClassLoader
  • 在JDK1.2之前,需要重写loadClass();JDK1.2之后,不需要重写loadClass()了,建议重写findClass()
  • 如果没有太多复杂的需求,可以直接继承URLCloassLoader类,就可以避免自己编写findClass()及其获取字节码流的方式,使自定义类加载器编写更加简洁。可以看test16

JVM中表示两个class对象是否为同一个类存在两个必要条件:

  • 类的完整类名必须一致,包括包名
  • 加载这个类的ClassLoader(指CLassLoader实例对象)必须相同
    换句话说,在JVM中,即使这两个类对象(class对象)来源同一个class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。

类加载器深入剖析:

  • Java虚拟机与程序的生命周期
  • 在如下几种情况下,java虚拟机将结束生命周期
    (1)执行了System.exit()方法
    (2)程序正常执行结束
    (3)程序在执行过程中遇到了异常或错误而异常终止
    (4)由于操作系统出现错误而导致虚拟机进程终止

获取类加载器的途径:

// 获取当前类的加载器
(1)clazz.getClassLoader(); 

// 获取当前线程上下文的加载器
(2)Thread.currentThread().getContextClassLoader(); 

// 获取系统的加载器
(3)ClassLoader.getSystemClassLoader(); 

// 获取调用者的加载器
(4)DriverManager.getCallerClassLoader(); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)

getParent();
loadClass(String);
findClass();
findLoadedClass();
defineClass();
resolveClass();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2.1.3_双亲委托(/派)机制

在这里插入图片描述

类加载器用来把类加载到java虚拟机中。从JDK1.2版本开始,类的加载过程采用父亲委托机制,这种机制能更好地保证Java平台的安全。在此委托机制中,除了java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。当java程序请求加载器loader1加载Sample类时,loader1首先委托自己的父加载器去加载Sample类,若父加载器能加载,则由父加载器完成加载任务,否则才由加载器loader1本身加载Sample类。

当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。

若有一个类能够成功加载Test类,那么这个类加载器被称为定义类加载器,所有能成功返回Class对象引用的类加载器(包括定义类加载器)称为初始类加载器

比如一个自定义类,可以由自定义类加载器和系统加载器加载,但是类路径下没有class文件,所以系统加载器加载不到,只有自定义加载器能加载到。此时,系统加载器和自定义加载器都是定义类加载器;而自定义加载器才是初试类加载器。详情可以看Test16

采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object对象。

类加载器双亲委托模型的好处:
(1)可以确保Java和核心库的安全:所有的Java应用都会引用java.lang中的类,也就是说在运行期java.lang中的类会被加载到虚拟机中,如果这个加载过程如果是由自己的类加载器所加载,那么很可能就会在JVM中存在多个版本的java.lang中的类,而且这些类是相互不可见的(命名空间的作用)。借助于双亲委托机制,Java核心类库中的类的加载工作都是由启动根加载器去加载,从而确保了Java应用所使用的的都是同一个版本的Java核心类库,他们之间是相互兼容的;
(2)确保Java核心类库中的类不会被自定义的类所替代;
(3)不同的类加载器可以为相同名称的类(binary name)创建额外的命名空间。相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器去加载即可。相当于在Java虚拟机内部建立了一个又一个相互隔离的Java类空间。

如果自定义了一个String类,会因为先加载到系统的,而无法使用main方法

先找到先使用,后面的不看

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

2.1.4_命名空间

  • 每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类构成
  • 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类;
  • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类;
  • 同一命名空间内的类是互相可见的,非同一命名空间内的类是不可见的
  • 子加载器可以见到父加载器加载的类,父加载器也不能见到子加载器加载的类

2.2_链接

类被加载后,就进入连接阶段。连接阶段就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。

  • 类的连接-验证
    1)类文件的结构检查
    2)语义检查
    3)字节码验证
    4)二进制兼容性的验证
  • 类的连接-准备
    在准备阶段,java虚拟机为类的静态变量分配内存,并设置默认的初始值。例如对于以下Sample类,在准备阶段,将为int类型的静态变量a分配4个字节的内存空间,并且赋予默认值0,为long类型的静态变量b分配8个字节的内存空间,并且赋予默认值0;
public class Sample{
    private static int a=1;
    public  static long b;
    public  static long c;
    static { b=2; }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2.3_类的初始化

首先明确是类的初始化而不是类实例的初始化。

tips:idea安装jclasslib插件可以看到<clinit>

初始化阶段就是执行构造器方法<clinit>,他不是我们普通的构造器。而是类的初始化而且他是自动生成的,编译器收集类中所以类静态变量的赋值动作和静态代码块中的语句合并而来。没有静态变量和静态代码块就不生成clinit方法了

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

JVM 会确保子类的clinit方法在父类 clinit已经执行结束。

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

在链接阶段已经定义好了,在这个阶段只是覆盖。但是不能在定义前调用它,这是编译器决定的。

而init是我们真正的构造器。

在初始化阶段,Java虚拟机执行类的初始化语句,为类的静态变量赋予初始值。在程序中,静态变量的初始化有两种途径:

  • (1)在静态变量的声明处进行初始化;
  • (2)在静态代码块中进行初始化。

类的初始化步骤:

  • (1)假如这个类还没有被加载和连接,那就先进行加载和连接
  • (2)假如类存在直接父类,并且这个父类还没有被初始化,那就先初始化直接父类
    • 当java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则不适用于接口。因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化只有当程序首次使用特定的接口的静态变量时,才会导致该接口的初始化
  • (3)假如类中存在初始化语句,那就依次执行这些初始化语句

Java程序对类的使用方式可分为两种

  • (1)主动使用
  • (2)被动使用

所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才能初始化他们

  • 主动使用(七种)
    • (1)new创建类的实例
    • (2)访问某个类或接口的静态变量( getstatic(助记符)),或者对该静态变量赋值 putstatic
    • (3)调用类的静态方法 invokestatic
    • (4)反射(Class.forName(“com.test.Test”))
    • (5)初始化一个类的子类
    • (6)Java虚拟机启动时被标明启动类的类
    • (7)JDK1.7开始提供的动态语言支持(了解)
  • 被动使用
    除了上面七种情况外,其他使用java类的方式都被看做是对类的被动使用,都不会导致类的初始化。如
    • 当访问一个静态域时,只有真正声明这个域的类才会被初始化。如:通过子类引用父类的静态变量,不会导致子类初始化
    • 通过数字定义类引用,不会触发此类的初始化
    • 引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中了)
    • 调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。

执行顺序

  • 静态代码块: 静态代码块在类被加载的时候就运行了,而且只运行一次,并且优先于各种代码块以及构造函数。如果一个类中有多个静态代码块,会按照书写顺序依次执行
    • 一般情况下,如果有些代码需要在项目启动的时候就执行,这时候就需要静态代码块。比如一个项目启动需要加载的很多配置文件等资源,我们就可以都放入静态代码块中。
    • 在类加载的时候,静态方法也已经加载了,但是我们必须要通过类名或者对象名才能访问,也就是说相比于静态代码块,静态代码块是主动运行的,而静态方法是被动运行的。
    • 静态变量要放在静态代码块前
  • 先初始化父类再初始化子类。先调用父类的构造函数再调用子类的构造函数
  • 构造代码块:在java类中使用{}声明的代码块(和静态代码块的区别是少了static关键字)。**构造代码块在创建对象时被调用,每次创建对象都会调用一次,但是优先于构造函数执行。**构造代码块依托于构造函数,也就是说,如果你不实例化对象,构造代码块是不会执行的
  •  

1、父类的静态变量和静态块赋值(按照声明顺序)
  2、自身的静态变量和静态块赋值(按照声明顺序)
  3、main方法
  3、父类的成员变量和块赋值(按照声明顺序)
  4、父类构造器赋值
  5、自身成员变量和块赋值(按照声明顺序)
  6、自身构造器赋值
  7、静态方法,实例方法只有在调用的时候才会去执行

3_内存结构

   

3.0 预备知识:javap工具与JVM参数设置

反编译工作javap:解析class文件

助记符

https://blog.csdn.net/shi1122/article/details/8053605

JVM虚拟机栈里有:操作数栈、局部变量表、方法返回地址、动态链接

局部变量表放到操作数栈中操作完后再放回局部变量表。

对于i++,++i原理是:

  • i++是先把原来的数放到操作数栈,再把局部变量表里的+1;
  • ++i是把操作数栈里的数直接加1,然后再把+1后的值放到局部变量表里
// 
// 下面的栈顶指的是操作数栈顶
助记符 ldc:表示将int、float或者String类型的常量值从常量池中推送至操作数栈顶
助记符 bipush:表示将单字节(-128-127)的常量值推送到栈顶
助记符 sipush:表示将一个短整型值(-32768-32369)推送至栈顶
助记符 iconst_1:表示将int型的1推送至栈顶(iconst_m1到iconst_5)
当int取值-1~5采用iconst指令,
取值-128~127采用bipush指令,
取值-32768~32767采用sipush指令,
取值-2147483648~2147483647采用 ldc 指令。

将一个局部变量加载到操纵栈的指令包括:iload、iload_、lload…
将一个数值从操作数栈存储到局部变量表的指令包括:istore、istore_、lstore…

偏移地址   操作指令  java源代码
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

JVM参数设置

说明参数
-Xss
堆初始大小-Xms
堆最大大小(新生代+老年大)-Xmx 或 -XX:MaxHeapSize=size
新生代大小-Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )
幸存区比例(动态调整)-XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例-XX:SurvivorRatio=ratio
晋升阈值-XX:MaxTenuringThreshold=threshold
GC详情-XX:+PrintTenuringDistribution
FullGC前先MinorGC-XX:+ScavengeBeforeFullGC
 -XX:+PrintGCDetails

https://docs.oracle.com/javase/8/docs/technotes/tools/windows/index.html

虚拟机设置官方文档:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

https://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html

https://docs.oracle.com/javase/specs/jvms/se8/html/index.html

https://docs.oracle.com/javase/specs/index.html

https://docs.oracle.com/javase/8/

3.1_程序计数器

  • 定义: Program Counter Register程序计数器(PC寄存器),就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码,这样线程切换后就可以知道从哪里开始执行了),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。他是线程私有的,这样cpu又回来执行的时候才知道从哪执行。
  • 作用: 记住下一条jvm指令的执行地址,用以完成分支、循环、跳转、异常处理、线程恢复等基础功能。。
  • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  • 线程私有
  • 程序计数器是内存中唯一一个不会出现内存溢出(OutOfMemory=OOM)的区域
  • 需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。
  • 场景:现实中程序往往是多线程协作完成任务的。JVM的多线程是通过CPU时间片轮转来实现的,某个线程在执行的过程中可能会因为时间片耗尽而挂起。当它再次获取时间片时,需要从挂起的地方继续执行。

3.2_JVM栈

定义: Java Virtual Machine Stacks(java虚拟机栈)栈也叫栈内存,主管Java程序的运行。

img

1.每个线程都有自己的栈,栈中的数据都是以**栈帧(Stack Frame)**的格式存在

2.在这个线程上正在执行的每个方法都对应各自的一个栈帧

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

4.JVM直接对java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循先进后出/后进先出的和原则。

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

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

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

8.不同线程中所包含的栈帧是不允许相互引用的,即不可能在另一个栈帧中引用另外一个线程的栈帧

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

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

  • 线程私有:在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,

  • 对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over。

  • -Xss设置栈内存大小,一般-Xss=1M

  • JVM栈是由栈帧组成的,:

  • 栈帧:栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集。每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。每个线程都只有一个活动栈帧,对应着线程当前执行的方法。活动栈帧即栈顶的帧。(在IDEA的Frames窗口中可以查看调用的帧)

    • 每个栈帧中存储着:
  • 1.局部变量表(Local Variables):输入参数和输出参数以及方法内的变量;8种基本类型的变量+对象的引用变量。栈里存放的是对象的引用ref,而对象实际上是存在堆里面的,对象引用ref指向堆里对象。实际上引用还指向了class文件,所以ref有两种指向情况:

    • ①ref先执行了两个指针,两个指针又分别指向对象数据+对象所属的类型(元数据class信息,元数据一个类只有一份,在方法区中)。
      • ②ref第一部分指向数据本身,第二部分是元数据的指针
    • 2.操作数栈(Operand Stack)(或表达式栈):记录出栈、入栈的操作;
    • 3.动态链接(Dynamic Linking)(或执行"运行时常量池"的方法引用)----深入理解Java多态特性必读!!
    • 4.方法返回地址(Return Adress)(或方法正常退出或者异常退出的定义)
    • 5.一些附加信息
  • 栈内存溢出:栈帧过多,栈被撑破了(递归)、栈帧过大(交叉引用)。栈没有GC

栈运行原理:

当一个方法A被调用时就产生了一个栈帧 F1,并被压入到栈中,
A方法又调用了 B方法,于是产生栈帧 F2 也被压入栈,
B方法又调用了 C方法,于是产生栈帧 F3 也被压入栈,
……
执行完毕后,先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧……
  • 1
  • 2
  • 3
  • 4
  • 5

遵循“先进后出”/“后进先出”原则。

栈的大小和具体JVM的实现有关,通常在256K~756K之间,与等于1Mb左右。

线程的局部变量是否线程安全?
答: 不一定。方法内的局部变量且没有逃离方法的作用访问时,是线程安全的。如果局部变量引用了对象,由于对象存在于堆中,一般其他线程可以访问修改,需要考虑线程安全。线程私有的,就不用考虑线程安全。是static的,就得考虑线程安全。

java虚拟机规范允许Java栈的大小是动态的或者是固定不变的

如果采用固定大小的Java虚拟机栈,那每一个线程的java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量,java虚拟机将会抛出一个 StackOverFlowError异常

public class StackErrorTest {
    public static void main(String[] args) {
        main(args);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5

如果java虚拟机栈可以动态拓展,并且在尝试拓展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那java虚拟机将会抛出一个 OutOfMemoryError异常

关于Error我们再多说一点,上面的讨论不涉及Exception
首先Exception和Error都是继承于Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。

Exception和Error体现了JAVA这门语言对于异常处理的两种方式。
Exception是java程序运行中可预料的异常情况,咱们可以获取到这种异常,并且对这种异常进行业务外的处理。

Error是java程序运行中不可预料的异常情况,这种异常发生以后,会直接导致JVM不可处理或者不可恢复的情况。所以这种异常不可能抓取到,比如OutOfMemoryError、NoClassDefFoundError等。

其中的Exception又分为检查性异常和非检查性异常。两个根本的区别在于,检查性异常 必须在编写代码时,使用try catch捕获(比如:IOException异常)。非检查性异常 在代码编写使,可以忽略捕获操作(比如:ArrayIndexOutOfBoundsException),这种异常是在代码编写或者使用过程中通过规范可以避免发生的。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
// 编译一段程序看看二进制文件及反编译的结果
public class Main11 {
    int a = 0;

    public Main11() {
    }

    public int add(int a) {
        System.out.println(111);
        return 1;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
G:\test\target\classes>javap -v Main11.class //反编译class文件
Classfile /G:/test/target/classes/Main11.class
  Last modified 2020-8-27; size 485 bytes
  MD5 checksum 1a7c4c8ffff290383477e59505da70e8
  Compiled from "Main11.java"
public class Main11
  minor version: 0
  major version: 49
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #5.#21         // Main11.a:I
   #3 = Fieldref           #22.#23        // java/lang/System.out:Ljava/io/PrintStream;
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(I)V
   #5 = Class              #26            // Main11
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               a
   #8 = Utf8               I
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               this
  #15 = Utf8               LMain11;
  #16 = Utf8               add
  #17 = Utf8               (I)I
  #18 = Utf8               SourceFile
  #19 = Utf8               Main11.java
  #20 = NameAndType        #9:#10         // "<init>":()V
  #21 = NameAndType        #7:#8          // a:I
  #22 = Class              #28            // java/lang/System
  #23 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(I)V
  #26 = Utf8               Main11
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (I)V
{
  int a;
    descriptor: I
    flags:

  public Main11();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_0
         6: putfield      #2                  // Field a:I
         9: return
      LineNumberTable:
        line 1: 0
        line 4: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   LMain11;

  public int add(int);
    descriptor: (I)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: bipush        111
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
         8: iconst_1
         9: ireturn
      LineNumberTable:
        line 7: 0
        line 8: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   LMain11;
            0      10     1     a   I
}
SourceFile: "Main11.java"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85

3.2.1 局部变量表

1.局部变量表也被称之为局部变量数组或本地变量表

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

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

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

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

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

利用javap命令对字节码文件进行解析查看main()方法对应栈帧的【局部变量表】,如图:

img

也可以在IDEA 上安装jclasslib byte viewcoder插件查看方法内部字节码信息剖析,以main()方法为例

img

img

img

img

变量槽slot的理解与演示

1.参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束

2.局部变量表,最基本的存储单元是Slot(变量槽)

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

4.在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot。

byte、short、char、float在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true;

long和double则占据两个slot。

 

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

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

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

8.如果当前帧是由构造方法或者实例方法创建的(意思是当前帧所对应的方法是构造器方法或者是普通的实例方法),那么==该对象引用this将会存放在index为0的slot处==,其余的参数按照参数表顺序排列。

9.静态方法中不能引用this,是因为静态方法所对应的栈帧当中的局部变量表中不存在this

示例代码:

public class LocalVariablesTest {

    private int count = 1;
    //静态方法不能使用this
    public static void testStatic(){
        //编译错误,因为this变量不存在与当前方法的局部变量表中!!!
        System.out.println(this.count);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

slot的重复利用

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

private void test2() {// slot的重复利用
    int a = 0;
    {
        int b = 0;
        b = a+1;
    }
    //变量c使用之前以及经销毁的变量b占据的slot位置
    int c = a+1;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

上述代码对应的栈帧中局部变量表中一共有多少个slot,或者说局部变量表的长度是几?

答案是3:this、a、c

变量b的作用域是

{
    int b = 0;
    b = a+1;
}
  • 1
  • 2
  • 3
  • 4

this占0号、a单独占1个槽号、c重复使用了b的槽号

静态变量与局部变量的对比及小结

变量的分类:

  • 按照数据类型分:
    • ①基本数据类型;
    • ②引用数据类型;
  • 按照在类中声明的位置分:
    • ①成员变量:在使用前,都经历过默认初始化赋值
      • static修饰:类变量:类加载链接的准备preparation阶段给类变量默认赋0值——>初始化阶段initialization给类变量显式赋值即静态代码块赋值;
      • 不被static修饰:实例变量:随着对象的创建,会在堆空间分配实例变量空间,并进行默认赋值
    • ②局部变量:在使用前,必须要进行显式赋值的!否则,编译不通过

补充说明

  • 在栈帧中,与性能调优关系最为密切的部分就是局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递
  • 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收

3.2.2 操作数栈(Operand Stack)

1.栈 :可以使用数组或者链表来实现

2.每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出的操作数栈,也可以成为表达式栈

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

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

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

 

操作数栈特点

  • 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
  • 操作数栈就是jvm执行引擎的一个工作区,当一个方法开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的
  • 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译器就定义好了,保存在方法的code属性中,为max_stack的值。
  • 栈中的任何一个元素都是可以任意的java数据类型
    • 32bit的类型占用一个栈单位深度
    • 64bit的类型占用两个栈深度单位
  • 操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈push和出栈pop操作来完成一次数据访问
  • **如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,**并更新PC寄存器中下一条需要执行的字节码指令。
  • 操作数栈中的元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类验证阶段的数据流分析阶段要再次验证。
  • 另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。

操作数栈代码追踪

// JVM虚拟机栈里有:操作数栈、局部变量表、方法返回地址、动态链接
// 下面的栈顶指的是操作数栈顶
// push是放到操作数栈的栈顶   load是从局部变量表加载到操作数栈  store是从操作数栈放到局部变量表
助记符 ldc:表示将int、float或者String类型的常量值从常量池中推送至操作数栈顶
助记符 bipush:表示将单字节(-128-127)的常量值推送到栈顶
助记符 sipush:表示将一个短整型值(-32768-32369)推送至栈顶
助记符 iconst_1:表示将int型的1推送至栈顶(iconst_m1到iconst_5)
当int取值-1~5采用iconst指令,
取值-128~127采用bipush指令,
取值-32768~32767采用sipush指令,
取值-2147483648~2147483647采用 ldc 指令。

将一个局部变量加载到操纵栈的指令包括:iload、iload_、lload…
将一个数值从操作数栈存储到局部变量表的指令包括:istore、istore_、lstore…

偏移地址   操作指令  java源代码
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

结合上图结合下面的图来看一下一个方法(栈帧)的执行过程

①15入栈;②存储15,15进入局部变量表

注意:局部变量表的0号位被构造器占用,这里的15从局部变量表1号开始

 

③压入8;④8出栈,存储8进入局部变量表;

 

⑤从局部变量表中把索引为1和2的是数据取出来,放到操作数栈;⑥iadd相加操作

 

⑦iadd操作结果23出栈⑧将23存储在局部变量表索引为3的位置上istore_3

 

bipush sipush

如果有返回值是什么样的?

  • 返回的那个变量把值压入栈然后当前栈帧结束
  • 下一个栈帧一上来就把就把值压入当前栈

i++和++i有什么区别

  • //
    int i1=10;
    i1++;
    int i2=10;
    ++i2;
    
    //
    int i3=10;
    int i4=i3++;
    int i5=10;
    int i6=++i5;
    
    //
    int i7=10;
    i7=i7++;
    int i8=10;
    i8=++i8;
    
    //
    int i9=10;
    i10=i9++ + ++i9;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

栈顶缓存技术ToS(Top-of-Stack Cashing)

  • 基于栈式架构的虚拟机所使用的零地址指令(即不考虑地址,单纯入栈出栈)更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数
  • 由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率

3.2.3 动态链接(Dynamic Linking)

这部分可以结合类加载链接阶段的解析阶段看

1.运行时常量池在JDK7之前位于方法区, JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆中开辟了一块区域存放运行时常量池。(每个类都有自己的常量池)

img

字节码中的常量池结构如下:

如下所示,栈帧包含4个部分:返回值、局部变量表、操作数栈、动态链接

这个动态链接引用指向的是当前类的运行时常量池

还可以注意到运行时常量池在方法区中

img

为什么需要常量池呢?
常量池的作用,就是为了提供一些符号和常量,便于指令的识别。下面提供一张测试类的运行时字节码文件格式

通过javap -v Test.class反编译出来的内容如下

img

上面的内容也可以用IDEA的jclasslib插件查看

img

上图在jclasslib插件的code中可以查看,下图不是该类的代码,但可以通过他看看规则

img

invokeVirtual代表调用方法,是#5所代表的字母的方法

  • getstatic:获取静态变量,如System.out,其中out就是System类的静态变量
  • invokevirtual调用方法

2.每一个栈帧内部都包含一个指向运行时常量池Constant pool或该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。比如invokedynamic指令

3.在Java源文件被编译成字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Refenrence)保存在class字节码文件(javap反编译查看)的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用(#)最终转换为调用方法的直接引用。

3.2.3.1 方法的调用

多态:在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关

  • 静态链接
    当一个 字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。
    • 绑定是一个字段、方法或者类将符号引用被替换为直接引用的过程,这仅仅发生一次。
    • 早期绑定
      早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
  • 动态链接
    如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。
    • 晚期绑定
      如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。

image-20200508101206296

Java中任何一个普通的方法其实都具备虚函数的特征,它们相当于C++语言中的虚函数(C++中则需要使用关键字virtual来显式定义)。如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。

3.2.3.2 虚方法和非虚方法

子类对象的多态性使用前提:实际开发编写代码中用的接口,实际执行是导入的的三方jar包已经实现的功能
①类的继承关系(父类的声明)②方法的重写(子类的实现)
  • 1
  • 2

非虚方法:如果方法在编译器就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法

虚方法:所有体现多态特性的方法称为虚方法

  • 非虚方法:
  • 1.invokestatic:调用静态方法,解析阶段确定唯一方法版本;(非虚方法)
  • 2.invokespecial:调用<init>方法、私有及父类方法,解析阶段确定唯一方法版本;(非虚方法)
  • 虚方法:
  • 3.invokevirtual:调用所有虚方法;(虚方法)
    • final修饰的除外,JVM会把final方法调用也归为invokevirtual指令,但要注意final方法调用不是虚方法
  • 4.invokeinterface:调用接口方法;(虚方法)
  • 动态调用指令(Java7新增):
    5.invokedynamic:动态解析出需要调用的方法,然后执行 .
    前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本。
    • JVM字节码指令集一直比较稳定,一直到Java7中才增加了一个invokedynamic指令,这是Java为了实现「动态类型语言」支持而做的一种改进。
    • 但是在Java7中并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令。直到Java8的Lambda表达式的出现,invokedynamic指 令的生成,在Java中才有了直接的生成方式。
    • Java7中增加的动态语言类型支持的本质是对Java虚拟机规范的修改,而不是对Java语言规则的修改,这一块相对来讲比较复杂,增加了虚拟机中的方法调用,最直接的受益者就是运行在Java平台的动态语言的编译器。

动态类型语言和静态类型语言。

动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。

说的再直白一点就是,静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。

方法的调用:方法重写的本质
Java语言中方法重写的本质:
1.找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C。

2.如果在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。

3.否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。

4.如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

IllegalAccessError介绍:
程序试图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。一般的,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。

虚方法表

  • 在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,为了提高性能,JVM采用在类的方法区建立一个虚方法表(virtual method table) (非虚方法不会出现在表中)来实现。使用索引表来代替查找。
    • virtual dispatch 机制会首先从 receiver(被调用方法的对象)的类的实现中查找对应的方法,如果没找到,则去父类查找,直到找到函数并实现调用,而不是依赖于引用的类型。
  • 每个类中都有一一个虚方法表,表中存放着各个方法的实际入口。
  • 那么虚方法表什么时候被创建?
    创建时机:虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。

https://blog.csdn.net/qq_29310729/article/details/106167943

方法调用:虚方法表

image-20200508172020646 image-20200508172604697

image-20200508174128592

image-20200510135739972

image-20200510140057975

多态原理

多态的两种实现方式:

  • 1、子类继承父类(extends)
  • 2、类实现接口(implements)
  • 无论是哪种方法,其核心之处就在于对父类方法的改写或对接口方法的实现,以取得在运行时不同的执行效果。
class Person {
    public String toString() { return "I'm a person.";}
    public void eat() {}
    public void speak() {}
}
 
class Boy extends Person {
    public String toString() {  return "I'm a boy"; }
    public void speak() {}
    public void fight() {}
}
 
class Girl extends Person {
    public String toString() { return "I'm a girl";}
    public void speak() {}
    public void sing() {}
}
// 当这三个类被载入到 Java 虚拟机之后,方法区中就包含了各自的类的信息。Girl 和 Boy 在方法区中的方法表可表示如下:
class Party {
    void happyHour() {
        Person girl = new Girl();// 注意用的是父类接收 //Girl实现了Person类
        girl.speak();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

注意方法表条目指向的具体的方法地址,如 Girl 的继承自 Object 的方法中,只有 toString() 指向自己的实现(Girl 的方法代码),其余皆指向 Object 的方法代码;其继承自于 Person 的方法 eat() 和 speak() 分别指向 Person 的方法实现和本身的实现。

继承的多态

重要的几句话:

  • Person 或 Object 的任意一个方法,在它们(作为父类)的方法表和其子类 Girl 和 Boy 的方法表中的位置 (index) 是一样的。这样 JVM 在调用实例方法其实只需要指定调用方法表中的第几个方法即可。

    • 刚开始我有点绕,我还合计要是有两个父类,一个父类的第3个索引是a()方法,第二个父类的第三个索引是b()方法,那么子类的第三个索引是哪个方法呢?原来这就是java为什么单继承的一部分原因,不会有两个父类,只有有1个父类,所以索引对应的方法是统一的。还值得注意的是这里说的是继承,没有说实现,也就是接口的多态原理并不是这个,一会我们再介绍
  • 如果子类改写了父类的方法,那么子类和父类的那些同名的方法共享一个方法表项。

  • 因此,方法表的偏移量总是固定的。所有继承父类的子类的方法表中,其父类所定义的方法的偏移量也总是一个定值。

  • Person 或 Object中的任意一个方法,在它们的方法表和其子类 Girl 和 Boy 的方法表中的位置 (index) 是一样的。这样 JVM 在调用实例方法其实只需要指定调用方法表中的第几个方法即可

当编译 Party 类的时候,生成 girl.speak()的方法调用假设为:

并且是个方法引用类型

#12 = Methodref          #21.#22        // com/example/demo/Person.speak:()V

#8 = Utf8               ()V
#21 = Class              #25            // com/example/demo/Person
#22 = NameAndType        #26:#8         // speak:()V
#26 = Utf8               speak

JVM 首先查看 Party 的常量池索引为 12 的条目(应为 CONSTANT_Methodref_info 类型,可视为方法调用的符号引用),进一步查看常量池(CONSTANT_Class_info,CONSTANT_NameAndType_info ,CONSTANT_Utf8_info)可得出要调用的方法是 Person 的 speak 方法(注意引用 girl 是其基类 Person 类型),查看 Person 的方法表,得出 speak 方法在该方法表中的偏移量 15(offset),这就是该方法调用的直接引用。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Invokevirtual #12,即调用方法,哪个方法呢?运行时常量池中的第12个字符串,而第12个字符串表示的是Person类的speak()

查看 Person 的方法表,得出 speak 方法在该方法表中的偏移量 15(offset),这就是该方法调用的直接引用。

JVM 执行真正的方法调用:根据实例方法调用的参数 this 得到具体的对象(即 girl 所指向的位于堆中的对象),据此得到该对象对应的方法表 (Girl 的方法表 ),进而调用方法表中的某个偏移量所指向的方法(Girl 的 speak() 方法的实现)

  • 如果this所指向的第15个索引对应的方法不是speak方法,那么就说明Girl没有实现speak方法,去父类中的第15个索引位置找这个方法即可。这里我们实现了,所以找到了,无需再找父类的第15个索引对应的方法

具体过程:

假设类B是类A的子类,以 A a = new B() 为例

① A a 作为一个引用类型数据,存储在JVM栈的本地变量表中。
② new B()作为实例对象数据存储在堆中
   B的对象实例数据(接口、方法、field、对象类型等)的地址也存储在堆中
   B的对象的类型数据(对象实例数据的地址所执行的数据)存储在方法区中,方法区对象类型数据中有一个指向该类方法的方法表。

③Java虚拟机规范中并未对引用类型访问具体对象的方式做规定,目前主流的实现方式主要有两种:

1. 通过句柄访问

在这种方式中,JVM堆中会专门有一块区域用来作为句柄池,存储相关句柄所执行的实例数据地址(包括在堆中地址和在方法区中的地址)。这种实现方法由于用句柄表示地址,因此十分稳定。

2.通过直接指针访问

通过直接指针访问的方式中,reference中存储的就是对象在堆中的实际地址,在堆中存储的对象信息中包含了在方法区中的相应类型数据。这种方法最大的优势是速度快,在HotSpot虚拟机中用的就是这种方式。

④实现过程

首先虚拟机通过reference类型(A的引用)查询java栈中的本地变量表,得到堆中的对象类型数据的地址,从而找到方法区中的对象类型数据(B的对象类型数据) ,然后查询方法表定位到实际类(B类)的方法运行。

接口的多态

因为 Java 类是可以同时实现多个接口的,而当用接口引用调用某个方法的时候,情况就有所不同了。原因我们刚才说了,java是单继承,多实现。多实现的话我们就不能用刚才的逻辑处理接口多态了,因为我们无法决定第15个方法应该去哪个接口类中找,万一多个接口类中都有这个方法,使用哪个呢?所以接口不能用刚才继承的逻辑了

这也就是为什么编译指令的时候要把虚方法的invokevirtual和invokeinterface分开写,而不是合并到一起的原因。

  • invokevirtual指令用于调用声明为类的方法;
  • invokeinterface指令用于调用声明为接口的方法;

Java 对于接口方法的调用是采用搜索方法表的方式,对如下的方法调用:

  1. invokeinterface #13
  2. JVM 首先查看常量池,获取方法调用的符号引用(名称、返回值等等),然后利用 this 指向的实例,得到该实例的方法表,进而搜索方法表来找到合适的方法地址。
  3. 因为每次接口调用都要搜索方法表,所以从效率上来说,接口方法的调用总是慢于类方法的调用的

注:实际上似乎并不是永远都是invokeinterface,我写了接口的实现后反编译出现的是invokevirtual,我个人的猜测是,jdk对此进行了优化,因为接口多态遍历费时,那在某些情况下用Invokevirtual直接定位到就省时了

下面程序是用来复习invoke的

interface MethodInterface{
    void methodA();
}

class Father{
    public Father(){
        System.out.println("father的构造器");
    }
    public static void showStatic(String str){
        System.out.println("father "+ str);
    }
    public final void showFinal(){
        System.out.println("father show final");
    }
    public void showCommon(){
        System.out.println("father 普通方法");
    }

}
public class Son extends Father{
    public Son(){
        //invokespecial
        super();
    }
    public Son(int age){
        //invokespecial
        this();
    }
    //不是重写的父类的静态方法,因为静态方法不能被重写
    public static void showStatic(String str){
        System.out.println("son "+ str);
    }
    public void showPrivate(String str){
        System.out.println("son private "+str);
    }
    public void show(){
        //invokestatic
        showStatic("p3wj.top");
        //invokestatic
        Father.showStatic("good!");
        //invokespecial
        showPrivate("hello!");
        //invokespecial
        super.showCommon();
        
        //虚方法:编译期间无法确定下来的
        //invokevirtual,虽然是这个但是被final修饰他不是一个虚方法
        showFinal();
        //invokespecial,加上super,显示地表示是一个父类地方法
        super.showFinal();
        
        //invokevirtural,因为有可能该子类会重写这方法,如果加上super就是invokespecial
        showCommon();
        info();

        MethodInterface in = null;
        //invokeinterface
        in.methodA();

    }
    public void info(){

    }
    public void display(Father f){
        f.showCommon();
    }

    public static void main(String[] args) {
        Son so = new Son();
        so.show();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72

3.2.4 方法返回地址(主要针对于正常退出的情况)

●存放调用该方法的pc寄存器的值。
●一个方法的结束,有两种方式:

➢正常执行完成
➢出现未处理的异常,非正常退出

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

交给执行引擎,去执行后续的操作

区别:

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

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

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

2、在方法执行的过程中遇到了异常(Exception) ,并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。简称异常完成出口。

方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码。

以上数字为字节码指令地址

如果在4-16行出现异常,则用19行处理,针对任何类型

Exception Table: 
fromtotargettype
41619any
192119any

3.3_本地方法栈

简单地讲,一个Native Method就 是一个Java调用非Java代码的接口。 一个Native Method是这样 一个Java方法:该方法的实现由非Java语言实现,比如C。这个特征并非Java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern "C"告 知C++编译器去调用-一个C的函数。

“A native method is a Java method whose implementation isprovided by non-java code.”

在定义一个native method时,并不提供实现体(有些像定义一个Javainterface),因为其实现体是由非java语言在外面实现的。

  • 虚拟机栈: 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
  • 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序。

public class IHaveNatives {
    public native void Native1(int x);

    native static public long Native2();

    native synchronized private float Native3(Object o);

    native void Native4(int[] art) throws Exception;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

本地方法栈
●Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
●本地方法栈,也是线程私有的。
允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)

➢如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError 异常。
➢如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个OutOfMemoryError 异常。

●本地方法是使用C语言实现的。
●它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。

●当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟
机限制的世界。它和虚拟机拥有同样的权限。

➢本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。
➢它甚至可以直接使用本地处理器中的寄存器
➢直接从本地内存的堆中分配任意数量的内存。

●并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求9
**本地方法栈的使用语言、具体实现方式、数据结构等。**如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。
●在Hotspot JVM中, 直接将本地方法栈和虚拟机栈合二为一。

本地方法栈:JAVA虚拟机调用本地方法时,给本地方法分配的内存空间。

本地方法native method:不是由java编写的代码,如C写的与操作系统底层打交道的方法。如Object类中的protected native Object clone();

本地方法栈类似于虚拟机栈,也是线程私有。

不同点:本地方法栈服务的对象是jvm运行的native方法,而虚拟机栈服务的是jvm执行的java方法。

本地接口的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++程序,Java 诞生的时候是 C/C++横行的时候,要想立足,必须有调用 C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是 Native Method Stack中登记 native方法,在Execution Engine 执行时加载native libraies。

目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用 Socket通信,也可以使用Web Service等等,不多做介绍。

3.4_堆

定义: Heap,通过new关键字创建的对象,都存放在堆内存中。

特点

  • 线程共享,堆中的对象都存在线程安全的问题。
  • 垃圾回收,垃圾回收机制重点区域。

根据垃圾回收的划分,逻辑上将堆划分为:

  • 新生代Young Generation
    • Eden伊甸园
    • 幸存区Survivor From
    • 幸存区Survivor To
  • 老年代Tenure generation
  • JDK7之前为Permanent永久区,JDK8之后为元空间
//演示堆内存溢出  //-Xms  -Xmx
public class Demo{
    public static void main(String[] args){
        int i=0;
        try{
            List<String> list=new ArrayList<>();
            String a="hello";
            while(true){
                list.add(a);//list对象始终被关联,无法被回收,死循环不断将list规模变大,最终大于堆内存大小,内存溢出。
                a=a+a;
                i++;
            }
        }catch(Throwable e){
            e.printStackTrace();
            System.out.println(i);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
//对象什么时候释放:
public void method(){
    Object obj= new Object();
}
生成了2部分的内存区域:1:obj这个引用变量,因为是方法内的变量,放到JVM栈里面(引用占4个字节)。2:真正Objectclass的实例对象放到Heap里面。(空对象栈8个字节)
方法结束后,对应栈中的变量马上回收,但是对重的对象要等到GC来回收
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

一个进程对应一个JVM实例,对应一个runtime data area运行时数据区

一个进程对应一个jvm实例,一个运行时数据区。一个进程中的多个线程共享同一个方法区、堆空间,各自拥有程序计数器、本地方法栈、虚拟机栈

●一个JVM实例只存在一个堆内存,堆也是Java内存管 理的核心区域。
●Java 堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
➢堆内存的大小是可以调节的。
●《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。(涉及到物理内存和虚拟内存)
●所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(ThreadLocal Allocation Buffer, TLAB)

堆空间大小的设置和查看:

  • Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,大家可以通过下面选项设置
    • "-Xms"用于表示堆区的起始内存,等价于-XX:InitialHeapSize
    • "-Xmx"则用于表示堆区的最大内存,等价于-XX:MaxHeapSize
  • ●一旦堆区中的内存大小超过“-Xmx"所指定的最大内存时,将会抛出OutOfMemoryError异常。
    通常会将-Xms 和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
    ●默认情况下,初始内存大小:物理电脑内存大小/ 64
    最大内存大小:物理电脑内存大小/ 4
Runtime.getRuntime().totalMemory();
Runtime.getRuntime().maxMemory();
  • 1
  • 2

堆内存诊断

  • jps工具
    • 查看系统有哪些进程。jps
  • jmap工具
    • 查看堆内存使用情况 jmap -heap 【进程号】
  • jconsole工具
    • 图形界面,多功能检测工具,连续监测。
  • jvisualVM

有下面程序:

public class Demo1 {
    public static  void main(String[] args) throws InterruptedException {
        System.out.println("1....");
        Thread.sleep(30000);
        byte[] array =  new byte[1024 * 1024 * 10];//10M
        System.out.println("2....");
        Thread.sleep(30000);
        array = null;//array没有引用,可以被回收了
        System.gc();
        System.out.println("3...");
        Thread.sleep(1000000L);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

运行上面代码,首先在终端输入jps得到Demo进程PID,根据PID再通过jmap -heap PID每次查看进程占用内存情况:

D:\openSourceProject\jvm1>jps
8916 Launcher
9876 RemoteMavenServer36
11656
13976 Demo1 //这个就是我们的进程号
13756 Jps
    
//----------------------------------------
//按进程号查看堆情况
D:\openSourceProject\jvm1>jmap -heap 13976
Attaching to process ID 13976, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.221-b11

using thread-local object allocation.
Parallel GC with 8 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4276092928 (4078.0MB)
   NewSize                  = 89128960 (85.0MB)
   MaxNewSize               = 1425014784 (1359.0MB)
   OldSize                  = 179306496 (171.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 67108864 (64.0MB)
   used     = 6711104 (6.40020751953125MB)//6M
   free     = 60397760 (57.59979248046875MB)
   10.000324249267578% used
From Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
To Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
PS Old Generation
   capacity = 179306496 (171.0MB)
   used     = 0 (0.0MB)
   free     = 179306496 (171.0MB)
   0.0% used
3175 interned Strings occupying 260400 bytes.
       
//----------------------------------------
//再次输入
D:\openSourceProject\jvm1>jmap -heap 13976//每次都得重新输入
Attaching to process ID 13976, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.221-b11

using thread-local object allocation.
Parallel GC with 8 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4276092928 (4078.0MB)
   NewSize                  = 89128960 (85.0MB)
   MaxNewSize               = 1425014784 (1359.0MB)
   OldSize                  = 179306496 (171.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 67108864 (64.0MB)
   used     = 17196880 (16.400222778320312MB)//16M=6+10
   free     = 49911984 (47.59977722167969MB)
   25.62534809112549% used
From Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
To Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
PS Old Generation
   capacity = 179306496 (171.0MB)
   used     = 0 (0.0MB)
   free     = 179306496 (171.0MB)
   0.0% used

3176 interned Strings occupying 260456 bytes.
//----------------------------------------
D:\openSourceProject\jvm1>jmap -heap 13976
Attaching to process ID 13976, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.221-b11

using thread-local object allocation.
Parallel GC with 8 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4276092928 (4078.0MB)
   NewSize                  = 89128960 (85.0MB)
   MaxNewSize               = 1425014784 (1359.0MB)
   OldSize                  = 179306496 (171.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 67108864 (64.0MB)
   used     = 1342200 (1.2800216674804688MB)//1M
   free     = 65766664 (62.71997833251953MB)
   2.0000338554382324% used
From Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
To Space:
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
PS Old Generation
   capacity = 179306496 (171.0MB)
   used     = 1106008 (1.0547714233398438MB)
   free     = 178200488 (169.94522857666016MB)
   0.6168253937659905% used

3162 interned Strings occupying 259464 bytes.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153

jconsole使用 控制台输入jconsole。图中竖条是代表控制台有输出?

image.png

可明显看出内存变化。

一个简单的案例

  • 执行多次垃圾回收后,内存占用依然很高
    • 1.控制台输入jvisitualvm,在左边选择对应进程,右面点“堆dump”。
    • 2.点击“查找”,点击第一条占用内存最大的记录。
    • 3.找到问题所在,list中有过多大对象student,无法被清除。
public class Demo2 {
    public static  void main(String[] args) throws InterruptedException {
        List<student> list = new ArrayList<>();
        for (int i = 0; i < 200;i++){
            list.add(new student());
        }
        Thread.sleep(10000000000L);
    }
}

class student{
    private byte[] big = new byte[1024 * 1024];
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

3.5_方法区

堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。(类信息和运行时常量池)

方法区Mthod Area:所有线程共享的一块区域,存储了每个类class结构的信息,包括:

  

这里的运行时常量池在JDK7之后放在了堆区

方法区是GC的非主要工作区域,java虚拟机规范表示可以不要求虚拟机在这区实现GC,这区GC的性价比一般比较低;在堆中,尤其是新生代,常规应用进行一次GC一般可以回收70%~95%的空间,而方法区的GC效率远小于此。当前的商业JVM都有实现方法区的GC,主要回收两部分:废弃常量和无用类。

类回收需要满足如下3个条件:

  • 该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例
  • 加载该类的ClassLoader已经被GC
  • 该类对应的java.lang.Class对象没有在任何地方被引用,如:不能再任何地方通过反射访问该类的方法

在大量使用反射、动态代理、CGLib等字节码框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要JVM具备类下载的支持以保证方法区不会溢出。

从JDK1.8开始就没有永久代了,变为了元空间

	实际而言,方法区(Method Area)和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码等等,虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
	
	对于HotSpot虚拟机,很多开发者习惯将方法区称之为“永久代(Parmanent Gen)” ,但严格本质上说两者不同,或者说使用永久代来实现方法区而已,永久代是方法区(相当于是一个接口interface)的一个实现,jdk1.7的版本中,已经将原本放在永久代的字符串常量池移走。
	
永久区(java7之前有)
	永久存储区是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。
	永久代 Permanent Generation,从JDK1.8彻底废弃,使用元空间 meta space
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

下图演示了栈帧里的局部变量指向了堆与方法区

运行时常量池

栈帧里的动态链接连接到了运行时常量池

  • Class文件的常量池与方法区的运行时常量池:我们写的每一个Java类被编译后,就会形成一份class文件;每一个Class文件中,都维护着一个常量池(这个保存在类文件里面,不要与方法区的运行时常量池搞混)。这个常量池的内容,在类加载的时候,被复制到方法区的运行时常量池 ;
  • class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool ),用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic References)
    • 字面量包括:1.文本字符串 2.八种基本类型的值 3.被声明为final的常量等;
    • 符号引用包括:1.类的全限定名,2.字段名和属性,3.方法名和属性。
  • 运行时常量池在哪:https://www.cnblogs.com/cosmos-wong/p/12925299.html 。总结:JDK6时在方法区永久代中,JDK7时在堆中

jvm在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,【jvm就会将class常量池中的内容存放到运行时常量池中,运行时常量池存在于内存中,也就是class常量池被加载到内存之后的版本】,不同之处是:它的字面量可以动态的添加(String#intern()),符号引用可以被解析为直接引用(里面的符号地址变为真实地址)。

运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,【每个class都有一个运行时常量池】,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。解析的过程会去查询全局字符串池,也就是我们下面所说的StringTable,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。

运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入运行时常量池中,这种特性被开发人员利用比较多的就是String类的intern()方法。

  • 运行时常量池:
/**1.8 以前会导致永久代内存溢出
 * 演示永久代内存溢出  java.lang.OutOfMemoryError: PermGen space
 * -XX:MaxPermSize=8m
 */
public class Demo1_8 extends ClassLoader {//可以用来加载类的二进制字节码
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo1_8 test = new Demo1_8();
            for (int i = 0; i < 20000; i++, j++) {
                ClassWriter cw = new ClassWriter(0);//ClassWriter作用是生成类的二进制字节码
                cw.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);//参数:类版本号、类的访问修饰符、类的名字、包名类的父类、类要实现的接口
                byte[] code = cw.toByteArray();//返回byte数组
                test.defineClass("Class" + i, code, 0, code.length);//触发类的加载//即生成了Class对象
            }
        } finally {
            System.out.println(j);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
/**1.8之后会导致元空间内存溢出
 * 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace 元空间
 * -XX:MaxMetaspaceSize=8m
 */
public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo1_8 test = new Demo1_8();
            for (int i = 0; i < 10000; i++, j++) {
                // ClassWriter 作用是生成类的二进制字节码
                ClassWriter cw = new ClassWriter(0);
                // 版本号, public, 类名, 包名, 父类, 接口
                cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                // 返回 byte[]
                byte[] code = cw.toByteArray();
                // 执行了类的加载
                test.defineClass("Class" + i, code, 0, code.length); // Class 对象
            }
        } finally {
            System.out.println(j);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

字段信息

  • 声明的顺序
  • 修饰符
  • 类型
  • 名字

方法信息

  • 声明的顺序
  • 修饰符
  • 返回值类型
  • 名字
  • 参数列表(有序保存)
  • 异常表(方法抛出的异常)
  • 方法字节码(native、abstract方法除外,)
  • 操作数栈和局部变量表大小

类变量(即static变量)

非final类变量

在java虚拟机使用一个类之前,它必须在方法区中为每个非final类变量分配空间。非final类变量存储在定义它的类中;

final类变量(不存储在这里)

由于final的不可改变性,因此,final类变量的值在编译期间,就被确定了,因此被保存在类的常量池里面,然后在加载类的时候,复制进方法区的运行时常量池里面 ;final类变量存储在运行时常量池里面,每一个使用它的类保存着一个对其的引用;

对类加载器的引用

jvm必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么jvm会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。

对Class类的引用

jvm为每个加载的类都创建一个java.lang.Class的实例(存储在堆上)。而jvm必须以某种方式把Class的这个实例和存储在方法区中的类型数据(类的元数据)联系起来, 因此,类的元数据里面保存了一个Class对象的引用;

方法表

为了提高访问效率,必须仔细的设计存储在方法区中的数据信息结构。除了以上讨论的结构,jvm的实现者还可以添加一些其他的数据结构,如方法表。jvm对每个加载的非虚拟类的类型信息中都添加了一个方法表,方法表是一组对类实例方法的直接引用(包括从父类继承的方法。jvm可以通过方法表快速激活实例方法。(译者:这里的方法表与C++中的虚拟函数表一样,但java方法全都 是virtual的,自然也不用虚拟二字了。正像java宣称没有 指针了,其实java里全是指针。更安全只是加了更完备的检查机制,但这都是以牺牲效率为代价的,个人认为java的设计者 始终是把安全放在效率之上的,所有java才更适合于网络开发)

字符串对象.intern()

  • 作用:将指定字符串尝试放入StringTable
此外String对象调用intern()方法时,会先在StringTable中查找是否存在于该对象相同的字符串,若存在直接返回String table中字符串的引用,若不存在则在StringTable中创建一个与该对象相同的字符串。


String s1 = "ma";
String s2 = "in";
String s3 = s1 +s2;//实际上指向的是堆
s3.intern();//main String,java等属于关键词,在一开始就在StringTable中存在了,所以s3.intern没能插入进去。
String s4 = "ma" + "in";
System.out.println(s3 == s4);//false
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

javap -v hello.class

  • v 显示反编译后的详细信息

常量池中的字符串仅是符号,第一次用到时才变为对象。利用串池的机制,来避免重复创建字符串对象

字符串常量池

字符串常量池在方法区中,1.8中使用原空间代替永久代来实现方法区,但方法区并没有改变。改变的是方法去中内容的物理存放位置。类信息被移动到元空间中,但运行时常量池和字符串常量池被移动到了堆中。但是不论他们物理上如何存放,逻辑上还是属于方法区的。

调用str1.intern()

  • 如果常量池中已经有了该字符串str1,那直接返回常量池中str1的引用。(注意返回的跟去时候的可能没有关系)
  • 如果常量池中没有该字符串str1,
    • JDK6会把字符串复制到常量池中,相当于常量池中是一个副本str2,并且返回的是该副本的引用str2,而该字符串str1还是指向堆中;
    • JDK7会把堆中字符串str1的引用写到常量池中str1,而不是复制,当新的变量被赋值该字符串str1时,直接指向的是该引用str1。如果原来就有同样的内容了,就返回原来内容我引用

一、new String都是在堆上创建字符串对象。当调用 intern() 方法时,编译器会将字符串添加到常量池中(stringTable维护),并返回指向该常量的引用。
这里写图片描述这里写图片描述

String s = new String("abc");//字符串常量池中有abc,堆中也有
String s1 = "abc";
String s2 = new String("abc");
System.out.println(s == s1.intern());//false
System.out.println(s == s2.intern());//false
System.out.println(s1 == s.intern());//true //intern返回的是并不是s了,而是常量池中的s1了
System.out.println(s1 == s2.intern());//true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

二、通过字面量赋值创建字符串(如:String str=”twm”)时,会先在常量池中查找是否存在相同的字符串,若存在,则将栈中的引用直接指向该字符串;若不存在,则在常量池中生成一个字符串,再将栈中的引用指向该字符串。

这里写图片描述

三、常量字符串的“+”操作,编译阶段直接会合成为一个字符串。如string str=“JA”+“VA”,在编译阶段会直接合并成语句String str=“JAVA”,于是会去常量池中查找是否存在”JAVA”,从而进行创建或引用。

四、对于final字段,编译期直接进行了常量替换(而对于非final字段则是在运行期进行赋值处理的)。

String s1 = "bc";
final String s2 = "b";//注意是final,即是常量
final String s3 = "c";
String s4 = s2 + s3;// 在编译时,直接替换成了String s4="b"+"c",根据第三条规则,再次替换成String s4="bc"
// 常量的时候编译后就是符号,可以理解为不是变量
System.out.println(s1 == s4);//true,因为final变量在编译后会直接替换成对应的值,所以实际上等于s4="b"+"c",而这种情况下,编译器会直接合并为s4="bc",所以最终s1==s4。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

五、常量字符串和变量拼接时(如:String str3=baseStr + "01";)会调用==stringBuilder.append()在堆上创建新的对象==。

六、JDK 1.7后,intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,

区别在于,如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。简单的说,就是往常量池放的东西变了:原来在常量池中找不到时,复制一个副本放到常量池,1.7后则是将在堆上的地址引用复制到常量池。

这里写图片描述

举例说明:

String str2 = new String("str")+new String("01");
str2.intern();//JDK6:复制一份,返回该副本引用,但str2还是指向堆中的。JDK7:在常量池中生成一个引用指向堆中。

String str1 = "str01";//JDK6:常量池中的副本。JDK7:这个引用从字符串常量池中指向堆中
System.out.println(str2==str1);//JDK6:false。JDK7:true
  • 1
  • 2
  • 3
  • 4
  • 5

在JDK 1.7下,当执行str2.intern();时,因为常量池中没有“str01”这个字符串,所以会在常量池中生成一个对堆中的“str01”的引用(注意这里是引用 ,就是这个区别于JDK 1.6的地方。在JDK1.6下是生成原字符串的拷贝),而在进行String str1 = “str01”;字面量赋值的时候,常量池中已经存在一个引用,所以直接返回了该引用,因此str1和str2都指向堆中的同一个字符串,返回true。

String str2 = new String("str")+new String("01");//JDK6:堆//JDK7:堆
String str1 = "str01";//JDK6: 常量池//JDK7:常量池

str2.intern();//JDK6:尝试复制,常量池已经有了,没有复制,返回了常量池中的引用,但str2还是指向堆中的//JDK7:尝试提供堆中的引用给常量池,常量池已经有自己的了,无需引用堆中你的了
System.out.println(str2==str1);//JDK6:false//JDK7:false//都是堆中一份,常量池中一份
  • 1
  • 2
  • 3
  • 4
  • 5

将中间两行调换位置以后,因为在进行字面量赋值(String str1 = “str01″)的时候,常量池中不存在,所以str1指向的常量池中的位置,而str2指向的是堆中的对象,再进行intern方法时,对str1和str2已经没有影响了,所以返回false。

String s = new String("1");//堆中,同时常量池中也有1了
s.intern();// JDK6,复制,复制失败 //JDK7:常量池中已经有了,无需复制
String s2 = "1";
System.out.println(s == s2);// JDK6和7都是false

String s3 = new String("1") + new String("1");//堆中有11,常量池中没有11
s3.intern();//JDK6复制成功//JDK7引用成功
String s4 = "11";

System.out.println(s3 == s4);//JDK6:false  JDK7:true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

再分别调整上面代码2.3行、7.8行的顺序:

String s = new String("1");//堆中,同时常量池中也有1了
String s2 = "1";//指向上一句在常量池中创建好的常量,但不是堆中的常量
s.intern();

System.out.println(s == s2);//JDK6:false  JDK7:false

 
String s3 = new String("1") + new String("1");//堆中有11,常量池中没有11
String s4 = "11";//常量池中也自己的有11了
s3.intern();//JDK6复制失败//JDK7引用失败

System.out.println(s3 == s4);//JDK6:false  JDK7:false
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

字符串常量池的位置

  • JDK6:StringTable在方法区。
  • JDK8:StringTable在堆区
//运行如下代码探究常量池的位置  
public static void main(String[] args) throws Throwable {  
    List<String> list = new ArrayList<String>();  
    int i=0;  
    while(true){  
        list.add(String.valueOf(i++).intern());  
    }  
}  
/*
用jdk1.6运行后会报错,永久代这个区域内存溢出会报:
Exception in thread “main” java.lang.OutOfMemoryError:PermGen space的内存溢出异常,表示永久代内存溢出。
jdk1.7 和1.8Exception in thread “main” java.lang.OutOfMemoryError: Java heap space说明1.6在永久带,1.7以后移动到了heap中
98%的垃圾回收,但只有2%的堆被重置
*/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

串常量垃圾回收

package JVMtest;
/*
* 演示stringTable垃圾回收
* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
* 打印字符串表的统计信息
* 打印垃圾回收的详细信息
* */
public class StringTable {
    public static void main(String[] args) {
        int i=0;
        try {
            for(int j=0;j<100;j++){//ctrl+alt+t快捷键try catch
                String.valueOf(j).intern();//这句话注释与打开
                i++;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }
    }
}
//输出信息如下:

100
Heap//堆
 PSYoungGen      total 2560K, used 1644K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 80% used [0x00000000ffd00000,0x00000000ffe9b3f0,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 7168K, used 0K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffd00000)
 Metaspace       used 3144K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 343K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics://符号表
Number of buckets       :     20011 =    160088 bytes, avg   8.000
Number of entries       :     13114 =    314736 bytes, avg  24.000
Number of literals      :     13114 =    562744 bytes, avg  42.912
Total footprint         :           =   1037568 bytes
Average bucket size     :     0.655
Variance of bucket size :     0.655
Std. dev. of bucket size:     0.810
Maximum bucket size     :         6
StringTable statistics://串常量分析
Number of buckets       :     60013 =    480104 bytes, avg   8.000//桶个数60013
Number of entries       :      1839 =     44136 bytes, avg  24.000//键值对个数1839
Number of literals      :      1839 =    161288 bytes, avg  87.704//字符串常量个数//什么都没做就有1000+了//注释了for之后显示1739
Total footprint         :           =    685528 bytes
Average bucket size     :     0.031
Variance of bucket size :     0.031
Std. dev. of bucket size:     0.175
Maximum bucket size     :         3

Process finished with exit code 0

//for改为10000后,//字符串常量并没有变为10000多个,而是满了之后就垃圾回收了。证明了StringTable也会发生垃圾回收
[GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->636K(9728K), 0.0012292 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
10000
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :      3174 =     76176 bytes, avg  24.000
Number of literals      :      3174 =    225688 bytes, avg  71.105
Total footprint         :           =    781968 bytes
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63

串常量池性能调优

  • 调整:XX:+StringTableSize=桶个数。将StringTable中的桶个数设为2000。 hash表桶的数量越多(数组部分长度越长),数据越分散,hashcode撞车的概率越小,速度越快。 默认值是6万多
  • 考虑将字符串对象是否入池
-Xms500m 设置堆内存为500mb
    -Xmx500m -XX:+PrintStringTableStatistics -XX:+StringTableSize=20000
    限制了桶大小为2W。
    变慢了
    往StringTable里放一个字符串,就要去哈希表里查找有没有。有就不能放进去。
 public static void main(String[] args) {
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(new File("f:\\test.txt"))));
            String line = null;
            long start = System.nanoTime();
            while (true) {
                line = reader.readLine();
                if (line == null) {
                    break;
                }
                line.intern();
            }
            System.out.println("cost:" + (System.nanoTime() - start) / 1000000);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

//通过读取文件将文件中的每一行逐行加入到StringTable中,修改桶的大小来测试所需要的时间(文件为8145行)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
StringTableSizeTime
128172 ms
1024116 ms
409687 ms

JDK6

img

在JDK1.6中所有的输出结果都是 false,因为JDK1.6以及以前版本中,常量池是放在 Perm 区(属于方法区)中的,熟悉JVM的话应该知道这是和堆区完全分开的。

使用引号声明的字符串都是会直接在字符串常量池中生成的,而 new 出来的 String 对象是放在堆空间中的。所以两者的内存地址肯定是不相同的,即使调用了intern()方法也是不影响的。

intern()方法在JDK1.6中的作用是:比如String s = new String(“SEU_Calvin”),再调用s.intern(),此时返回值还是字符串"SEU_Calvin",表面上看起来好像这个方法没什么用处。但实际上,在JDK1.6中它做了个小动作:检查字符串池里是否存在"SEU_Calvin"这么一个字符串,如果存在,就返回池里的字符串;如果不存在,该方法会把"SEU_Calvin"添加到字符串池中,然后再返回它的引用。然而在JDK1.7中却不是这样的,后面会讨论。

JDK7

针对JDK1.7以及以上的版本,我们将上面两段代码分开讨论。先看第一段代码的情况:

img**

String s = new String("1");//生成了常量池中的“1” 和堆空间中的字符串对象
s.intern();// s对象去常量池中寻找后发现"1"已经存在于常量池中了。
String s2 = "1";//生成一个s2的引用指向常量池中的“1”对象。
System.out.println(s == s2);// JDK6和7都是false

String s3 = new String("1") + new String("1");//在字符串常量池中生成“1” ,并在堆空间中生成s3引用指向的对象(内容为"11")。注意此时常量池中是没有 “11”对象的。
s3.intern();//将 s3中的“11”字符串放入 String 常量池中,此时常量池中不存在“11”字符串,JDK1.6的做法是直接在常量池中生成一个 "11" 的对象。
//但是在JDK1.7中,常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用直接指向 s3 引用的对象,也就是说s3.intern() ==s3会返回true。
String s4 = "11";//直接去常量池中创建,但是发现已经有这个对象了,此时也就是指向 s3 引用对象的一个引用。因此s3 == s4返回了true。

System.out.println(s3 == s4);//JDK6:false  JDK7:true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

下面继续分析第二段代码:

img

再把第二段代码贴一下便于查看:

String s = new String("1");//生成了常量池中的“1” 和堆空间中的字符串对象。
String s2 = "1";//这行代码是生成一个s2的引用指向常量池中的“1”对象,但是发现已经存在了,那么就直接指向了它。
s.intern();//这一行在这里就没什么实际作用了。因为"1"已经存在了。

System.out.println(s == s2);// 引用地址不同//JDK6:false  JDK7:false

 
String s3 = new String("1") + new String("1");//在字符串常量池中生成“1” ,并在堆空间中生成s3引用指向的对象(内容为"11")。注意此时常量池中是没有 “11”对象的。
String s4 = "11";//直接去生成常量池中的"11"。
s3.intern();//这一行在这里就没什么实际作用了。因为"11"已经存在了。

System.out.println(s3 == s4);//引用地址不同//JDK6:false  JDK7:false
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
String str1 = new String("SEU") + new String("Calvin");

System.out.println(str1.intern() == str1);//JDK6:false//JDK7:true

System.out.println(str1 == "SEUCalvin");//JDK6:false//JDK7:true
  • 1
  • 2
  • 3
  • 4
  • 5
String str2 = "SEUCalvin";//新加的一行代码,其余不变

String str1 = new String("SEU")+ new String("Calvin");

System.out.println(str1.intern() == str1);//JDK6:false//JDK7:false

System.out.println(str1 == "SEUCalvin");//JDK6:false//JDK7:false
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

也很简单啦,str2先在常量池中创建了“SEUCalvin”,那么str1.intern()当然就直接指向了str2,你可以去验证它们两个是返回的true。后面的"SEUCalvin"也一样指向str2。所以谁都不搭理在堆空间中的str1了,所以都返回了false。

new String()究竟创建几个对象?

1. 由来

遇到一个Java面试题,是关于String的,自己对String还有点研究?下面是题目的描述:

在Java中,new String("hello")这样的创建方式,到底创建了几个String对象?

题目下答案,各说纷纭,有说1个的,有说2个的。我觉得都对,但也都不对,因为要加上一定的条件,下面来分析下!

2. 解答

2.1. 分析

题目中的String创建方式,是调用String的有参构造函数,而这个有参构造函数的源码则是这样的public String(String original),这就是说,我们可以把代码转换为下面这种:

String temp = "hello";  // 在常量池中
String str = new String(temp); // 在堆上
  • 1
  • 2

这段代码就创建了2个String对象,temp指向在常量池中的,str指向堆上的,而str内部的char value[]则指向常量池中的char value[],所以这里的答案是2个对象。(这里不再详述内部过程,之前的文章有写,参考深入浅出Java String)

那之前我为什么说答案是1个的也对呢,假如就只有这一句String str = new String("hello")代码,并且此时的常量池的没有"hello"这个String,那么答案是两个;如果此时常量池中,已经存在了"hello",那么此时就只创建堆上str,而不会创建常量池中temp,(注意这里都是引用),所以此时答案就是1个。

https://blog.csdn.net/w605283073/article/details/72753494

《深入理解java虚拟机》第二版 57页

对String.intern()返回引用的测试代码如下:

String str1 = new StringBuilder("计算机").append("软件").toString();
// String str3= new StringBuilder("计算机软件").toString();
System.out.println(str1.intern() == str1);//JDK6:false//JDK7:true

String str2 = new StringBuilder("Java(TM) SE ").append("Runtime Environment").toString();
;//堆中有,问题是常量池中在intern之前是否有拼接完的字符串
System.out.println(str2.intern() == str2);//JDK6:false//JDK7:false
//jdk6因为是复制,所以不可能相等,问题是jdk7可能是引用,按理说应该是true,为什么是false
//这个因为jdk源码中已经有了这个拼接完的字符串,在标注版本的时候定义过了
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

可能很多人觉得这个结果很奇怪,在这里我们进行深入地探究。

因为JDK1.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串的实例的引用,而StringBulder创建的字符串实例在Java堆上,所以必然不是同一个引用,将返回false。

在JDK1.7中,intern()的实现不会在复制实例,只是在常量池中记录首次出现的实例引用,因此返回的是引用和由StringBuilder.toString()创建的那个字符串实例是同一个。

str2的比较返回false因为"java"这个字符串在执行StringBuilder.toString()之前已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”的原则,而“计算机软件”这个字符串是首次出现,因此返回true。

那么就有疑问了,这个“java”字符串在哪里出现过呢?显然并不是直接出现在这个类里面。

我们分别打开String 、StringBuilder和System类的源码看看有啥发现,

其中在System类里发现

有java版本的字符串

因此sun.misc.Version 类会在JDK类库的初始化过程中被加载并初始化,而在初始化时它需要对静态常量字段根据指定的常量值(ConstantValue)做默认初始化,此时被 sun.misc.Version.launcher 静态常量字段所引用的"java"字符串字面量就被intern到HotSpot VM的字符串常量池——StringTable里了。

因此我们修改一下代码:

  1. String str2 = new StringBuilder("Java(TM) SE ").append("Runtime Environment").toString();
  2. System.out.println(str2.intern() == str2)

发现结果还是false

从而更加证实了我们的猜测。

再遇到类似问题的时候,希望大家可以多从源码角度去追本溯源,能够多分享出来。

https://www.cnblogs.com/clamp7724/p/11751278.html

字符串常量池:String table又称为String pool,

  • 字符串常量池在Java内存区域的哪个位置
    • 在JDK6.0及之前版本,字符串常量池是放在【Perm Gen区(也就是方法区)】中;
    • 在JDK7.0版本,字符串常量池被移到了【堆】中了。至于为什么移到堆内,大概是由于方法区的内存空间太小了。但是字符串常量池与堆对象还是不一样
  • 字符串常量池放的是什么:
    • 在JDK6.0及之前版本中,String Pool里放的都是字符串常量
    • 在JDK7.0中,由于String#intern()发生了改变,因此String Pool中也可以存放放于堆内的字符串对象的引用
  • StringTable还存在一个hash表的特性∶里面不存在相同的两个字符串。
  • main String,java等属于关键词,在一开始就在StringTable中存在了,所以str.intern没能插入进去。
String s1 = "ha";
String s2 = "ha";
String s3 = s1 +s2;//s3本质调用了 new StringBuilder.append("a").append("b").toString(); 声明了新的引用变量,开辟了新的空间,所以指向的是堆中的对象地址而不是StringTable中的字符串了。
String s4 = "ha" + "ha";//因为是两个常量拼接,在编译时就会直接变成"haha"进行处理,进入StringTable
String s5 = "haha";//因为也是常量,会先在StringTable中查找,找到后s5指向了StringTable中的"haha"
String s6 = new String("haha");
System.out.println(s3 == s4);//false
System.out.println(s4 == s5);//true
System.out.println(s5 == s6);//false
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

3.6_直接内存

在JAVA中,JVM内存指的是堆内存。

机器内存中,不属于堆内存的部分即为堆外内存。

堆外内存也被称为直接内存。

内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。使用未公开的Unsafe和NIO包下ByteBuffer来创建堆外内存。

堆内内存是属于jvm的,由jvm进行分配和管理,属于"用户态",而堆外内存是由操作系统管理的,属于"内核态"。

在jdk1.4中新加入了NIO类,他可以调用native函数库直接分配堆外内存,然后通过java堆中的DirectByteBuffer对象来指向这块内存,进行内存分配等工作。

直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中定义的内存区域。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

JAVA通过调用ByteBuffer.allocateDirect及 MappedByteBuffer 来进行内存分配。不过JVM对Direct Memory可申请的大小也有限制,可用-XX:MaxDirectMemorySize=1M设置,这部分内存不受JVM垃圾回收管理。

  • 堆外内存:Direct Memory,也叫堆外内存。这部分内存不是由jvm管理和回收的。需要我们手动的回收。
  • 常见于 NIO 操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受 JVM 内存回收管理

为什么使用堆外内存:

  • 1、减少了垃圾回收:使用堆外内存的话,堆外内存是直接受操作系统管理( 而不是虚拟机 )。这样做的结果就是能保持一个较小的堆内内存,以减少垃圾收集对应用的影响。
  • 2、提升复制速度(io效率):堆内内存由JVM管理,属于“用户态”;而堆外内存由OS管理,属于“内核态”。如果从堆内向磁盘写数据时,数据会被先复制到堆外内存,即内核缓冲区,然后再由OS写入磁盘,使用堆外内存避免了这个操作。(不需要经过对内)

堆外内存申请:

  • JDK的ByteBuffer类提供了一个接口allocateDirect(int capacity)进行堆外内存的申请,底层通过unsafe.allocateMemory(size)实现。Netty、Mina等框架提供的接口也是基于ByteBuffer封装的。

堆外内存释放:

  • unsafe.allocateMemory(size)最底层是通过malloc方法申请的,但是这块内存需要进行手动释放,JVM并不会进行回收,幸好Unsafe提供了另一个接口freeMemory可以对申请的堆外内存进行释放。

当初始化一块堆外内存时,对象的引用关系如下:

img

《JAVA对象引用》叫告诉了我们有ReferenceQueue引用监视器。

当一个 DirectByteBuffer初始化的时候,都会创建cleaner对象( 继承PhantomReference)并把 其注册进ReferenceQueue中。

当DirectByteBuffer=null的时候,如果引用在放入PhantomReference过程中,JVM就会调用cleaner.clean 并放弃通知ReferenceQueue。

其中firstCleaner类的静态变量,Cleaner对象在初始化时会被添加到Clenear链表中,和first形成引用关系,ReferenceQueue是用来保存需要回收的Cleaner对象。

如果该DirectByteBuffer对象在一次GC中被回收了

img

此时,只有Cleaner对象唯一保存了堆外内存的数据(开始地址、大小和容量),在下一次FGC时,把该Cleaner对象放入到ReferenceQueue中,并触发clean方法。

Cleaner对象的clean方法主要有两个作用:
1、把自身从Cleaner链表删除,从而在下次GC时能够被回收
2、释放堆外内存

如果JVM一直没有执行FGC的话,无效的Cleaner对象就无法放入到ReferenceQueue中,从而堆外内存也一直得不到释放,内存岂不是会爆?

其实在初始化DirectByteBuffer对象时,如果当前堆外内存的条件很苛刻时,会主动调用System.gc()强制执行FGC。

Unsafe类操作堆外内存

sun.misc.Unsafe提供了一组方法来进行堆外内存的分配,重新分配,以及释放。

  1. public native long allocateMemory(long size); —— 分配一块内存空间。
  2. public native long reallocateMemory(long address, long size); —— 重新分配一块内存,把数据从address指向的缓存中拷贝到新的内存块。
  3. public native void freeMemory(long address); —— 释放内存。

参考:Unsafe类操作JAVA内存

public static void main(String[] args) {
    Unsafe unsafe = new Unsafe();
    unsafe.allocateMemory(1024);
}
  • 1
  • 2
  • 3
  • 4

然而Unsafe类的构造器是私有的,报错。

而且,allocateMemory方法也不是静态的,不能通过Unsafe.allocateMemory调用。

幸运的是可以通过Unsafe.getUnsafe()取得Unsafe的实例。

public class UnsafeTest {

    public static void main(String[] args) {
        Unsafe unsafe = Unsafe.getUnsafe();
        unsafe.allocateMemory(1024);
        unsafe.reallocateMemory(1024, 1024);
        unsafe.freeMemory(1024);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

此外,也可以通过反射获取unsafe对象实例

参考:危险代码:如何使用Unsafe操作内存中的Java类和对象

NIO类操作堆外内存

用NIO包下的ByteBuffer分配直接内存则相对简单。

public class TestDirectByteBuffer {
    public static void main(String[] args) throws Exception {
        ByteBuffer buffer = ByteBuffer.allocateDirect(10 * 1024 * 1024);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5

然而运行时报错了。

java(51146,0x7000023ed000) malloc: *** error for object 0x400: pointer being realloc’d was not allocated
*** set a breakpoint in malloc_error_break to debug

img

错误信息

参考:JAVA堆外内存

然而在小伙伴的电脑上跑这段的代码是可以成功运行的。

二:堆外内存垃圾回收

对于内存,除了关注怎么分配,还需要关注如何释放。

从JAVA出发,习惯性思维是堆外内存是否有垃圾回收机制。

考虑堆外内存的垃圾回收机制,需要了解以下两个问题:

  1. 堆外内存会溢出么?
  2. 什么时候会触发堆外内存回收?

问题一

通过修改JVM参数:-XX:MaxDirectMemorySize=40M,将最大堆外内存设置为40M。

既然堆外内存有限,则必然会发生内存溢出。

为模拟内存溢出,可以设置JVM参数:-XX:+DisableExplicitGC,禁止代码中显式调用System.gc()。

可以看到出现OOM。

得到的结论是,堆外内存会溢出,并且其垃圾回收依赖于代码显式调用System.gc()。

参考:JAVA堆外内存

问题二

关于堆外内存垃圾回收的时机,首先考虑堆外内存的分配过程。

JVM在堆内只保存堆外内存的引用,用DirectByteBuffer对象来表示。

每个DirectByteBuffer对象在初始化时,都会创建一个对应的Cleaner对象。

这个Cleaner对象会在合适的时候执行unsafe.freeMemory(address),从而回收这块堆外内存。

当DirectByteBuffer对象在某次YGC中被回收,只有Cleaner对象知道堆外内存的地址。

当下一次FGC执行时,Cleaner对象会将自身Cleaner链表上删除,并触发clean方法清理堆外内存。

此时,堆外内存将被回收,Cleaner对象也将在下次YGC时被回收。

如果JVM一直没有执行FGC的话,无法触发Cleaner对象执行clean方法,从而堆外内存也一直得不到释放。

其实,在ByteBuffer.allocateDirect方式中,会主动调用System.gc()强制执行FGC。

JVM觉得有需要时,就会真正执行GC操作。

img

显式调用

参考:堆外内存的回收机制分析—占小狼

三:为什么用堆外内存?

堆外内存的使用场景非常巧妙。

第三方堆外缓存管理包ohc(off-heap-cache)给出了详细的解释。

摘了其中一段。

When using a very huge number of objects in a very large heap, Virtual machines will suffer from increased GC pressure since it basically has to inspect each and every object whether it can be collected and has to access all memory pages. A cache shall keep a hot set of objects accessible for fast access (e.g. omit disk or network roundtrips). The only solution is to use native memory - and there you will end up with the choice either to use some native code (C/C++) via JNI or use direct memory access.

大概的意思如下:

考虑使用缓存时,本地缓存是最快速的,但会给虚拟机带来GC压力。

使用硬盘或者分布式缓存的响应时间会比较长,这时候「堆外缓存」会是一个比较好的选择。

参考:OHC - An off-heap-cache — Github

四:如何用堆外内存?

在第一章中介绍了两种分配堆外内存的方法,Unsafe和NIO。

对于两种方法只是停留在分配和回收的阶段,距离真正使用的目标还很遥远。

在第三章中提到堆外内存的使用场景之一是缓存。

那是否有一个包,支持分配堆外内存,又支持KV操作,还无需关心GC。

答案当然是有的。

有一个很知名的包,Ehcache

Ehcache被广泛用于Spring,Hibernate缓存,并且支持堆内缓存,堆外缓存,磁盘缓存,分布式缓存。

此外,Ehcache还支持多种缓存策略。

其仓库坐标如下:

<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.4.0</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5

接下来就是写代码进行验证:

public class HelloHeapServiceImpl implements HelloHeapService {

    private static Map<String, InHeapClass> inHeapCache = Maps.newHashMap();

    private static Cache<String, OffHeapClass> offHeapCache;

    static {
        ResourcePools resourcePools = ResourcePoolsBuilder.newResourcePoolsBuilder()
                .offheap(1, MemoryUnit.MB)
                .build();

        CacheConfiguration<String, OffHeapClass> configuration = CacheConfigurationBuilder
                .newCacheConfigurationBuilder(String.class, OffHeapClass.class, resourcePools)
                .build();

        offHeapCache = CacheManagerBuilder.newCacheManagerBuilder()
                .withCache("cacher", configuration)
                .build(true)
                .getCache("cacher", String.class, OffHeapClass.class);


        for (int i = 1; i < 10001; i++) {
            inHeapCache.put("InHeapKey" + i, new InHeapClass("InHeapKey" + i, "InHeapValue" + i));
            offHeapCache.put("OffHeapKey" + i, new OffHeapClass("OffHeapKey" + i, "OffHeapValue" + i));
        }
    }

    @Data
    @AllArgsConstructor
    private static class InHeapClass implements Serializable {
        private String key;
        private String value;
    }

    @Data
    @AllArgsConstructor
    private static class OffHeapClass implements Serializable {
        private String key;
        private String value;
    }

    @Override
    public void helloHeap() {
        System.out.println(JSON.toJSONString(inHeapCache.get("InHeapKey1")));
        System.out.println(JSON.toJSONString(offHeapCache.get("OffHeapKey1")));
        Iterator iterator = offHeapCache.iterator();
        int sum = 0;
        while (iterator.hasNext()) {
            System.out.println(JSON.toJSONString(iterator.next()));
            sum++;
        }
        System.out.println(sum);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

其中.offheap(1, MemoryUnit.MB)表示分配的是堆外缓存。

Demo很简单,主要做了以下几步操作:

  1. 新建了一个Map,作为堆内缓存。
  2. 用Ehcache新建了一个堆外缓存,缓存大小为1MB。
  3. 在两种缓存中,都放入10000个对象。
  4. helloHeap方法做get测试,并统计堆外内存数量,验证先插入的对象是否被淘汰。

使用Java VisualVM工具Dump一个内存镜像。

Java VisualVM是JDK自带的工具。

工具位置如下:

/Library/Java/JavaVirtualMachines/jdk1.7.0_71.jdk/Contents/Home/bin/jvisualvm

也可以使用JProfiler工具。

打开镜像,堆里有10000个InHeapClass,却没有OffHeapClass,表示堆外缓存中的对象的确没有占用JVM内存。

img

内存镜像

接着测试helloHeap方法。

输出:

{“key”:“InHeapKey1”,“value”:“InHeapValue1”}
null
……(此处有大量输出)
5887

输出表示堆外内存启用了淘汰机制,插入10000个对象,最后只剩下5887个对象。

如果堆外缓存总量不超过最大限制,则可以顺利get到缓存内容。

总体而言,使用堆外内存可以减少GC的压力,从而减少GC对业务的影响。

import java.nio.ByteBuffer;

/**
 * 直接内存 与  堆内存的比较
 */
public class ByteBufferCompare {

    public static void main(String[] args) {
        allocateCompare();   //分配比较
        operateCompare();    //读写比较
    }

    /**
     * 直接内存 和 堆内存的 分配空间比较
     * 结论: 在数据量提升时,直接内存相比非直接内的申请,有很严重的性能问题
     */
    public static void allocateCompare(){
        int time = 10000000;    //操作次数                           


        long st = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {
            //ByteBuffer.allocate(int capacity)   分配一个新的字节缓冲区。
            ByteBuffer buffer = ByteBuffer.allocate(2);  //非直接内存分配申请     
        }
        long et = System.currentTimeMillis();

        System.out.println("在进行"+time+"次分配操作时,堆内存 分配耗时:" + (et-st) +"ms" );

        long st_heap = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {
            //ByteBuffer.allocateDirect(int capacity) 分配新的直接字节缓冲区。
            ByteBuffer buffer = ByteBuffer.allocateDirect(2); //直接内存分配申请
        }
        long et_direct = System.currentTimeMillis();

        System.out.println("在进行"+time+"次分配操作时,直接内存 分配耗时:" + (et_direct-st_heap) +"ms" );
    }

    /**
     * 直接内存 和 堆内存的 读写性能比较
     * 结论:直接内存在直接的IO 操作上,在频繁的读写时 会有显著的性能提升
     */
    public static void operateCompare(){
        int time = 1000000000;

        ByteBuffer buffer = ByteBuffer.allocate(2*time);  
        long st = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {

            //  putChar(char value) 用来写入 char 值的相对 put 方法
            buffer.putChar('a');
        }
        buffer.flip();
        for (int i = 0; i < time; i++) {
            buffer.getChar();
        }
        long et = System.currentTimeMillis();

        System.out.println("在进行"+time+"次读写操作时,非直接内存读写耗时:" + (et-st) +"ms");

        ByteBuffer buffer_d = ByteBuffer.allocateDirect(2*time);
        long st_direct = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {

            //  putChar(char value) 用来写入 char 值的相对 put 方法
            buffer_d.putChar('a');
        }
        buffer_d.flip();
        for (int i = 0; i < time; i++) {
            buffer_d.getChar();
        }
        long et_direct = System.currentTimeMillis();

        System.out.println("在进行"+time+"次读写操作时,直接内存读写耗时:" + (et_direct - st_direct) +"ms");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77

原来的方案:

  • CPU:用户态java→内核态system→用户态java
  • 内存:磁盘文件放到系统内存中的系统缓冲区,然后再从系统缓存区转到java堆内存的java缓冲区byte[]

新方案:

增加了直接内存区域。

直接将磁盘文件放到直接内存中,不经过系统内存,而是新画出了一个直接内存区,java代码可以直接访问,系统也可以访问它。可以通过代码import java.nio.ByteBuffer; ByteBuffer.allocate(内存大小)申请直接内存区

分配和回收原理

package MM;

import java.nio.ByteBuffer;
//可以这样申请堆外内存 
public class Buffer {

    public static void main(String[] args) {

        while(true) {
            ByteBuffer.allocate(10*1024*1024);
        }
    }
}//运行结果:控制台无任何输出,也未结束。
//可以看到我们一直在申请内存,却一直没有内存溢出。直接内存被释放了。到底堆外内存(直接内存)是怎么释放的呢?(直接内存也会导致内存溢出)
//---------程序2------------
public class test {

    public static void main(String[] args) {

        ByteBuffer byteBuffer=ByteBuffer.allocateDirect(_1Gb);
        System.out.println("分配完毕");
        System.in.read();
        System.out.println("开始释放");
        byteBuffer=null;//后台显示释放成功
        System.gc();
        System.in.read();
    }
}
//-----------程序3---------
public class test {

    Static int _1Gb=1024*1024*1024;

    public static void main(String[] args) {

        Unsafe unsafe=getUnsafe();
        long base=unsafe.allocateMemoy(_1Gb);
        unsafe.setMemory(base,_1Gb,(byte)0);
        System.in.read();
        unsafe.freeMemory(base);
        System.in.read();
    }
    public static Unsafe getUnsafe(){
        try {
            Field f=Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            Unsafe unsafe=(Unsafe) f.get(null);
            return unsafe
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
}

/*
NIO申请直接内存总结:

我们用NIO类申请的内存不受JVM的管理,但是其实是由jvm进行回收的,并不像unsave那样要我们自己对内存进行管理。这时候系统是不断回收直接内存的,由NIO申请的直接内存是需要System.gc()来进行内存回收的。系统会帮助我们回收直接内存的。

不过为了提高gc的利用率,我们可能会在代码中加入-XX:+DisableExplicit禁止代码中显示调用gc(System.gc)。采取并行gc,就是由jvm来自动管理内存回收,而jvm主要是管理堆内内存,也就是当对堆内对象回收的时候,才有可能回收直接内存,这种不对称性很有可能产生直接内存内存泄漏。

需要注意的是当我们没有指向堆外内存的引用的时候,也会把直接内存回收,这也是上面我们内存没有泄漏的原因。

采用直接内存的优点:

1:对于频繁的io操作,我们需要不断把内存中的对象复制到直接内存。然后由操作系统直接写入磁盘或者读出磁盘。

这时候用到直接内存就减少了堆的内外内存来回复制的操作。

2:我们在运行程序的过程中可能需要新建大量对象,对于一些声明周期比较短的对象,可以采用对象池的方式。但

是对于一些生命周期较长的对象来说,不需要频繁调用gc,为了节省gc的开销,直接内存是必备之选。

3:扩大程序运行的内存,由于jvm申请的内存有限,这时候可以通过堆外内存来扩大内存。

*/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76

使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法 ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦
ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存

JDK=jre+ development kit

JRE=jvm+ core lib

4_垃圾回收

如何判断对象可以回收:

  • 1 引用计数法:对象没有一个引用计算器,就+1。
    缺陷:循环引用。如AB对象互相引用,但没有其他对象引用AB对象时,AB对象本该回收却不能回收
  • 2 可达性分析(根搜索)算法:从根对象的点作为起始进行向下搜索,当一个对象到根对象没有任何引用链相连,则证明此对象是不可用的 。
    • 根对象GC Root:肯定不能被垃圾回收的对象。然后扫描堆中所有对象,判断是否被根对象直接或间接引用,如果引用了就不能回收。如果没有被直接/间接引用,就可以当做垃圾回收。
    • GC roots包括:
      • 在VM栈(帧的本地变量)中的引用
      • 方法区的静态引用
      • JNI(即一般说的Native方法)中的引用

哪些对象可以作为 GC Root ?

  • Memory Analyzer (MAT)堆分析工具:需要先使用jmap分析出堆内存,拿到快照,再由MAT进行分析。jmap -dump:format=b,live,file=1.bin 【进程号】。把bin文件导入MAT后,可以通过java Basics–GC Roots查看根对象。
    • dump:要把当前堆内存情况存储为一个文件
    • format=b:转出文件的格式,b表示二进制
    • live:只关心存活的,不关心垃圾回收的。自动在进行快照前会进行一次垃圾回收。
    • file=1.bin:文件名

四种引用

https://www.jianshu.com/p/825cca41d962

我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。 很多系统的缓存功能都符合这样的应用场景。

我们把引用分为4种,用法如下

  • Strong:默认通过Object o=new Object();这种方式赋值的引用
  • Soft、Weak、Phantom:这三种则都是继承Refrence。如SoftReference<byte[]> cacheRef = new SoftReference<>(4*1024*1024);

说明:

  • 强引用Strong:只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收。我们平时new的对象都是强引用。强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用 对象来解决内存不足的问题。
  • 软引用(SoftReference):仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象可以配合引用队列来释放软引用自身。软引用是用来描述一些还有用但并非必须的对象。软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。
  • 弱引用(WeakReference):仅有弱引用引用该对象时(没有任何强引用关联他),在垃圾回收时,无论内存是否充足,都会回收弱引用对象。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。可以配合引用队列来释放弱引用自身。
  • 虚引用(PhantomReference):必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存。
    "虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
    虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
  • 终结器引用(FinalReference):无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象

在Full GC时会对Reference类型的引用进行特殊处理

  • Soft:内存不够时一定被GC,长期不用也会被GC

  • Weak:一定被GC,当做标记为dead,会在ReferenceQueue中通知。

  • Phantom:本来就没引用,当从jvm堆中释放时会通知。

软引用

package JVMtest;

import java.lang.ref.SoftReference;
import java.util.ArrayList;
// -Xmx20m -XX:+PrintGCDetails -verbose:gc
public class Ref {
    private static final int _4MB=4*1024*1024;

    public static void main(String[] args) {
        ArrayList<byte[]> list = new ArrayList<>();//强引用
        for (int i=0;i<5;i++){
            list.add(new byte[_4MB]);
        }
    }

    public static void soft(){

        ArrayList<SoftReference<Byte[]>> list = new ArrayList<>();//软引用
        for (int i = 0; i < 5; i++) {
            SoftReference<Byte[]> ref=new SoftReference<>(new Byte[_4MB]);
            System.out.println(ref.get());//正常显示
            list.add(ref);
            System.out.println(list.size());

        }//内存不够进行了垃圾回收,软引用垃圾回收后内容扔不足就会把软引用扔掉
        System.out.println("循环结束"+list.size());
        for (SoftReference<Byte[]> ref:list) {
            System.out.println(ref.get());//前4个都变为null
        }
    }
}
/*
[Ljava.lang.Byte;@1b6d3586
1
[Ljava.lang.Byte;@4554617c
2
[Ljava.lang.Byte;@74a14482
3
[Ljava.lang.Byte;@1540e19d
4
[Ljava.lang.Byte;@677327b6
5
循环结束:5
null
null
null
null
[Ljava.lang.Byte;@677327b6
*/

Ljava.lang.Byte;@1b6d3586
1
[Ljava.lang.Byte;@4554617c
2
[Ljava.lang.Byte;@74a14482
3
[GC (Allocation Failure) [PSYoungGen: 1819K->488K(6144K)] 14107K->12968K(19968K), 0.0020269 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] //调用了一次新生代垃圾回收,从1.8M回收到了0.4M
[Ljava.lang.Byte;@1540e19d
4
[GC (Allocation Failure) --[PSYoungGen: 4696K->4696K(6144K)] 17176K->17216K(19968K), 0.0023349 secs] [Times: user=0.08 sys=0.00, real=0.00 secs] //一次新生代垃圾回收,没回收多少
[Full GC (Ergonomics) [PSYoungGen: 4696K->4536K(6144K)] [ParOldGen: 12520K->12472K(13824K)] 17216K->17008K(19968K), [Metaspace: 3225K->3225K(1056768K)], 0.0069859 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] //一次FULL垃圾回收,还是没回收多少
[GC (Allocation Failure) --[PSYoungGen: 4536K->4536K(6144K)] 17008K->17008K(19968K), 0.0045728 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] //触发软连接垃圾回收
[Full GC (Allocation Failure) [PSYoungGen: 4536K->0K(6144K)] [ParOldGen: 12472K->606K(8704K)] 17008K->606K(14848K), [Metaspace: 3225K->3225K(1056768K)], 0.0054783 secs] [Times: user=0.08 sys=0.00, real=0.00 secs] //再一次FULL垃圾回收,回收了4M
[Ljava.lang.Byte;@677327b6
5
循环结束:5
null
null
null
null
[Ljava.lang.Byte;@677327b6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

引用队列

 总结:
 软引用的list中有的为null了,但还没从list中清除掉。
 可以配合引用队列清楚。
 ArrayList<SoftReference<Byte[]>> list = new ArrayList<>();
 
 ReferenceQueue<byte[]> queue=new  ReferenceQueue<>();//创建引用队列
 
 SoftReference<Byte[]> ref=new SoftReference<>(new Byte[_4MB],queue);//关联引用队列
 //当软引用所关联的的byte[]回收时,软引用自身就会被加入到queue中去。遍历时,就先到queue中查找,
 Reference<?extends byte[]> poll=queue.poll();//每次取一个
while(poll!=null){
    list.remove(poll);
    poll=queue.poll()
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

弱引用

package JVMtest;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
// 演示弱引用
// -Xmx20m -XX:+PrintGCDetails -verbose:gc
public class Weak {
    private static final int _4MB=4*1024*1024;

    public static void main(String[] args) {
        List<WeakReference<byte[]>> list= new ArrayList<>();//弱引用
        for (int i = 0; i < 5; i++) {
            WeakReference<byte[]> ref=new WeakReference<>(new byte[_4MB]);
            list.add(ref);
            System.out.println("第"+(i+1)+"次循环");
            for ( WeakReference<byte[]> w:list) {
                System.out.println(w.get()+"");
            }
            System.out.println();
        }
        System.out.println("循环结束:"+list.size());
    }
}


第1次循环//一个数组
[B@1b6d3586

第2次循环
[B@1b6d3586
[B@4554617c

第3次循环
[B@1b6d3586
[B@4554617c
[B@74a14482

[GC (Allocation Failure) [PSYoungGen: 1819K->488K(6144K)] 14107K->13016K(19968K), 0.0011633 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] //触发了一次GC
第4次循环//虽然GC了,但还存活
[B@1b6d3586
[B@4554617c
[B@74a14482
[B@1540e19d

[GC (Allocation Failure) [PSYoungGen: 4696K->488K(6144K)] 17224K->13016K(19968K), 0.0006622 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] //又一次GC
第5次循环
[B@1b6d3586
[B@4554617c
[B@74a14482
null //内存不够了,刚才GC清理了这个
[B@677327b6

循环结束:5
 //FULL GC
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

垃圾回收算法

  • 1 标记、清除Mark-Sweep
  • 2 标记、整理Mark-Compact
  • 3 复制Copying
  • 分代Generational

1 标记+清除

标记:哪些对象可以当成垃圾(不被GC Root间接引用的)+清除(是否标记的那些空间)。没有需要就标记为不需要了。

缺点:效率不高,两个过程效率都不高。会造成内存碎片。空闲的区域都是小碎片,放不了大的对象。空间碎片太多可能会导致后续事宜中无法找到足够的连续内存而提前触发另一次垃圾搜集动作。

image-20200403131413997

红色的对象应该被回收

2 标记+整理

清除碎片的过程中会把后面的对象往前移到可用的内存

定义:Mark Compact 没有内存碎片
缺点:速度慢

3 复制

将可用内存划分为两块(两块Survivor区 To和From),每次只使用其中的一块,当半区内存用完了,仅将还存活的对象复制到另外一块上面,然后就把原来半块内存空间一次性清除掉,整理过程。清掉整块的速度非常快,但是浪费内存,一半不可用

即新生代和老年代。不会有内存碎片
缺点:需要占用双倍内存空间,在对象存活率较高的时候,效率有所下降。如果不想浪费50%的空间,就需要有额外的空间进行分配担保用于应付半区内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

分代垃圾回收机制

https://blog.csdn.net/hollis_chuang/article/details/91349868

分为:新生代+老年代

  • 新生代:伊甸园Eden+幸存区From+幸存区To。Oracle Hotspot虚拟机默认比例是8:1:1。每次只有10%的内存是浪费的。可以通过-XX:SurvivorRatio=8调整,但是有自适应比较,可以通过-XX:-UseAdaptiveSizePolicy关掉。-Xmn可以设置新生代空间大小,但一般不设置Xmn
  • 老年代:经历N次垃圾回收都存活的对象
  • 新生代老年代默认比例:-XX:NewRatio=,(默认)老年代:新生代=2

思想:需要长时间使用的对象放到老年区。永远就可以丢弃的对象放到新生区中。

  • 对象首先分配在伊甸园区域
  • 新生代空间(伊甸园)不足时,触发 minor gc,伊甸园和 from 中存活的对象复制到 to 中,存活的对象年龄+1,交换 from与to标识。当对象寿命超过==最大寿命是15(4bit)==阈值时,会晋升至老年代。当To区也满的时候也会放到老年代。
    • minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
  • 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,对新生代和老年代全部区域做一次垃圾清理。STW(Stop the World)的时间更长
  • 老年区Full GC后还是不能保存对象,就触发内存不足OOM异常“OutOfMemoryError”。
  • 经历多次GC后,存活的对象会在From和To之间来回存放,而这里面的一个前提则是这两个空间有足够的大小来存放这些数据,在GC算法中,会计算每个对象年龄的大小,如果达到某个年龄后发现总大小已经大于了幸存区空间的50%,那么这是就需要调整阈值,不能再继续等到默认的15次GC后才晋升。因为这样会导致幸存区空间不足,所以需要调整阈值,让这些存活对象尽快完成晋升。

GC的时机:

  • ①Minor GC (YGC,Scavenge GC)
    • 触发时机:新对象生成时,新生代中Eden空间满了
    • 理论上Eden区大多数对象会在Minor GC回收,复制算法的执行效率会很高,Minor GC时机比较短
  • ②Full GC(Major GC)
    • 主要的触发时机:Old满了、Perm满了、system.gc()
    • 对整个JVM(新生代+老年代)进行整理,包括Young、Old和(Perm[JVM1.6之前])
    • 效率很低,尽量减少Full GC。Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。

针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:

  • Partial GC:并不收集整个GC堆的模式

    • Young GC:只收集young gen的GC
    • Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式
    • Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式
  • Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。

Major GC通常是跟full GC是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说“major GC”的时候一定要问清楚他想要指的是上面的full GC还是old GC。

public class GC {
    private static final int _7MB=7*1024*1024;
    // -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
    //堆初始大小  堆最大大小20m  新生代大小10m
    public static void main(String[] args) {
        ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_7MB]);
    }
}

//下面的结果是main中什么都什么时的结果
Heap //堆 8M伊甸园,1From+1To
//新生代 9M,不计入幸存区
 def new generation   total 9216K, used 1814K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  22% used [0x00000000fec00000, 0x00000000fedc5868, 0x00000000ff400000)//伊甸园初试时候就有一些必要的类
  from space 1024K,   0% use d [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 //老年代
 tenured generation   total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,   0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
 //元空间
 Metaspace       used 3116K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 337K, capacity 388K, committed 512K, reserved 1048576K
//-------第二次运行--------添加了7M------
 //触发了一次minor GC 
//数字代表:[DefNew:回收前K->(总K),耗时]堆回收前->堆回收后(堆总大小),堆耗时
[GC (Allocation Failure) [DefNew: 1649K->594K(9216K), 0.0043403 secs] 1649K->594K(19456K), 0.0055600 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 8336K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  94% used [0x00000000fec00000, 0x00000000ff38f7b8, 0x00000000ff400000)
//放入To后,From和To交换了,所以这里的from是原来的To
  from space 1024K,  58% used [0x00000000ff500000, 0x00000000ff594980, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,   0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
 Metaspace       used 3214K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 348K, capacity 388K, committed 512K, reserved 1048576K


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

大对象直接晋升到老年代:通过控制_XX:+PretenureSizeThreshold=,-XX:UserSerialGC

MaxTenuringThreshold的作用:在可以自动调节晋升到老年代的GC中,设置该阈值的最大值。该参数默认值位15,CMS中默认值为6,G1中默认值为15。-XX:MaxTenuringThreshold=,-XX:PrintTenuringDistribution

分配担保机制

简单解释一下为什么会出现这种情况: 因为给 allocation2 分配内存的时候 eden 区内存几乎已经被分配完了,我们刚刚讲了当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC.GC 期间虚拟机又发现 allocation1 无法存入 Survivor 空间,所以只好通过 **分配担保机制:**把新生代的对象提前转移到老年代中去,老年代上的空间足够存放 allocation1,所以不会出现 Full GC。执行 Minor GC 后,后面分配的对象如果能够存在 eden 区的话,还是会在 eden 区分配内存。可以执行如下代码验证:

public class GCTest {

    public static void main(String[] args) {
        byte[] allocation1, allocation2,allocation3,allocation4,allocation5;
        allocation1 = new byte[32000*1024];
        allocation2 = new byte[1000*1024];
        allocation3 = new byte[1000*1024];
        allocation4 = new byte[1000*1024];
        allocation5 = new byte[1000*1024];
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

大对象直接进入老年代:

大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。

为什么要这样呢?

为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。

为对象分配内存:TLAB
为什么有TLAB ( Thread Local Allocation Buffer ) ?
●堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
●由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
●为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。

什么是TLAB ?
●从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
●多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
●据说所有OpenJDK衍生出来的JVM都提供了TLAB的设计。

image-20200527164854508

每个线程有一份,使用完了再用公共的。默认是开启的

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

在程序中,开发人员可以通过选项“-XX:UseTLAB”设置是否开启TLAB空间。
●默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项“-XX:TLABWasteTargetPercent”设置TLAB空间所占用Eden空间的百分比大小。
●一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。

垃圾收集器

前面我们讲了垃圾回收的算法,还需要有具体的实现,在jvm中,实现了多种垃圾收集器,包括:串行垃圾收集器、并行垃圾收集器、CMS(并发)垃圾收集器、G1垃圾收集器,接下来,我们一个个的了解学习。

 >

  • (红色虚线)由于维护和兼容性测试的成本,在JDK 8时将Serial+CMS、 ParNew+Serial Old这两个组合声明为废弃(JEP 173) ,并在JDK 9中完全取消了这些组合的支持(JEP214),即:移除。
  • (绿色虚线)JDK 14中:弃用Parallel Scavenge和SerialOld GC组合(JEP366 )
  • (青色虚线)JDK 14中:删除CMS垃圾回收器 (JEP 363)

查看默认的垃圾收集器
方法1:-xx:+PrintCommandLineFlags: 查看命令行相关参数(包含使用的垃圾收集器)

方法2:使用命令行指令: jinfo -flag 相关垃圾回收器参数 进程ID

/**
 *  -XX:+PrintCommandLineFlags
 *
 *  -XX:+UseSerialGC:表明新生代使用Serial GC ,同时老年代使用Serial Old GC
 *
 *  -XX:+UseParNewGC:标明新生代使用ParNew GC
 *
 *  -XX:+UseParallelGC:表明新生代使用Parallel GC
 *  -XX:+UseParallelOldGC : 表明老年代使用 Parallel Old GC
 *  说明:二者可以相互激活
 *
 *  -XX:+UseConcMarkSweepGC:表明老年代使用CMS GC。同时,年轻代会触发对ParNew 的使用
 */
public class GCUseTest {
    public static void main(String[] args) {
        ArrayList<byte[]> list = new ArrayList<>();

        while(true){
            byte[] arr = new byte[100];
            list.add(arr);
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
//-XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

JDK8使用的是PS PO。JDK9是G1

3.1 串行垃圾收集器Serial GC

串行垃圾收集器,是指使用单线程进行垃圾回收,垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停,等待垃圾回收的完成。这种现象称之为STW(Stop-The-World)。

对于交互性强的应用而言,这种垃圾收集器是不能接受的。

一般在Javaweb应用中是不会采用该收集器的。

年轻代里用是的Serial,对应到年老代的较Serial Old

除了年轻代之外,Serial收集器还提供用于执行老年代垃圾收集的Serial Old收集器。 Serial Old收集器同样也采用了串行回收 和"Stop the World"机制。

  • 只不过内存回收算法使用的是标记一压缩算法。
  • ➢Serial Old是运行在Client模式下默认的老年代的垃圾回收器
  • ➢Serial 0Od在Server模式下主要有两个用途:①与新生代的ParallelScavenge配合使用; ②作为老年代CMS收集器的后备垃圾收集方案。

img

  • 简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Seria1收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
    • ➢运行在Client模式下的虛拟机是个不错的选择。
  • 在用户的桌面应用场景中,可用内存一般不大(几十MB至一两百MB), 可以在较短时间内完成垃圾收集(几十ms至一百多ms) ,只要不频繁发生,使用串行回收器是可以接受的。
  • 在HotSpot虛拟机中,使用-XX: +UseSerialGC 参数可以指定年轻代和老年代都使用串行收集器。
    • 等价于新生代用Serial GC,且老年代用Serial Old GC
    • 控制台输出 -XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseSerialGC

3.1.1 编写测试代码

public class TestGC {
    //实现:不断产生新的数据(对象),随机的废弃对象(垃圾)
    public static void main(String[] args) throws Exception {
        List<Object> list = new ArrayList<>();
        while (true) {
            int sleep = new Random().nextInt(100);
            if (System.currentTimeMillis() % 2 == 0) {
                //当前的时间戳为偶数
                list.clear();//清空
            } else {
                //向list中添加10000个对象
                for (int i = 0; i<10000; i++) {
                    Properties properties = new Properties();
                    properties.put("key_" + i,"value_" + System.currentTimeMillis() + i);
                    list.add(properties);
                }
            }
            Thread.sleep(sleep);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

3.1.2 设置垃圾回收为串行收集器

在程序运行参数中添加2个参数,如下:

  • -XX:+UseSerialGC:指定年轻代和老年代都使用串行垃圾收集器

  • -XX:+PrintGCDetails:打印垃圾回收的详细信息

# 为了测试GC,将堆的初始和最大内存都设置为16M
‐XX:+UseSerialGC ‐XX:+PrintGCDetails ‐Xms16m ‐Xmx16m

img

启动程序,可以看到下面信息:括号内为垃圾回收原因

[GC (Allocation Failure) [DefNew: 4416K->512K(4928K), 0.0034563 secs] 4416K->1841K(15872K), 0.0126067 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 因为分配内存失败而进行垃圾回收。DefNew代表是串行垃圾回收器

[Full GC (Allocation Failure) [Tenured: 10943K->10943K(10944K), 0.0205414 secs] 15871K->13831K(15872K), [Metaspace: 3311K->3311K(1056768K)], 0.0205747 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]

GC日志信息解读:

年轻代的内存GC前后的大小:

  • DefNew

​ 表示使用的是串行垃圾收集器。

  • 4416K->512K(4928K)

​ 表示,年轻代GC前,占有4416K内存,GC后,占有512K内存,总大小4928K

  • 0.0034563 secs

​ 表示,GC所用的时间,单位为毫秒。

  • 4416K->1841K(15872K)

​ 表示,GC前,堆内存占有4416K,GC后,占有1841K,总大小为15872K

  • Full GC

​ 表示,内存空间全部进行GC

3.2 并行垃圾收集器

并行收集器组合

并行垃圾收集器在串行垃圾收集器的基础上做了改进,将单线程改为了多线程进行垃圾回收,这样可以缩短垃圾回收的时间。(这里是指,并行能力较强的机器)

当然了,并行垃圾收集器在收集的过程中也会暂停应用程序,这个和串行垃圾收集器是一样的,只是并行执行,速度更快些,暂停的时间更短一些。

并行收集器与串行收集器工作模式相似,都是stop-the-world方式,只是暂停时并行地进行垃圾收集。年轻代采用复制算法,老年代采用标记-整理,在回收的同时还会对内存进行压缩。关注吞吐量主要指年轻代的Parallel Scavenge收集器,通过两个目标参数-XX:MaxGCPauseMills-XX:GCTimeRatio,调整新生代空间大小,来降低GC触发的频率。并行收集器适合对吞吐量要求远远高于延迟要求的场景,并且在满足最差延时的情况下,并行收集器将提供最佳的吞吐量。

3.2.1 ParNew垃圾收集器(年轻代)+PS(年轻代)

ParNew垃圾收集器是工作在年轻代上的,只是将串行的垃圾收集器改为了并行。parallel Scavenge的增强,为了匹配年老代的CMS

通过-XX:+UseParNewGC参数设置年轻代使用ParNew回收器,老年代使用的依然是串行收集器。

  • 如果说Serial GC是年轻代中的单线程垃圾收集器,那么ParNew收集器则是Serial收集器的多线程版本。
    • ➢Par是Parallel的缩写,New: 只能处理的是新生代
  • ParNew收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。ParNew收集器在年轻代中同样也是采用复制算法、"Stop-the-World"机制。
  • ParNew是很多JVM运行在Server模式下新生代的默认垃圾收集器。

img

  • 由于ParNew收集器是基于并行回收,那么是否可以断定ParNew收集器的回收效率在任何场景下都会比Serial收集器更高效?

    • ➢ParNew 收集器运行在多CPU的环境下,由于可以充分利用多CPU、 多核心等物理硬件资源优势,可以更快速地完成垃圾收集,提升程序的吞吐量。
    • ➢但是在单个CPU的环境下,ParNew收 集器不比Serial收集器更高效。虽然Serial收集器是基于串行回收,但是由于CPU不需要频繁地做任务切换,因此可以有效避免多线程交互过程中产生的一些额外开销。
  • 除Serial Old外,目前ParNew GC还可以与CMS收集器配合工作

  • 在程序中,开发人员可以通过选项"-XX: +UseParNewGC"手动指定使用.ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代。

  • -XX:ParallelGCThreads 限制线程数量,默认开启和CPU数据相同的线程数。.

对于新生代,回收次数频繁,使用并行方式高效。

对于老年代,回收次数少,使用串行方式节省资源。(CPU并行 需要切换线程,串行可以省去切换线程的资源)

测试:

img

#参数
‐XX:+UseParNewGC ‐XX:+PrintGCDetails ‐Xms16m ‐Xmx16m

#打印出的信息

[GC (Allocation Failure) [ParNew: 4416K->512K(4928K), 0.0026548 secs] 4416K->1863K(15872K), 0.0026831 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

-XX:ParallelGCThread可以限制线程数量,默认开启和CPU数量相同的线程数

由以上信息可以看出,ParNew: 使用的是ParNew收集器。其他信息和串行收集器一致。

3.2.2 PS+PO垃圾收集器

ParallelGC即Parallel Scan+Parallel Old。JDK8的默认回收器

PS是吞吐量优先收集器

Parallel GC收集器工作机制和ParNew GC收集器一样,只是在此基础上,新增了两个和系统吞吐量相关的参数,使得其使用起来更加的灵活和高效。相当于原来是一个人打扫,现在是多个人打扫快速打扫完。

高吞吐量则可以高效率地利用CPU的时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务,不要求低延迟。因此,常见在服务器环境中使用 内存回收性能很不错。例如,那么执行批量处理、订单处理、工资支付、科学计算的应用程序。

HotSpot的年轻代中除了拥有ParNew收集器是基于并行回收的以外, Parallel Scavenge收集器同样也采用了复制算法、并行回收和"Stop the World"机制。那么Parallel收集器的出现是否多此一举?

  • ➢和ParNew收集器不同,Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器。
    • 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
  • 自适应调节策略也是Parallel Scavenge 与ParNew一个重要区别。自适应:根据情况调整内存分配情况。

高吞吐量则可以高效率地利用CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。

而其他CMS的停顿时间段是为了适合需要与用户交互的程序。

PO

  • Parallel收集器在JDK1.6时提供了用于执行老年代垃圾收集的 Parallel Old收集器,用来代替老年代的Serial Old收集器。

  • Parallel Old收集器采用了标记-压缩算法,但同样也是基于并行回收和”Stop-the-World"机制。

  • 在程序吞吐量优先的应用场景中,Parallel 收集器和Parallel Old收集器的组合,在Server模式下的内存回收性能很不错。

  • 在Java8中,默认是PO垃圾收集器

img

相关参数如下:

  • -XX:+UseParallelGC: 年轻代使用PS垃圾收集器,老年代使用Serial串行回收器。

  • -XX:+UseParallelOldGC: 年轻代使用PS垃圾回收器,老年代使用PO垃圾回收器。

  • -XX:MaxGCPauseMillis:设置最大的垃圾收集时的停顿时间,单位为毫秒

​ 需要注意的是,ParallelGC为了达到设置的停顿时间,可能会调整堆大小或其他的参数,如果堆的大小设置的较小,就会导致GC工作变得很频繁,反而可能会影响到性能。比如堆满了我们得GC了,但是GC设置是时间比较短,还没清完垃圾又到时得去工作了,还没new几个对象又满了,又得去GC,GC又释放不了几个又去工作了。。。恶性循环

​ 该参数使用需谨慎。对于客户来讲是低延迟好。但我们服务器端注重高并发,整体的吞吐量,所以服务器端使用PS+PO

  • -XX:GCTimeRatio
    • 设置垃圾回收时间占程序运行时间的百分比,公式为1/(1+n)
    • 它的值为1~100之间的数字,默认值为99,也就是垃圾回收时间不能超过1%。一般不设置
  • -XX:UseAdaptiveSizePolicy: 自适应GC模式,垃圾回收器将自动调整年轻代、老年代等参数,达到吞吐量、堆大小、停顿时间之间的平衡。一般用于,手动调整参数比较困难的场景,让收集器自动进行调整。

img

#参数

‐XX:+UseParallelGC ‐XX:+UseParallelOldGC ‐XX:MaxGCPauseMillis=100 ‐XX:+PrintGCDetails ‐Xms16m ‐Xmx16m

#打印的信息

[GC (Allocation Failure) [PSYoungGen: 4096K->480K(4608K)] 4096K->1700K(15872K), 0.0108734 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

[Full GC (Ergonomics) [PSYoungGen: 498K->0K(2560K)] [ParOldGen: 8492K->1889K(11264K)] 8991K->1889K(13824K), [Metaspace: 3306K->3306K(1056768K)], 0.0182486 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]

由以上信息可以看出,年轻代和老年代都使用了ParalledGC垃圾回收器。

CPU数量大于8个的时候,回收线程个数=3+(5×CPU)/8

将CMS之前先明确下并发并行的概念:

我们经常提高并发,并发是指多个cpu同时运行,在gc中即运行代码cpu与垃圾回收cpu同时进行,

并发是依次发生。我们只记高并发即可,高并发是多cpu。

3.3 CMS垃圾收集器(老年代)+PN(年轻代)+Serial Old

并发标记清除收集器:并发标记清除收集器组合 ParNew + CMS + Serial Old

在JDK1.5时期, HotSpot推出了一款在强交互应用中几乎可认为有划 时代意义的垃圾收集器: CMS (Concurrent -Mark -Sweep)收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。

CMS全称Concurrent Mark Sweep(并发标记清除,工作线程和垃圾回收线程同时执行),是一款并发的、使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的,通过参数-XX:+UseConcMarkSweepGC进行设置。是为了解决停顿的问题,所以他的优点是垃圾回收的时候程序可以继续执行,因为我们只回收没用的位置,有用的位置不改变,程序还能找到。

CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时 间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。目前很大一部分的Java应用集中在互联网站或者B/s系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。

关注低延迟

当年老代达到特定的占用比例时,CMS开始执行。

与之对应的年轻代垃圾回收器是PN,PN是PS为了匹配CMS升级的

PN在jdk9被移除了,CMS在JDK10被移除

  • CMS的垃圾 收集算法采用标记-清除算法,并且也会"stop the world"
  • 不幸的是,CMS 作为老年代的收集器,却无法与JDK 1.4.0 中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK 1. 5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。
  • 在G1出现之前,CMS使用还是非常广泛的。一直到今天,仍然有很多系统使用CMS GC。

CMS垃圾回收器的执行过程如下:

img并发标记清除收集器组合

img

主要有初始标记,并发标记,重新标记,并发清理。刚要扔的时候又有别的指向过来了,又不能扔了。

其实不只这四个阶段,中间还有一些其他操作,如预清理、concurrent Abortable Preclean

①初始化标记

初始化标记(CMS-initial-mark):标记根对象root,会导致stw,但因为只标记根对象和从年轻代顺过来的对象,所以stw很短。一旦标记完成之后就会恢复之前被暂停的所有应用线程。

  • 标记老年代中所有的GC Roots对象,如下图节点1;
  • 标记年轻代中活着的对象引用到的老年代的对象(指的是年轻带中还存活的引用类型对象,引用指向老年代中的对象)如下图节点2、3;

  

根对象

  • 在Java语言里,可作为GC Roots对象的包括如下几种:
    1. 虚拟机栈(栈桢中的本地变量表)中的引用的对象 ;
    2. 方法区中的类静态属性引用的对象 ;
    3. 方法区中的常量引用的对象 ;
    4. 本地方法栈中JNI的引用的对象;
      ps:为了加快此阶段处理速度,减少停顿时间,可以开启初始标记并行化,-XX:+CMSParallelInitialMarkEnabled,同时调大并行标记的线程数,线程数不要超过cpu的核数。

②并发标记

  • 并发标记(CMS-concurrent-mark):与用户线程同时运行;捋着一部分根对象,进行一部分标记。从GC Roots的 直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
  • 遍历InitialMarking阶段标记出来的存活对象,然后继续递归标记这些对象可达的对象。
    • 因为该阶段并发执行的,在运行期间可能发生新生代的对象晋升到老年代、或者是直接在老年代分配对象、或者更新老年代对象的引用关系等等,对于这些对象,都是需要进行重新标记的,否则有些对象就会被遗漏,发生漏标的情况。(总结为新到老年代的)
    • 为了提高重新标记的效率,该阶段会把上述对象所在的Card标识为Dirty,后续只需扫描这些Dirty Card的对象,避免扫描整个老年代。(注:始终不会遍历整个老年代,只遍历其中的dirty。而且刚开始的时候都是从gc root和年轻代顺过来的。在并发标记过程中可能有年轻代晋升到老年代的情况,我们就直接标记为dirty,这样我们就能顺着我们已经知道的全找到老年代对象,而不是遍历老年代所有)
    •  
    • img
    • 并发标记时并不是老年代所有存活对象都会被标记,因为在标记期间用户的程序可能会改变一些引用。比如3那么那个节点断开了

预清理:

  • 预清理(CMS-concurrent-preclean):预用户线程同时运行;前一个阶段已经说明,不能标记出老年代全部的存活对象,是因为标记的同时应用程序会改变一些对象引用,这个阶段就是用来处理前一个阶段因为引用关系改变导致没有标记到的存活对象的,它会扫描所有标记为Dirty的Card

    • 如下图所示,在并发清理阶段,节点3的引用指向了6;则会把节点3的card标记为Dirty

 

  • 最后将6标记为存活,如下图所示:在预清理阶段,那些从dirty对象可达的对象也会被标记,这个标记做完之后,dirty card标记就会被清除了

Concurrent Abortable Preclean:

这也是一个并发阶段,但是同样不会影响用户的应用线程

这个阶段是为了尽量承担STW中最终标记阶段的工作。这个阶段持续时间依赖于很多的因素,由于这个阶段是在重复做很多相同的工作,直接满足一些条件(比如:重复迭代的次数、完成的工作量或者时钟时间等)

③重新标记

想象一下下面这个情形:

A对象已经处理过, 但是B对象正在处理中。在并发标记阶段,与此同时用户线程正在执行,

现在用户标记完与A相关的对象了,而B对象原来引用的C现在不引用C了,但A(的属性)又引用到了C。

但是与A相关的对象已经标记过了,不会再标记了,系统就会认为C没有被其他对象引用,会被垃圾回收。

为了避免这种情况,就需要暂停所有的用户线程,重新扫描一遍全部对象,这样就能扫描到C被A引用了。

因为这个阶段中大多数对象已经在并发标记阶段标记过了,所以只需重新标记像C这种对象,所以stw很短。

此外,当C被其他对象引用时,JVM就会给C加入写屏障,写屏障的代码就会被执行,C就会被加入到队列中,把C变成灰色,即还没标记完的对象。并发标记结束后,重新标记时就会从队列中取出对象进行检查,发现是灰色的话,进一步处理标记

  • 重新标记(CMS-remark):会导致stw,重新标记的内存范围是整个堆,包含_young_gen_old_gen,只标记上个阶段标记错误的,也很快;(最终标记,为什么要标记两次呢?因为前面并发标记的时候也有程序正在运行,应用程序也在不断地申请内存空间,有可能会有新对象,也可能会有垃圾,所以需要二次标记)。这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。

  • 这个阶段会导致第二次stop the word,该阶段的任务是完成标记整个年老代的所有的存活对象。由于之前的阶段是并发执行的,gc线程可能跟不上应用程序的变化,为了完成标记老年代所有存活对象的目标,STW就非常有必要了。

    通过CMS的重新标记阶段会在年轻代尽可能感觉的时候运行,目的是为了减少连续STW发生的可能性(年轻代存活对象过多的话,也会导致老年代涉及的存活对象会很多)。

这个阶段,为什么要扫描新生代呢,因为对于老年代中的对象,如果被新生代中的对象引用,那么就会被视为存活对象,即使新生代的对象已经不可达了,也会使用这些不可达的对象当做CMS的“gc root”,来扫描老年代; 因此对于老年代来说,引用了老年代中对象的新生代的对象,也会被老年代视作“GC ROOTS”:当此阶段耗时较长的时候,可以加入参数-XX:+CMSScavengeBeforeRemark在重新标记之前,先执行一次young gc,回收掉年轻带的对象无用的对象,并将对象放入幸存区或晋升到老年代,这样再进行年轻带扫描时,只需要扫描幸存区的对象即可,一般幸存区非常小,这大大减少了扫描时间。
由于之前的预处理阶段是与用户线程并发执行的,这时候可能年轻代的对象对老年代的引用已经发生了很多改变,这个时候,remark阶段要花很多时间处理这些改变,会导致很长stop the word,所以通常CMS尽量运行Final Remark阶段在年轻代是足够干净的时候。
另外,还可以开启并行收集:-XX:+CMSParallelRemarkEnabled。

  • 并发标记阶段还可能产生其他新的引用关系如下:

    • 老年代的新对象被GC Roots引用
    • 老年代的未标记对象被新生代对象引用
    • 老年代已标记的对象增加新引用指向老年代其它对象
    • 新生代对象指向老年代引用被删除
    • 也许还有其它情况…
  • 上述对象中可能有一些已经在Precleaning阶段和AbortablePreclean阶段被处理过,但总存在没来得及处理的,所以还有进行如下的处理:

    • 遍历新生代对象,重新标记
    • 根据GC Roots,重新标记
    • 遍历老年代的Dirty Card,重新标记,这里的Dirty Card大部分已经在clean阶段处理过

经历过上面5个阶段之后,老年代所有存活对象都被标记过了,现在可能通过清除算法去清理那么老年代不再使用的对象。

④并发清除

  • 并发清除(CMS-concurrent-sweep):与用户线程同时运行;此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
  • 调整堆大小:设置CMS在清理之后进行内存压缩,目的是清理内存中的碎片;
  • 并发重置状态等待下次CMS的触发(CMS-concurrent-reset),与用户线程同时运行;

 

清理过后老年代只剩下123456

CMS缺点

他的缺点是当碎片特别多的时候会采取极端的方式用Serial Old把年老代清理一遍(CMS运行期间预留的内存无法满足程序需要,就出现Cocurrent Mode Failure,启动Serial Old)。所以任何一个jdk默认的垃圾回收器都不是CMS。

  • CMS收集器对CPU资源非常敏感
  • 由于并发进行,CMS在收集与应用线程会同时会增加对堆内存的占用,也就是说,CMS必须要在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,将触发担保机制,串行老年代收集器将会以STW的方式进行一次GC,从而造成较大停顿时间;
  • 标记清除算法无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽,最后将不得不通过担保机制对堆内存进行压缩。CMS也提供了参数-XX:CMSFullGCsBeForeCompaction(默认0,即每次都进行内存整理)来指定多少次CMS收集之后,进行一次压缩的Full GC。
  • CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrnet Mode Failure"失败而导致另一次 Full GC的产生。如果在应用中老年代增长不是太快,可以适当调高参数-XX:CMSInitiating OccupancyFrac的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能。要是CMS运行期间预留的内存无法满足程序需要时,虚拟机将启动后备预案:临时启用Seriat Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置得太高很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。
  • 收集结束时会有大量空间碎片产生,空间碎片过多时,将会给大对象分配带来很大麻烦,往往出现老年代还有很大空间剩余但是无法找到足够大的连续空间来分配当前对象,不得不提前进行一次Full GC.M收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶 Full不住要进行 GC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。

并发标记清除(CMS)是以关注低延迟为目标、十分优秀的垃圾回收算法,开启后,年轻代使用STW式的并行收集,老年代回收采用CMS进行垃圾回收,对延迟的关注也主要体现在老年代CMS上。

年轻代ParNew与并行收集器类似,而老年代CMS每个收集周期都要经历:初始标记、并发标记、重新标记、并发清除。其中,初始标记以STW的方式标记所有的根对象;并发标记则同应用线程一起并行,标记出根对象的可达路径;在进行垃圾回收前,CMS再以一个STW进行重新标记,标记那些由mutator线程(指引起数据变化的线程,即应用线程)修改而可能错过的可达对象;最后得到的不可达对象将在并发清除阶段进行回收。值得注意的是,初始标记和重新标记都已优化为多线程执行。CMS非常适合堆内存大、CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器。

年轻代为什么不用cms:年轻代复制算法更好

1.由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。

2.尽管CMS收集器采用的是并发回收(非独占式),但是在其初始化标记和重新标记这两个阶段中仍然需要执行“Stop一the一World”机制暂停程序中的工作线程,不过暂停时间并不会太长。

3.因此可以说明目前所有的垃圾收集器都做不到完全不需要“Stop一the一World”,只是尽可能地缩短暂停时间。

4.另外,由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial 0ld收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。

5.CMS收集器的垃圾收集算法采用的是标记-清除算法,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免地将会产生一些内存碎片。

那么CMS在为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer) 技术,而只能够选择空闲列表(Free List) 执行内存分配。

有人会觉得既然Mark Sweep会造成内存碎片,那么为什么不把算法换成Mark Compact呢?
答案其实很简答,因为当并发清除的时候,用Compact整理内存的话,原来的用户线程使用的内存还怎么用呢?要保证用户线程能继续执行,前提的它运行的资源不受影响嘛。Mark Compact更适合“Stop the World”这种场景”下使用。

  • 1)会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发Full GC。
  • 2) CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
  • 3) CMS收集器无法处理浮动垃圾。可能出现“Concurrent Mode Failure" 失败而导致另一次Full GC的产生。
  • 浮动垃圾:在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将 无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间。
  • CMS GC线程建议设置为总CPU数的1/4

-XX:CMSInitiatingOccupantFraction默认92%时开始CMS GC

3.3.1 cms测试

#设置启动参数
‐XX:+UseConcMarkSweepGC ‐XX:+PrintGCDetails ‐Xms16m ‐Xmx16m

#运行日志

[GC (Allocation Failure) [ParNew: 4416K->512K(4928K), 0.0074759 secs] 4416K->1859K(15872K), 0.0075204 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
#第一步,初始标记

[GC (CMS Initial Mark) [1 CMS-initial-mark: 6160K(10944K)] 6759K(15872K), 0.0004109 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
#第二步,并发标记

[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.003/0.003 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

#第三步,预处理

[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

#第四步,重新标记

[GC (CMS Final Remark) [YG occupancy: 687 K (4928 K)][Rescan (parallel) , 0.0001925 secs][weak refs processing, 0.0000504 secs][class unloading, 0.0002354 secs][scrub symbol table, 0.0004174 secs][scrub string table, 0.0001073 secs][1 CMS-remark: 6160K(10944K)] 6847K(15872K), 0.0010680 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
#第五步,并发清理

[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.003/0.003 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

#第六步,重置
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

由以上日志信息,可以看出CMS执行的过程。

JVM GC收集器成员

场景:订单。虽然有垃圾,但是最后1s提交的还不是垃圾,此时并不是放入s0,而是因为大对象直接放入了老年代。然后老年代太满了就触发full gc,stw时间更长。此时把survivor区调大即可

3.4 G1垃圾收集器

G1收集器

G1(Garbage First)。G1最大的特点是引入分区的思路,弱化了分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至CMS的众多缺陷。

  • JDK6U14体验
  • jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)。JDK7U4官方支持G1
  • jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)简称PS PO。分布式锁续期时是不建议的,用g1。-XX:+UseG1GC
  • jdk1.9 默认垃圾收集器G1

适用场景:

  • 同时注重吞吐量和低延迟,默认暂停目标为200ms
  • 超大堆内存,会将堆划分为多个大小相等的region
  • 整体上是标记+整体算法,两个区域之间是复制算法
  • 官方给G1设定的目标是:在延迟可控的情况下获得尽可能高的吞吐量。“全功能收集器”

-XX:+UseG1GC

-XX:+PrintCommandLineFlagsjvm参数可查看默认设置收集器类型

-XX:+PrintGCDetails亦可通过打印的GC日志的新生代、老年代名称判断

  • -XX:G1HeapRegionSize=size
  • -XX:MaxGCPauseMillis=time

G1的设计原则就是简化JVM性能调优,开发人员只需要简单的三部即可完成调优:

  1. 第一步,开启G1垃圾收集器
  2. 第二步,设置堆的最大内存
  3. 第三步,设置最大的停顿时间

G1中提供了三种模式垃圾回收模式,Young GC、Mixed GC和Full GC,在不同的条件下被触发。

3.4.1 原理

https://www.jianshu.com/p/aef0f4765098

G1垃圾收集器相对比其他收集器而言,最大的区别在于它取消了年轻代、老年代的物理划分,取而代之的是将堆划分为若干个区域(Region),这些区域中包含了又逻辑上的年轻代、老年代区域。

G1采用了分区(Region)的思路,将整个堆空间分成若干个大小相等的内存区域,每个region可以是年轻代、老年代的一个,每次分配对象空间将逐段地使用内存。因此,在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可;每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。

卡片:在每个分区内部又被分成了若干个大小为512 Byte卡片(Card),标识堆内存最小可用粒度所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片,当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象(见RSet)。每次对内存的回收,都是对指定分区的卡片进行处理。

这样做的好处就是,我们再也不用单独的空间对每个代进行设置了,不用担心每个代的内存是否足够

imgimg

  • 比如将Eden小块拷贝到了某个Survivor小块,此时原来Eden区就可以放其他内容了。垃圾回收+内存压缩。

  • E

  • S

  • O

  • H:Humongous区域(巨型对象),如果一个对象占用的空间超过了分区容量的50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在老年代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。

  • 每一个分配的Region,都可以分成两个部分,已分配的和未被分配的。它们之间的界限被称为top。总体上来说,把一个对象分配到Region内,只需要简单增加top的值。这个做法实际上就是bump-the-pointer。过程如下:

    img

    Region可以说是G1回收器一次回收的最小单元。即每一次回收都是回收N个Region。这个N是多少,主要受到G1回收的效率和用户设置的软实时目标有关。每一次的回收,G1会选择可能回收最多垃圾的Region进行回收。与此同时,G1回收器会维护一个空间Region的链表。每次回收之后的Region都会被加入到这个链表中。
    每一次都只有一个Region处于被分配的状态中,被称为current region。在多线程的情况下,这会带来并发的问题。G1回收器采用和CMS一样的TLABs的手段。即为每一个线程分配一个Buffer,线程分配内存就在这个Buffer内分配。但是当线程耗尽了自己的Buffer之后,需要申请新的Buffer。这个时候依然会带来并发的问题。G1回收器采用的是CAS(Compate And Swap)操作。

    为线程分配Buffer的过程大概是:

    1. 记录top值;
    2. 准备分配;
    3. 比较记录的top值和现在的top值,如果一样,则执行分配,并且更新top的值;否则,重复1;

    显然的,采用TLABs的技术,就会带来碎片。举例来说,当一个线程在自己的Buffer里面分配的时候,虽然Buffer里面还有剩余的空间,但是却因为分配的对象过大以至于这些空闲空间无法容纳,此时线程只能去申请新的Buffer,而原来的Buffer中的空闲空间就被浪费了。Buffer的大小和线程数量都会影响这些碎片的多寡。

G1的设计目标

  • 与应用线程同时关注,几乎不需要STW(与CMS类似)
  • 整理剩余空间,不产生内存碎片(CMS只能在Full GC时,用STW整理内存碎片)
  • GC停顿更加可控
  • 不牺牲系统的吞吐量
  • gc不需要额外的内存空间(CMS需要预留空间存储浮动垃圾)

在G1划分的区域中,年轻代的垃圾收集依然采用暂停所有应用线程的方式,将存活的对象拷贝到老年代或者Survivor空间,G1收集器通过将对象从一个区域复制到另一个区域,完成了清理工作。

这也就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。

吞吐量:吞吐量关注的是,在一个指定的时间内,最大化一个应用的工作量。

对于关注吞吐量的系统,卡顿是可以接受的,因为这个系统关注长时间的大量任务的执行能力,单词快速的响应并不值得考虑

G1 GC有计划第避免在整个java堆中进行全区域的垃圾收集。G1收集各个region里面的垃圾堆积的价值大小(回收锁获得的空间大小自己回收所需使劲的经验值),后后台维护一下优先列表,每次根据允许的手机时间,优先回收价值最大的region

由于这种方式的侧重点在于回收最大量的空间,所以G1叫垃圾优先

无需回收整个堆,而是选择一个Collection Set (CS)

两种GC:

  • Fully young GC
  • Mixed GC

估计每个region中的垃圾比例,优先回收垃圾多的region

问题:老年代对象可能持有年轻代的引用(跨代引用)

不同的region间互相引用

我们想要知道这个region外的哪些对象引用了要回收的region的。有下面两种机制

 

3.4.2 Young GC

  • 触发时机:Eden空间满时会被触发

  • 针对区域:Young GC主要是对Eden区进行GC

  • 初始标记

  • Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。

  • Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。

  • 最终Eden空间的数据为空,GC停止工作,应用线程继续执行。

  • E到S有STW,时间短

 

img

img

在引用变更时通过 post-write barrier + dirty card queue
concurrent refinement threads 更新 Remembered Set

 

3.4.2.1 Remembered Set(已记忆集合)

在GC年轻代的对象时,我们**如何找到年轻代中对象的根对象**呢?(即A引用了B,我们有B,如何知道谁引用B)

根对象可能是在年轻代中,也可能在老年代中,那么老年代中的所有对象都是根对象吗?

如果全量扫描老年代,那么这样扫描下来会耗费大量的时间。

已记忆集合RSet:于是,G1引进了RSet的概念。它的全称是Remembered Set,每个Region初始化时,会初始化一个RSet,该集合用来记录并跟踪其他Region指向本Region中对象的引用,每个Region默认按照每512kb划分成多个Card,所以RSet需要记录的东西应该是xx Region的xx Card。

比如本region1的card引用了别的region2内容,那么就把该card记录在别的region2里的Remember Set里,回收region2的时候就可以发现region1的引用了。

Reset的价值在于使得垃圾回收器不需要扫描整个堆就能找到谁引用了当前分区中的对象,只需要扫描RSet即可。

G1 gc是在 points-out的 card table之上再加了一层结构来构成 points-into RSet:每个 region会记录下到底哪些别的region有指向自己的指针,而这些指针分别在哪些card的范围内

这个RSet其实是一个 hash table,key是别的 region的起始地址,value是一个集合,里面的元素是 card table的 index。举例来说,如果 region A的RSet里有一项的key是 region B ,value里有 index为1234的card,它的意思就是 region B的个card里有引用指向 region A。所以对 fregion A来说,该RSe记录的是 points-into的关系;而 card table仍然记录了 points-out的关系。

Snapshot-At-The-Beginning(SATB)是GC GC在并发标记阶段使用的增量式的标记算法

如果card改变了,比如从null赋值成值了,就在card table里标记为dirty,这样Rset就可以指向卡表里对应的一个entry。

如图,region1和region3中的对象都引用了region2中的对象,因此region2中的rset中记录了这两个引用

img

Young Collection 跨代引用
新生代回收的跨代引用(老年代引用新生代)问题

新生代的根对象有一部分来自老年代,这时如果遍历老年代很耗时,所以使用card table,如果老年代对象引用了新生代对象,就把老年代card table这块标记为dirty card。这样就不要找整个老年代了,减少搜索范围。下面粉色的是脏卡区。而E也知道有哪些脏卡引用它,记录在remember set中

 

在引用变更时通过 post-write barrier写屏障 + dirty card queue
concurrent refinement threads 更新 Remembered Set

3.4.3 Young GC+ConMark

  • 在 Young GC 时会进行 GC Root 的初始标记
    • 初始标记是找到根对象,并发标记是顺着根对象找到其他对象。初始标记是新生代GC时发生,并发标记是老年代占用比例达到阈值时
  • 触发时机:老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),阈值由-XX:InitialingHeapOccupancyPercent=percent控制,默认45%

 转存失败重新上传取消 

3.4.3 Mixed GC

当越来越多的对象晋升到老年代时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即Mixed GC,该算法并不是一个Old GC,除了回收整个Young Region,还会回收一部分的Old Region,这里需要注意:是一部分老年代(回收价值高的,这也是为什么叫g1的原因 ),而不是全部老年代,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制,根据的是最大暂停时间。也要注意的是Mixed GC并不是Full GC。

  • 会对E、S、O进行全面垃圾回收
  • 重新标记remark会STW
  • 拷贝存活会STW
  • G1根据暂停时间有选择地回收,找回收价值高的

MixedGC什么时候触发?由参数-XX:InitiatingHeapOccupancyPercent=n决定。默认:45%,该参数的意思是:当老年代大小占整个堆大小百分比达到该阈值时触发。

它的GC步骤分2步:

  • 全局并发标记(global concurrent marking)
  • 拷贝存活对象(evacuation)

3.4.3.1 全局并发标记

全局并发标记,执行过程分为五个步骤:

  • 1 初始标记(initial mark,STW)

​ 标记从根节点直接可达的对象,这个阶段会执行一次年轻代GC,会产生全局停顿stw。

  • 2 根区域扫描(root region scan)

​ G1 GC在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。

​ 该阶段与应用程序(非STW)同时运行,并且只有完成该阶段后,才能开始下一次STW年轻代垃圾回收。

  • 3 并发标记(Concurrent Marking)

​ G1 GC在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被STW年轻代垃圾回收中断。

  • 4 重新标记(Remark,STW)

​ 该阶段是STW回收,因为程序在运行,针对上一次的标记进行修正。

  • 5 清除垃圾(Cleanup,STW)

​ 清点和重置标记状态,该阶段会STW,这个阶段并不会实际上去做垃圾的收集,等待evacuation阶段来回收。

3.4.3.2 拷贝存活对象

Evacuation阶段是STW的。该阶段把一部分Region里的活对象拷贝到另一部分Region中,从而实现垃圾的回收清理。

Full GC:

SerialGC

  • 新生代内存不足发生的垃圾收集 - minor gc
  • 老年代内存不足发生的垃圾收集 - full gc

ParallelGC

  • 新生代内存不足发生的垃圾收集 - minor gc
  • 老年代内存不足发生的垃圾收集 - full gc

CMS

  • 新生代内存不足发生的垃圾收集 - minor gc
  • 老年代内存不足:
    • 垃圾回收速度跟不上垃圾产生速度时,并发收集失败,此时CMS会退化为串行收集,

G1

  • 新生代内存不足发生的垃圾收集 - minor gc
  • 老年代内存不足
    • 触发时机:老年代与整个堆占比达到阈值45%,触发并发标记及混合收集。如果并发收集比垃圾产生快,这时还不叫full GC。但也会有重标记、拷贝的过程,暂停时间短
    • 重新标记后的筛选回收:筛选回收(stop the world事件 根据用户期望的GC停顿时间回收)(注意:CMS 在这一步不需要stop the world)(阿里问为何停顿时间可以设置,参考:G1 垃圾收集器架构和如何做到可预测的停顿(阿里)

3.4.4 G1收集器相关参数

  • -XX:+UseG1GC

    使用G1垃圾收集器

  • -XX:MaxGCPauseMillis

    设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到),默认值是200毫秒。尽可能保证回收时间小于200ms。

  • -XX:G1HeapRegionSize=n

    设置的G1区域的大小(每个小块多大)。值是2的幂,范围是1MB到32MB之间。目标是根据最小的Java堆大小划分出约2048个区域。

    默认是堆内存的1/2000。

  • -XX:ParallelGCThreads=n

    设置STW工作线程数的值。将n的值设置为逻辑处理器的数量。n的值与逻辑处理器的数量相同,最多为8。

  • -XX:ConcGCThreads=n

    设置并行标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。

  • -XX:InitiatingHeapOccupanyPercent=n

​ 设置触发标记周期的Java堆占用率阈值。默认占用率是整个Java堆的45%。

3.4.5 测试

-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+PrintGCDetails -Xmx16m

#日志

[GC pause (G1 Evacuation Pause) (young), 0.0046811 secs]
[Parallel Time: 3.7 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 156.2, Avg: 158.1, Max: 159.6, Diff: 3.4]

#扫描根节点
[Ext Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.4, Diff: 0.4, Sum: 0.4]

#更新RS区域所消耗的时间
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
  [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]

#对象拷贝
[Object Copy (ms): Min: 0.0, Avg: 1.4, Max: 3.0, Diff: 3.0, Sum: 5.4]
[Termination (ms): Min: 0.0, Avg: 0.1, Max: 0.3, Diff: 0.3, Sum: 0.4]
  [Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[GC Worker Total (ms): Min: 0.0, Avg: 1.6, Max: 3.4, Diff: 3.4, Sum: 6.3]
[GC Worker End (ms): Min: 159.6, Avg: 159.7, Max: 159.9, Diff: 0.3]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms] #清空CardTable
[Other: 0.9 ms]
[Choose CSet: 0.0 ms]  #选取CSet
[Ref Proc: 0.9 ms] #弱引用、软引用的处理耗时
[Ref Enq: 0.0 ms]  #弱引用、软引用的入队耗时
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.0 ms] #大对象区域注册耗时
[Humongous Reclaim: 0.0 ms]  #大对象区域回收耗时
[Free CSet: 0.0 ms]

#年轻代的大小统计
[Eden: 6144.0K(6144.0K)->0.0B(5120.0K) Survivors: 0.0B->1024.0K Heap: 6144.0K(16.0M)->2791.0K(16.0M)]
[Times: user=0.00 sys=0.00, real=0.00 secs] 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

3.4.6 对于G1垃圾收集器的优化建议

  • 年轻代大小

    • 避免使用-Xmn选项或-XX:NewRatio等其他相关选项显示设置年轻代大小。
    • 固定年轻代的大小会覆盖暂停时间目标。
  • 暂停时间目标不要太过严苛

    • G1 GC的吞吐量目标是90%的应用程序时间和10%的垃圾回收时间。
    • 评估G1 GC的吞吐量时,暂停时间目标不要太严苛。目标太过严苛表示您愿意承受更多的垃圾回收开销,而这会直接影响到吞吐量。

G1新功能

  1. JDK 8u20 字符串去重。

开启:-XX:+UseStringDeduplication

优点:节省大量内存
缺点:略微多占用了 cpu 时间,新生代回收时间略微增加

String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
  • 1
  • 2

G1会将所有新分配的字符串放入一个队列。当新生代回收时,G1并发检查队列中是否有字符串重复。如果它们值一样,让它们引用同一个 char[]

注意,与 String.intern() 不一样,String.intern() 关注的是字符串对象,而字符串去重关注的是 char[]
在 JVM 内部,使用了不同的字符串表

  1. JDK 8u40 并发标记类卸载

所有对象都经过并发标记后,就能知道哪些类不再被使用,占用着内存也很浪费内存。

当一个类加载器的所有类都不再使用,则卸载它所加载的所有类 -XX:+ClassUnloadingWithConcurrentMark 默认启用

  1. JDK 8u60 回收巨型对象

巨型对象:一个对象大于 region 的一半的对象。

如下,巨型对象可能占用多个region

老年代的对象引用了巨型对象的话该老年代对象的卡表会被标记为脏的。当某个巨型对象从老年代的引用为0时,他就可以在新生代的垃圾回收时被回收掉。这是为了巨型对象越早回收越好。

G1 不会对巨型对象进行拷贝
G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0 的巨型对象就可以在新生代垃圾回收时处理掉

  1. JDK 9 并发标记起始时间的调整

并发标记必须在堆空间占满前完成,否则退化为 FullGC
JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent
JDK 9 可以动态调整 -XX:InitiatingHeapOccupancyPercent 用来设置初始值
进行数据采样并动态调整
总会添加一个安全的空档空间

垃圾回收调优

5.1 调优领域

  • 内存
  • 锁竞争
  • cpu 占用
  • io

5.2 确定目标
【低延迟】还是【高吞吐量】,选择合适的回收器

  • 低延迟:CMS ,G1,ZGC
  • 高吞吐量:ParallelGC
  • Zing

5.3 最快的 GC

低延迟

查看 FullGC 前后的内存占用,考虑下面几个问题
数据是不是太多?
resultSet = statement.executeQuery(“select * from 大表 limit n”)查大表前查出来很占内存,所以用法limit限制一下
数据表示是否太臃肿?
对象图
对象大小 16 Integer 24 int 4
是否存在内存泄漏?
static Map map =
软、弱
第三方缓存实现

5.4 新生代调优
新生代的特点
所有的 new 操作的内存分配非常廉价
TLAB thread-local allocation buffer
死亡对象的回收代价是零
大部分对象用过即死
Minor GC 的时间远远低于 Full GC

越大越好吗?
-Xmn Sets the initial and maximum size (in bytes) of the heap for the young generation (nursery).
GC is performed in this region more often than in other regions. If the size for the young
generation is too small, then a lot of minor garbage collections are performed. If the size is too
large, then only full garbage collections are performed, which can take a long time to complete.
Oracle recommends that you keep the size for the young generation greater than 25% and less
than 50% of the overall heap size.
新生代能容纳所有【并发量 * (请求-响应)】的数据
幸存区大到能保留【当前活跃对象 +需要晋升对象】
晋升阈值配置得当,让长时间存活对象尽快晋升

  • XX:MaxTenuringThreshold=threshold
  • XX:+PrintTenuringDistribution
    Desired survivor size 48286924 bytes, new threshold 10 (max 10)
  • age 1: 28992024 bytes, 28992024 total
  • age 2: 1366864 bytes, 30358888 total
  • age 3: 1425912 bytes, 31784800 total

5.5 老年代调优
以 CMS 为例
CMS 的老年代内存越大越好
先尝试不做调优,如果没有 Full GC 那么已经…,否则先尝试调优新生代
观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3

  • XX:CMSInitiatingOccupancyFraction=percent

5.6 案例
案例 1 Full GC 和 Minor GC频繁
案例 2 请求高峰期发生 Full GC,单次暂停时间特别长 (CMS)
案例 3 老年代充裕情况下,发生 Full GC (CMS jdk1.7)

4 可视化GC日志分析工具

4.1 GC日志输出参数

前面通过-XX:+PrintGCDetails可以对GC日志进行打印,我们就可以在控制台查看,这样虽然可以查看到GC的信息,但是并不直观,可以借助于第三方的GC日志分析工具进行查看。

在日志打印输出涉及到的参数如下:

可选值:
‐XX:+PrintGC 输出GC日志
‐XX:+PrintGCDetails 输出GC的详细日志
‐XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
‐XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2019‐05‐04T21:53:59.234+0800)
‐XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
‐Xloggc:../logs/gc.log 日志文件的输出路径
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

测试:

-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xmx256m 
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps 
-XX:+PrintGCDateStamps -XX:+PrintHeapAtGC 
-Xloggc:F://test//gc.log
# 运行后就可以在F盘下生成gc.log文件
  • 1
  • 2
  • 3
  • 4
  • 5

4.2 GC Easy可视化工具

GC Easy是一款在线的可视化工具,易用、功能强大,网站:http://gceasy.io/

img

上传后,点击“Analyze”按钮,即可查看报告

下面是堆大小

img

img

img

1_串行
-XX:+UseSerialGC 相当于 Serial + SerialOld。复制+标记整理

-XX:+UseParallelGC ~ -XX:+UseParallelOldGC(默认)
-XX:GCTimeRatio=ratio调整垃圾回收与总时间的占比1/1+ration
-XX:MaxGCPauseMillis=ms
-XX:ParallelGCThreads=n
开启一个另一个就自动开启了。
多个垃圾回收同时进行。个数等于CPU个数。垃圾回收时候CPU利用率是100%。可以指定GC线程数。调整新生代的大小。
-XX:+UserAdaptiveSizePolicy动态调整新生代大小,伊甸园和幸存区比例,晋升阈值
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

3_响应时间优先

-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark

并发的,而不是并行的。 用户进程与垃圾回收进程是并发的,都会抢占CPU。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Hotspot JVM提供多种垃圾回收器,我们需要根据具体应该的需要采用不同的回收器

垃圾回收器的并行和并发:

  • 并行Parallel:指多个收集器的线程同时工作,但是用户线程处于等待状态
  • 并发concurrent:指收集器在工作的时候,可以运行用户线程工作。
    • 并发不代表解决了GC停顿的问题,在关键的步骤还是要停顿。比如在收集器标记垃圾的时候。但在清除垃圾的时候,用户线程可以和GC线程并发执行。

内存泄露的经典原因

Java内存泄露的经典原因:

  • 对象定义在错误的范围(Wrong Scope)
  • 异常处理不当
  • 集合数据管理不当

对象定义在错误的范围:

//如果Foo实例对象的声明较长,会导致临时性内存泄露(这里的names变量其实只有临时作用)
class Foo{
    private String[] names;
    public void doIt(int length){
        if(names=null || names.length<length){
            names=new String[length];
        }
        popolate(names);
        print(names);
    }//names只在doIt这个方法中使用,没必要定义在外面,定义在外面的话Foo对象存在还会占用空间。
}
//JVM喜欢生命周期短的对象,这样做已经足够高效:将成员变量转换成局部变量
class Foo{
    public void doIt(int length){
    String[] names=new String([length]);
    populate(names);
    print(names);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

异常处理不当:

//错误的用法
数据库连接的关闭close应该放到finally中
  • 1
  • 2

集合数据管理不当:

当使用Array-based的数据结构(ArrayList,HashMap等)时,尽量减少resize
    比如new ArrayList时,尽量估算size,在创建的时候确定size
    减少resize可以避免没有必要的aray copying,gc碎片等问题
如果一个List只需要顺序访问,不需要随机访问,用LinkedList代替ArrayList,LinkedList是链表,不需要resize
  • 1
  • 2
  • 3
  • 4
//-verbose:gc输出详细垃圾回收日志//回收前和回收后情况
//-Xms20M堆初始大小
//-Xmx20M堆最大大小
//-Xmn10M堆新生代大小
//-XX:+PrintGCDetails//各个堆信息
//-XX:SurvivorRatio=8//Eden8:1:1
int size=1024*1024;//1M
byte[] myAlloc1=new byte[2*size];
byte[] myAlloc2=new byte[2*size];
byte[] myAlloc3=new byte[2*size];
byte[] myAlloc4=new byte[2*size];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

5_字节码

java虚拟机不和包括java在内的任何语言绑定,它只与“Class”特定的二进制文件格式关联,Class文件中包含Java虚拟机指令集和符号表以及若干其他辅助信息。本文将以字节码的角度来研究Java虚拟机。

字节码

  • Java跨平台的原因是JVM不跨平台
  • 首先编写一个简单的java代码,一次为例进行讲解

方法的执行过程:

  • 原始java代码

  • 编译后的字节码文件

  • 常量池载入运行时常量池

  • 方法字节码载入方法区

  • main线程开始执行,分配栈帧内存

  • 执行引擎开始执行字节码

测试代码(原始java代码)

package JVMtest;

public class MyTest1{
    private int a=1;
    public int getA(){
        return a;
    }
    public void setA(int a){
        this.a=a;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

编译生成MyTest1.class文件
使用反编译命令:javap MyTest1 ,对文件进行反编译,生成以下数据

Compiled from "MyTest1.java"
public class JVMtest.MyTest1 {
  public JVMtest.MyTest1();
  public int getA();
  public void setA(int);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

增加参数,使用反编译命令:javap -c MyTest1,生成以下数据

Compiled from "MyTest1.java"
public class JVMtest.MyTest1 {
  public JVMtest.MyTest1();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iconst_1
       6: putfield      #2                  // Field a:I
       9: return

  public int getA();
    Code:
       0: aload_0
       1: getfield      #2                  // Field a:I
       4: ireturn

  public void setA(int);
    Code:
       0: aload_0
       1: iload_1
       2: putfield      #2                  // Field a:I
       5: return
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

javap -v查看(二进制字节码)

使用反编译命令:javap -verbose MyTest1.class,生成以下数据

javap -verbose MyTest1.class
Classfile /F:/JVMtest/out/production/JVMtest/JVMtest/MyTest1.class
  Last modified 2020-3-30; size 461 bytes
  MD5 checksum f4687563763f0dcca1cd899030c582fb
  Compiled from "MyTest1.java"
public class JVMtest.MyTest1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#21         // JVMtest/MyTest1.a:I
   #3 = Class              #22            // JVMtest/MyTest1
   #4 = Class              #23            // java/lang/Object
   #5 = Utf8               a
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               LJVMtest/MyTest1;
  #14 = Utf8               getA
  #15 = Utf8               ()I
  #16 = Utf8               setA
  #17 = Utf8               (I)V
  #18 = Utf8               SourceFile
  #19 = Utf8               MyTest1.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = NameAndType        #5:#6          // a:I
  #22 = Utf8               JVMtest/MyTest1
  #23 = Utf8               java/lang/Object
{
  public JVMtest.MyTest1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_1
         6: putfield      #2                  // Field a:I
         9: return
      LineNumberTable:
        line 3: 0
        line 4: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   LJVMtest/MyTest1;

  public int getA();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field a:I
         4: ireturn
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LJVMtest/MyTest1;

  public void setA(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: putfield      #2                  // Field a:I
         5: return
      LineNumberTable:
        line 9: 0
        line 10: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   LJVMtest/MyTest1;
            0       6     1     a   I
}
SourceFile: "MyTest1.java"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84

字节码分析

常量池分析

使用UltraEdit打开MyTest1.class二进制文件:

如下,00 18(即24)代表常量池有#1-#24项,注意#0项不计入,也没有值。

每个常量分为2/3个部分,比如方法的常量格式为u1,u2,u3

如下面第一个u1=0A(十进制的10),根据后面常量池的表可以查到0A代表的是一个方法的引用,此时结构后面还跟着2个值,而#4和#20又对应别的常量池,最终得到#1代表方法的【返回值类型】和【方法名+参数】的常量池号,

网站:http://www.ab126.com/GOJU/1711.HTML
经测试,将16进制转换为ASCII后如下,刚好是javap -verbose生成Constant pool的结果
#1 = Methodref          #4.#20         // java/lang/Object."<init>":()V
#2 = Fieldref           #3.#21         // JVMtest/MyTest1.a:I
#3 = Class              #22            // JVMtest/MyTest1
#4 = Class              #23            // java/lang/Object

#5, 61 == a
#6, 49 == I
#7, 3C 69 6E 69 74 3E == <init>
#8, 28 29 56 == ()V代表无参返回值void
#9, 43 6F 64 65 == Code
#10, 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 == LineNumberTable
#11, 4C 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65 == LocalVariableTable
#12, 74 68 69 73 == this
#13, 4C 4A 56 4D 74 65 73 74 2F 4D 79 54 65 73 74 31 3B == LJVMtest/MyTest1;
#14, 67 65 74 41 == getA
#15, 28 29 49 == ()I代表无参返回值为int
#16, 73 65 74 41 == setA
#17, 28 49 29 56 == (I)V代表参数为int,返回值void
#18, 53 6F 75 72 63 65 46 69 6C 65 == SourceFile
#19, 4D 79 54 65 73 74 31 2E 6A 61 76 61 == MyTest1.java
##20,名称#7和类型#8
##21,名称#5和类型#6
#22, 4A 56 4D 74 65 73 74 2F 4D 79 54 65 73 74 31 == JVMtest/MyTest1
#23, 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 ==java/lang/Object
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

字节码结构

1.使用javap -verbose MyTest 命令分析一个字节码文件时,将会分析该字节码文件的魔数,版本号,常量池,类信息,类的构造方法,类中的方法信息,类变量与成员变量的信息。

魔数

2.魔数:所有的.class文件的前四个字节都是魔数,魔数值为固定值:0xCAFEBABE(咖啡宝贝)

版本号

3.版本号:魔数后面4个字节是版本信息,前两个字节表示minor version(次版本号),后两个字节表示major version(主版本号),十六进制34=十进制52。所以该文件的版本号为1.8.0。低版本的编译器编译的字节码可以在高版本的JVM下运行,反过来则不行。

常量池

4.常量池(constant pool):版本号之后的就是常量池入口,一个java类定义的很多信息都是由常量池来维护和描述的,可以将常量池看作是class文件的资源仓库,包括java类定义的方法和变量信息,常量池中主要存储两类常量:字面量和符号引用。字面量如文本字符串、java中生命的final常量值等,符号引用如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符等。

5.常量池的整体结构:Java类对应的常量池主要由常量池数量和常量池数组两部分共同构成,常量池数量紧跟在主版本号后面,常量池数量占据两个字节,而常量池数组在常量池数量之后。常量池数组与一般数组不同的是,常量池数组中元素的类型、结构都是不同的,长度当然也就不同,但是每一种元素的第一个数据都是一个u1类型标志位,占据一个字节,JVM在解析常量池时,就会根据这个u1类型的来获取对应的元素的具体类型。 值得注意的是,常量池数组中元素的个数=常量池数-1,(其中0暂时不使用)。目的是满足某些常量池索引值的数据在特定的情况下需要表达不引用任何常量池的含义。根本原因在于索引为0也是一个常量,它是JVM的保留常量,它不位于常量表中。这个常量就对应null,所以常量池的索引从1而非0开始。

常量池结构表

在这里插入图片描述

以前面的#1为例,第1字节u1=10,23字节u2代表类,45字节u2代表名字和类型

  • u2的23字节又指向了#4,第二个u2的56字节指向#20
    • #4的u1=7是类名指向#23(字符串java/lang/Object),
    • #20的u1=12是又分为
      • 方法名称(指向#7字符串"")+
      • 方法描述符(指向#8字符串()V,代表无参返回值void)。
      • #20最终为"<init>":()V

所以#1最终为java/lang/Object."<init>":()V,其中.为U的分割号

总结:最后都会指向字符串

此外分析#2最后的结果为JVMtest/MyTest1.a:I,代表是一个属性a,类型为int

所以上图的MethodRef代表着一个方法,FieldRef代表是一个属性,其余的是一些基本类型为utf8字符串

类型表示

6.在JVM规范中,每个变量/字段都有描述信息,主要的作用是描述字段的数据类型,方法的参数列表(包括数量、类型和顺序)与返回值。根据描述符规则,

  • 基本数据类型和代表无返回值的void类型都用一个大写字符来表示,
  • 而对象类型使用字符L+对象的全限定名称来表示。
  • 为了压缩字节码文件的体积,对于基本数据类型,JVM都只使用一个大写字母来表示。如下所示:B-byte,C-char,D-double,F-float,I-int,J-long,S-short,Z-boolean,V-void,L-对象类型,
  • 如Ljava/lang/String;
    对于数组类型来说,每一个维度使用一个前置的[ 来表示,如int[]表示为[I ,String [][]被记录为[[Ljava/lang/String;

7.用描述符描述方法的时候,用先参数列表后返回值的方式来描述。参数列表按照参数的严格顺序放在一组()之内,如方法String getNameByID(int id ,String name)转换成(I,Ljava/lang/String;)Ljava/lang/String;

Java字节码整体结构:

在这里插入图片描述

Class字节码中有两种数据类型:
(1)字节数据直接量:这是基本的数据类型。共细分为u1、u2、u4、u8四种,分别代表连续的1个字节、2个字节、4个字节、8个字节组成的整体数据。
(2)表/数组:表是由多个基本数据或其他表,按照既定顺序组成的大的数据集合。表是有结构的,它的结构体:组成表的成分所在的位置和顺序都是已经严格定义好的。

访问权限Access Falgs:

2个字节,访问标志信息包括了该class文件是类还是接口,是否被定义成public,是否是abstract,如果是类,是否被定义成final。

在这里插入图片描述

  • 0x0021是0x0020和0x0001的并集,表示ACC_PUBLIC和ACC_SUPER。(我们的字节码文件正是21,可以调用父类方法)
  • 0x0002:private

类名

2个字节,对应常量池#3

父类名

2个字节,对应常量池#4

接口

u2接口数量+u2接口名。我们的是0000,没有接口。

字段表Fields

u2+每字段结构*个数

字段表用于描述类和接口中声明的变量。这里的字段包含了类级别变量和实例变量,但是不包括方法内部声明的局部变量。

每字段结构

类型名称数量
u2access-flags1
u2names-index1
u2descriptor-index1
u2attributes-count1
attribute-infoattributesattributes-count
  • 成员个数:00 01代表有一个成员
  • 第一个成员:
    • 成员属性access-flags:00 02:代表private
    • 成员名称names-index:00 05:代表#5(a)
    • 成员类型descriptor-index:00 06:代表#6(I即int)
    • 属性个数attributes-count:00 00

方法

方法个数(2字节)+每方法结构*个数(我们的class为3个get+set+构造器)

每方法的结构:

类型名称数量
u2access-flags1
u2names-index1
u2descriptor-index1
u2attributes-count1
attribute-infoattributesattributes-count

方法个数:00 03:3个方法set+get+构造器

{
  public JVMtest.MyTest1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_1
         6: putfield      #2                  // Field a:I
         9: return
      LineNumberTable:
        line 3: 0
        line 4: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   LJVMtest/MyTest1;

  public int getA();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field a:I
         4: ireturn
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LJVMtest/MyTest1;

  public void setA(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: putfield      #2                  // Field a:I
         5: return
      LineNumberTable:
        line 9: 0
        line 10: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   LJVMtest/MyTest1;
            0       6     1     a   I
}
SourceFile: "MyTest1.java"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 方法1:
    • 访问权限access-flags:00 01:代表PUBLIC
    • 方法名names-index:00 07:#7(构造器<init>
    • 方法修饰符descriptor-index:00 08:#8(()V
    • 方法属性个数attributes-count:00 01:1个
    • 第一个属性attribute_info:
      • 属性名attribute_name_index:00 09:#9(方法的属性Code,方法总是有Code这个属性)
      • 属性长度attribute_length:00 00 00 38:56个字节,56个字节后是第二个方法
      • info[56]
        • 操作数最大深度max-stack:00 02
        • 局部变量数量max_locals:00 01
        • 方法字节长度code_length:00 00 00 0A:10
        • 往后00 00 00 0A个字节:方法运行时候的字节码:2A B7 00 01 2A 04 B5 00 02 B1,对应jclassLib中的ByteCode信息(init方法):0 aload_0【2A:索引为0推送到栈顶】 1 invokespecial #1 <java/lang/Object.<init>>【B7:调用父类构造方法00 01:#1】 4 aload_0【2A】 5 iconst_1【04】 6 putfield #2 <JVMtest/MyTest1.a>【B5 /00 02】 9 return
        • 异常表00 00
        • Code属性个数:00 02(LineNumberTable和LocalVaribaleTable)
        • 00 0A:#10:LineNumberTable,字节码与源代码的行号对应
        • 00 00 00 0A:往后10个字节
        • 00 02 /00 00 00 03 /00 04 00 04
        • 00 0B:#11:LocalVaribaleTable
        • 00 00 00 0C往后12字节是LocalVaribaleTable
        • 00 01局部变量个数/ 00 00局部变量起始位置. 00 0A结束位置 /00索引 /0C局部变量对应#10this /00 0D局部变量的描述#13/ 00 00检查
        • 对应java里非静态方法,至少有一个局部变量this
  • 方法2:…省略
  • 方法3:…省略

方法中的每个属性都是一个attribute_info结构:

(1)JVM预定义了部分attribute,但是编译器自己也可以实现自己的attribute写入class文件里,供运行时使用;
(2)不同的attribute通过attribute_name_index来区分。

attribute_info格式:

attribute_info{
    u2 attribute_name_index;//eg.Code
    u4 attribute_length;
    u1 info[attribute_length]
}
  • 1
  • 2
  • 3
  • 4
  • 5

Code结构:

attribute_name_index值为code,则为Code结构

Code的作用是保存该方法的结构,所对应的的字节码

Code_attribute{//info
    //u2 attribute-name-index;
    //u4 attibute-length;
    u2 max-stack;
    u2 max-locals;
    u4 code-length;
    u1 code[code-length];//往后code-length个字节是ByteCode
    u2 exception-table-length;
    {
        u2 start-pc;
        u2 end-pc;
        u2 handler-pc;
        u2 catch-type;
    }exception-table[exception-table-length];
    u2 attibute-count;
    attribute-info attributes[attibutes-count];
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

构造器的Code结构组成:

  • attribute_length:表示attribute所包含的字节数,不包含attribute_name_index和attribute_length字段
  • max_stacks:表示这个方法运行的任何时刻所能达到的操作数栈的最大深度
  • max_locals:表示方法执行期间创建的局部变量的数目,包含用来表示传入的参数的局部变量
  • code_length:表示该方法所包含的字节码的字节数以及具体的指令码。具体的字节码是指该方法被调用时,虚拟机所执行的字节码
  • exception_table:存放处理异常的信息,每个exception_table表,是由start_pc、end_pc、hangder_pc、catch_type组成
    • start_pc、end_pc:表示在code数组中从start_pc到end_pc(包含start_pc,不包含end_pc)的指令抛出的异常会由这个表项来处理
    • hangder_pc:表示处理异常的代码的开始处。
    • catch_type:表示会被处理的异常类型,它指向常量池中的一个异常类。当catch_type=0时,表示处理所有的异常。

附加方法其他属性:

LineNumbeTable_attribute:

LineNumberTable_attribute{
    u2 attribute-name-index;
    u4 attribute-length;
    u2 line-number-table-length;
    {u2 start-pc;
     u2 line-number;
    }line-number-table[line-number-table-length];
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这个属性表示code数组中,字节码与java代码行数之间的关系,可以在调试的时候定位代码执行的行数。

LocalVariableTable :结构类似于 LineNumbeTable_attribute
对于Java中的任何一个非静态方法,至少会有一个局部变量,就是this。

public JVMtest.MyTest1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_1
         6: putfield      #2                  // Field a:I
         9: return
      LineNumberTable:
        line 3: 0
        line 4: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   LJVMtest/MyTest1;

  public int getA();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field a:I
         4: ireturn
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LJVMtest/MyTest1;

  public void setA(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: putfield      #2                  // Field a:I
         5: return
      LineNumberTable:
        line 9: 0
        line 10: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   LJVMtest/MyTest1;
            0       6     1     a   I
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

字节码查看工具:jclasslib
http://github.com/ingokegel/jclasslib

测试2 ------- 反编译分析MyTest2.class
static变量会导致出现static代码块

public class MyTest2{
    String str="Welcome";
    private int x=5;
    public static Integer in=5;
    public static void main(String[] args){
        MyTest2  myTest2=new MyTest2();
        myTest2.setX(8);
        in=20;
    }
    private synchronized void setX(int x){
        thisx=x;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

javap -verbose -p Abc -p:将private修饰的方法显示出来
synchronized关键字:
moniterenter
monitorexit

测试3

public class MyTest3{
    public void test(){
        try{
            InputStream is=new FileInputStream("test.txt");
            ServerSocket ss=new ServerSocket(9999);
            ss.accept();

        }catch(FileNotFoundException e){

        }catch(IOException e){

        }catch(Exception e){

        }finally{
            System.out.println("finally");
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

Java字节码对于异常的处理方式:

1.统一采用异常表的方式来对异常进行处理;
2.在jdk1.4.2之前的版本中,并不是使用异常表的方式对异常进行处理的,而是采用特定的指令方式;
3.当异常处理存在finally语句块时,现代化的JVM采取的处理方式是将finally语句内的字节码拼接到每个catch语句块后面。也就是说,程序中存在多少个catch,就存在多少个finally块的内容。
栈帧(stack frame):
用于帮助虚拟机执行方法调用和方法执行的数据结构
栈帧本身是一种数据结构,封装了方法的局部变量表,动态链接信息,方法的返回地址以及操作数栈等信息。
符号引用:符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,符号引用和虚拟机的布局无关。(在编译的时候一个每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址,多以就用符号引用来代替,而在这个解析阶段就是为了把这个符号引用转化成为真正的地址的阶段。)
直接引用:(1)直接指向目标的指针(指向对象,类变量和类方法的指针)(2)相对偏移量。(指向实例的变量,方法的指针)(3)一个间接定位到对象的句柄。
有些符号引用在加载阶段或者或是第一次使用时,转换为直接引用,这种转换叫做静态解析;另外一些符号引用则是在运行期转换为直接引用,这种转换叫做动态链接。
助记符:
1.invokeinterface:调用接口的方法,在运行期决定调用该接口的哪个对象的特定方法。
2.invokestatic:调用静态方法
3.invokespecial:调用私有方法, 构造方法(),父类的方法
4.invokevirtual:调用虚方法,运行期动态查找的过程
5.invokedynamic:动态调用方法

测试4

public class MyTest4{
    public static void test(){
            System.out.println("static test");
    }
    public static void main(Stirng[] args){
        test();             //invokestatic
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

静态解析的四种场:静态方法、父类方法、构造方法、私有方法。以上四种方法称为非虚方法,在类加载阶段将符号引用转换为直接引用。

73没看

字节码结束龙JVM笔记

JVM调优

请看:https://blog.csdn.net/hancoder/article/details/108312012

张龙类的加载过程代码:

  • 测试1:
/**
        对于静态字段来说,只有直接定义了该字段的类才会被初始化
        当一个类在初始化时,要求父类全部都已经初始化完毕
        -XX:+TraceClassLoading,用于追踪类的加载信息并打印出来

        -XX:+<option>,表示开启option选项
        -XX:-<option>,表示关闭option选项
        -XX:<option>=value,表示将option的值设置为value
*/
public class MyTest{
    public static void main(String[] args){
        System.out.println(MyChild.str);  //输出:MyParent static block 、 hello world   (因为对MyChild不是主动使用)
       //对parent的主动使用,因为没有使用到child中的str2,所以child的静态代码块没有执行。子类引用不算主动使用
        System.out.println(MyChild.str2);  //输出:MyParent static block  、MyChild static block、welcome
        
 /* 输出
MyParent static block
hello world
MyChild static block
welcome
  */
    }
}
class MyParent{
    public static String str="hello world";
    static {
        System.out.println("MyParent static block");
    }
}
class MyChild extends MyParent{
    public static String str2="welcome";
    static {
        System.out.println("MyChild static block");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 测试2:
/** javap -c 类
        常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中
        本质上,调用类并没有直接调用到定义final常量的类,因此并不会触发定义常量的类的初始化
        注意:这里指的是将常量存到MyTest2的常量池中,之后MyTest2和MyParent就没有任何关系了。
        甚至我们可以将MyParent2的class文件删除

        助记符 ldc:表示将int、float或者String类型的常量值从常量池中推送至栈顶
        助记符 bipush:表示将单字节(-128-127)的常量值推送到栈顶
        助记符 sipush:表示将一个短整型值(-32768-32369)推送至栈顶
        助记符 iconst_1:表示将int型的1推送至栈顶(iconst_m1到iconst_5)
*/
public class MyTest2{
    public static void main(String[] args){
        System.out.println(MyParent2.str);    //输出 hello world
        System.out.println(MyParent2.s);  
        System.out.println(MyParent2.i);  
        System.out.println(MyParent2.j);  
  /*
hello world
7
129
1
   */
    }
}
class MyParent2{
    public static final String str="hello world";
    public static final short s=7;
    public static final int i=129;
    public static final int j=1;
    static {
        System.out.println("MyParent static block");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 测试3
/**
但是常量也有例外:
当一个常量的值并非编译期间可以确定的,那么其值就不会放到调用类的常量池中
这时在程序运行时,会导致主动使用这个常量所在的类,显然会导致这个类被初始化
*/
public class MyTest3{
    public static void main(String[] args){
        System.out.println(MyParent3.str);  
        /*  输出
        MyParent static block //触发了类的初始化
        kjqhdun-baoje21w-jxqioj1-2jwejc9029
        */
    }
}
class MyParent3{
    public static final String str=UUID.randomUUID().toString();
    static {
        System.out.println("MyParent static block");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 测试4
/**
对于数组实例来说,其类型是由JVM在运行期动态生成的,表示为 [L com.hisense.classloader.MyParent4 这种形式。
对于数组来说,JavaDoc经构成数据的元素成为Component,实际上是将数组降低一个维度后的类型。
一维/二维数组的getSuperClass都是Object

助记符:anewarray:表示创建一个引用类型(如类、接口)的数组,并将其引用值压入栈顶
助记符:newarray:表示创建一个指定原始类型(int boolean float double)d的数组,并将其引用值压入栈顶
    */
public class MyTest4{
    public static void main(String[] args){
        MyParent4[] myParent4s=new MyParent4[1];    //不是主动使用
        System.out.println("--------");
        MyParent4 myParent4=new MyParent4();        //创建类的实例,属于主动使用,会导致类的初始化

        System.out.println(myParent4s.getClass());  //输出 [L com.hisense.classloader.MyParent4
        System.out.println(myParent4s.getClass().getSuperClass());    //输出Object

        int[] i=new int[1];
        System.out.println(i.getClass());          //输出 [ I
        System.out.println(i.getClass().getSuperClass());    //输出Object
    }
    /*
------------
MyParent static block
class [LJVMtest.MyParent4;
class java.lang.Object
class [I
class java.lang.Object
    */
}
class MyParent4{
    static {
        System.out.println("MyParent static block");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 测试5
/**
        当一个接口在初始化时,并不要求其父接口都完成了初始化
        只有在真正使用到父接口的时候(如引用接口中定义的常量),才会初始化
*/
public class MyTest5{
    public static void main(String[] args){
         public static void main(String[] args){
            System.out.println(MyChild5.b)
         }
    }
}
interfacce MParent5{
    public static Thread thread=new thread(){
        System.out.println(" MParent5 invoke")
    };
}
interface MyChild5 extends MParent5{     //接口属性默认是 public static final
    public static int b=6;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 测试6
/**
 准备阶段和初始化的顺序问题
*/
public class MyTest6{
    public static void main(String[] args){
         public static void main(String[] args){
            Singleton Singleton=Singleton.getInstance();
            System.out.println(Singleton.counter1);     //输出1,1
            System.out.println(Singleton.counter2);
         }
    }
}
class Singleton{
    public static int counter1;
    public static int counter2=0;  
    private static Singleton singleton=new Singleton();
    
    private Singleton(){
        counter1++;
        counter2++;
    }
    
    // public static int counter2=0;       //   若改变此赋值语句的位置,输出  1,0
    public static Singleton getInstance(){
        return singleton;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

java编译器在它编译的每一个类都至少生成一个实例化的方法,在java的class文件中,这个实例化方法被称为<init>。针对源代码中每一个类的构造方法,java编译器都会产生一个“<init>”方法。

测试7

/** //类加载器测试
 java.lang.String是由根加载器加载,在rt.jar包下
*/
public class MyTest7{
    public static void main(String[] args){
         public static void main(String[] args){
            Class<?> clazz=Class.forName("java.lang.String");
            System.out.println(clazz.getClassLoader());  //返回null
            
            Class<?> clazz2=Class.forName("C");
           System.out.println(clazz2.getClassLoader());  //输出sun.misc.Launcher$AppClassLoader@18b4aac2  其中AppClassLoader:系统应用类加载器
         }
    }
}
class C{
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 测试8
/**
    调用ClassLoader的loaderClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化
*/
public class MyTest8{
    public static void main(String[] args){
        ClassLoader loader=ClassLoader.getSystemClassLoader();
        Class<?> clazz1=loader.loadClass("CL"); //不会初始化
        System.out.println(clazz1);
        System.out.println("-------------------");

        Class<?> clazz=Class.forName("CL");
        System.out.println(clazz);  //反射初始化
        
/*
class another.CL
-------------------
FinalTest static block
class another.CL
 */
    }
}

class CL{
    static {
        System.out.println("FinalTest static block");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 测试9-12忽略
  • 测试13
/**
    输出AppClassLoader、ExtClassLoader、null
*/
public class MyTest13{
    public static void main(String[] args){
         public static void main(String[] args){
            ClassLoader loader=ClassLoader.getSystemClassLoader();
            System.out.println(loader);
            
            while(loader!=null){
                loader=loader.getParent();
                 System.out.println(loader);
            }
         }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 测试14
public class MyTest14{
    public static void main(String[] args){
         public static void main(String[] args){
            ClassLoader loader=Thread.currentThread().getContextClassLoader();
            System.out.println(loader);         //输出AppClassLoader
            //下面这段没整明白什么用,先记录下来
            String resourceName="com/hisense/MyTest13.class";
            Enumeration<URL> urls=loader.getResources(resourceName);
            whilr(urls.hasMoreElements()){
                URL url=urls.nextElement();
                System.out.println(url);
            }
         }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

测试15

/**
    对于数组,它对应的class对象不是由类加载器加载,而是由JVM在运行期动态的创建。然而对于数组类的类加载器来说,它返回的类加载器和数组内元素的类加载器是一样的。如果数组类元素是原生类,那么数组是没有类加载器的。
*/
public class MyTest15{
    public static void main(String[] args){
            String[] strings=new String[2];
            System.out.println(strings.getClass());
            System.out.println(strings.getClass().getClassLoader());    //输出null
            
            MyTest15[] mytest15=new MyTest15[2];
            System.out.println(mytest15.getClass().getClassLoader());   //输出应用类加载器
            
            int[] arr=new int[2];
            System.out.println(arr.getClass().getClassLoader());        //输出null,此null非彼null
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

并行类加载器可支持并发加载,需要在类初始化期间调用ClassLoader.registerAaParallelCapable()方法进行注册。ClassLoader类默认支持并发加载,但是其子类必须在初始化期间进行注册。

loadClass()包含如下方法

  • findLoadedClass(String);//检查是否已经被加载

  • loadClass();

  • findClass();//我们重写的

  • 测试16

/**
    创建自定义加载器,继承ClassLoader
*/
public class MyTest16 extends ClassLoader{
    private String classLoaderName;
    private String path;
    private final String fileExtension=".class";
    
    public MyTest16(String classLoaderName){
        super();        //将系统类当做该类的父加载器
        this.classLoaderName=classLoaderName;
    }
    public MyTest16(ClassLoader parent,String classLoaderName){
        super(parent);      //显式指定该类的父加载器
        this.classLoaderName=classLoaderName;
    }
    
   public MyTest16(ClassLoader parent){
        super(parent);      //显式指定该类的父加载器
    }
    
    public void setPath(String path){//指定加载的路径(系统加载器路径为out目录)
        this.path=path;
    }
    @Override
    protect Class<?> findClass(String className){
        System.out.println("calssName="+className);
        className=className.replace(".",File.separator);
        byte[] data=loadClassData(className);
        return defineClass(className,data,0,data.length); //define方法为父类方法
    }
    
    private byte[] loadClassData(String name){
        InputStream is=null;
        byte[] data=null;
        ByteArrayOutputStream baos=null;
        try{
            is=new FileInputStream(new File(this.path+name+this.fileExtension));
            baos=new ByteArrayOutputStream();
            int ch;
            while(-1!=(ch=is.read())){
                baos.write(ch);
            }
            data=baos.toByteArray();
            
        }catch(Exception e){
        }finally{
            is.close();
            baos.close();
             return data;
        }
    }
    public static void test(ClassLoader classLoader){
        Class<?> clazz=classLoader.loadClass("com.hisense.MyTest1");  //loader不一样,但是类一样
        //loadClass()是父类方法,在方法内部调用findClass
        System.out.println(clazz.hashCode());
        Object  object=clazz.newInstance();
        System.out.println(object);
    }
    public static void main(String[] args){
        //父亲是系统类加载器,根据父类委托机制,MyTest1由系统类加载器加载了
        MyTest16 loader1=new MyTest16("loader1");       
        test(loader1);
        
        //仍然是系统类加载器进行加载的,因为路径正好是classpath
        MyTest16 loader2=new MyTest16("loader2");  //如果都是由系统加载器加载的,那么class就一样
        loader2.path="D:\Eclipse\workspace\HiATMP-DDMS\target\classes\";
        test(loader2);
        
         //自定义的类加载器被执行,findClass方法下的输出被打印。前提是当前classpath下不存在MyTest1.class,MyTest16的父加载器-系统类加载器会尝试从classpath中寻找MyTest1。
        MyTest16 loader3=new MyTest16("loader3");  
        loader3.path="C:\Users\weichengjie\Desktop\";//
        test(loader3);
        
        //与3同时存在,输出两个class的hash不一致,findClass方法下的输出均被打印,原因是类加载器的命名空间问题。
        MyTest16 loader4=new MyTest16("loader4");  
        loader4.path="C:\Users\weichengjie\Desktop\";
        test(loader4);
        
        //将loader3作为父加载器
        MyTest16 loader5=new MyTest16(loader3,"loader3");  
        loader3.path="C:\Users\weichengjie\Desktop\";
        test(loader5);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85

类的卸载

  • 当一个类被加载、连接和初始化之后,它的生命周期就开始了。当此类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,类在方法区内的数据也会被卸载。
  • 一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。
  • 由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。Java虚拟机本身会始终引用这些加载器,而这些类加载器则会始终引用他们所加载的类的Class对象,因此这些Class对象是可触及的。
  • 由用户自定义的类加载器所加载的类是可以被卸载的。
/**
    自定义类加载器加载类的卸载
    -XX:+TraceClassUnloading
*/
   public static void main(String[] args){
        MyTest16 loader2=new MyTest16("loader2");  
        loader2.path="D:\Eclipse\workspace\HiATMP-DDMS\target\classes\";
        test(loader2);
        loader2=null;
        System.gc();   //让系统去显式执行垃圾回收
        
        输出的两个对象hashcode值不同,因为前面加载的已经被卸载了
        loader2=new MyTest16("loader6"); //  
        test(loader2);
   }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

gvisualvm命令 查看当前java进程(gvisualvm在jdk/bin下面)

  • 测试17
/**
    创建自定义加载器,继承ClassLoader
*/
class MyCat{
    public MyCat(){
        System.out.println("MyCat is loaded..."+this.getClass().getClassLoader());
    }
}

class MySample{
    public MySample(){
        System.out.println("MySample is loaded..."+this.getClass().getClassLoader());
        new MyCat();
    }
}

public class MyTest17 extends ClassLoader{
    private String classLoaderName;
    private String path;
    private final String fileExtension=".class";
    
    public MyTest17(String classLoaderName){
        super();        //将系统类当做该类的父加载器
        this.classLoaderName=classLoaderName;
    }
    public MyTest17(ClassLoader parent,String classLoaderName){
        super(parent);      //显式指定该类的父加载器
        this.classLoaderName=classLoaderName;
    }
    
    public void setPath(String path){
        this.path=path;
    }
    @Override
    protect Class<?> findClass(String className){
        System.out.println("calssName="+className);
        className=className.replace(".",File.separator);
        byte[] data=loadClassData(className);
        return defineClass(className,data,0,data.length); //define方法为父类方法
    }//系统加载器就能加载类了,所以可能不通过我们自定义的类加载器。
    
    private byte[] loadClassData(String name){
        InputStream is=null;
        byte[] data=null;
        ByteArrayOutputStream baos=null;
        try{
            is=new FileInputStream(new File(this.path+name+this.fileExtension));
            baos=new ByteArrayOutputStream();
            int ch;
            while(-1!=(ch=is.read())){
                baos.write(ch);
            }
            data=baos.toByteArray();
        }catch(Exception e){
        }finally{
            is.close();
            baos.close();
             return data;
        }
    }
    public static void main(String[] args){
        MyTest17 loader1=new MyTest17("loader1");
        Class<?> clazz=loader1.loadClass("com.hisense.MySample");  
        System.out.println(clazz.hashCode());
        //如果注释掉该行,就并不会实例化MySample对象,不会加载MyCat(可能预先加载)
        Object  object=clazz.newInstance(); //加载和实例化了MySample和MyCat
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

测试17_1

public class MyTest17_1 extends ClassLoader{
    private String classLoaderName;
    private String path;
    private final String fileExtension=".class";
    
    public MyTest17_1(String classLoaderName){
        super();        //将系统类当做该类的父加载器
        this.classLoaderName=classLoaderName;
    }
    public MyTest17_1(ClassLoader parent,String classLoaderName){
        super(parent);      //显式指定该类的父加载器
        this.classLoaderName=classLoaderName;
    }
    
    public void setPath(String path){
        this.path=path;
    }
    @Override
    protect Class<?> findClass(String className){
        System.out.println("calssName="+className);
        className=className.replace(".",File.separator);
        byte[] data=loadClassData(className);
        return defineClass(className,data,0,data.length); //define方法为父类方法
    }
    
    private byte[] loadClassData(String name){
        InputStream is=null;
        byte[] data=null;
        ByteArrayOutputStream baos=null;
        try{
            is=new FileInputStream(new File(this.path+name+this.fileExtension));
            baos=new ByteArrayOutputStream();
            int ch;
            while(-1!=(ch=is.read())){
                baos.write(ch);
            }
            data=baos.toByteArray();
        }catch(Exception e){
        }finally{
            is.close();
            baos.close();
             return data;
        }
    }
    public static void main(String[] args){
        MyTest17_1 loader1=new MyTest17_1("loader1");
        loader1.path="C:\Users\weichengjie\Desktop";
        Class<?> clazz=loader1.loadClass("com.hisense.MySample");  
        System.out.println(clazz.hashCode());
        //MyCat是由加载MySample的加载器去加载的:
        如果只删除classpath下的MyCat,则会报错,NoClassDefFoundError;
        如果只删除calsspath下的MySample,则由自定义加载器加载桌面上的MySample,由系统应用加载器加载MyCat。
        Object  object=clazz.newInstance(); 
    }
    
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

测试17_1_1

//修改MyCat和MySample
class MyCat{
    public MyCat(){
        System.out.println("MyCat is loaded..."+this.getClass().getClassLoader());
        System.out.println("from MyCat: "+MySample.class);
    }
}

class MySample{
    public MySample(){
        System.out.println("MySample is loaded..."+this.getClass().getClassLoader());
        new MyCat();
        System.out.println("from MySample :"+ MyCat.class);
    }
}

public class MyTest17_1 {
        public static void main(String[] args){
        //修改MyCat后,仍然删除classpath下的MySample,留下MyCat,程序报错
        //因为命名空间,父加载器找不到子加载器所加载的类,因此MyCat找不到        
        //MySample。
        MyTest17_1 loader1=new MyTest17_1("loader1");
        loader1.path="C:\Users\weichengjie\Desktop";
        Class<?> clazz=loader1.loadClass("com.hisense.MySample");  
        System.out.println(clazz.hashCode());
        Object  object=clazz.newInstance(); 
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

关于命名空间重要说明:

  1. 子加载器所加载的类能够访问父加载器所加载的类;
  2. 而父加载器所加载的类无法访问子加载器所加载的类。

加载路径:

测试18

public class MyTest18{
    public static void main(String[] args){
        System.out.println(System.getProperty("sun.boot.class.path"));//根加载器路径
        System.out.println(System.getProperty("java.ext.dirs"));//扩展类加载器路径
        System.out.println(System.getProperty("java.class.path"));//应用类加载器路径
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 测试18_1
public class MyTest18_1{
    public static void main(String[] args){
        MyTest16 loader1=new MyTest16("loader1");
        loader1.setPath("C:\Users\weichengjie\Desktop");
        
        //把MyTest1.class文件放入到根类加载器路径中,则由根类加载器加载MyTest1
        Class<?> clazz= loader1.loadClass("MyTest1");
        
        System.out.println("clazz:"+clazz.hashCode());
        System.out.println("class loader:"+clazz.getClassLoader());
        
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 测试19
/**
    各加载器的路径是可以修改的,修改后会导致运行失败,ClassNotFoundExeception
*/
public class MyTest19{
    public static void main(String[] args){
        AESKeyGenerator aesKeyGenerator=new AESKeyGenerator();
        System.out.println(aesKeyGenerator.getClass().getClassLoader());//输出扩展类加载器
        System.out.println(MyTest19.class.getClassLoader());//输出应用类加载器
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 测试20
 class Person{
    private Person person;
    public setPerson(Object object){
        this.person=(Person)object;
    }
 }
 
 public class MyTest20{
    public static void main(String[] args){
        MyTest16 loader1=new MyTest16("loader1");
        MyTest16 loader2=new MyTest16("loader2");
        
        Class<?> clazz1=load1.loadClass("Person");
        Class<?> clazz2=load1.loadClass("Person");
        //clazz1和clazz均由应用类加载器加载的,第二次不会重新加载,结果为true
        System.out.println(clazz1==clazz2);
        
        Object object1=clazz1.getInstance();
        Object object2=clazz2.getInstance();
        
        Method method=clazz1.getMethod("setPerson",Object.class);
        method.invoke(object1,object2);
        
    }
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 测试21
 public class MyTest21{
    public static void main(String[] args){
        MyTest16 loader1=new MyTest16("loader1");
        MyTest16 loader2=new MyTest16("loader2");
        loader1.setPath("C:\Users\weichengjie\Desktop");
        loader2.setPath("C:\Users\weichengjie\Desktop");
        //删掉classpath下的Person类
        Class<?> clazz1=load1.loadClass("Person");
        Class<?> clazz2=load1.loadClass("Person");
        //clazz1和clazz由loader1和loader2加载,结果为false
        System.out.println(clazz1==clazz2);
        
        Object object1=clazz1.getInstance();
        Object object2=clazz2.getInstance();
        
        Method method=clazz1.getMethod("setPerson",Object.class);
        //此处报错,loader1和loader2所处不用的命名空间
        method.invoke(object1,object2);
    }
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 测试22
 public class MyTest22{
    static{
        System.out.println("MyTest22 init...");
    }
    public static void main(String[] args){
        System.out.println(MyTest22.class.getClassLoader());
        
        System.out.println(MyTest1.class.getClassLoader());
    }
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

扩展类加载器只加载jar包,需要把class文件打成jar

  • 测试23
/*
    在运行期,一个Java类是由该类的完全限定名(binary name)和用于加载该类的定义类加载器所共同决定的。如果同样名字(完全相同限定名)是由两个不同的加载器所加载,那么这些类就是不同的,即便.class文件字节码相同,并且从相同的位置加载亦如此。
    在oracle的hotspot,系统属性sun.boot.class.path如果修改错了,则运行会出错:
    Error occurred during initialization of VM
    java/lang/NoClassDeFoundError: java/lang/Object
*/
 public class MyTest23{
    public static void main(String[] args){
        System.out.println(System.getProperty("sun.boot.class.path"));
        System.out.println(System.getProperty("java.ext.dirs"));
        System.out.println(System.getProperty("java.calss.path"));
        
        System.out.println(ClassLoader.class.getClassLoader);
        System.out.println(Launcher.class.getClassLoader);
        
        //下面的系统属性指定系统类加载器,默认是AppClassLoader
        System.out.println(System.getProperty("java.system.class.loader"));
    }
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 类加载器本身也是类加载器,类加载器又是谁加载的呢??(先有鸡还是现有蛋)
    类加载器是由启动类加载器去加载的,启动类加载器是C++写的,内嵌在JVM中。
  • 内嵌于JVM中的启动类加载器会加载java.lang.ClassLoader以及其他的Java平台类。当JVM启动时,一块特殊的机器码会运行,它会加载扩展类加载器以及系统类加载器,这块特殊的机器码叫做启动类加载器。
  • 启动类加载器并不是java类,其他的加载器都是java类。
  • 启动类加载器是特定于平台的机器指令,它负责开启整个加载过程。

OpenJDK

grepcode.com:源码分析
Launcher类

Class.forName(String name, boolean initialize, ClassLoader loader);

—利用给定的加载器,返回字符串对应的Class对象。当initialize为true时,会对该类进行初始化(该类之前未初始化),默认为true。
Class.forName(“Foo”) 等同于== Class.forName(“Foo”,true,this.getClass.getClassLoader());

public static Class<?> forName(String name, boolean initialize, ClassLoader loader)
    throws ClassNotFoundException{
    if (loader == null) {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            ClassLoader ccl = ClassLoader.getCallerClassLoader();  //获取调用者类的ClassLoader
            if (ccl != null) {
                sm.checkPermission(
                    SecurityConstants.GET_CLASSLOADER_PERMISSION);
            }
        }
    }
    return forName0(name, initialize, loader);  //forName0 是一个native方法
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

上下文类加载器 Thread.getContextClassLoader()

 public class MyTest24{
    public static void main(String[] args){
        System.out.println(Thread.currentThread().getContextClassLoader());
        //
        System.out.println(Thread.class.getClassLoader());
    }
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

当前类加载器(Current ClassLoader)
每个类都会尝试使用自己的类加载器去加载依赖的类。
如果ClassX引用ClassY,那么ClassX的类加载器会尝试加载ClassY。(前提是ClassY尚未被加载)
线程上下文类加载器(Context ClassLoader)
线程上下文加载器 @ jdk1.2
线程Thread类中的 getContextClassLoader() 与 setContextClassLoader(ClassLoader loader)
如果没有通过setContextClassLoader()方法设置,线程将继承父线程的上下文类加载器,java应用运行时的初始线程的上下文类加载器是系统类加载器。该线程中运行的代码可以通过该类加载器加载类和资源。
线程上下文类加载器的作用:
父ClassLoader可以使用当前线程Thread.currentThread().getContextClassLoader()所制定的ClassLoader加载的类,这就改变了父加载器加载的类无法使用子加载器或是其他没有父子关系的ClassLoader加载的类的情况,即改变了双亲委托模型。
· 线程上下文类加载器就是当前线程的Current ClassLoader。
在双亲委托模型下,类加载是由下至上的,即下层的类加载器会委托父加载器进行加载。但是有些接口是Java核心库所提供的的(如JDBC),Java核心库是由启动类记载器去加载的,而这些接口的实现却来自不同的jar包(厂商提供),Java的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足要求。通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载。

  • 测试25
 public class MyTest25 implement Runable{
    private Thread thread;
    public MyTest25(){
        thread =new Thread(this);
        thread.start();
    }
    
    public void run(){
        ClassLoader classLoader=this.thread.getContextLoader();
        this.setContextLoader(classLoader);
        
        System.out.println("Class:"+classLoader.getClass());
        System.out.println("Parent:"+classLoader.getParent().getClass());
    }
    
    public static void main(String[] args){
        new MyTest25();
    }
 }
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
        try {
            Thread.currentThread().setContextClassLoader(targetTccl);
            myMethod();//调用了Thread.currentThead().getContextClassLoader();
        } finally {
            Thread.currentThread().setContextClassLoader(contextClassLoader);
        }
        //如果一个类由类加载器A加载,那么这个类的依赖类也是由相同的类加载器加载(如果没被加载过)
        //ContextClassLoader的作用就是为了破坏java的类加载委托机制。
        //当高层提供了统一的接口让底层实现,同时又要被高层加载(或实例化)底层类时,
        //就必须通过线程上下文类加载器来帮助高层的classloader找到并加载类
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

线程上下文类加载器的一般使用模式:获取-使用-还原
伪代码:
ClassLoader classLoader=Thread.currentThread().getContextLoader();
try{
Thread.currentThread().setContextLoader(targetTccl);
myMethod();
}finally{
Thread.currentThread().setContextLoader(classLoader);
}

—在myMethod中调用Thread.currentThread().getContextLoader()做某些事情
—ContextClassLoader的目的就是为了破坏类加载委托机制
—使用线程上下文类加载器就可以成功的加载到当前的类的加载器无法加载到的类。
—当高层提供了统一的接口让底层去实现,同时又要在高层加载(或实例化)底层的类时,就必须通过上下文类加载器来帮助高层的ClassLoader找到并加载该类。

SPI:Service Provide Interface 服务提供者接口

双亲委托机制在父类加载器加载的类中访问子类加载器加载的类时会出现问题,比如JDBC。JDBC中规定,Driver(数据库驱动)必须向DriverManage注册自己,而DriverManage是BootStrapClassloader加载的,所以DriverManage 中是无法加载到具体的Driver。
此时,服务提供者可以将配置文件放到资源目录的META-INF/services下,高层的接口通过SPI的方式,读取META-INF/services下文件中的类名。
SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。当外部程序装配这个模块的时候,就能通过该jar包META-INF/services里的配置文件找到具体的实现类,并装载实例化,完成模块的注入。基于这一个约定就能很好的找到服务接口的实现类,而不需要在代码里指定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader

  • 测试27—JDBC案例分析
//跟踪代码
 public class MyTest27{
    public static void main(String[] args){
        //Class.forName("com.mysql.jdbc.Driver");//由于ServiceLoader机制的存在,此行可以注释掉不影响
        Connection connection=DriverManager.getConnection(
  "jdbc:mysql://localhost:3306//mydb","user","password");
        
    }
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 当调用DriverManager的静态方法是,会造成类的初始化
    //类初始化时会运行静态代码块
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    private static void loadInitialDrivers() {
       String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                }
                return null;
            }
        });
        println("DriverManager.initialize: jdbc.drivers = " + drivers);
        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

在loadInitialDrivers()方法的代码中,可以发现DriverManager加载Driver的包括两部分:
1.通过System.getProperty(“jdbc.drivers”)进行获取,使用系统类加载器进行加载。但是系统参数"jdbc.drivers"为null,因此不会进行Driver的加载;
2.通过SPI的方式,读取META-INF/services文件夹下的类名,使用当前线程类加载器进行加载。

ServiceLoader

ServiceLoader是由BootStrap Classloader加载的,所以类中引用的其它类也会由BootStrap 尝试去加载。

  public static <S> ServiceLoader<S> load(Class<S> service) {
        //ServiceLoader中会尝试用BootStrap 加载具体的Mysql Driver,
        //但ServiceLoader中是不可见的,这样就无法加载。
        //所以取出当前线程的上下文类加载器即appCL,用于后面加载具体的Mysql Driver
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
    
 public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader)
    {
        return new ServiceLoader<>(service, loader);
    }

private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
    
        //loader 为ServiceLoader的私有常量,在后面加载具体实现类时会用该加载器进行加载。
        // loader 在构造方法内赋了值,即为上文取到的线程上下文类加载器。
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }
    
public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }
//LazyIterator是一个ServiceLoader的私有内部类
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

DriverManager初始化完毕,我们再来看一下mysql提供的Driver类内部的情况

  • ↓ com.mysql.jdbc.Driver是java.sql.Driver的具体实现类,在初始化时会向DriverManager注册自己。就是将自身加入到一个名为registeredDrivers的静态成员CopyOnWriteArrayList中。但是实际中,Driver已经在初始化的过程总使用SPI的方式将其进行了注册。
static 
    { 
        try { 
        //会向DriverManager注册自己,注册时会先完成DriverManager的加载和初始化
        DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) { 
            throw new RuntimeException("Can't register driver!");    
          }
   }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

到此为止,DriverManager类在初始化的过程中,已经使用SPI的方式将mysql提供的Driver加载完毕。
最后再来看看DriverManager调用DriverManager.getConnection( “URL”,“user”,“password”)的内容:
//使用了mysql提供的具体方法获取连接。

public static Connection getConnection(String url,tring user, String password) throws SQLException {
        //将用户名和密码加入到Properties中
        java.util.Properties info = new java.util.Properties();
        if (user != null) {
            info.put("user", user);
        }
        if (password != null) {
            info.put("password", password);
        }
        return (getConnection(url, info, Reflection.getCallerClass()));
    }
private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }
        if(url == null) {
            throw new SQLException("The url cannot be null", "08001");、
        }
        println("DriverManager.getConnection(\"" + url + "\")");
        SQLException reason = null;
        
        //遍历注册到registeredDrivers的Driver类
        for(DriverInfo aDriver : registeredDrivers) {
            //检查Driver类的有效性
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println("    trying " + aDriver.driver.getClass().getName());
                    //调用com.myql.jdbc.Driver.connect(...)方法获取连接
                    Connection con = aDriver.driver.connect(url, info);
                    if (con != null) {
                        println("getConnection returning " + 
aDriver.driver.getClass().getName());
                        return (con);
                    }
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }
            } else {
                println("    skipping: " + aDriver.getClass().getName());
            }
        }
        if (reason != null)    {
            println("getConnection failed: " + reason);
            throw reason;
        }
        println("getConnection: no suitable driver found for "+ url);
        throw new SQLException("No suitable driver found for "+ url, "08001");
    }
}
private static boolean isDriverAllowed(Driver driver, ClassLoader 
classLoader) {
        boolean result = false;
        if(driver != null) {
            Class<?> aClass = null;
            try {
                //传入的classloader为调用getConnection的当前类加载器,从而寻找driver的class对象  
                aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
            } catch (Exception ex) {
                result = false;
            }
            
            //注意,只有同一个类加载器的Class使用==比较时才会相等,此处就是校验用户注册Driver时该Driver所属的类加载器和调用的类加载器是否是同一个
            //driver.getClass()拿到的就是当初执行Class.forName("com.mysql.jdbc.Driver")时的应用AppClassLoader
             result = ( aClass == driver.getClass() ) ? true : false;
        }
        return result;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 测试26
/*
 public class MyTest26{
    public static void main(String[] args){
    
    //一旦加入下面此行,将使用ExtClassLoader去加载Driver.class, ExtClassLoader不会去加载classpath,因此无法找到MySql的相关驱动。
 //Thread.getCurrentThread().setContextClassLoader(MyTest26.class.getClassLoader().parent());    
 
        ServiceLoader服务提供者,加载实现的服务
        ServiceLoader<Driver> loader=ServiceLoader.load(Driver.class);
        Iterator<Driver> iterator=loader.iterator();
        while(iterator.hasNext()){
            Driver driver=iterator.next();
            System.out.println("driver:"+driver.class+
                                ",loader"+driver.class.getClassLoader());
        }
        System.out.println("当前上下文加载器"
                    +Thread.currentThread().getContextClassLoader());
        System.out.println("ServiceLoader的加载器"
                    +ServiceLoader.class.getClassLoader());
    }
 }     
*/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

完结

张龙内存机制

字节码先看完了45

指针碰撞(前提是堆内的空间通过一个指针进行分割,一侧是已经被占用的空间,另一侧是未被占用的空间):当未被占用的空间被放置一个对象之后,指针就发生偏移。
空闲列表(前提是堆内存空间中已被使用或未被使用的空间是交织在一起的):这时,虚拟机就需要一个列表来记录哪些空间是可以使用的,哪些空间是已被使用的,接下来找出可以容纳下新创建对象的且未被使用的空间,在此空间存放对象,同时修改列表上的记录)。

对象在内存中的布局:

  • 对象头
  • 实例数据(类中所声明的各项信息)
  • 对齐填充(可选)

对象引用访问的两种方式:

  • 句柄
  • 直接指针

附1:另一个字节文件解析

这个文件的字节码部分:https://download.csdn.net/download/hancoder/12834607

参考

  • 类加载:https://blog.csdn.net/weixin_38405354/article/details/100042169
  • 字节码:https://blog.csdn.net/weixin_38405354/article/details/100041386
  • 内存机制:https://blog.csdn.net/weixin_38405354/article/details/104712746
  • https://note.youdao.com/ynoteshare1/index.html?id=9ff70d936a330dcd9d42ecb427602975&type=notebook

标签:String,引用,笔记,内存,JVM,张龙,方法,public,加载
来源: https://blog.csdn.net/wangzhipeng47/article/details/117000526

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有