深入理解JVM(学习过程)

2019/04/10 10:10
阅读数 24

深入理解JVM

这,仅是我学习过程中记录的笔记。确定了一个待研究的主题,对这个主题进行全方面的剖析。笔记是用来方便我回顾与学习的,欢迎大家与我进行交流沟通,共同成长。不止是技术。

2020年02月06日22:43:09 - 记录学习过程

终于开始了。在学习这个之前,看了zhanglong老师的 java 8 和springboot

迫不及待了。先开始吧。

写在前边

论方法论

听说之前还有netty 和 kotlin 。学习风格就是,每一门课程之前,前两节课不进入主题,讲方法论。

从他人身上学习优点。加强自己的学习。从人去学习,从事去学习。我们只有亲身经历一件事情,才会产生自己的想法。从事情学习付出的成本会相对的高一点。只有一件事,你失败了,才会发现你存在什么问题。从过程中吸收一点经验,指导着你未来学习前进的方向。从人去学习来说,不是你自己亲身经历的,要学习辨别能力。为什么大家在看书的时候,看书的印象不如你自己操作的印象深刻呢?这些都是值得去思考的。更为高效的方式,还是看别人的故事,揣摩自己的人生。将别人拥有的技能转换成自己的技能,这样才是高效的学习。

学习的过程中,一定要做到两点。

  1. 输入。输入是必要的。
  2. 有输入,必定要有输出。记笔记,写博客,给别人去讲。

能给别人讲明白的前提是自己要明白。自己明白了,就一定能给别人讲明白了吗?你自己明白,给别人讲完之后,你自己也不明白了(值得反思一下,之前学习消耗的时间是否是高效的)。

每个人都值得去自己思考这个问题。为什么当时学习的时候学习的非常透彻,过一段时间跟没学过一样?产生这种问题的根源是什么呢?为什么会产生这样的问题?《后会无期》电影。它是没有标准答案的。之所以看了很多书,看了很多视频,看了很多资料,为什么没有产生持久化的效果?什么是持久化,他要最终落实到一个存储上边。在学习技术的时候,不断的在脑海中去过滤。在当时看的时候会认识的非常时刻。只有输入,没有输出,肯定保存的时间不会太长。什么是输出呢?在学习的过程中,一定要把输入的东西给吐出来,选择一种最适合你的输出方式。就像人呼吸,如果光吸气,不吐气,人早就憋死了。所以输入和输出一定是同时存在的。通过输出,把当时学习到的知识点,以文字,图表,思维导图的方式给呈现出来。做到这一点,你会很难忘掉你学过的重要的内容。做项目的过程也是很重要的输出过程,不经意间,就记住了。刻意练习,针对某一门技术有意为之。刻意输出,当你忘记的时候,回顾一下,会很快的回忆起你之前的学习过程。

即便你在多大的公司,你了解到的技术,用到的技术只占冰山一角。不要把个人绑死在一个上面。当你脱离当前工作时,你还有竞争力才是有用的。不能说项目用什么学什么。不用什么不学什么。那简直就不是程序员。学习技术,百里无一害。就算你现在用不到,你觉得重要的东西一定要去学,去输出。可以做一个实验。半天时间各学一个框架,一个记笔记,一个不记笔记。可以看一下实验结果。学习视频是一种很好的方式,不建议多次去重复的看视频。把视频中的技术要点吸收成你自己的东西才是重要的。视频不能用来检索。

印象笔记,有道云笔记,小小的工具,可能给你生活带来很大的改变。甚至改变你的一生。讲课是一件非常辛苦的事情。平时的工作中就深有体会。

论学习曲线

如果去学习JVM,每一个来学习JVM的人,都渴望成功。每一个Java开发人员的终极目标都是在日常生活中深入理解JVM的运行原理。JVM和平时的应用框架明显的区别,应用框架学习之后,可以直接拿来写项目了,就可以运行起来看到helloworld。然而对于JVM,是一个特别枯燥的事情。涵盖的内容太多了。本次根据java8来学习。必须要笔记。因为一扭头就会忘记。绝对没有速成的,突击的。有节奏,有计划的去学习。关于这门技术,范围太广,从通用的层面来进行学习。

推荐一些学习资料:一边学习视频,一边学习资料;

《深入理解Java虚拟机》 , 《深入Java虚拟机》 , R大

可能会遇到的问题:

  1. 学习了十几节课,感觉什么都没有学到。学到的东西在平时用不上。
  2. 学习完JVM之后,可能会感觉,学的越多,不知道的越多。大功已成。以前没听过的,现在听过了。
  3. 学习JVM,期间学习的文档全是英文的。阅读能力制约了你对JVM的学习。
  4. 耐心。任重而道远。看不到曙光的那种。没有案例让你来做。沉下心来,有的放矢的推进。

开发过程中遇到问题是常态。如果遇到了JVM崩溃,就算你拿到了日志,你也不能定位到问题是什么。对于原理,对于基础的学习,能够增强我们的自信心。

课程大纲

**介绍:**JVM是一个令人望而却步的领域,因为它博大精深,涉及到的内容与知识点非常之多。虽然Java开发者每天都在使用JVM,但对其有所研究并且研究深入的人却少之又少。然而,JVM的重要性却又是不言而喻的。基于JVM的各种动态与静态语言生态圈已经异常繁荣了,对JVM的运行机制有一定的了解不但可以提升我们的竞争力,还可以让我们在面对问题时能够沉着应对,加速问题的解决速度;同时还能够增强我们的自信心,让我们更加游刃有余。

  • JVM介绍
  • HotSpot虚拟机讲解
  • 垃圾收集方式详解
  • 垃圾收集算法详解
  • 垃圾收集器详解
  • 分代垃圾收集机制详解
  • 新生代讲解
  • 老年代讲解
  • G1收集器分析与实例
  • 常见且重要虚拟机参数示例
  • 方法区
  • 线程共享内存区
  • 根搜索算法
  • Serial收集器
  • ParNew收集器
  • 类加载机制详解
  • 类加载的双亲委托机制
  • 字节码文件生成与分析
  • 魔数
  • 常量池与方法表
  • 各种指令详解
  • 锁详解
  • 线程安全
  • 偏向锁、自旋锁与轻量级锁
  • JIT编译器
  • GC日志生成与分析
  • 虚拟机监控工具详解
  • jConsole使用方式详解
  • 何为逃逸与逃逸分析
  • 方法内联
  • 虚拟机内存模型详解
  • = = = = = = = = = = = =

以前都不知道这些工具的存在:

jConsole

Jvusualvm

jmap

image-20200207000103348

类加载

  • 在Java代码中,类型加载、连接与初始化过程都是在程序运行期间完成的。

    (类型,并不代表类产生的对象,而是类本身。类型是在程序运行期间生成出来的,run time)

  • 提供了更大的灵活性,增加了更多的可能性

    (为有创意的开发者提供了很多的功能。)

类加载器深入剖析

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

类的加载、连接与初始化

  • 加载: 查找并加载类的二进制数据
  • 连接
    • -验证:确保被加载的类的正确性
    • -准备:为类的静态变量分配内存,并将其初始化为默认值
    • -解析:把类中的符号引用转换为直接引用
    • 初始化:为类的静态变量赋予正确的初始值
  • 使用
  • 卸载
从代码来理解: 
class Test{
  public static int a = 1;
}
//我们程序中给定的是  public static int a = 1;
//但是在加载过程中的步骤如下:

1. 加载阶段
  编译文件为class文件,然后通过类加载,加载到JVM
  
2. 连接阶段
 第一步(验证):确保Class类文件没问题
  第二步(准备):先初始化为 a=0。(因为你int类型的初始值为0)
 第三步(解析):将引用转换为直接引用

3. 初始化阶段:
  通过此解析阶段,把1赋值为变量a

4. 使用阶段
  我们平时使用的对象,操作,方法调用,等等都是使用阶段
  
5. 卸载阶段
  类在卸载之后,就不能够继续new对象,平时开发很少接触到这个卸载阶段。比如-OSGI技术会使用到卸载

图解:

image-20200208165100079

  • Java程序对类的使用方式可分为两种
    • 主动使用(七种)
      • 创建类的使用
      • 访问某个类或者接口的静态变量,或者对该静态变量赋值
      • 调动类的静态方法(助记符: getstatic putstatic invokestatic )
      • 反射(如:Class.forName("com.test.Test"))
      • 初始化一个类的子类
      • Java虚拟机启动时被表明为启动类的类(Java Test)
      • JDK1.7开始提供的动态语言支持
    • 被动使用
      • 除了以上七种主动使用的情况,其他使用Java类的方式都被看做是对类的被动使用,都不会导致类的初始化。
  • 所有Java虚拟机实现必须在每个类或者接口被Java程序“首次主动使用”时才初始化他们

类的加载

类的加载指的是将类的.class文件中二进制数据读入到内存中,将其放在运行时数据区内的方法去内,然后再内存中创建一个java.lang.Class对象(规范并未说明Class对象谓语哪里,HotSpot虚拟机将其放在了方法去中)用来封装类在方法区内的数据结构

  • 加载.class文件的方式
    • 从本地系统中直接加载
    • 通过网络下载.class文件
    • 从zip,jar等归档文件中加载.class文件
    • 从专有数据库中提取.class文件
    • 将Java源文件动态编译为.class文件(动态代理,web开发jsp转成servlet)
/*
举例说明:
	对于静态字段来说,只有直接定义了该字段的类才会被初始化;
	当一个类在初始化是,要求其父类全部都已经初始化完毕了;
	-XX:+TraceClassLoading,用于追种类的加载信息并打印出来。
	
	所有的参数都是:
	-XX:+<option> , 表示开启option选项
	-XX:+<option> ,表示关闭option选项
	-XX:+<option>=<value>  表示将option选项的值设置为value
	
*/
public class MyTest1 {
    public static void main (String[] args){
        System.out.println(MyChild1.str2);
    }
}

class MyParent1{
    public static String str = "hello world";
    static {
        System.out.println("MyParent1 static block");
    }
}

class MyChild1 extends MyParent1{
    public static String str2 = "welcome";
    static{
        System.out.println("MyChild1 static block");
    }
}

输出结果:
> Task :MyTest1.main()
MyParent1 static block
MyChild1 static block
welcome

查看类的加载信息,并打印出来。

jvm 参数介绍: -XX:+<option> , 表示开启option选项 -XX:+<option> ,表示关闭option选项 -XX:+<option>=<value> 表示将option选项的值设置为value

image-20200208173021317

类加载基础概念

常量池的概念

/*
  常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中,本质上,调用类并没有直接用用到定义常量的类,因此并不会触发定义常量的类的初始化。
  注意:这里指的是将常量存放到了MyTest2的常量池中,之后MyTest2与MyParent2就没有任何关系了。
  甚至,我们可以将MyParent2的Class文件删除

*/
public class MyTest2{
  public static void main(String[] args){
    System.out.println(MyParent2.str);
  }
}

class MyParent2{
   public static final String str = "hello world";
   public static final short s = 127;
   public static final int a = 3;
   public static final int m = 6;
  
   static {
    System.out.println("Myparent2 static block");// 这一行能输出吗?不会
  }
}

反编译

javap -c com.erwa.jvm.class.mytest2

反编译之后会有助记符。

助记符

  • ldc表示将int、float或是String类型的常量值从常量池中推送至栈顶。

  • bipush表示将单字节(-128~127)的常量值推送至栈顶。

  • sipush表示将短整型(-32767~32768)的常量值推送至栈顶。

  • inconst_1表示将int类型1推送至栈顶 (inconst_m1 ~inconst_5)。

  • anewarray 表示创建一个引用类型(如类,接口,数组)的数组,并将其引用值推至栈顶。

  • newarray 表示创建一个指定的原始类型(如int,float,char等)的数组,并将其引用值推至栈顶。

类的初始化规则

/*
* 当一个常量的值并非编译期间可以确定的,那么其值就不会被放到调用类的常量池中,
	这是在程序运行时,会导致主动使用这个常量所在的类,显然就会导致这个类被初始化。
*/
public class MyTest3{
  public static void main(String[] args){
    System.out.println(MyParent3.str);
  }
}

class MyParent3{
   public static final String str = UUID.randomUUID().toString();
   
   static {
    System.out.println("Myparent3 static block");  // 这一行能输出吗?会
  }
}

为什么第二个例子不会输出,第三个例子就输出了呢? 
因为第三个例子的值,是只有当运行期才会被确定的值。而第二个例子的值,是编译时就能被确定的值。
public class MyTest4{
  public static void main(String[] args){
   MyParent4 myParent4 = new MyParent4(); 
  }
}

class MyParent4{
   static {
    System.out.println("Myparent4 static block");  // 这一行能输出吗?会
  }
}
因为MyParent4 myParent4 = new MyParent4();  属于主动使用的第一条,类的使用。
/**
	对于数组实例来说,其类型是由JVM在运行期间动态生成的,表示为 [Lcom.erwa.MyTest4
	这种形式,动态生成的类型,其父类型就是Object
	
*/
public class MyTest4{
  public static void main(String[] args){
   MyParent4[]  myParent4s = new MyParent4[1]; 
        System.out.println(myParent4s.getClass());
        System.out.println(myParent4s.getClass().getSuperclass());
  }
}

class MyParent4{
   static {
    System.out.println("Myparent4 static block");  // 这一行能输出吗?不会
  }
}

因为 MyParent4[]  myParent4s = new MyParent4[1];  并不属于主动使用的方式。
  
> Task :MyTest4.main()输出结果为:
class [Lcom.erwa.jvm.MyParent4;
class java.lang.Object
 				int[] ints = new int[1];
        System.out.println(ints.getClass());

        boolean[] booleans = new boolean[1];
        System.out.println(booleans.getClass());

        short[] shorts = new short[1];
        System.out.println(shorts.getClass());

        double[] doubles = new double[1];
        System.out.println(doubles.getClass());

class [I
class [Z
class [S
class [D

接口的初始化规则

接口本身的成员变量 都是 public static final 的

/**
 	当一个接口在初始化时,并不要求其父接口都完成了初始化。
 	只有在真正使用到父接口的时候(如 引用接口中所定义的常量时),才会初始化。
*/
public class MyTest5{
  public static void main(String[] args){
    System.out.println(myParent5.b);
  }
}

interface MyParent5{
  public static int a = 5;
}

interface MyChild5 extends MyParent5{
  public static int b = 6;
}

/**
class MyChild5 implements MyParent5{
  public static int b = 6;
}
**/

类的初始化顺序

/*
	这个例子很好的阐述了类初始化的顺序问题。
*/
public class MyTest6{
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        System.out.println("counter1:"+Singleton.counter1);
        System.out.println("counter2:"+Singleton.counter2);
    }
}

class Singleton{
    public static int counter1 = 1;
  
    private static Singleton singleton= new Singleton();
    private Singleton(){
        counter1++;
        counter2++;   // 准备阶段的重要意义
      	System.out.println(counter1);
        System.out.println(counter2);
    }
  
    public static int counter2 = 0;
  
    public static Singleton getInstance(){
        return singleton;
    }
}

> Task :MyTest6.main()
2
1  
counter1:2
counter2:0

类加载的加载顺序

image-20200208221610181

<img src="https://tva1.sinaimg.cn/large/0082zybply1gbpcrfww60j313x0u0qkh.jpg" alt="image-20200208221655905" />

类加载器

类加载的概念回顾

  • 类的加载

    • 类的加载的最终产品是位于内存中的Class对象
    • Class对象封装了类在方法去内的数据结构并且向Java程序员提供了访问方法区内的数据结构的接口。
  • 有两种类型的类加载器

    • Java虚拟机自带的加载器
      • 根类加载器(BootStrap)(BootClassLoader)
      • 扩展类加载器(Extension)(ExtClassLoader)
      • 系统(应用)类加载器(System)(AppClassLoader)
    • 用户自定义的类加载器
      • Java.long.ClassLoader的子类
      • 用户可以定制类的加载方式
  • 类加载器并不需要等到某个类被“首次使用”时再加载它。

    • jvm规范允许类加载器在预料某个类将要被使用时就预先加载他,如果在预先加载的过程中遇到了.class文件缺失或者存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)
    • 如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误

image-20200212204737909

image-20200212171808157

image-20200208223421181

image-20200208223434835

image-20200208223505045

image-20200208223542725

依次执行初始化语句:按照CLass类文件中的顺序加载静态方法和静态代码块。

image-20200208223642407

image-20200208223705030

![image-20200212205713902](/Users/erwa/Library/Application Support/typora-user-images/image-20200212205713902.png)

image-20200208223738483

重点介绍一下上图的概念。举例说明。

  • 在初始化一个接口时,并不会先初始化它的父接口。
// 当一个类初始化时,它实现的接口是不会被初始化的。
public class MyTest5{
  public static void main(String[] args){
    System.out.println(MyChild5.b);
  }
}

interface MyParent5{
  public static Thread thread = new Thread(){
    // 每次被实例化的时候都会执行下方的代码块。 如果是 static{} 的时候,只会被加载一次。
    {
      System.out.println("myParent5 invoked");
    }    
  }
}

class MyChild5 implements MyParent5{
  public static int b = 6;
}
// 在初始化一个接口时,并不会先初始化它的父接口
public class MyTest5{
  public static void main(String[] args){
    System.out.println(MyParent5_1.thread);
  }
}

interface MyParent5{
  public static Thread thread = new Thread(){
    {
      System.out.println("myParent5 invoked");
    }    
  }
}

interface MyParent5_1 extends MyGrandpa5_1{
  public static Thread thread = new Thread(){
    {
      System.out.println("myParent5 invoked");
    }    
  }
}

image-20200208223917870

image-20200208223931946

调用CLassLoader类的loadCLass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。

类加载器深度剖析

image-20200208223947354

image-20200208224052752

image-20200208224219186

下图:不是继承关系,是包含关系。

graph TD

根类加载器 --> 扩展类加载器

扩展类加载器 --> 系统类加载器

系统类加载器 --> 用户自定义类加载器

image-20200212210718684

线程上下文类加载器 作用:打破双亲委托机制。加载SPI提供的类。

image-20200212172526162

类加载器的双亲委托机制

一层一层的 让父类去加载,最顶层父类不能加载往下数,依次类推。

image-20200208231639491

image-20200212211132533

image-20200212211208653

image-20200208232907118

image-20200208232951439

能够成功加载自定义类加载器的系统类加载器 被称为 定义类加载器

所有能够返回Class对象引用的类加载器都被称为初始类加载器

image-20200212211347665

image-20200212211400339

image-20200217073556952

image-20200212211651815

public class MyTest7 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> c = Class.forName("java.lang.String");
        System.out.println(c.getClassLoader()); // 返回针对于这个类的类加载器

        Class<?> c1 = Class.forName("com.erwa.jvm.C");
        System.out.println(c1.getClassLoader());
    }
}

class C{

}

> 输出结果为:
> Task :MyTest7.main()
null 
sun.misc.Launcher$AppClassLoader@659e0bfd

getClassLoader()的 Javadoc: 
java.lang.Class<T> public ClassLoader getClassLoader()
Returns the class loader for the class. Some implementations may use null to represent the bootstrap class loader. This method will return null in such implementations if this class was loaded by the bootstrap class loader.
  
 // 返回类的类加载器。一些实现可能使用null来表示引导类加载器。如果这个类是由引导类加载器加载的,那么这个方法在这样的实现中将返回null。
  
If a security manager is present, and the caller s class loader is not null and the caller s class loader is not the same as or an ancestor of the class loader for the class whose class loader is requested, then this method calls the security manager s checkPermission method with a RuntimePermission("getClassLoader") permission to ensure its ok to access the class loader for the class.
If this object represents a primitive type or void, null is returned.
//如果此对象表示基本类型或void,则返回null。

Returns:
the class loader that loaded the class or interface represented by this object.
Throws:
SecurityException – if a security manager exists and its checkPermission method denies access to the class loader for the class.

对于final的理解深入

class FinalTest{
	public static final int x=3;  // final的话 就是编译期间的常量
  static {
    System.out.println("FinalTest stataic block");  // 会输出吗?不会
  }
}

public class MyTest8 {
    public static void main(String[] args){
        System.out.println(FinalTest.x); 
    }
}

类的初始化顺序举例练习

class Parent {
    static int a = 3;
    static {
        System.out.println("parent stataic block ");
    }
}

class Child extends Parent{
    static int b = 4;
    static {
        System.out.println("child static block ");
    }
}

public class MyTest9 {
    static {
        System.out.println("Mytest9 static block ");
    }
    public static void main(String[] args){
        System.out.println(Child.b);
    }
}

> 输出结果:
> Task :MyTest9.main()
Mytest9 static block 
parent stataic block 
child static block 
4
  
类的加载顺序: 
1. MyTest9
2. Parent 
3. Child 
// 这是一个好例子。  明白了。 类初始化的顺序    的练习。

// 每一个类,在初始化的时候,只会被初始化一次。
class Parent2 {
    static int a = 3;
    static {
        System.out.println("parent2 stataic block ");
    }
}

class Child2 extends Parent2{
    static int b = 4;
    static {
        System.out.println("child2 static block ");
    }
}

public class MyTest10 {
    static {
        System.out.println("MyTest10 static block ");
    }
    public static void main(String[] args){
      Parent2 parent2;  // 不属于主动使用
      System.out.println("- - -- - - ");
        parent2 = new Parent2();  // 主动使用, 初始化 Parent2 
      System.out.println("- - -- - - ");
        System.out.println(parent2.a);
      System.out.println("- - -- - - ");
        System.out.println(Child2.b);
    }
}

> 输出结果:
> Task :MyTest10.main()
MyTest10 static block 
- - -- - - 
parent2 stataic block 
- - -- - - 
3
- - -- - - 
child2 static block 
4 
// 使用子类,访问父类中的方法。 等于是主动使用父类,而不是主动使用子类。
class Parent3 {
    static int a = 3;
    static {
        System.out.println("parent3 stataic block ");
    }
  	static void doSomething(){
        System.out.println("do something ")
    }
}

class Child3 extends Parent3{
    static int b = 4;
    static {
        System.out.println("child3 static block ");
    }
}

public class MyTest11 {
  
    public static void main(String[] args){
      System.out.println(Child3.a);  // a 是定义在父类中的, 等于是 主动使用父类
      Child3.doSomething();
    }
}

> 预计结果:
> Task :MyTest11.main()
parent3 static block 
do something 
child3 static block
3
do something 

> 实际结果:  
> Task :MyTest11.main()
parent3 stataic block 
3
do something 
  
错误原因:  a 是定义在父类中的, 等于是 主动使用父类;并没有主动使用Child
//  调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化
//  反射,是对类的主动使用,会初始化类
class CL{
    static {
        System.out.println("Class CL ");
    }
}

public class MyTest12 {

    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader loader = ClassLoader.getSystemClassLoader(); 
        Class<?> clazz = loader.loadClass("com.erwa.jvm.CL");// 不会初始化CL类
        System.out.println(clazz);
        System.out.println("- - - - - -");
        clazz = Class.forName("com.erwa.jvm.CL"); // 反射会初始化CL类
        System.out.println(clazz);
    }
}

> 输出结果:
> Task :MyTest12.main()
class com.erwa.jvm.CL
- - - - - -
Class CL 
class com.erwa.jvm.CL

2020年02月09日09:41:01 继续吧。--- 今天是周末。原来说的要加班。不加了。学习

类加载器的层次关系

public class MyTest13{
  psvm{
    ClassLoader classLoader = ClassLoader.getSystemClassLoader();
    sout(classLoader);
    while (null != classLoader){
      classLoader = classLoader.getParent();
      sout(classLoader);
    }
  }
}

> 输出结果:
> Task :MyTest13.main()
sun.misc.Launcher$APPClassLoader 	应用类加载器
sun.misc.Launcher$ExtClassLoader  扩展类加载器
null  根类加载器
//如何通过给定的字节码路径,打印出类的信息
public class MyTest14{
  psvm{   //上下文类加载器是由当前的线程提供的
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    String resourceName = "com/erwa/jvm/MyTest13.class";
    
    Enumeration<URL> urls = classLoader.getResources(resourceName);
    while(urls.hasMoreElements()){
      URL url = urls.nextElement();
      sout(url); // 输出结果为 资源在本机上的完整路径。
    }
  }
}

获得ClassLoader的途径

  • 获得当前类的CLassLoader

    clazz.getClassLoader();

  • 获得当前线程上下文的ClassLoader

    Thread.currnetThread().getContextClassLoader();

  • 获得系统的ClassLoader

    CLassLoader.getSystemClassLoader();

  • 获得调用者的ClassLoader

    DriberManager.getCallerClassLoader();

sun.misc.Launcher$APPClassLoader 	应用类加载器
sun.misc.Launcher$ExtClassLoader  扩展类加载器
null  根类加载器

  比如: Class<?> clazz = String.class; 
       sout(clazz.getClassLoader()); //打印结果为 null . 因为是系统的类。
			 Class<?> clazz = MyTest14.class; 
       sout(clazz.getClassLoader()); //打印结果为 APPClassLoader . 因为自定义的类。

ClassLoader类源码剖析

ClassLoader的Javadoc:
java.lang public abstract class ClassLoader  extends  Object
   
A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a "class file" of that name from a file system.
/* 
一个类加载器是一个对象,用于加载类。 ClassLoader是一个抽象类,如果我们给了二进制的名字,ClassLoader应该尝试生成一些数据(构成了这个类定义的数据)。 一种典型的策略是将二进制名字转换成文件名字,然后从系统中读取这个文件。
*/
Every Class object contains a reference to the ClassLoader that defined it.
// 每一个Class对象都会包含 定义这个类的类加载器对象
Class objects for array classes are not created by class loaders, but are created automatically as required by the Java runtime. The class loader for an array class, as returned by Class.getClassLoader() is the same as the class loader for its element type; if the element type is a primitive type, then the array class has no class loader.
Applications implement subclasses of ClassLoader in order to extend the manner in which the Java virtual machine dynamically loads classes.
/*  数组类 比较特殊:
对于数组内的Class对象,并不是由类加载器创建的,而是由Java运行时根据需要自动创建的(其他类是由类加载器创建的)(数组类型不会导致类的初始化)。对于一个数组类的ClassLoader来说,返回结果是数组当中元素的结果是一样的。如果数组中的元素是原生类型的,那么此时数组类是没有类加载器的。
应用程序实现类装入器的子类,以扩展Java虚拟机动态加载类的方式。
*/
  
- - - - -- - 数组类的类加载器举例: - - - -- - - 
public class MyTest15{
  psvm{
    String[] strings = new String[2];
    sout(strings.getClass().getClassLoader()); // 输出结果为null ; 因为是String 的类加载器(此时的null代表根类加载器的类型);
    MyTest15[] mytest15 = new MyTest15[2];
    sout(mytest15.getClass().getClassLoader());// 结果为 AppClassLoader ; 因为是自自定义的类
    Integer[] integet = new Integer[2];
    sout(integet.getClass().getClassLoader()); // 输出结果为null;因为是原生类型。(此时的null不代表根类加载器的类型,就是null)
  }
}

Class loaders may typically be used by security managers to indicate security domains.
// 类加载器 会伴随着安全管理器来确保安全的。
The ClassLoader class uses a delegation model to search for classes and resources. Each instance of ClassLoader has an associated parent class loader. When requested to find a class or resource, a ClassLoader instance will delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself. The virtual machines built-in class loader, called the "bootstrap class loader", does not itself have a parent but may serve as the parent of a ClassLoader instance.
/*
ClassLoader使用双亲委托模型来寻找资源。ClassLoader的每一个实例都有一个关联的父的ClassLoader。当要求ClassLoader去需要资源时,ClassLoader在自己寻找资源之前,会将资源寻找委托给它的父ClassLoader。层层往上递进。虚拟机内荐的类加载器叫做启动类加载器,它本身是没有双亲的,但是它可以当做是其他类加载器的双亲。
*/
Class loaders that support concurrent loading of classes are known as parallel capable class loaders and are required to register themselves at their class initialization time by invoking the ClassLoader.registerAsParallelCapable method. Note that the ClassLoader class is registered as parallel capable by default. However, its subclasses still need to register themselves if they are parallel capable. In environments in which the delegation model is not strictly hierarchical, class loaders need to be parallel capable, otherwise class loading can lead to deadlocks because the loader lock is held for the duration of the class loading process (see loadClass methods).
/*
并行的ClassLoader注册。
支持类的并发加载的类加载器称为支持并行的类加载器,需要通过调用类加载器在类初始化时注册它们自己。registerAsParallelCapable方法。注意,默认情况下ClassLoader类被注册为支持并行的。但是,它的子类仍然需要注册它们自己,如果它们是并行的。在委托模型没有严格层次结构的环境中,类装入器需要具有并行能力,否则类装入可能会导致死锁,因为装入器锁在类装入过程期间一直持有(请参阅loadClass方法)。
*/  
Normally, the Java virtual machine loads classes from the local file system in a platform-dependent manner. For example, on UNIX systems, the virtual machine loads classes from the directory defined by the CLASSPATH environment variable.
 /*
通常情况下,jvm会从本地文件系统中去加载类。  例如在unix 上,jvm会从CLasspath环境变量定义的目录中去加载。
*/ 
However, some classes may not originate from a file; they may originate from other sources, such as the network, or they could be constructed by an application. The method defineClass converts an array of bytes into an instance of class Class. Instances of this newly defined class can be created using Class.newInstance.
/*
然而,,一些类并不是来源于文件。  如通过网络,动态代理等。 
这种情况下,定义类会将字节数组转换成class的一个实例。这个新定义的实例可能通过Class.newInstance来创建。
*/  
The methods and constructors of objects created by a class loader may reference other classes. To determine the class(es) referred to, the Java virtual machine invokes the loadClass method of the class loader that originally created the class.
/*
类加载器创建的对象的方法和构造函数可以引用其他类。为了确定引用的类,Java虚拟机调用最初创建类的类加载器的loadClass方法。
*/  
For example, an application could create a network class loader to download class files from a server. Sample code might look like:
// 例如,应用程序可以创建一个网络类装入器来从服务器下载类文件。示例代码可能如下:
     ClassLoader loader = new NetworkClassLoader(host, port);
     Object main = loader.loadClass("Main", true).newInstance();
          . . .
   
The network class loader subclass must define the methods findClass and loadClassData to load a class from the network. Once it has downloaded the bytes that make up the class, it should use the method defineClass to create a class instance. A sample implementation is:
//网络类加载器子类必须定义findClass和loadClassData方法,以便从网络加载类。一旦下载了构成类的字节,就应该使用defineClass方法来创建类实例。一个示例实现是:
       class NetworkClassLoader extends ClassLoader {
           String host;
           int port;
  
           public Class findClass(String name) {
               byte[] b = loadClassData(name);
               return defineClass(name, b, 0, b.length);
           }
  
           private byte[] loadClassData(String name) {
               // load the class data from the connection
                . . .
           }
       }
   
Binary names  // `二进制的名字` 介绍: 
Any class name provided as a String parameter to methods in ClassLoader must be a binary name as defined by The Java™ Language Specification.
Examples of valid class names include:
     "java.lang.String"   // 字符串的类 
     "javax.swing.JSpinner$DefaultEditor"  // DefaultEditor是JSpinner的一个内部类
     "java.security.KeyStore$Builder$FileBuilder$1" // FileBuilder里的第一个匿名内部类
     "java.net.URLClassLoader$3$1" //  URLClassLoader里的第三个匿名内部类中的第一个匿名内部类

自定义一个类加载器

image-20200212212108254

package com.erwa.jvm;

import java.io.*;

public class MyTest16 extends ClassLoader {
    private String classLoaderName ;
    private final String fileExtension =".class";

    //构造方法
    public MyTest16(String classLoaderName){
        super();// 将系统类加载器当做该类加载器的父加载器
        this.classLoaderName = classLoaderName;
    }

    public MyTest16(ClassLoader parent,String classLoaderName){
        super(parent);// 显示指定该类加载器的父加载器
        this.classLoaderName = classLoaderName;
    }

    @Override
    public String toString() {
        return "MyTest16{" +
                "classLoaderName='" + classLoaderName + '\'' +
                ", fileExtension='" + fileExtension + '\'' +
                '}';
    }

    @Override
    protected Class<?> findClass(String classname) throws ClassNotFoundException {
        // 重写findClass
        byte[] data = this.loadClassDate(classname);
        return this.defineClass(classname, data, 0, data.length);
    }

    private byte[] loadClassDate(String name){
        InputStream is = null;
        byte[] data = null;
        ByteArrayOutputStream baos = null;

        try{
            this.classLoaderName = this.classLoaderName.replace(".","/");

            is = new FileInputStream(new File(name + this.fileExtension));
            baos = new ByteArrayOutputStream();

            int ch = 0;
            while (-1 != (ch = is.read())) {
                baos.write(ch);
            }

            data = baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                assert is != null;
                is.close();
                assert baos != null;
                baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return data;
    }

    public static void test(ClassLoader classLoader) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        Class<?> clazz = classLoader.loadClass("com.erwa.jvm.MyTest1");
        //创建类的实例
        Object object = clazz.newInstance();
        System.out.println(object);
    }

    public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
        MyTest16 loader1 = new MyTest16("loader1");
        test(loader1);
    }
}

> 输出结果: 
> Task :MyTest16.main()
com.erwa.jvm.MyTest1@6d06d69c
  
  定义的第一个自定义的类加载器结束。

类加载器重要方法详解

启动类加载器

扩展类加载器

应用类加载器

自定义类加载器

拒绝低效率的学习

通过defineClass,将byte转换成Class的对象。
protected final Class<?> defineClass(String name, byte[] b, int off, int len){}

image-20200209150126348

image-20200209142333383案例结果如下:

image-20200209142624312

类加载器的命名空间

image-20200212211744291

  • 每个类加载器都有 自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成
    • 子加载器所加载的类能够访问父加载器所加载的类
    • 父加载器加载的类无法访问子加载器所加载的类
  • 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类
  • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类

image-20200209143258500

输出结果一样,说明loader1 和loader2成为了父子关系,在同一命名空间中。

image-20200209144439558

image-20200209144532735

不同类加载器的命名空间关系

image-20200212173018051

类的卸载

image-20200209144847859

image-20200209144920023

image-20200209145030174

类的卸载举例:

image-20200209145335522

image-20200209145532976

也可以通过一个小工具,来查看 jvisualvm

image-20200209145833650

自定义类加载器在复杂类加载情况下的运行分析

public class MyCat{
    public MyCat(){
        System.out.println("MyCat is loaded by : "+ this.getClass().getClassLoader());
    }
}

public class MySample{
    public MySample(){
        System.out.println("MySample is loaded by : "+ this.getClass().getClassLoader());
        new MyCat();
    }
}

public class MyTest17{
    public static void main(String[] args) throws Exception{
        MyTest16 loader1 = new MyTest16("loader1");
        Class<?> clazz = loader1.loadClass("com.erwa.jvm.MySample");
        System.out.println(clazz.hashCode());
      
      // 如果注释掉该行,那么并不会实例化MySample对象,即MySample构造方法不会被调用
      //因此不会实例化MyCat对象,即没有对MyCat进行主动使用。这里就不会加载MyCat的CLass
        Object object = clazz.newInstance();
    }
}

> 输出结果
> Task :MyTest17.main()
2018699554
MySample is loaded by : sun.misc.Launcher$AppClassLoader@659e0bfd
MyCat is loaded by : sun.misc.Launcher$AppClassLoader@659e0bfd
  
这个例子有多种情况,结果是不同的。
ClassPath 中删除 MySample
ClassPath 中删除 MyCat
ClassPath 中删除 MyCat  MySample  

》 总体来说,就是命名空间的概念。
- 子加载器所加载的类能够访问父加载器所加载的类
- 父加载器加载的类无法访问子加载器所加载的类

2020年02月10日21:36:38

对于类加载器,具体是从哪个地方加载的?

系统类加载器:sun.boot.class.path (加载系统的包,包含jdk核心库里的类)

扩展类加载器:java.ext.dirs(加载扩展jar包中的类)

应用类加载器:java.class.path(加载你编写的类,编译后的类)

​ 可以打印出各个加载器加载的目录: System.getProperty("上述的地址");

public class MyPerson{
  private MyPerson myperson;
  public void setMyPerson(Object object){
    this.myperson = (MyPerson) object
  }
}

public class MyTest20{
  psvm{
    MyTest16 loader1 = new MyTest16("loader1");
    MyTest16 loader2 = new MyTest16("loader2");
    
    Class<?> clazz1 = loader1.loadClass("com.erwa.jvm.MyPerson");//系统类加载器加载MyPerson的类
    Class<?> clazz2 = loader2.loadClass("com.erwa.jvm.MyPerson");//系统类加载器已经加载过此类。就不会再次加载,直接返回之前加载过的对象。 所以下方的结果为true.

    sout(clazz1 == clazz2);//结果为true 
    //原因是: loader1的父亲为系统类加载,系统类加载器可以加载MyPerson的类。 到了loader2时,加载时,系统类加载器已经加载过此类。就不会再次加载,直接返回之前加载过的对象。 所以结果为true.
    //并不是因为 loader1 和loader2 是由MyTest16类生成的同样的实例。
    
    Object object1 = clazz1.newInstance();
    Object object2 = clazz2.newInstance();

    //发射,取到MyPerson中的set方法
    Method methos = clazz1.getMethod("setMyPerson",Object.class);
    method.invoke(object1,object2);// 这个地方可以正常运行
  }
}
// 命名空间深度剖析
//命名空间,由该加载器及所有父加载器加载的类组成
public class MyTest20{
  psvm{
    MyTest16 loader1 = new MyTest16("loader1");
    MyTest16 loader2 = new MyTest16("loader2");
    
    loader1.setPath("user/erwa/Desktop/");
    loader2.setPath("user/erwa/Desktop/");
    //然后把编译生成的class文件剪贴到桌面上。此时,执行此类的输出结果是什么?
    
   Class<?> clazz1 = loader1.loadClass("com.erwa.jvm.MyPerson");//自定义加载器1加载MyPerson的类
   Class<?> clazz2 = loader2.loadClass("com.erwa.jvm.MyPerson");//自定义加载器2加载MyPerson的类
	// clazz1 和clazz2加载的文件不在同一个命名空间中。
    
    sout(clazz1 == clazz2);//结果为false
    //原因是: loader1的父亲为系统类加载,系统类加载器不能加载MyPerson的类,由自定义加载器加载。 到了loader2时,加载时同loader1,由自定义加载器的实例再次加载,直接返回之前加载过的对象。 所以结果为true.
    //并不是因为 loader1 和loader2 是由MyTest16类生成的同样的实例。
    
    Object object1 = clazz1.newInstance();
    Object object2 = clazz2.newInstance();

    //发射,取到MyPerson中的set方法
    Method methos = clazz1.getMethod("setMyPerson",Object.class);
    method.invoke(object1,object2);  //并且这个地方抛异常
  }
}

image-20200210222934649

image-20200210222615920

类加载器命名空间总结

类加载器的双亲委托模型的好处:

  1. 可以确保Java核心库的类型安全:(jdk自带的rt.jar等)所有的Java应用都至少会引用Java.lang.Object类,也就是说,在运行期,这个类会被加载器到Java虚拟机中;如果这个加载过程是由Java应用自己提供的类加载器完成的,那么就很可能在jvm中存在多个Java.lang.Object类的版本,而且这些类之间还是不兼容的,相互不可见的(正是命名空间在发挥着作用)。借助于双亲委托机制,Java核心类库中的类的加载工作都是由启动类(根类)加载器来统一完成。从而确保了Java应用使用的都是同一个版本的Java核心类库,他们之间是相互兼容的。
  2. 可以确保Java核心类库所提供的类,不会被自定义的类所替代。
  3. 不同的类加载器可以被相同名称(binary name)的类创建额外的命名空间;相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器加载他们即可。不同类加载器所加载的类之间是不兼容的,这就相当于在Java虚拟机中创建了一个又一个相互隔离的Java类空间,这类技术在多框架中都得到了实际应用。

扩展类加载器类特性介绍

public class MyTest22{
  static{
    sout("MyTest22 initializer");
  }
  
  psvm{
    sout(MyTest22.class.getClassLoader());
    sout(MyTest1.class.getClassLoader());
  }
}
> 1.此时的输出结果是什么?
  AppClassLoader
  AppClassLoader

> 2.修改本地的扩展类加载器的加载路径。
  1. 进入到当前编译的 out路径
  2. java -Djava.ext.dirs=./ com.erwa.jvm.MyTest22
  
> 3.此时的输出结果是什么?
  AppClassLoader
  AppClassLoader
 
4.还是一样的,为什么不是扩赞类加载器加载的呢?因为扩展类加载的类还不能是Class文件的形式,要以jar包的形式加载。
  
> 5.将当前的com打成jar包 
  jar cvf com.erwa.jvm
  
> 6.此时的输出结果是什么?
  AppClassLoader
  ExtClassLoader

> 7.  java -Djava.ext.dirs=/ com.erwa.jvm.MyTest22
 
> 8.此时的输出结果是什么?
  AppClassLoader
  AppClassLoader

关于命名空间的补充:

在运行期,一个Java类是由该类的完全限定名(binary name,二进制名)和用于加载该类的定义类加载器共同确定的。

如果同样的名字(即相同的完全限定名)的类是由两个不同的加载器所加载,那么这些类就是不同的。即便.class文件的字节码完全一样,并且从相同的位置加载亦如此。

在Oracle的Hotspot实现中,系统属性sun.boot.class.path如果修改错了,则运行时会出错,信息下图:

image-20200211055535115

类加载器的特例,数组。数组不是由类加载器的加载的,是由JVM加载的。

先有鸡还是先有蛋?

类加载器本身也是一个Java类,他们是怎么加载到JVM当中的呢?

他们是由启动类加载器加载的。BootStrapClassLoader加载的。他不是由Java代码写的,由C++代码编写的。启动的时候回自动加载出启动类加载器的实例。

  1. 内建与JVM中的启动类加载器会加载java.lang.ClassLoader以及其他的Java类加载器。

  2. 当JVM启动时,一块特殊的机器码会执行,它会加载类加载器与系统类加载器,这块特殊的机器码叫做启动类加载器(BootStarp)

  3. 启动类加载器并不是Java类,而其他的加载器则都是Java类

  4. 启动类加载器是特定于平台的机器指令,它负责开启整个加载过程

  5. 所有类加载器(除了启动类加载器)都被时限为Java类。不过,中国要有一个组件来加载第一个Java类加载器,从而让整个过程能够顺利的进行下去,加载第一个纯Java类加载器就是启动类加载器的职责。

  6. 启动类加载器还会负责加载供JRE正常运行所需要的基本组件,这包括java.util与java.lang包中的内容。

sout(ClassLoader.class.getClassLoader());
> 输出结果:
  null    // 因为ClassLoader是由启动类加载器的加载的。
  
ExtClassLoader 和 APPCLassLoader 位于 Launcher.class中的静态内部类。

sout(Launcher.class.getClassLoader());
> 输出结果:
  null    // 因为扩展类加载器与应用类加载器也是由启动类加载器的加载的。

自定义系统类加载器

image-20200211061413889

image-20200211061508983

image-20200211061353721

在自定义类加载器中加一个父类的构造方法,就可以正常的运行了。

用Java命令行的话,和idea里边执行的结果有时候类加载器的是不一样的。

因为idea也重新定义了类加载器的路径。

getSystemClassLoader();
java.lang.ClassLoader public static ClassLoader getSystemClassLoader()
Returns the system class loader for delegation. This is the default delegation parent for new ClassLoader instances, and is typically the class loader used to start the application.
 // 通常 系统类加载器用来加载我们的应用。
This method is first invoked early in the runtimes startup sequence, at which point it creates the system class loader and sets it as the context class loader of the invoking Thread.
// 这个方法会创建系统类加载器,并将AppClassLoader设置为上下文类加载器
The default system class loader is an implementation-dependent instance of this class.
  
If the system property "java.system.class.loader" is defined when this method is first invoked then the value of that property is taken to be the name of a class that will be returned as the system class loader. The class is loaded using the default system class loader and must define a public constructor that takes a single parameter of type ClassLoader which is used as the delegation parent. An instance is then created using this constructor with the default system class loader as the parameter. The resulting class loader is defined to be the system class loader.
  // 如果我们自定义了 java.system.class.loader。 那么就会把这个值当做系统类加载器。这个类是被AppClassLoader加载器加载器的。这个自定义的系统类加载器必须要有一个 带有父加载器参数的构造方法。
If a security manager is present, and the invoker's class loader is not null and the invoker's class loader is not the same as or an ancestor of the system class loader, then this method invokes the security managers checkPermission method with a RuntimePermission("getClassLoader") permission to verify access to the system class loader. If not, a SecurityException will be thrown.

   @CallerSensitive
    public static ClassLoader getSystemClassLoader() {
        initSystemClassLoader();
        if (scl == null) {
            return null;
        }
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkClassLoaderPermission(scl, Reflection.getCallerClass());
        }
        return scl;
    }

    private static synchronized void initSystemClassLoader() {
        if (!sclSet) {
            if (scl != null)
                throw new IllegalStateException("recursive invocation");
            sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); // 这个地方值得深入理解一下
            if (l != null) {
                Throwable oops = null;
                scl = l.getClassLoader(); // 返回了AppClassLoader
                try {
                  // 已经获取到了 scl 为什么还要进行一步这样的操作呢?
                  // 加载自定义系统类加载器的属性。如果自定义了系统类加载器,在这里进行了处理,如果没有的话,则使用默认的系统类加载器。(可以点进去看看,里边的run方法摘抄出来放下了下方)
                    scl = AccessController.doPrivileged(
                        new SystemClassLoaderAction(scl));
                } catch (PrivilegedActionException pae) {
                    oops = pae.getCause();
                    if (oops instanceof InvocationTargetException) {
                        oops = oops.getCause();
                    }
                }
                if (oops != null) {
                    if (oops instanceof Error) {
                        throw (Error) oops;
                    } else {
                        // wrap the exception
                        throw new Error(oops);
                    }
                }
            }
            sclSet = true;  //设置成功了
        }
    }

// 处理自定义加载器的方法。
 public ClassLoader run() throws Exception {
        String cls = System.getProperty("java.system.class.loader");
        if (cls == null) {// 如果设置了自定义加载器的属性,则不为null,进行后续的操作。
            return parent;
        }

   // 在下方,会单独讲解一下 Class.forName方法的使用。
        Constructor<?> ctor = Class.forName(cls, true, parent)
            .getDeclaredConstructor(new Class<?>[] { ClassLoader.class });
        ClassLoader sys = (ClassLoader) ctor.newInstance( //获取到自定义的类加载器。返回
            new Object[] { parent });
        Thread.currentThread().setContextClassLoader(sys);
        return sys;
    }

grepcode.com 在线查看源代码的网站。

为什么扩展类加载的路径是java.ext.dirs 源代码里边写的。

image-20200211063824377

Launcher();
//双亲处理的机制。精髓
public Launcher() {
        // Create the extension class loader  创建扩展类加载器
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader", e);
        }

        // Now create the class loader to use to launch the application 创建应用类加载器
        try {
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader", e);
        }

        // Also set the context class loader for the primordial thread.
  // 为当前的执行线程设置上下文类加载器。(重要。重要。重要。)
 // 如果没有通过setContextCLassLoader(ClassLoader cl)进行设置的话,线程将继承其父线程的上下文类加载器。Java应用运行时的初始线程的上下文类加载器是系统类加载器
        Thread.currentThread().setContextClassLoader(loader);

        // Finally, install a security manager if requested
        String s = System.getProperty("java.security.manager");
        if (s != null) {
            // init FileSystem machinery before SecurityManager installation
            sun.nio.fs.DefaultFileSystemProvider.create();

            SecurityManager sm = null;
            if ("".equals(s) || "default".equals(s)) {
                sm = new java.lang.SecurityManager();
            } else {
                try {
                    sm = (SecurityManager)loader.loadClass(s).newInstance();
                } catch (IllegalAccessException e) {
                } catch (InstantiationException e) {
                } catch (ClassNotFoundException e) {
                } catch (ClassCastException e) {
                }
            }
            if (sm != null) {
                System.setSecurityManager(sm);
            } else {
                throw new InternalError(
                    "Could not create SecurityManager: " + s);
            }
        }
    }
Class.forName();

第一次使用这个方法的时候,是在学习JDBC的时候获取数据库连接。那个时候只用到了一个参数的构造方法

java.lang.Class<T> public static Class<?> forName(@NonNls String name,
                               boolean initialize,
                               ClassLoader loader)  throws ClassNotFoundException
Returns the Class object associated with the class or interface with the given string name, using the given class loader. Given the fully qualified name for a class or interface (in the same format returned by getName) this method attempts to locate, load, and link the class or interface. The specified class loader is used to load the class or interface. If the parameter loader is null, the class is loaded through the bootstrap class loader. The class is initialized only if the initialize parameter is true and if it has not been initialized earlier.
//使用给定的类加载器返回与具有给定字符串名称的类或接口关联的Class对象。 给定类或接口的完全限定名称(采用getName返回的相同格式),此方法尝试查找,加载和链接该类或接口。 指定的类加载器用于加载类或接口。 如果参数加载器为null,则通过引导类加载器加载该类。 仅当initialize参数为true且之前尚未初始化时,才对类进行初始化。
If name denotes a primitive type or void, an attempt will be made to locate a user-defined class in the unnamed package whose name is name. Therefore, this method cannot be used to obtain any of the Class objects representing primitive types or void.
If name denotes an array class, the component type of the array class is loaded but not initialized.
//如果name表示原始类型或void,则将尝试在名称为name的未命名包中定位用户定义的类。 因此,该方法不能用于获取表示原始类型或void的任何Class对象。如果name表示数组类,则将加载但不初始化该数组类的组件类型。
For example, in an instance method the expression:
Class.forName("Foo")
is equivalent to:
Class.forName("Foo", true, this.getClass().getClassLoader())
Note that this method throws errors related to loading, linking or initializing as specified in Sections 12.2, 12.3 and 12.4 of The Java Language Specification. Note that this method does not check whether the requested class is accessible to its caller.
If the loader is null, and a security manager is present, and the caller's class loader is not null, then this method calls the security manager's checkPermission method with a RuntimePermission("getClassLoader") permission to ensure it's ok to access the bootstrap class loader.
/*例如,在实例方法中,表达式为:
Class.forName(“ Foo”)
等效于:
Class.forName(“ Foo”,yes,this.getClass().getClassLoader())
请注意,此方法会引发与Java语言规范的12.2、12.3和12.4节中指定的加载,链接或初始化有关的错误。 请注意,此方法不检查其调用者是否可以访问所请求的类。
如果加载程序为null,并且存在安全管理器,并且调用方的类加载器不为null,则此方法使用RuntimePermission(“ getClassLoader”)权限调用安全管理器的checkPermission方法,以确保可以访问引导程序类加载器 。*/
Params:
name – fully qualified name of the desired class
initialize – if true the class will be initialized. See Section 12.4 of The Java Language Specification.
loader – class loader from which the class must be loaded
/*参数:
名称-所需类别的完全限定名称
初始化–如果为true,则将初始化该类。 参见Java语言规范的12.4节。
loader –必须从中加载类的类加载器*/
  @CallerSensitive
    public static Class<?> forName(String name, boolean initialize,
                                   ClassLoader loader)
        throws ClassNotFoundException
    {
        Class<?> caller = null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            // Reflective call to get caller class is only needed if a security manager
            // is present.  Avoid the overhead of making this call otherwise.
            caller = Reflection.getCallerClass();
            if (sun.misc.VM.isSystemDomainLoader(loader)) {
                ClassLoader ccl = ClassLoader.getClassLoader(caller);
                if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
                    sm.checkPermission(
                        SecurityConstants.GET_CLASSLOADER_PERMISSION);
                }
            }
        }
        return forName0(name, initialize, loader, caller);
    }
  
  
     @CallerSensitive
    public static Class<?> forName(String className)
                throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }

线程上下文类加载器

当前类加载器(Current ClassLadert):加载当前类的加载器。

每一个类都会尝试使用自身的类加载器(即加载自身的类加载器)来加载其他的类(指的是所依赖的类)。

如: 如果CLassx引用了ClassY,那么ClassX的类加载器就会去加载CLassY(前提是ClassY尚未被加载)

线程上下文类加载器(Context CLassloader)

线程上下文类加载器是从jdk1.2开始引入的,类Thread中的getContextClassLoader();与setContextClassLoadert();分别用来获取和设置上下文类加载器。

如果没有通过setContextCLassLoader(ClassLoader cl)进行设置的话,线程将继承其父线程的上下文类加载器。Java应用运行时的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过该类加载器来加载类与资源。

线程上下文类加载器的重要性:

SPI(Service Proider Interface) 服务提供厂商;

父ClassLoader可以使用当前线程Thread.currentThread().getContextClassLoader()所指定的ClassLoader加载的类,这就改变了父ClassLoader不能使用子ClassLoader或者是其他没有直接父子关系的ClassLoader加载的类的情况,即这就改变了双亲委托模型。

线程上下文类加载器就是当前线程的Current ClassLoader。

在双亲委托模型下,类加载是由下至上的,即下层的类加载器会委托上层进行加载。但是对于SPI来说,有些类是由Java核心库提供的。而Java核心库是由启动类加载器来加载的,而这些接口的实现却来源于不同的jar包(不同的厂商提供),Java的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求,而通过给当前线程设置上下文类加载器,就可以由上下文类加载器来实现对于接口实现类的加载。

Thread.currentThread().setContextClassLoader(loader);

public class MyTest24{
  psvm{
    sout(Thread.currentThread().getContextClassLoader());
    sout(Thread.class.getClassLoader());
  }
}

输出结果:
  AppClassLoader
  null 
  

数据库厂商肯定会满足jdbc规范的接口。

Tomcat和spring的类加载机制和jdk的不同。

/**
如果没有通过setContextCLassLoader(ClassLoader cl)进行设置的话,线程将继承其父线程的上下文类加载器。Java应用运行时的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过该类加载器来加载类与资源。
**/
public class MyTest25 implements Runnable {
    private Thread thread;

    public MyTest25() {
        thread = new Thread(this);
        thread.start();
    }

    @Override
    public void run() {
        ClassLoader classLoader = this.thread.getContextClassLoader();
        this.thread.setContextClassLoader(classLoader);
        System.out.println("class:"+ classLoader.getClass());
        System.out.println("Parent:"+ classLoader.getParent().getClass());
    }

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

> Task :MyTest25.main()
class:class sun.misc.Launcher$AppClassLoader
Parent:class sun.misc.Launcher$ExtClassLoader

线程上下文类加载器的一般使用模式(获取-使用-还原)

线程上下文类加载器的一般使用模式(获取-使用-还原)
 ClassLoader classloader= Thread.currentThread().getConTextCLassLoader(); // 获取
try{
  Thread.currentThread().setConTextCLassLoader(targetTccl);//使用
  myMethod();
}finally{
  Thread.currentThread().setConTextCLassLoader(classLoader);//还原。
}

1. myMethod里边调用了Thread.currentThread().getConTextCLassLoader();获取当前线程的上下文类加载器某些事情。
2.如果一个类由类加载器A加载,那么这个类的依赖类也是由相同的类加载器加载器的(如果该依赖类没有加载过的话)
3.ContextClassLoader的作用就是为了破坏Java的类加载委托机制。
4.当高层提供了同一的接口让低层去实现,同时又要在高层加载(或者实例化)低层的类时,就必须通过上下文类加载器来帮助高层找到高层的CLassLoader找到并加载该类。
举例: 
  1、 首先加入MySQL的依赖 :  “mysql:mysql-connector-java:5.1.34 ”
public class MyTest26{
    public static void main(String[] args) {
        ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
        Iterator<Driver> iterator = loader.iterator();
        while(iterator.hasNext()){
		 Driver driver = iterator.next();    System.out.println("driver:"+driver.getClass()+",loader:"+driver.getClass().getClassLoader());
        }
        System.out.println("当前线程上下文类架加载器的对象:"+Thread.currentThread().getContextClassLoader());
        System.out.println("ServiceLoader的类加载器"+ServiceLoader.class.getClassLoader());
    }
}

> Task :MyTest26.main()
driver:class com.mysql.jdbc.Driver,loader:sun.misc.Launcher$AppClassLoader@659e0bfd
driver:class com.mysql.fabric.jdbc.FabricMySQLDriver,loader:sun.misc.Launcher$AppClassLoader@659e0bfd
当前线程上下文类架加载器的对象:sun.misc.Launcher$AppClassLoader@659e0bfd
ServiceLoader的类加载器null
ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);便能获取到驱动的加载的类。
ServiceLoader解析: 提供了很好的 对 SPI的支持。
  
java.util public final class ServiceLoader<S>
extends Object
implements Iterable<S>
  
A simple service-provider loading facility.
// 一个简单的服务提供商加载工具。
A service is a well-known set of interfaces and (usually abstract) classes. A service provider is a specific implementation of a service. The classes in a provider typically implement the interfaces and subclass the classes defined in the service itself. Service providers can be installed in an implementation of the Java platform in the form of extensions, that is, jar files placed into any of the usual extension directories. Providers can also be made available by adding them to the applications class path or by some other platform-specific means.
//服务是一组著名的接口和(通常是抽象的)类。 服务提供者是服务的特定实现。 提供程序中的类通常实现接口,并子类化服务本身中定义的类。 服务提供程序可以扩展的形式安装在Java平台的实现中,也就是说,将jar文件放置在任何常用扩展目录中。 也可以通过将提供者添加到应用程序类路径或通过其他一些特定于平台的方式来使提供者可用。
For the purpose of loading, a service is represented by a single type, that is, a single interface or abstract class. (A concrete class can be used, but this is not recommended.) A provider of a given service contains one or more concrete classes that extend this service type with data and code specific to the provider. The provider class is typically not the entire provider itself but rather a proxy which contains enough information to decide whether the provider is able to satisfy a particular request together with code that can create the actual provider on demand. The details of provider classes tend to be highly service-specific; no single class or interface could possibly unify them, so no such type is defined here. The only requirement enforced by this facility is that provider classes must have a zero-argument constructor so that they can be instantiated during loading.
//出于加载的目的,服务由单一类型表示,即单一接口或抽象类。 (可以使用一个具体的类,但是不建议这样做。)给定服务的提供者包含一个或多个具体类,这些具体类使用该提供者特定的数据和代码来扩展此服务类型。 提供者类通常不是整个提供者本身,而是包含足够信息以决定提供者是否能够满足特定请求以及可以按需创建实际提供者的代码的代理。 提供程序类的细节往往是高度特定于服务的; 没有单个类或接口可能会统一它们,因此此处未定义此类。 此功能强制执行的唯一要求是,提供程序类必须具有零参数构造函数,以便可以在加载期间实例化它们。
A service provider is identified by placing a provider-configuration file in the resource directory META-INF/services. The file's name is the fully-qualified binary name of the service's type. The file contains a list of fully-qualified binary names of concrete provider classes, one per line. Space and tab characters surrounding each name, as well as blank lines, are ignored. The comment character is '#' ('\u0023', NUMBER SIGN); on each line all characters following the first comment character are ignored. The file must be encoded in UTF-8.
//通过将提供者配置 文件放置在资源目录META-INF / services中来标识服务提供者。 文件名是服务类型的标准二进制名称。 该文件包含一个具体的提供程序类的标准二进制名称列表,每行一个。 每个名称周围的空格和制表符以及空白行将被忽略。 注释字符为“#”(“ \ u0023”,数字符号); 在每一行中,第一个注释字符之后的所有字符都将被忽略。 该文件必须使用UTF-8编码。
If a particular concrete provider class is named in more than one configuration file, or is named in the same configuration file more than once, then the duplicates are ignored. The configuration file naming a particular provider need not be in the same jar file or other distribution unit as the provider itself. The provider must be accessible from the same class loader that was initially queried to locate the configuration file; note that this is not necessarily the class loader from which the file was actually loaded.
//如果一个特定的具体提供程序类在多个配置文件中被命名,或者在同一配置文件中被多次命名,则重复项将被忽略。 命名特定提供程序的配置文件不必与提供程序本身位于同一jar文件或其他分发单元中。 该提供程序必须可以从最初查询定位配置文件的同一类加载程序进行访问; 请注意,这不一定是实际从中加载文件的类加载器。
Providers are located and instantiated lazily, that is, on demand. A service loader maintains a cache of the providers that have been loaded so far. Each invocation of the iterator method returns an iterator that first yields all of the elements of the cache, in instantiation order, and then lazily locates and instantiates any remaining providers, adding each one to the cache in turn. The cache can be cleared via the reload method.
//提供商的位置很懒,即按需实例化。 服务加载器维护到目前为止已加载的提供者的缓存。 每次迭代器方法的调用都会返回一个迭代器,该迭代器首先按实例化顺序生成高速缓存的所有元素,然后懒惰地定位和实例化任何剩余的提供程序,依次将每个提供程序添加到高速缓存中。 可以通过reload方法清除缓存。
Service loaders always execute in the security context of the caller. Trusted system code should typically invoke the methods in this class, and the methods of the iterators which they return, from within a privileged security context.
//服务加载程序始终在调用方的安全上下文中执行。 受信任的系统代码通常应从特权安全上下文中调用此类中的方法以及它们返回的迭代器的方法。
Instances of this class are not safe for use by multiple concurrent threads.
Unless otherwise specified, passing a null argument to any method in this class will cause a NullPointerException to be thrown.
//此类的实例不适用于多个并发线程。
//除非另有说明,否则将null参数传递给此类中的任何方法都将引发NullPointerException。
Example Suppose we have a service type com.example.CodecSet which is intended to represent sets of encoder/decoder pairs for some protocol. In this case it is an abstract class with two abstract methods:
   public abstract Encoder getEncoder(String encodingName);
   public abstract Decoder getDecoder(String encodingName);
Each method returns an appropriate object or null if the provider does not support the given encoding. Typical providers support more than one encoding.
  //每个方法都返回一个适当的对象,如果提供者不支持给定的编码,则返回null。典型的提供程序支持多种编码。
If com.example.impl.StandardCodecs is an implementation of the CodecSet service then its jar file also contains a file named
   META-INF/services/com.example.CodecSet
This file contains the single line:
   com.example.impl.StandardCodecs    # Standard codecs
The CodecSet class creates and saves a single service instance at initialization:
   private static ServiceLoader<CodecSet> codecSetLoader
       = ServiceLoader.load(CodecSet.class);
To locate an encoder for a given encoding name it defines a static factory method which iterates through the known and available providers, returning only when it has located a suitable encoder or has run out of providers.
   public static Encoder getEncoder(String encodingName) {
       for (CodecSet cp : codecSetLoader) {
           Encoder enc = cp.getEncoder(encodingName);
           if (enc != null)
               return enc;
       }
       return null;
   }
A getDecoder method is defined similarly.
Usage Note If the class path of a class loader that is used for provider loading includes remote network URLs then those URLs will be dereferenced in the process of searching for provider-configuration files.
//使用说明如果用于提供程序加载的类加载程序的类路径包括远程网络URL,则在搜索提供程序配置文件的过程中将取消引用这些URL。
This activity is normal, although it may cause puzzling entries to be created in web-server logs. If a web server is not configured correctly, however, then this activity may cause the provider-loading algorithm to fail spuriously.
//此活动是正常的,尽管它可能会导致在Web服务器日志中创建令人费解的条目。但是,如果未正确配置Web服务器,则此活动可能导致提供商加载算法错误地失败
A web server should return an HTTP 404 (Not Found) response when a requested resource does not exist. Sometimes, however, web servers are erroneously configured to return an HTTP 200 (OK) response along with a helpful HTML error page in such cases. This will cause a ServiceConfigurationError to be thrown when this class attempts to parse the HTML page as a provider-configuration file. The best solution to this problem is to fix the misconfigured web server to return the correct response code (HTTP 404) along with the HTML error page.
//当请求的资源不存在时,Web服务器应返回HTTP 404(未找到)响应。但是,在某些情况下,有时会错误地将Web服务器配置为返回HTTP 200(OK)响应以及有用的HTML错误页面。当此类尝试将HTML页面解析为提供程序配置文件时,这将引发ServiceConfigurationError。解决此问题的最佳方法是修复配置错误的Web服务器,以返回正确的响应代码(HTTP 404)和HTML错误页面。

private static final String PREFIX = "META-INF/services/";
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

至于为什么会输出两个jdbc的驱动;由下图所示。

image-20200211220809384

image-20200211221135025

听懂了,但是要不要记下来呢?? 源码里边都有,但是手速已经跟不上了。如果暂停下来去粘贴过来的话,会大量的浪费时间。所以这里就不粘贴过来了,认真的听吧。

image-20200211222949194

image-20200211224115417

通过jdbc驱动加载,深刻理解线程上下文类加载器的机制

分析一下 这两行代码底层是怎么实现的。已经都学习过了。

// 初始化的作用
// 线程上下文类加载器的作用。
// 精辟。   
 public class MyTest27 {
    public static void main(String[] args) throws Exception{
      // 初始化,会加载类中华的静态方法和内部类。  层层的,如果静态方法中调用静态类,会初始化内部的静态类
        Class.forName("com.mysql.jdbc.Driver");  
     
        Connection connection = DriverManager.getConnection("jdbc:localhost:8080", "username", "password");
    }
}

2020年02月11日23:12:09 。 睡觉。

2020年02月12日17:07:52

类加载器阶段性总结

总结回顾了一下之前学习的所有笔记中的内容。

论笔记的重要性。学习方式的升级。学习,遗忘,复习。笔记来做一个载体。

不要给后续留坑,前期欠的东西,以后总归是要还的。

技术学习与思维方式谈心

学习效率,学习方法,重要吗?

学习一个知识的时候太着急了。并不是一个视频学完了,这门技术就掌握了。

你学习的时候,目标是什么呢?把视频中讲解的内容给吸收理解了。

时间和效率两个因素来说,时间是固定的。我们只能从效率入手。

突击学习只会让你误以为你了解了这个技术。有节奏的学习。张龙老师的视频的准备和录制时间,是在工作之余准备和录制的。

再次复习一下之前学习的规程。

类加载器学习到此结束。开启一个新的阶段。2020年02月12日21:31:10

Java字节码

.class字节码文件是跨平台的。

Java字节码文件介绍

Java字节码文件的结构及组成部分的学习。

javap -c com.erwa.jvm.MyTest1

Javap -verbose com.erwa.jvm.MyTest1 -- 返回class文件的冗余内容

.class字节码文件中的内容都是什么?待学习 Hex_Fiend工具

image-20200212215112336

image-20200213062442534

image-20200213053157620

完整的Java字节码结构

image-20200213070039138

上下两个表相互对应。

image-20200213070557356

image-20200213071256875

image-20200213071319163

Java字节码基础概念

常量池(constant pool)

  1. 使用java -verbose命令分析一个字节码文件时,将会分析该字节文件的魔数,版本号,常量池,类信息,类的构造方法,类中的信息,类变量与成员变量等信息。
  2. 魔数:所有的.class字节码文件的前4个字节都是魔数,魔数值为固定值:0xCAFEBABE。
  3. 魔数之后的4个字节为版本信息,前两个字节表示minor version(次版本号),major versio(主版本号)。这里的00 00 00 34 ,换算成十进制,表示次版本号为0,主版本号为52。所以,该文件的版本信息就是1.8.0。可以通过java -version 命令查看一下Java版本号。
  4. 常量池(constant pool): 紧接着主版本号之后的就是常量池入口。一个Java类中定义的很多信息都是由常量池来维护和描述的,可以将常量池看作是class文件的资源仓库,比如说Java类中的定义的方法与变量信息,都是存储在常量池中。常量池中主要存储两类常量:字面量与符号引用。
    • 字面量:如文本字符串,Java中声明final的常量等。
    • 符号引用:如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符等。
  5. 常量池的总体结构:Java类所对应的常量池主要由常量池数量与常量池数组这两部分共同构成。常量池数量紧跟在主版本号后边,占据2个字节;而常量池数组紧跟在常量池数量之后。常量池数组与一般的数组不同的是,常量池数组中不同的元素的类型、结构都是不同的,长度当然也就不同。但是,每一种元素的第一个数据都是一个u1类型。该字节是一个标志位,占据1个字节。JVM在解析常量池时,会根据这个u1类型来获取元素的具体类型。值得注意的是,常量池数组中元素的个数=常量池数-1 (其中0暂时不使用)。目的是为了满足某些常量池索引值的数据在特定情况下需要表达「不引用任何一个常量池」的含义;其根本在于,索引0也是一个常量(保留常量),只不过他不位于常量表中,这个常量就对应null;所以常量池的索引是从1而非0开始的。

常量池中11中数据类型的结构总表:

image-20200213055103543

image-20200215223948170

  1. 在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.
  1. 对于数组类型来说,每一个维度使用一个前置的[来表示,如int[]被记录为I[]。 String[] [] 被记录为[[Ljava/lang/String;
  2. 用描述符描述方法时,按照先参数列表,后返回值的顺序来描述。参数列表按照参数的严格顺序放在一组()之内,如方法:String getRealnameByIdAndNickname(int id,String name)的描述符为:(I,Ljava/lang/String;)Ljava/lang/String;

Java是一个单继承,多实现的语言。

访问标志(Access_Flag)

image-20200213071722662

image-20200213071636271

0x0002 private

image-20200213071856863

字段表集合(field_info)

image-20200213072228709

image-20200213072343009

字段表结构:

字段表结构

方法表集合(method_info)

image-20200213073217941

方法表结构:

image-20200213073331287

image-20200213073827813

image-20200213073919786

image-20200213073953094

方法表结构实例:

image-20200213073800643

code结构(code attribute):每一个方法的结构内容

image-20200213074025226

image-20200213074123290image-20200213074218184

code_length : 之后的字节 会转换成 助记符

image-20200213074400768

附加属性

image-20200213074436709

image-20200213075217293

LineNumberTable : 源代码和行号的对应关系。方便抛异常时,定义出错的位置。

在字节码文件中。 this 属性是默认当做第一个参数给传入进去的。

image-20200213074613208

字节码查看工具 的GitHub地址

idea也有插件。 jclasslib

image-20200213102548517

字节码反编译练习

// 编译下边的文件,然后反编译。对应字节码去翻译一遍,学习 字节码结构。
// 整体的复习。
 public class MyTest2{
   String str = "welcome";
   private int x = 5;
   public static Integer in = 10;
   public static void main(String[] args){
     MyTest2 myTest2 = new MyTest2();
     myTest2.setX(8);
     in = 20;
   }
   public void setX(int x){
     this.x = x ;
   }
 }
-- 反编译文件内容为:
➜  jdk8 javap -verbose build.classes.java.main.com.erwa.bytecode.MyTest2     
(此时的命令时不打印私有的信息的。需要加上一个 -p 的参数) 
警告: 二进制文件build.classes.java.main.com.erwa.bytecode.MyTest2包含com.erwa.bytecode.MyTest2
Classfile /Users/erwa/Desktop/work/workspace/idea/jdk8/build/classes/java/main/com/erwa/bytecode/MyTest2.class
  Last modified 2020-2-13; size 833 bytes
  MD5 checksum c119f2eae8cb307e9b90f835d460a4ad
  Compiled from "MyTest2.java"
public class com.erwa.bytecode.MyTest2
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #10.#34        // java/lang/Object."<init>":()V
   #2 = String             #35            // welcome
   #3 = Fieldref           #5.#36         // com/erwa/bytecode/MyTest2.str:Ljava/lang/String;
   #4 = Fieldref           #5.#37         // com/erwa/bytecode/MyTest2.x:I
   #5 = Class              #38            // com/erwa/bytecode/MyTest2
   #6 = Methodref          #5.#34         // com/erwa/bytecode/MyTest2."<init>":()V
   #7 = Methodref          #5.#39         // com/erwa/bytecode/MyTest2.setX:(I)V
   #8 = Methodref          #40.#41        // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
   #9 = Fieldref           #5.#42         // com/erwa/bytecode/MyTest2.in:Ljava/lang/Integer;
  #10 = Class              #43            // java/lang/Object
  #11 = Utf8               str
  #12 = Utf8               Ljava/lang/String;
  #13 = Utf8               x
  #14 = Utf8               I
  #15 = Utf8               in
  #16 = Utf8               Ljava/lang/Integer;
  #17 = Utf8               <init>
  #18 = Utf8               ()V
  #19 = Utf8               Code
  #20 = Utf8               LineNumberTable
  #21 = Utf8               LocalVariableTable
  #22 = Utf8               this
  #23 = Utf8               Lcom/erwa/bytecode/MyTest2;
  #24 = Utf8               main
  #25 = Utf8               ([Ljava/lang/String;)V
  #26 = Utf8               args
  #27 = Utf8               [Ljava/lang/String;
  #28 = Utf8               myTest2
  #29 = Utf8               setX
  #30 = Utf8               (I)V
  #31 = Utf8               <clinit>
  #32 = Utf8               SourceFile
  #33 = Utf8               MyTest2.java
  #34 = NameAndType        #17:#18        // "<init>":()V
  #35 = Utf8               welcome
  #36 = NameAndType        #11:#12        // str:Ljava/lang/String;
  #37 = NameAndType        #13:#14        // x:I
  #38 = Utf8               com/erwa/bytecode/MyTest2
  #39 = NameAndType        #29:#30        // setX:(I)V
  #40 = Class              #44            // java/lang/Integer
  #41 = NameAndType        #45:#46        // valueOf:(I)Ljava/lang/Integer;
  #42 = NameAndType        #15:#16        // in:Ljava/lang/Integer;
  #43 = Utf8               java/lang/Object
  #44 = Utf8               java/lang/Integer
  #45 = Utf8               valueOf
  #46 = Utf8               (I)Ljava/lang/Integer;
{
  java.lang.String str;
    descriptor: Ljava/lang/String;
    flags:

  public static java.lang.Integer in;
    descriptor: Ljava/lang/Integer;
    flags: ACC_PUBLIC, ACC_STATIC

  public com.erwa.bytecode.MyTest2();
    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: ldc           #2                  // String welcome
         7: putfield      #3                  // Field str:Ljava/lang/String;
        10: aload_0
        11: iconst_5
        12: putfield      #4                  // Field x:I
        15: return
      LineNumberTable:
        line 3: 0
        line 4: 4
        line 5: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0  this   Lcom/erwa/bytecode/MyTest2;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #5                  // class com/erwa/bytecode/MyTest2
         3: dup
         4: invokespecial #6                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: bipush        8
        11: invokevirtual #7                  // Method setX:(I)V
        14: bipush        20
        16: invokestatic  #8                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        19: putstatic     #9                  // Field in:Ljava/lang/Integer;
        22: return
      LineNumberTable:
        line 8: 0
        line 9: 8
        line 10: 14
        line 11: 22
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      23     0  args   [Ljava/lang/String;
            8      15     1 myTest2   Lcom/erwa/bytecode/MyTest2;

  public void setX(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: putfield      #4                  // Field x:I
         5: return
      LineNumberTable:
        line 13: 0
        line 14: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lcom/erwa/bytecode/MyTest2;
            0       6     1     x   I

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: bipush        10
         2: invokestatic  #8                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         5: putstatic     #9                  // Field in:Ljava/lang/Integer;
         8: return
      LineNumberTable:
        line 6: 0
}
SourceFile: "MyTest2.java"

加上synchronized关键字字节码

为什么除了访问权限多了synchronized之外,其他的地方都一样。

因为synchronized使用的方式有多种:

  1. 如果synchronized用在实例方法上,表示对此方法加了一个锁。
  2. 如果用在了对象上,则字节码会生成锁的入口和出口。
  3. synchronized用在类中的静态方法上, 其实是给当前的类的对象上的锁。

关于锁:能不用synchronized就不用。使用轻量级的 lock 。

构造方法和静态代码块字节码

  1. 如果没有设置构造方法,会默认生成一个无参构造。如果有静态方法,会初始化一个clinit的方法。
  2. 全局变量是在构造方法初始化的时候完成赋值的。
  3. 一个构造方法对应一个init方法。(全局变量的赋值,会在每个init方法中都重新执行一次)
  4. 所有的静态变量和静态代码块赋值都是在clinit中执行的。

字节码对this关键字和异常的处理

/**   -=this关键字的总结=-
	对于Java类中的每一个实例方法(非static方法),其在编译后所生成的字节码当中,方法参数的数量总会比源代码方法中的参数量多一个(this),它位于方法的第一个参数位置处,这样我们就可以在Java的实例方法中使用this来访问当前对象的属性以及其他方法。
	这个操作是在编译期间完成的。由Javac编译器在编译的时候将对this的访问转化成对一个普通实例方法参数的访问;接下来的运行期间,由JVM在调用实例方法时,自动向实例方法传入该this参数。所以在实例方法的局部变量中,至少会有一个指向当前对象的局部变量。
*/

/**  -=Java字节码对于异常的处理方式:=-
	1. 统一采用异常表的方式来对异常进行出来的。
	2. 在jdk1.4.2之前的版本中,并不是使用异常表的方式来对异常进行处理的,而是采用特定的指令方式进行处理的。
	3. 当异常处理存在finally语句块时,现代化的JVM才去的处理方式是将finally语句块的字节码拼接到每一个catch块后边,换句话说,程序中存在多少个catch块,就会在每一个catch块后面重复多少个finally语句块的字节码。
*/
public class MyTest3 {
    public void test(){
        try{
            FileInputStream fileInputStream = new FileInputStream("t.txt");

            ServerSocket serverSocket = new ServerSocket(9999);
            serverSocket.accept();
        }catch(FileNotFoundException exc){

        }catch(IOException ex){

        }catch(Exception e){

        }finally {
            System.out.println("exception");
        }
    }
}

test()中的Code中三个参数的说明:

stack=3 ;"最大栈深度" (对于方法栈,压栈弹出。)

locals =4 ; "成员变量的个数" (局部变量有: this,is ,serverSocket , ex (同时只能有一个异常的参数))

agrs_size=1 "参数的个数“(第一个默认的都是this)

image-20200214064208939image-20200214064251773

image-20200214064341107

LineNumberTable 是 字节码和源代码对应的行号

image-20200214070640848

LocalVariableTabel 是 对于局部变量的存储信息

image-20200214070737725

StackMapTable 是针对于安全的。

image-20200214064803780

ByteCode内容:

image-20200214065328559

goto 语句是发生异常的时候进行跳转到catch的位置。

Exception table 信息:

image-20200214065509512

最后的一个any来说,是处理所有上边不可能处理的异常。是字节码生成的时候帮助我们生成的。

字节码的运行机制

字节码基础概念

栈帧(stack frame)

  1. 栈帧是由栈和帧组合而成的概念。

栈帧一种用于帮助虚拟机执行方法调用与方法执行的数据结构。他是独立于线程的,一个线程有自己的一个栈帧。不存在并发的情况。

栈帧本是一种数据结构,封装了方法的局部变量表、动态链接信息、方法的返回地址以及操作数栈等信息。

  1. java中的引入的概念:符号引用:直接引用。

有些符号引用是在类加载阶段或是第一次使用时就会转换成直接引用,这种转换叫做静态解析;

另外一些符号引用则是在每次运行期转换为直接引用,这种转换叫做动态链接,这种体检为Java的多态性。

如: Animal a = new Cat();

a.sleep(); //实际上应该调用Cat()的sleep();

a = new Dog();

a.sleep(); //实际上应该调用Dog()的sleep();

a = new Tiger();

a.sleep(); //实际上应该调用Tiger()的sleep();

但是在程序编译的时候,字节码能看到的是 a调用的都是Animal的sleep();

应用了invokevirtual的概念。

入栈,出栈

image-20200214072043992

结合上图,我用文字描述一下两个概念。看自己能否理解。

首先将2和3压入栈中。现在要做一个减法,3-2 。 需要先把2从栈中取出,3也从栈中取出,做完减法后,把1压入栈中。

局部变量表的概念(slot)
//slot  为栈帧中的一个个小格,上图中的一个三角区域。  
public void test(){
    int a = 3;
    if (a<4){
      int b = 4;
      int c = 5;
    }
    int d = 7;
  }
// slot是可以复用的。方法体中声明的局部变量。
// 因为方法体本身又可以存在更小的作用域。
// 所以一个方法中有10个变量,可能使用7个8个slot,也可能使用10个slot。
invoke 的概念(调用)
  1. invokeinterface : 调用接口中的方法,实际上是在运行期决定的,决定到底调用实现该接口的哪个对象的特定方法。
  2. invokestatic : 调用静态方法。
  3. invokespecial : 调用自己的私有方法、构造方法(<Init>)以及父类的方法。
  4. invokevirtual : 调用虚方法、运行期动态查找的过程。
  5. Invokedynamic: 动态调用方法。
/**
静态解析的4种情形: (字节码还没有运行的时候,就已经能够确定调用哪个方法)

1. 静态方法
2. 父类方法
3. 构造方法
4. 私有方法(无法被重写)

以上4类方法称为非虚方法,他们是在类加载阶段就可以将符号引用转换成直接引用。

*/
public class MyTest4{
  public static void test(){
    sout();
  }
  public static void main(String[] args){
    test();
  }
}

方法重载(方法的静态分派机制)

/**  方法的静态分派
 Grandpa g1 = new Father(); G1的静态类型是Grandpa,而g1的实际类型(真正指向的类型)是Father。
 我们可以得到这样一个结论:变量的静态类型是不会发生变化的,而变量的实例类型是可以发生变化的(多态的一种实现),实例类型是在运行期方可确定。
*/
public class MyTest5{
    // 方法重载,是一种静态的行为,编译器就可以完全确定。
    public void test(Grandpa grandpa){
        System.out.println("Grandpa");
    }
    public void test(Father father){
        System.out.println("Father");
    }
    public void test(Son son){
        System.out.println("Son");
    }
    public static void main(String[] args){
        Grandpa g1 = new Father();
        Grandpa g2 = new Son();

        MyTest5 mytest5 = new MyTest5();
        mytest5.test(g1); // 实例调用的时候,获取到的参数的类型是获取到实例的静态类型。所有都是Grandpa
        mytest5.test(g2);
    }

}
class Grandpa{}
class Father extends Grandpa{}
class Son extends Father{}

> Task :MyTest5.main()
Grandpa
Grandpa

image-20200214080027838

方法重写(方法的动态分派机制)

/**  方法的动态分派
	方法的动态分派涉及到一个重要概念:方法接受者。
	invokevirtual字节码指令的多态查找流程。
	流程: 找到操作数栈顶的第一个元素,对象引用的实际类型。

	比较方法重载(overload)与方法重写(overwrite),我们可以得到这样一个结论:
	方法重载是静态的,是编译期行为;方法重写是动态的,是运行期行为。
*/
public class MyTest6{
  psvm(){
    Fruit apple = new Apple();
    Fruit orange = new Orange();
    
    apple.test();
    orange.test();
    
    apple = new Orange();
    apple.test();
  }
}

class Fruit(){
  public void test(){
    sout("fruit");
  }
}
class Apple extends Fruit(){
  @Override
  public void test(){
    sout("Apple");
  }
}
class Orange extends Fruit(){
  @Override
  public void test(){
    sout("Orange");
  }
}

输出结果为:
  apple 
  orange 
  orange 

new 关键字的作用: 1、开辟一个内存空间,2、调用构造方法 3、赋值给局部变量

虚方法表与动态分派机制详解

针对于方法调用动态分派的过程,虚拟机会在类的方法区建立一个虚方法表的数据结构。(virtual method table ,简称 vtable),是在加载连接阶段完成的加载。

针对于invokeinterface指令来说,虚拟机会建立一个叫做接口范发表的数据结构。(interface method table , 简称itable)

image-20200214212732468

输出结果:

animal str

Dog date

image-20200214213530201

子类和父类的相同的方法 元素的索引号是一样的。这样会提高性能和效率。

基于栈的指令集与基于寄存器的指令集详细对比
/**
现代JVM在执行Java代码的时候,通常都会将解释执行与编译执行结合起来进行。

所谓解释执行,就是通过解释器来读取字节码,遇到相应的指令就去执行指令。
所谓编译执行,就是通过即时编译器(Just In Time ,JIT)将字节码转换成本地机器执行;现在的JVM会根据代码热点来生成。

基于栈的指令集与基于寄存器的指令集之间的关系:
1. JVM执行指令时所采取的的方式是基于栈的指令集。
2. 基于栈的指令集主要的操作有入栈与出栈两种。
3. 基于栈的指令集的优势在于它可以在不同平台之间移植,而基于寄存器的指令集与操作系统结构紧密关联,无法做到可移植性。
4. 基于栈的指令集的缺点在于完成相同的操作,指令数量通常要比基于寄存器的指令集数量要多;基于栈的指令集是在内存中完成操作的,而基于寄存器的指令集是直接由CPU来执行的,它是在高速缓存区中进行执行的,速度要快很多。虽然虚拟机可以采用一些优化手段,但总体来说,基于栈的指令集的执行速度要慢一些。
*/

举例: 2-1:  在栈中的指令 与 在寄存器中的指令。
栈的指令: 
  1.iconst_1  (将数字1 压入栈)
  2.iconst_2  (将数字2 压入栈)
  3.isub   (将栈顶的两个元素弹出,执行减法操作,然后将结果压入栈顶)
  4.istore  (取出栈顶的数据)
寄存器的指令: 
  1.将2放入到一个寄存器中。
  2.调用减法的指令,后边跟一个参数。然后将结果放入到这个寄存器中。
  
// JVM执行指令时所采取的的方式是基于栈的指令集。举例: 
public class MyTest8{
  public int mCalculate(){
    int a = 1;
    int b = 2;
    int c = 3;
    int d = 4;
    int result = (a+b+c)*D;
    return result;
  }
 
编译上方代码,然后反编译class文件。以字节码的方式查看此运算的过程
  
  iconst_<i>:将int常量推送到操作数栈当中。(i = 0, 1 ,2 ,3 ,4 ,5 ); i 为栈中的索引;
  istore_<i>:将操作数栈顶的元素弹出。(i = 1 , 2 , 3)
  istore 4 : (istore 的值最大到3)
  iload_<i>:从局部变量中加载一个值。(i = 0 ,1 , 2 , 3 )
  iload 4 : (iload 的 i的最大值为3 )
  iadd: 完成整形的加法。(从操作栈中弹出两个值进行相加,然后把值压入栈顶)
  isub: 完成数组的减法。(从操作栈中弹出两个值进行相减,然后把值压入栈顶)
  imul: 完成整数的乘法。(从操作栈中弹出两个值进行相乘,然后把值压入栈顶)

透过字节码生成审视Java动态代理运作机制

//动态代理类的实现。
//动态代理的优势:在真实对象还不存在的情况下就可以把代理对象创建出来,代理对象可以代理多种真实对象。比如说Spring中的APO,我们面面向接口编程的时,通过动态代理给我们生成实例。还有SPring继承的CJLib  
public interface Subject {
    public void  request();
}

public class RealSubject implements Subject {
    @Override
    public void request() {
        System.out.println("From real subject");
    }
}
	
public class DynamicSubject implements InvocationHandler {
    private Object sub;
    public DynamicSubject(Object object) {
        this.sub = object;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("before calling: " + method);
        method.invoke(this.sub, args);
        System.out.println("after calling: " + method);
        return null;
    }
}

public class Client {
    public static void main(String[] args) {
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        RealSubject rs = new RealSubject();
        DynamicSubject ds = new DynamicSubject(rs);
        Class<? extends RealSubject> cls = rs.getClass();
        Subject subject = (Subject) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), cls.getInterfaces(), ds);// 属性文件的设置,是从这个里边找到的。 
        subject.request(); // 真正指向的实例是 $Proxy 
        System.out.println(subject.getClass());
        System.out.println(subject.getClass().getSuperclass());
    }
}

> Task :Client.main()
before calling: public abstract void com.erwa.bytecode.Subject.request()
From real subject
after calling: public abstract void com.erwa.bytecode.Subject.request()
class com.sun.proxy.$Proxy0
class java.lang.reflect.Proxy
  
  并且生成了一个$Proxy0文件的class文件: 如下
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
// 如果使用了动态代理,除了eques toString hashCode  的其他方法。并不受动态代理的影响。
An invocation of the hashCode, equals, or toString methods declared in java.lang.Object on a proxy instance will be encoded and dispatched to the invocation handlers invoke method in the same manner as interface method invocations are encoded and dispatched, as described above. The declaring class of the Method object passed to invoke will be java.lang.Object. Other public methods of a proxy instance inherited from java.lang.Object are not overridden by a proxy class, so invocations of those methods behave like they do for instances of java.lang.Object.
'如上所述,将对代理实例上java.lang.Object中声明的hashCode,equals或toString方法的调用进行编码,并将其分派到调用处理程序invoke方法,方法与对接口方法调用进行编码和分派的方式相同,如上所述。传递给调用的Method对象的声明类将是java.lang.Object。从java.lang.Object继承的代理实例的其他公共方法不会被代理类覆盖,因此对这些方法的调用的行为就像对java.lang.Object实例的调用一样。'
  
package com.sun.proxy;

import com.erwa.bytecode.Subject;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements Subject {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public $Proxy0(InvocationHandler var1) throws  { // 只有这一个构造方法
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final void request() throws  {  // 我们自己要求被代理的方法。
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {  // 
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m3 = Class.forName("com.erwa.bytecode.Subject").getMethod("request");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

java字节码总结

不要把源代码和字节码混为一谈,他们没有相关的对应关系。

2020年02月15日23:00:50 。 回顾完毕。晚安。

JVM内存

JVM内存空间划分与作用

/**  JVM的内存空间划分介绍与堆空间的用途分析:(以下7个空间)
 1. 虚拟机栈(栈帧):每一个方法都会生成 Stack Frame 栈帧。属于线程私有的一份空间(伴随着线程的存在而存在)。内含局部变量表,用来存放方法的局部变量。虚拟机栈可能会抛出堆栈溢出错误。
 2. 程序计数器(Program Counter):是一块很小的内存空间,主要的作用用来区分当前执行线程的行号与顺序。本身与虚拟机栈类似,是属于线程私有的数据结构。在多线程的环境下,多核,每一个核心也最多执行一个线程,但是可以有多个线程相互切换着执行,此时就需要程序计数器来记录线程执行的行号来区分维护。(它是一定不会出现栈溢出的情况)
 3. 本地方法栈:主要用于处理本地方法(native方法,由其他语言写的方法); Oracle的虚拟机把虚拟机栈和本地方法栈合二为一了。
 4. 堆(Heap)(重点研究):JVM中最大的一块内存,是一块共享区域(被Java所有的线层所共享的,几乎所有的对象都是在堆上进行分配的),用于存放对象实例。我们一般通过引用来操作对象,而不是直接操作对象。对象引用位于栈当中。堆中发生垃圾回收的概率是最高的。方法区是极少出现垃圾回收的可能。与堆相关的一个重要概念就是垃圾收集器(现在所有的收集器都是采用的分代收集算法,所以堆空间也基于这一点进行了相应的划分:新生代,老年代;Eden空间,From Survivor空间与To Survivor空间)。Java的堆空间在物理上既可以是连续的,也可以是不连续的。
 5. 方法区(Method Area):存储元信息。永久代(Permanent Generation)从JDK1.8之后就被废弃了,使用元空间(meta space)来替代。
 6. 运行时的常量池:方法区中的一部分内容。
 7. 直接内存(Direct Memory):堆外内存(并不是由虚拟机直接管理的内存,在Java虚拟机外开辟的一片空间),与Java NIO密切相关。JVM通过堆上的DirectByteBuffer来操作直接内存。
*/

引用指向的对象并不是目标本身,而是一个指针指向对象的实例数据,另一个指针指向方法区中的类型数据(元数据)。如图 所示。Oracle的JVM采用下方的引用方式(使用直接指针的方式)。

使用句柄的好处(引用永远指向他,引用不会跟着修改)

直接指针的好处(在压缩的时候效率比句柄的方式更高)

image-20200216055201640

Java对象内存分配原理与布局

/**
	关于Java对象创建的过程:
	
	new 关键字创建对象的3个步骤:
	1. 在堆内存中创建出对象的实例。
	2. 为对象的实例成员变量赋初值。(调用init方法,进行初始化)
	3. 将对象的引用返回。(将结果返回给栈帧)
	
	对象存储的两种方式(出现两种情况的原因与垃圾收集器密不可分):
	1. 指针碰撞:(前提是堆中的空间通过一个指针进行分割,一侧是已经被占用的空间,另一侧是未被占用的空间)
	2. 空闲列表:(前提是堆内存空间中已被使用与未被使用的空间是交织在一起的,这时虚拟机就需要通过一个列表来记录哪些空间是已被使用的,哪些空间是未被使用的,接下来找出可以容纳下新创建对象的且未被使用的空间,在此空间存放该对象,同时还要修改列表上的记录)
	
	对象在内存中的布局:
	1. 对象头
	2. 实例数据(即我们在一个类中所声明的各项信息,成员变量的信息)
	3. 对齐填充(可选)
	
	引用访问对象的方式:
	1. 使用句柄的方式。
		好处:(引用永远指向他,引用不会跟着修改)
	2. 使用直接指针的方式。(Oracle的JVM使用的方式)
		好处:(在压缩的时候效率比句柄的方式更高)
*/

堆溢出、栈溢出、方法区溢出实例练习

堆溢出举例,实战jvisualvm
// 堆溢出举例 (CPU会起飞)
public class MyTest1{
  psvm{
    List<MyTest1> list = new ArrayList();
    for( ; ; ){
      list.add(new MyTest());
      
   //   System.gc(); // 不建议日常开发中使用。垃圾回收:运行垃圾回收器。虚拟机去尝试回收不再使用的对象。此时就不会报错了,正是由于这一行代码起作用,进行垃圾收集了。
    }
  }
}
// 在 vm options中添加参数:  -Xms5m -Xmx5m -XX:+HeapDumpOnOutOfMemoryError  
// 设置堆内存为5M ,打印出堆溢出的转储文件

image-20200216065542670

对应的JVM的运行信息:类,实例数等信息都可以在这里通过工具查看。

截屏2020-02-16上午7.01.28

我这里jdk8带的jvisualvm打不开,没有跟着练习看一下。

image-20200216082653541

image-20200216070228154

线程栈溢出监控与Jconsole 工具使用介绍
/**
	虚拟机栈溢出 举例:使用递归,可能导致栈溢出。

*/

public class MyTest2 {
    private int length;  //记录一共调用了多少层。
    public void setLength(int length) {
        this.length = length;
    }
    public int getLength() {
        return length;
    }
    public void test(){
        this.length ++;
        test();
    }
    public static void main(String[] args) {
        MyTest2 myTest2 = new MyTest2();
        try{
            myTest2.test();
        }catch(Throwable ex ){
            System.out.println(myTest2.length);
            ex.printStackTrace();
        }
    }
}
// 在 vm options中添加参数:  -Xss160k  // 设置栈的大小为160k(JVM虚拟机要求的最小栈内存为160k)
运行结果如下图。   	

image-20200216095202893

image-20200216095139957

Jvisualvm 监控栈溢出过程。

image-20200216095554940

Jconsole 工具 监控。 也是Oracle自带的。

image-20200216100604706

线程死锁检测与分析工具深度学习
//自己编写一个可以产生死锁的代码。使用Jconsole检测死锁。
public class MyTest3 {
    public static void main(String[] args) {
        new Thread(A::method,"ThreadA").start(); //使用lambda表达式启动线程.
        new Thread(B::method,"ThreadB").start();
    }
}

class A{
    public static synchronized void method(){ // 当方法持有锁时,线程过来时检测的锁是当前class的锁。
        System.out.println("method from A");

        try {
            Thread.sleep(5000);
            B.method();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class B{
    public static synchronized void method(){
        System.out.println("method from B");

        try {
            Thread.sleep(5000);
            A.method();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
ThreadA 和 ThreadB 是main的子线程。
 // 启动程序后,检测死锁,检测到了。结果如下图所示。

使用Jconsole检测死锁的结果。

image-20200216100604706

使用jvisualvm工具检测的结果:

image-20200216102553738

image-20200216102855414

方法区产生内存溢出错误
/**
	方法区产生内存溢出错误
	// 在方法区产生错误是极难的,有两种手段。
	1. 要手动设置元空间的大小,让元空间不会自动扩展。(jdk默认的元空间大小是21M。如果超过了之后,默认的会自动进行垃圾回收,之后会自动扩展元空间的大小。)
	2. 要明确的知道元空间存放的信息。存放的是元信息,并不存放对象实例。对象实例是存在堆上边的。我们可以采取特殊的手段让对象实例存放在元信息上。使用动态代理的方式创建出来的元信息是放在元空间当中的(使用cjlib库来操作的)。
*/

'操作步骤'
// 1. 添加依赖:  cglib:cglib:3.2.8
// 2. 代码如下第一图
// 3. 设置元空间的大小:   VM options : -XX:MaxMetaspaceSize = 10M 
// 4. 输出结果为下方第二图。

image-20200216104133763

image-20200216104328690

使用jconsole来进行检测:

image-20200216104624696

使用jvisoualvm进行检测: 能够明确的看到元空间使用了多少M

image-20200216105008572

元空间的概念

原文链接:https://www.infoq.cn/article/Java-PERMGEN-Removed/

image-20200216105741362

image-20200216105714487

image-20200216105838292

image-20200216105951254

image-20200216110141517

适当监控工具的实践

存在两种工具。(图示化工具能看到的,命令行都可以得到。)

  1. 命令行工具 : jmap ; jstat ; jcmd ;jhat
  2. GUI工具: jconsole ; jvisoualvm ;jmc

jmap 工具介绍

image-20200216113233697

image-20200216112503752

jstat 工具介绍

image-20200216113317660

image-20200216113503081

上图中的MC的作用:当前的元空间的容量

MU:元空间已经被使用的容量

jcmd 命令介绍和 jps命令

jps 命令: 查看当前左右线程的pid .

jcmd 和 jps 配合使用,查看JVM的多种信息 。 PID等

jcmd (从jdk1.7开始新增的命令)命令介绍:

  1. jcmd pid VM.flags : 查看JVM的启动参数
  2. jcmd pid help : 列出当前运行的Java进程可以进行的操作。
  3. jcmd pid help JFR.dump: 查看当前命令的具体操作含义
  4. jcmd pid PerfCounter.pring: 查看JVM性能相关的参数
  5. jcmd pid VM.uptime: 查看 JVM的启动时长
  6. jcmd pid GC.class_histogram : 查看系统当中类的统计信息
  7. jcmd pid Thread.print : 查看当前线程的堆栈信息。
  8. jcmd pid GC.head_dump path : 查看当前堆的信息,并打印到path路径
  9. jcmd pid VM.system_properties : 查看JVM的属性信息。
  10. jcmd pid VM.version: 查看目标进行JVM版本的信息。
  11. jcmd pid VM.command_line : 命令行的参数

jstack pid : 查看或者导出当前线程的堆栈信息。

命令行:jps : 查看PID

image-20200216114145788

jcmd 和 jps 配合使用,查看JVM的多种信息

image-20200216114538731

jmc命令 - 可视化工具

jmc : JAVA Mission Conreol

jfr : java flight recodrding 飞行记录器

我本地不能打开。

image-20200216173617561

image-20200216173652966

jhat 对于堆转储的分析

功能和visualvm类似。涉及到了OQL 对象查询语言,随用随查即可。

image-20200216174439926

来一个系统性的总结与回顾

  • ClassLoader
  • Java字节码
  • JVM的内存空间

2020年02月17日20:58:02 复习结束。开始新的课程啦。

垃圾回收

垃圾回收基础概念介绍

垃圾回收最主要的是在堆中进行的。- - 了解了垃圾回收的原理,才能写出来更高效的代码。

JVM运行时内存数据区域

image-20200217210008919

个人理解:

灰色区域为线程共享的,白色区域为线程隔离。

方法区:存放元数据信息。常量池空间就在这个里边。

本地方法栈:存放navtion的,非Java代表编写的本地方法。

Java虚拟机栈(栈帧):存放对象的引用

堆:创建的对象存储在这里边。

image-20200217210403016

image-20200217210432667

image-20200217210531570

JVM垃圾回收(GC)模型

image-20200217210842193

垃圾判断的算法

image-20200217210856009

引用计数算法

image-20200217210911714

什么是无法解决对象循环引用的问题。

已经没有外部的引用引用这两个对象了,是一个完全孤立的系统,但是这个两个对象又相互引用。所以这两个对象一直无法被回收。

image-20200217211248678

根搜索算法

image-20200217211335917

image-20200217211451800

方法区

image-20200217211546895

image-20200217211619064

image-20200217211839428

JVM常见GC算法

每一种算法都有自己的好处与缺点

image-20200217211903176

新生代,一般使用复制算法。

老年代,一般使用标记算法。

(选取算法的根据是 1、生命周期 2、有一个老年代作为分配担保的后盾 )

标记-清除算法

image-20200217212048099

image-20200217212308066

上图中有引用的如下图所示,绿色的是垃圾回收时不会回收的,红色的是被回收的内容。

image-20200217212442454

执行完一次垃圾回收之后,结果如下图所示。

image-20200217212537982

image-20200217212547976

复制收集算法

image-20200217212646498

image-20200217212851458

image-20200217213143347

image-20200217213229766

复制后的样子:

image-20200217213319395

垃圾回收后的样子

image-20200217213411692

image-20200217213432398

标记-整理算法

image-20200217213853257

​ 示意图:

image-20200217213928751

image-20200217213952602

image-20200217214019697

分代收集算法

image-20200217214129412

image-20200217214324773

image-20200217214508783

image-20200217214550859

2020年02月17日22:05:23 这个时候发生了一个有趣的问题,原本把笔记全部给记录到一块,现在地下的时候卡的要死,现在重新创建了一个文件,完全没有一点问题了。

image-20200217220537756

image-20200217220637780

永久代从jdk8之后就被替换成了元空间。用来存放元信息。

内存结构、分配与回收

内存结构

image-20200217220726901

内存分配

image-20200217220820346

内存回收

image-20200217220851306

强引用 ;软引用 ;弱引用 ; 虚引用

image-20200217220938375

image-20200217221042545

image-20200217221058345

scavenge : 打扫

Full : 完全的。(会导致FW的出现。占用业务线程。 要在开发中完全避免这种GC);

垃圾回收器

image-20200217221326189

image-20200218151805932

Serual收集器

image-20200218151955316

image-20200218152110707

serial: 连续的、

ParNew收集器

image-20200218152220636

image-20200218152409230

Parallel Scavenge 收集器

image-20200218152436140

parallel : 平行的 、 scavenge : 打扫

Serial Old 收集器

image-20200218152647396

Parallel Old 收集器

image-20200218152658893

image-20200218152712997

CMS(Concurrent Mark Sweep)收集器

image-20200218154541565

image-20200218154616399

image-20200218154636363

GC垃圾收集器的JVM参数定义

image-20200218154712889

JAVA内存泄漏的经典原因

image-20200218154742610

对象定义在错误的范围

下图中的names的定义,定义为全局变量,但是只使用了一次。全局变量不会被立马被回收,但是局部变量会立马被回收的。

image-20200218154825601

异常处理不当

image-20200218155024167

image-20200218155134558

集合数据管理不当

image-20200218155209690

垃圾回收日志与算法深度解读

新生代与老年代垃圾收集器实现详解
VM options:
-verbose:gc    	//会输出详细的垃圾回收的日志
-Xms20M  				//堆容量的初始大小
-Xmx20M  				//堆容量的最大大小   -- 两个值一般设置为一样的,这样不会出现抖动的现象。
-Xmn10M  				//新生代的大小是10M  
-XX:+PrintGCDetails 	//打印出垃圾回收详细的信息 
-XX:SurvivorRatio=8 	//eden和 survivor的比例为8:1的比例。	 

//添加上方的VM options 在运行
public class MyTest1 {
    public static void main(String[] args) {
        int size = 1024 * 1024; // 1M 的容量
        byte[] myAlloc1  = new byte[2 * size]; // 创建之后每一个元素都是0
        byte[] myAlloc2  = new byte[2 * size]; // 创建之后每一个元素都是0
        byte[] myAlloc3  = new byte[2 * size]; // 创建之后每一个元素都是0
	// 	byte[] myAlloc4  = new byte[2 * size]; // 创建之后每一个元素都是0   添加上这行代码便可垃圾回收
        System.out.println("hello world");
    }
}
//随着上边数组的参数的变化,结果不一样。
> Task :MyTest1.main()
GC (Allocation Failure) [PSYoungGen: 6815K->544K(9216K)] 6815K->6696K(19456K), 0.0042369 secs] [Times: user=0.04 sys=0.01, real=0.00 secs] 
// GC(分配失败)。  PSYoungGen = Parallel Scavenge收集器;  新生代的空间是9M  、
// 6815 - 544 = 6371执行完gc之后,新生代释放的空间容量
// 6815 - 6696 = 119执行完gc之后,总的堆空间释放的空间容量 
[Full GC (Ergonomics) [PSYoungGen: 544K->0K(9216K)] [ParOldGen: 6152K->6426K(10240K)] 6696K->6426K(19456K), [Metaspace: 2798K->2798K(1056768K)], 0.0043323 secs] [Times: user=0.03 sys=0.00, real=0.00 secs] 
// Full GC(Ergonomics 工程学) , 会让业务线程暂停。    新生代被清空了。老年代不减反增。
  // 对元空间也进行了垃圾回收,但是一个字节都没有被回收。
hello world
Heap            ( 0x00000007bf600000  内存占据的空间地址。)
 PSYoungGen      total 9216K, used 2290K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000) 
  eden space 8192K, 27% used [0x00000007bf600000,0x00000007bf83c8d8,0x00000007bfe00000)
  from space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
  to   space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
 ParOldGen       total 10240K, used 6426K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
 //老年代的空间是10M                                           
  object space 10240K, 62% used [0x00000007bec00000,0x00000007bf246998,0x00000007bf600000)
 Metaspace       used 2805K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 291K, capacity 386K, committed 512K, reserved 1048576K

 数组大小设置的不同,出现的情况不同:                              
 2 2 2 2 的时候出现Full GC 
 2 2 3 3 的时候就没有出现Full GC了。 
 '出现上述情况的原因:当新生代的空间不够完成分配时,直接会在老年代的空间完成分配。  
           
jdk1.8默认的使用的垃圾回收期为:                                  
PSYoungGen: Parallel Scavenge (新生代垃圾收集器)
ParOldGen:Parallel Old  (老年代垃圾收集器)

JVM的默认参数 查看: java -XX:+PrintCommandLineFlags -version

UseParallelGC : 默认的是使用ParallelGC

image-20200218170631737

阈值和垃圾收集器类型对于对象分配的影响实践分析
VM options:
-verbose:gc    	//会输出详细的垃圾回收的日志
-Xms20M  				//堆容量的初始大小
-Xmx20M  				//堆容量的最大大小   -- 两个值一般设置为一样的,这样不会出现抖动的现象。
-Xmn10M  				//新生代的大小是10M  
-XX:+PrintGCDetails 	//打印出垃圾回收详细的信息 
-XX:SurvivorRatio=8 	//eden和 survivor的比例为8:1的比例。	 
-XX:PretenureSizeThreshold=4194304  // 大小的阈值  4M的大小 新建的对象大小超过4M直接在老年代生成
-XX:+UseSerialGC     //阈值的使用,需要搭配串行垃圾的收集器 -不添加此参数,上一个参数不起作用。

/**
PretenureSizeThreshold: 设置对象超过多大时,直接在老年代进行分配空间
*/
// 执行前添加上述参数
public class MyTest2 {
    public static void main(String[] args) {
        int size = 1024 * 1024; // 1M 的容量
        byte[] myAlloc1  = new byte[5 * size]; // 创建之后每一个元素都是0
    }
}

数组的大小设置为5 和8 和10 的结果各不一样。
'使用之前介绍工具,可以查看当前执行的所有的内存分布情况及垃圾回收情况'
System.gc(): 对JVM进行一次 Full GC 。
MaxtenuringThreshold与阈值的动态调整详解
-verbose:gc    	//会输出详细的垃圾回收的日志
-Xms20M  				//堆容量的初始大小
-Xmx20M  				//堆容量的最大大小   -- 两个值一般设置为一样的,这样不会出现抖动的现象。
-Xmn10M  				//新生代的大小是10M  
-XX:+PrintGCDetails 	//打印出垃圾回收详细的信息 
-XX:+PrintCommandLineFlags // 打印命令行的标志,我们自己设置的启动参数
-XX:SurvivorRatio=8 	//eden和 survivor的比例为8:1的比例。	 
-XX:MaxTenuringThreshold=5 //在可以自动调节对象晋升(Promote)到老年代阈值的GC中,设置该阈值的最大值。
-XX:+PrintTenuringDistribution  // 打印出年龄为1的字节,年龄为2的字节,等等

/**
MaxtenuringThreshold作用:在可以自动调节对象晋升(Promote)到老年代阈值的GC中,设置该阈值的最大值。该参数的默认值为15,CMS中默认值为6,G1中默认为15(在JVM中,该数值是由4个bit来表示的,1111,即15)
	
经历了多次GC后,新生代存货的对象会在From Survivor与To Survivor之间来回存放,而这里面的一个前提则是这两个空间有足够的大小来存放这些数据。在GC算法中,会计算每一个对象年龄的大小,如果达到某个年龄后发现总大小已经大于Survivor空间的50%,那么这时候就需要调整阈值,不能再继续等到默认的15次GC后才完成晋升,因为这样会导致Survivor空间不足,所以需要调整阈值,让这些存活对象尽快完成晋升。

*/
 //  执行前添加上述参数
public class MyTest3 {
    public static void main(String[] args) {
        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]; 
        sout("hello world");
    }
}

> 输出结果:
> Task :MyTest3.main()
  // PrintCommandLineFlags 打印命令行的标志,我们自己设置的启动参数
-XX:InitialHeapSize=20971520 -XX:InitialTenuringThreshold=5 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:MaxTenuringThreshold=5 -XX:NewSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintTenuringDistribution -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC 
[GC (Allocation Failure) 
Desired survivor size 1048576 bytes, new threshold 5 (max 5)
[PSYoungGen: 6815K->560K(9216K)] 6815K->6712K(19456K), 0.0036073 secs] [Times: user=0.04 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 560K->0K(9216K)] [ParOldGen: 6152K->6426K(10240K)] 6712K->6426K(19456K), [Metaspace: 2798K->2798K(1056768K)], 0.0032972 secs] [Times: user=0.03 sys=0.00, real=0.00 secs] 
hello world
Heap
 PSYoungGen      total 9216K, used 2290K [ 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 27% used [0x00000007bf600000,0x00000007bf83c8d8,0x00000007bfe00000)
  from space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
  to   space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
 ParOldGen       total 10240K, used 6426K [ 0x00000007bf600000, 0x00000007bf600000)
  object space 10240K, 62% used [0x00000007bec00000,0x00000007bf246998,0x00000007bf600000)
 Metaspace       used 2805K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 291K, capacity 386K, committed 512K, reserved 1048576K

VM options: 
-verbose:gc    	//会输出详细的垃圾回收的日志
-Xms200M  				//堆容量的初始大小
-Xmn50M  				//新生代的大小是50M  
-XX:TargetSurvivorRatio=60  // 当Survivor空间占据了百分之60,将从新计算阈值。
-XX:+PrintTenuringDistribution  // 打印出年龄为1的字节,年龄为2的字节,等等
-XX:+PrintGCDetails 	//打印出垃圾回收详细的信息 
-XX:+PrintGCDateStamps //打印垃圾回收的时间戳
-XX:+UseConcMarkSweepGC  //设置老年代的垃圾回收器  
-XX:+UseParNewGC  //设置新生代的垃圾回收器
-XX:MaxTenuringThreshold=3 //在可以自动调节对象晋升(Promote)到老年代阈值的GC中,设置该阈值的最大值。

// 综合的例子
public class MyTest4 {
    public static void main(String[] args) throws InterruptedException {
        byte[] byte_1  = new byte[512 * 1024];
        byte[] byte_2  = new byte[512 * 1024];

        myGc();
        Thread.sleep(1000);
        System.out.println("1111111这里进行了垃圾回收");

        myGc();
        Thread.sleep(1000);
        System.out.println("2222222这里进行了垃圾回收");

        myGc();
        Thread.sleep(1000);
        System.out.println("3333333这里进行了垃圾回收");

        myGc();
        Thread.sleep(1000);
        System.out.println("4444444这里进行了垃圾回收");

        byte[] byte_3  = new byte[1024 * 1024];
        byte[] byte_4  = new byte[1024 * 1024];
        byte[] byte_5  = new byte[1024 * 1024];

        myGc();
        Thread.sleep(1000);
        System.out.println("55555555这里进行了垃圾回收");

        myGc();
        Thread.sleep(1000);
        System.out.println("66666666这里进行了垃圾回收");

        System.out.println("hello world");
    }

    private static void myGc(){
        // 真正会被收集的数组。方法调用介绍之后就会被垃圾回收。
        for(int i = 0 ;i<40;i++){
            byte[] byteArray = new byte[1024*1024];
        }
    }
}

》输出结果:
> Task :MyTest4.main()
2020-02-18T19:44:39.419-0800: [GC (Allocation Failure) 2020-02-18T19:44:39.419-0800: [ParNew
Desired survivor size 3145728 bytes,' new threshold 3 (max 3)
- age   1:    1339912 bytes,    1339912 total
: 40551K使用空间->1361K垃圾回收后大小(46080K总大小), 0.0014325 secs] 40551K->1361K(199680K), 0.0014891 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
//3145728 bytes = 3M  50*0.1*0.6 = 3M
1111111这里进行了垃圾回收
2020-02-18T19:44:40.430-0800: [GC (Allocation Failure) 2020-02-18T19:44:40.430-0800: [ParNew
Desired survivor size 3145728 bytes,' new threshold 3 (max 3)
- age   1:        584 bytes,        584 total
- age   2:    1338144 bytes,    1338728 total
: 42100K->1409K(46080K), 0.0012810 secs] 42100K->1409K(199680K), 0.0013128 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
2222222这里进行了垃圾回收
2020-02-18T19:44:41.435-0800: [GC (Allocation Failure) 2020-02-18T19:44:41.435-0800: [ParNew
Desired survivor size 3145728 bytes,' new threshold 3 (max 3)
- age   1:         72 bytes,         72 total
- age   2:        584 bytes,        656 total
- age   3:    1337920 bytes,    1338576 total
: 41940K->1439K(46080K), 0.0005344 secs] 41940K->1439K(199680K), 0.0005592 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
3333333这里进行了垃圾回收
2020-02-18T19:44:42.441-0800: [GC (Allocation Failure) 2020-02-18T19:44:42.441-0800: [ParNew
Desired survivor size 3145728 bytes, 'new threshold 3 (max 3)
- age   1:         72 bytes,         72 total
- age   2:         72 bytes,        144 total
- age   3:        584 bytes,        728 total
: 42170K->60K(46080K), 0.0027252 secs] 42170K->1397K(199680K), 0.0027533 secs] [Times: user=0.01 sys=0.01, real=0.00 secs] 
4444444这里进行了垃圾回收
2020-02-18T19:44:43.454-0800: [GC (Allocation Failure) 2020-02-18T19:44:43.454-0800: [ParNew
Desired survivor size 3145728 bytes, 'new threshold 1 (max 3)  // 当前的年龄为1 重新设置了阈值
- age   1:    3145848 bytes,    3145848 total
- age   2:         72 bytes,    3145920 total
- age   3:         72 bytes,    3145992 total
: 40794K->3087K(46080K), 0.0019052 secs] 42131K->4424K(199680K), 0.0019409 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
55555555这里进行了垃圾回收
2020-02-18T19:44:44.465-0800: [GC (Allocation Failure) 2020-02-18T19:44:44.465-0800: [ParNew
Desired survivor size 3145728 bytes,' new threshold 3 (max 3)
- age   1:         80 bytes,         80 total
: 43823K->4K(46080K), 0.0026372 secs] 45160K->4413K(199680K), 0.0026724 secs] [Times: user=0.03 sys=0.00, real=0.00 secs] 
66666666这里进行了垃圾回收
hello world
Heap
 par new generation   total 46080K, used 13912K [0x00000006c3200000, 0x00000006c3200000)
  eden space 40960K,  33% used [0x00000006c0000000, 0x00000006c0d95228, 0x00000006c2800000)
  from space 5120K,   0% used [0x00000006c2800000, 0x00000006c2801020, 0x00000006c2d00000)
  to   space 5120K,   0% used [0x00000006c2d00000, 0x00000006c2d00000, 0x00000006c3200000)
 concurrent mark-sweep generation total 153600K, used 4409K [0x00000006c3200000, 0x00000006cc800000, 0x00000007c0000000)
 Metaspace       used 2806K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 291K, capacity 386K, committed 512K, reserved 1048576K

CMS垃圾回收器(新版本不建议使用)

CMS 垃圾回收器,复杂性比较高。在jdk新版本中不建议被使用。

CMS(Concurrent Mark Sweep):并发标记清除

枚举根节点概念

image-20200218195923581

安全点(Safepoint)概念

image-20200218200138311

程序执行时,并非在所有地方都能停顿下来开始GC,只有在达到安全点时才能暂停。

image-20200218200336027

安全点的选定基本上是以“是否具有让程序长时间执行的特征”为标准来进行选定的。

image-20200218200622418

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

安全区域(Safe Region)概念

image-20200218201116346

image-20200218201032392

CMS垃圾收集器深入详解

CMS收集器,以获取最短回收停顿时间为目标,多数应用于互联网站或者BS系统的服务器端上。

image-20200218201457879

image-20200218201733833

image-20200218202018765

初始标记的时候,会Stop the world , 此时没有用户线程执行。

并发标记的时候,可以与用户线程同时执行。

重新标记的时候,会Stop the world , 此时也没有用户线程执行。

并发清除的时候,可以与用户线程同时执行。

image-20200218202408587

停顿,停顿的是用户线程停顿的少。

image-20200218202435285

image-20200218202845482

CMS收集器收集步骤

![image-20200218203158095](/Users/erwa/Library/Application Support/typora-user-images/image-20200218203158095.png)

  1. 初始标记
  2. 并发标记
  3. 并发预先清理阶段
  4. 并发可丢弃的阶段
  5. 最终的重新标记
  6. 并发清除
  7. 并发重置

出现Concurrent即代表GC线程可以和用户线程同步执行的阶段。

image-20200218203341656

image-20200218203454390

image-20200218203950009

image-20200218204548449

image-20200218204650881

image-20200218204749664

将标记拆分成了前5个阶段。

image-20200218204826808

image-20200218204843337

image-20200218204929489

image-20200218205007371

举例分析CMS垃圾收集器执行过程

-verbose:gc    	//会输出详细的垃圾回收的日志
-Xms20M  				//堆容量的初始大小
-Xmx20M  				//堆容量的最大大小   -- 两个值一般设置为一样的,这样不会出现抖动的现象。
-Xmn10M  				//新生代的大小是10M  
-XX:+PrintGCDetails 	//打印出垃圾回收详细的信息 
-XX:SurvivorRatio=8 	//eden和 survivor的比例为8:1的比例。	 
-XX:UseConcMarkSweepGC		//指定老年代的收集器

/**

*/
 //  执行前添加上述参数
public class MyTest5 {
    public static void main(String[] args) {
        int size = 1024 * 1024; // 1M 的容量
        byte[] myAlloc1  = new byte[4 * size]; 
   		  sout("111111");
        byte[] myAlloc2  = new byte[4 * size];
    	  sout("222222");
        byte[] myAlloc3  = new byte[4 * size];
    	  sout("3333333");
        byte[] myAlloc4  = new byte[2 * size];  // 这个地方设置成4的话,也会报错。
        sout("44444444");
    }
}

》 CMS垃圾收集器 垃圾回收的7步骤。
> Task :MyTest5.main()
111111
[GC (Allocation Failure) [ParNew: 4767K->353K(9216K), 0.0023425 secs] 4767K->4451K(19456K), 0.0023675 secs] [Times: user=0.03 sys=0.00, real=0.00 secs] 
222222
[GC (Allocation Failure) [ParNew: 4607K->437K(9216K), 0.0027693 secs] 8705K->8633K(19456K), 0.0028041 secs] [Times: user=0.03 sys=0.01, real=0.00 secs] 
[GC (CMS Initial Mark) // 第一步
 [1 CMS-initial-mark: 8196K'已使用空间大小'(10240K'老年代总的空间大小')] 12729K(19456K'堆的总空间'), 0.0004582 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[CMS-concurrent-mark-start] // 第二部
3333333
[CMS-concurrent-mark: 0.000/0.000 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] 
[CMS-concurrent-abortable-preclean-start]   // 第四部
[CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (CMS Final Remark)  // 第五步
 	[YG occupancy: 6904 K (9216 K)][Rescan (parallel) , 0.0005171 secs][weak refs processing, 0.0000047 secs][class unloading, 0.0003175 secs][scrub symbol table, 0.0002325 secs][scrub string table, 0.0001332 secs][1 CMS-remark: 8196K(10240K)] 15100K(19456K), 0.0012469 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[CMS-concurrent-sweep-start]  //第六部
44444444
[CMS-concurrent-sweep: 0.000/0.000 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] 
Heap
 par new generation   total 9216K, used 7068K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  80% used [0x00000007bec00000, 0x00000007bf279f40, 0x00000007bf400000)
  from space 1024K,  42% used [0x00000007bf400000, 0x00000007bf46d400, 0x00000007bf500000)
  to   space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
 concurrent mark-sweep generation total 10240K, used 8196K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
 Metaspace       used 2806K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 291K, capacity 386K, committed 512K, reserved 1048576K

G1(Gerbage First Collector)(目前被主流使用)

从JDK9开始,G1已经成为了默认的垃圾收集器。

物理内存中的新生代与老年代被消除。

image-20200219070255660

系统吞吐量几个重要参数:QPS(TPS)、并发数、响应时间

**QPS(TPS):**每秒钟request/事务 数量

并发数: 系统同时处理的request/事务数

响应时间: 一般取平均响应时间

image-20200219070622204

image-20200219070858423

G1有很好的吞吐量和响应能力。

G1收集器的设计目标

image-20200219071134726

强调: GC停顿时间更加可控。

image-20200219071833361

G1可以回收部分老年代,CMS要回收全部的老年代。

所以说G1在停顿时间上更可控。

image-20200219073631465

哇,竟然都学过了。图中的内容也都知道,但是刚拿到这个图的时候不敢去看他。

execution engine : 执行引擎

JIT compiler :即时编译器。

internal : 内部的 native : 本地的

image-20200219073905525

G1收集器堆结构划分

image-20200219074517106

整个堆空间,不做块的区分。就当成是一个堆空间。每一个空间的使用情况都可能是三种情况之一。

image-20200219074937169

对每种分代内存的大小,可以动态变化。

image-20200219075542713

G1 vs CMS

image-20200219075821884

这些差异,源于G1对物理模型设计的变化。

image-20200219080026755

G1重要概念

分区(Region)

image-20200219080118259

image-20200219080219989

G1 : 垃圾优先。 即首先收集垃圾最多的分区。

image-20200219080501983

新生代满的时候,G1 对整个新生代进行回收。

image-20200219080549430

收集集合(CSet)

image-20200219080612134

CSet:一组将要被回收的空间的集合。

已记忆集合(RSet)

image-20200219080727462

image-20200219080856614

RSet 存在的原理及价值。如上图所示,记录了Region中的引用关系。使得垃圾收集器不需要扫描整个堆找谁引用了当前分区中的对象,只需要扫描RSet即可。

image-20200219081057890

Points-into :代表谁指向了我;

Point-out: 我指向了谁

SATB(Snapshot-At-The-Beginning)起始快照

image-20200219081349357

G1垃圾收集器官方文档解读

https://www.oracle.com/technetwork/tutorials/tutorials-1876574.html

G1垃圾收集器深度理论讲解

G1 是将来Oracle HotSpot的未来。

又开始卡了,图片放的太多了。哈哈哈哈。快结束了,坚持住。

image-20200219113229630

G1的适合场景

image-20200219114045574

G1 GC模式

image-20200219114251559

global concurrent marking 全局并行标记

G1只有 Young GC 和 Mixed GC

image-20200219114650369

G1是不提供Full GC的

global concurrent marking

image-20200219114855269

image-20200219114951967

image-2020021911215242055

G1 在运行过程中的主要模式

image-20200219115340259

并发阶段,指的是全局并发标记的阶段

混合模式,指的是 Mixed GC

image-20200219115414291

Mixed GC的参数详解

什么时候发生Mixed GC

由一些参数控制,另外也控制着哪些老年代Region会被选入CSet当中

image-20200219115622574

image-20200219120006252

垃圾占比已经超过此参数设定的值

image-20200219120325434

image-20200219120446748

image-20200219132227828

image-20200219132242454

image-20200219132409136

image-20200219132524622

image-20200219143018081

上图描述了RSet的记录情况,存放各个引用关系。

image-20200219143202741

image-20200219143332001

image-20200219143456786

image-20200219143645002

RSet其实是一个Hash Tabella,key 是引用它的起始地址,value是一个集合,元素是card table 的index。

image-20200219143722790

image-20200219143756710

混合GC, 不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。

image-20200220115905110

image-20200219143913072

image-20200219144024812

三色标记算法(黑白灰)

在进行并发标记时出现的标记错误的情况。而出来的算法

image-20200219144118388

image-20200219144710680

被标记,意思是可达的对象。被标记的对象都不应该被回收掉,都不是垃圾。

没有被标记的才是垃圾。

image-20200219153141626

image-20200219153306004

image-20200219153535527

三色标记算法在并发情况下的漏标问题分析。

image-20200219153753559

image-20200219153817891

image-20200219153905751

image-20200219154029727

此时。C也不应该被垃圾回收。

SATB

image-20200219155752707

image-20200219155817076

image-20200219161048878

image-20200219161124751

image-20200219161837501

image-20200219162107459

image-20200219162224465 image-20200219162630745

image-20200219162652666

image-20200219163318289

灰色移除的目标标记为灰色。黑色引用新产生的对象标记为黑色

image-20200219163437779

误标没什么关系,但是漏标的结果是致命的,影响线程的正确性。

image-20200219163523280

image-20200219164359738

针对于是上一个问题 的解决方案

image-20200219165415827

image-20200219165605864

G1的收集模式

image-20200219165739537

image-20200219165810130

image-20200219170003558

G1的最佳实践

image-20200219171651815

暂停时间设置的太短,就会导致出现G1跟不上垃圾产生的速度,最终退化成Full GC

image-20200219211816198

image-20200219212017987

每天写业务代码,怎么能和别人拉开距离。

确定了一个待研究的主题,对这个主题进行全方面的剖析。高注意力的研究一个主题。

G1回收器日志内容详细分析

VM options: 
  -verbose:gc 
  -Xms10m
  -Xmx10m   	
  -XX:+UseG1GC	//使用G1的垃圾回收器
  -XX:+PrintGCDetails 
  -XX:+PrintGCDateStamps 
  -XX:MaxGCPauseMillis=200  // 指定最大的回收时间

public class MyTest1 {
    public static void main(String[] args) {
        int size = 1024 * 1024; // 1M 的容量
        byte[] myAlloc1  = new byte[size]; // 创建之后每一个元素都是0
        byte[] myAlloc2  = new byte[size]; // 创建之后每一个元素都是0
        byte[] myAlloc3  = new byte[size]; // 创建之后每一个元素都是0
        byte[] myAlloc4  = new byte[size]; // 创建之后每一个元素都是0
        System.out.println("hello world ");
    }
}

> 使用G1之后的打印内容
> Task :MyTest1.main()
2020-02-21T05:25:01.316-0800: [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0012901 secs]  // 针对于年轻代的GC的5个阶段
   [Parallel Time: 0.7 ms, GC Workers: 13]  // 有13个线程进行回收
      [GC Worker Start (ms): Min: 65.2, Avg: 65.3, Max: 65.3, Diff: 0.1]
      [Ext Root Scanning (ms): Min: 0.2, Avg: 0.3, Max: 0.5, Diff: 0.3, Sum: 4.4]//1.根扫描
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]//2.更新RS。 记忆集合
         [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0] //3.处理RS
      [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.1, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 1.7]//4.对象拷贝的时间
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]//5.处理引用队列
         [Termination Attempts: Min: 1, Avg: 2.3, Max: 5, Diff: 4, Sum: 30]
		// 下边三行,总结性的信息
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.4]
      [GC Worker Total (ms): Min: 0.4, Avg: 0.5, Max: 0.6, Diff: 0.1, Sum: 6.5]
      [GC Worker End (ms): Min: 65.8, Avg: 65.8, Max: 65.8, Diff: 0.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.1 ms]  // CT: care table  卡表
   [Other: 0.4 ms]
      [Choose CSet: 0.0 ms]  //选择 回收集合
      [Ref Proc: 0.2 ms] //引用的处理时间
      [Ref Enq: 0.0 ms] 
      [Redirty Cards: 0.1 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.0 ms]
   [Eden: 1024.0K(6144.0K)->0.0B(2048.0K) Survivors: 0.0B->1024.0K Heap: 2663.2K(10240.0K)->2640.1K(10240.0K)] //执行完一个young GC之后的结果汇总。 Eden全部被清空。 一个对象进入Survivors
 [Times: user=0.00 sys=0.00, real=0.00 secs]   
// 并发处理的过程 (比老师学习视频中的过程,少了一次年轻代的收集)
2020-02-21T05:25:01.317-0800: [GC concurrent-root-region-scan-start]
2020-02-21T05:25:01.318-0800: [GC concurrent-root-region-scan-end, 0.0002879 secs]
2020-02-21T05:25:01.318-0800: [GC concurrent-mark-start]
2020-02-21T05:25:01.318-0800: [GC concurrent-mark-end, 0.0000480 secs]
2020-02-21T05:25:01.318-0800: [GC remark 2020-02-21T05:25:01.318-0800: [Finalize Marking, 0.0002741 secs] 2020-02-21T05:25:01.318-0800: [GC ref-proc, 0.0000487 secs] 2020-02-21T05:25:01.318-0800: [Unloading, 0.0003851 secs], 0.0008481 secs]
 [Times: user=0.01 sys=0.00, real=0.00 secs] 
2020-02-21T05:25:01.319-0800: [GC cleanup 4688K->4688K(10240K), 0.0004242 secs]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
hello world 
Heap
  // G1总的大小是10240K   
 garbage-first heap   total 10240K, used 4688K [0x00000007bf600000, 0x00000007bf700050, 0x00000007c0000000)
// Region size 的初始大小为 1024k                                                 
  region size 1024K, 2 young (2048K), 1 survivors (1024K)
 // 元空间的大小                                               
 Metaspace       used 2805K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 291K, capacity 386K, committed 512K, reserved 1048576K

两个问题。

  1. 在Java当中,一个全局变量会被初始化几次。 (问的问题不对。Java当中承担全局变量的是静态变量)> 静态变量只会被初始化一次,在类加载器初始化的时候,静态变量会被首先初始化。

  2. Java当中的泛型是怎么实现的。

强引用,软引用,弱引用,虚引用等概念。

https://www.cnblogs.com/yngjian/p/12059428.html

基础的重要性。

谁都会忘,忘了去复习就好了。自己复习自己的笔记,能够瞬间勾起你的回忆。

学习,为什么不可以去看官方文档学习呢?看不懂英文的吗?

要知道,老师们讲的顺序都是按照官网的顺序来的。

总结,复习。有来有回。复盘。

2020年02月21日07:57:58 整理笔记结束。原来这里边只能有一个一级标题。重新整理了两遍。哈哈哈

原文出处:https://www.cnblogs.com/wobushitiegan/p/12340091.html

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部