码迷,mamicode.com
首页 > 其他好文 > 详细

JVM看这一篇就够了

时间:2021-04-22 16:00:09      阅读:0      评论:0      收藏:0      [点我收藏+]

标签:demo   classpath   one   位置   ret   额外   指定   做什么   super   

JVM问答

为什么是jvm

首先需要搞明白顺序,是先后的java,才有的jvm,又因为java语言的特性,导致必须出现一个像jvm这样的平台,才能满足java的跨平台特性。所以必须是jvm

为什么是class字节码文件

字节码文件是一个二进制文件,其实无论是什么文件都是二进制文件,class文件是一种遵守了jvm规范的二进制文件,所以可以被jvm解释执行,而这个规范才是字节码文件的文本。出现字节码文件是为了让jvm可以使用,而不是单纯的为了创建而创建。

class文件里面有什么

cafe babe 0000 0034 0010 0a00 0300 0d07
000e 0700 0f01 0006 3c69 6e69 743e 0100
0328 2956 0100 0443 6f64 6501 000f 4c69
6e65 4e75 6d62 6572 5461 626c 6501 0012
4c6f 6361 6c56 6172 6961 626c 6554 6162
6c65 0100 0474 6869 7301 001c 4c63 6f6d
2f63 6f6d 7061 6e79 312f 436c 6173 7343
6f64 6544 656d 6f3b 0100 0a53 6f75 7263
6546 696c 6501 0012 436c 6173 7343 6f64
6544 656d 6f2e 6a61 7661 0c00 0400 0501
001a 636f 6d2f 636f 6d70 616e 7931 2f43
6c61 7373 436f 6465 4465 6d6f 0100 106a
6176 612f 6c61 6e67 2f4f 626a 6563 7400
2100 0200 0300 0000 0000 0100 0100 0400
0500 0100 0600 0000 2f00 0100 0100 0000
052a b700 01b1 0000 0002 0007 0000 0006
0001 0000 0003 0008 0000 000c 0001 0000
0005 0009 000a 0000 0001 000b 0000 0002
000c 

字节码认知

字节码文件就是一个约定,class和JVM约定好在什么位置的什么数据是什么含义,这样JVM就可以解析了

按照class执行原理,字节码中应该有的内容

  • 是一个class文件
  • 记录版本信息
  • 记录类信息
    • 名称
    • 位置
    • 类型,是否抽象
  • 记录类接口与继承信息
  • 记录变量信息
    • 名称
    • 类型
    • 属性,是否时静态变量
    • 是否被finnal修饰
    • 变量的值
  • 记录方法信息
    • 名称
    • 类型
    • 属性,是否是静态方法,是否抽象
    • 是否可以继承
    • 返回值类型
    • 返回值内容
    • 参数类型
    • 参数值
  • 记录
第一步,我们要明白字节码文件的用途,他是为了可以让jvm认识从而编译出来的一种文件,既然需要认识,我们就要标识出来这是一个字节码文件,所以当你用二进制查看class文件的时候,很容易就能看到每个字节码文件的构成
	1.开头部分都是一样,是cafe babe,这是16进制表示,这就是magic Number
	2.然后是Minor version 小的版本号 占两个字节
	3.0034占两个字节是Major Version java8==52
	4.0010是constant_pool_count 常量池计数 16
	**********************
	常量池开始
	**********************
	#1	1.0a是CONSTANT_Methodref_info,该常量存在两个指向,都是两个字节,一个指向0003,指明声明方法的类或者接口(CONSTANT_Class_info),也可以理解为方法的拥有者,另一个指向字段描述符,指向CONSTANT_NameAndType的索引项,这里执行000a,也就是第十个常量。
	#2	2.07是指向类的全限定类名,此处是000e索引位置,占两个字节
	#3	3.07是指向父类的全限定类名,这里是000f索引的位置,占两个字节 
    #4  1.01是CONSTANT_Utf8_info,这里占用一个字节,指向00,内容是<init>
        2.0006是长度,表示字符串长度是6,长度占用两个字节
        3.3c69 6e69 743e是CONSTANT_Utf8_info的内容,这里占用6个字节
	#5	1.01是CONSTANT_Utf8_info占一个字节,内容是,指向的内容是()V
		2.0003是长度
		3.28 2956是内容,占3个字节
	#6	1.01是CONSTANT_Utf8_info占一个字节,内容是,指向的内容是Code
		2.0004是长度
		3.43 6f64 65是内容,占用4个字节
	#7	1.01 内容 LineNumberTable
		2.000f 长度
		3.4c69 6e65 4e75 6d62 6572 5461 626c 65 15个字节
	#8	1.01 内容 LocalVariableTable
		2.0012 长度
		3.4c6f 6361 6c56 6172 6961 626c 6554 6162 6c65 18个字节
	#9	1.01 内容 this
		2.0004 长度
		3.74 6869 73 4字节
	#10	1.01 内容 Lcom/company1/ClassCodeDemo;
		2.001c 长度
		3.4c63 6f6d 2f63 6f6d 7061 6e79 312f 436c 6173 7343 6f64 6544 656d 6f3b 28字节
	#11	1.01 内容 SourceFile
		2.00 0a 长度
		3.53 6f75 7263 6546 696c 65 10字节
	#12	1.01 内容 ClassCodeDemo.java
		2.0012 长度
		3.436c 6173 7343 6f64 6544 656d 6f2e 6a61 7661
	#13	1. 0C CONSTANT_NameAndType 指向类的名字和描述符
		2.0004 类的名字
		3.0005 类的描述符
	#14	1.01 内容 com/company1/ClassCodeDemo
		2.001a 长度
		3.636f 6d2f 636f 6d70 616e 7931 2f43 6c61 7373 436f 6465 4465 6d6f 26字节
	#15	1.01 内容 java/lang/Object
		2.0010 长度
		3.6a 6176 612f 6c61 6e67 2f4f 626a 6563 74 16字节
	*************************
	常量池结束 
	*************************
	1.0021 —— acess_flags 表示访问标识,
		这里的值是许多属性相加的结果
			ACC_PUBLIC 	0X0001 public 属性
			ACC_FINAL	0X0010 是否声明final属性,仅可以作用在类上
			ACC_SUPER	0X0020 在jdk1.2之后都默认为真
			ACC_INTERFACE 0X0200 这是一个接口
			ACC_ABSTRACT	0X0400 这是一个抽象类
			ACC_SYNTHETIC	0X1000 这个类不是用户产生的
			ACC_ANNOTATION 	0X2000 这是一个注解
			ACC_ENUM		0X4000这是一个枚举类
		我们这里是0021说明是0020+0001 即说明使用的类是public属性的
	2.0002 —— this_class 指向常量池中的 #2 占2字节
	3.0003 —— super_class 执行常量池中的 #3 占2字节
    4.00 00 —— interfaces_count 接口数,这里是0 --如果有的话,会指向常量池的某一个值
    5.00 00 —— 变量信息	--如果有的话,会指向常量池的某一个值		
    	这里的值是许多属性相加的结果
			ACC_PUBLIC 	0X0001 public 属性
			ACC_PRIVATE	0X0002 private 属性
			ACC_PRITECTED	0X0004 protected 属性
			ACC_STATIC	0X0008 static 属性
			ACC_FINAL	0X0010 是否声明final属性,
			ACC_VOLATILE 0X0040 volatile属性
			ACC_TRANSIENT 0X0080 这是一个不需要被序列化的
			ACC_SYSTHETIC	0X1000 这是一个加锁的
			ACC_ENUM		0X4000这是一个枚举类
		我们这里是0001说明是0001 即说明使用的变量是public属性的
    6.0001 —— 方法数 这里是1
    7.0001 —— 方法属性,access_flags 2字节
    	这里的值是许多属性相加的结果
			ACC_PUBLIC 	0X0001 public 属性
			ACC_PRIVATE	0X0002 private 属性
			ACC_PRITECTED	0X0004 protected 属性
			ACC_STATIC	0X0008 static 属性
			ACC_FINAL	0X0010 是否声明final属性,
			ACC_SYNCHRONIZED 0X0020 加锁的方法
			ACC_BRIDGE 0X0040 编译器产生的桥接方法
			ACC_VARARGS	0X0080 是否存在可变参数
			ACC_NATIVE	0X0100 这是本地方法
			ACC_ABSTRACT 0X0400 静态方法
			ACC_STRCTFP 0X0800 运算结果精度要求
			ACC_SYNTHETIC 0X1000 编译器生成的方法
		我们这里是0001说明是0001 即说明使用的方法是public属性的
	8.0004 —— 方法名字,指向常量池中的 #4 2字节
	9.0005 —— 方法描述符,参数、返回值信息,指向常量池中 #5  2字节
	10.00 01 —— attibutes_count  2字节
	11 00 06 —— attibutes 指向方法表
	后面的需要我在捋捋才能明白

参考文档:

https://www.jianshu.com/p/8d5cc8ba5e9a

类加载过程

问题导入:我有一个class文件,我也明白这个文件内数据含义,现在怎么执行呢?第一步是什么,第二部是什么。。。。

类加载步骤简单描述

  1. 加载
  2. 连接
    1. 验证
    2. 准备
    3. 解析
  3. 初始化
  4. 使用
  5. 卸载

加载(类加载器)

所有的类都是被类加载器加载到内存里面,这点很重要!!!

类加载器到底是做什么的?

我们上面说到了类的加载步骤,第一步就是将class文件加载到内存,然后通过一个对象去操作这个class文件。从这里我们可以明白,类加载器作业的内容是加载class文件到内存中 ,在内存中指定一个区域产生该class加载区域的对象,这个对象就是这个class内存的入口

类加载器类型

  • bootstrap 加载java核心类,基本都是 lib下面的类
  • extension 加载额外的类,基本都在 jre\lib\ext 文件下的类
  • app 加载classpath中指定的jar包,这就是我们只能加载当前目录和lib目录下jar包的原因。因为其他jar包不存在这些目录里面
  • 自定义类加载器,定义类加载位置,继承classload类

类加载器也是一个类,他们的加载器是bootstrapClassLoad

啥是双亲委派?为什么要用双亲委派?

双亲委派:当一个class文件需要加载到内存中,从上面我们了解到,系统中存在四种类加载器,所以我们针对需要加载的class文件,我们需要使用哪个类加载器呢?上面的类加载器是存在顺序的,当一个class文件被加载到内存,首先是按照按照以下顺序进行交付

? 自定义类加载器 -> appClassload -> extensionClassLoad -> bootstrapClassLoad

如果在上一级加载器可以加载,则直接返回结果,举个例子:

? 如果一个class被加载,首先会交给自定义类加载器,然后上一级,上一级、、、假设这个class文件可以在bootstrapClassLoad加载器中加载,此时就会直接返回结果,而不会再返回到 extensionClassLoad 进行加载。

这个过程实现的原理是每个加载内部有一个ClassLoad 属性的 parent 加载器,当需要加载类时,首先用 parent加载器进行加载

这样做的好处是: 安全

我们考虑没有这个机制的情况,如果没有这个机制,那么我们拿到的类就应该被第一个可以加载的类进行加载,如果有恶意开发者重写了java.lang.String的加载器,那么用户在使用字符串的时候都会使用重写的加载器,系统自带的会被覆盖,开发者通过类加载器拿到用户输入数据是很可怕的一件事。

同时这个方式还避免了重复加载的问题,因为只要有一个加载器加载了文件,其他的都不会在执行。

自定义类加载器

1.首先我们新建一个java类

package com.company1;
public class ClassDemo {
    public void disPlay(){
        System.out.println("Hi,definedLoader");
    }
}

2.编译生成class文件

javac ClassDemo.java

3.编写自定义类加载器

package com.company;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ClassLoaderTest extends  ClassLoader {

    @Override
    public Class<?> findClass(String name){
        //通过包名取得class文件
        String path = name.replace(".","/");
        //加载文件
        File file = new File("D:/Load/"+path+".class");
        try {
            //加载文件流
            FileInputStream fis = new FileInputStream(file);
			//byte数组输出流
            ByteArrayOutputStream bao = new ByteArrayOutputStream();
            int len = -1;
            while((len=fis.read())!=-1)
            {
                bao.write(len);
            }
            byte[] bytes = bao.toByteArray();
            bao.close();
            fis.close();

            return defineClass(name,bytes,0,bytes.length);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }


    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        //初始化自定义类加载器
        ClassLoader cl = new ClassLoaderTest();
        //加载已经被转移到D盘的class文件
        Class clazz = cl.loadClass("com.company1.ClassDemo");
        //获取加载的类对象
        Object classDemo = clazz.newInstance();
        //通过反射获取到对象的方法
        Method method = classDemo.getClass().getMethod("disPlay");
        //执行方法
        method.invoke(classDemo);
        //输出此时的类加载器
        System.out.println(clazz.getClassLoader());
        //输出测试工具类的类加载器
        System.out.println(ClassLoaderTest.class.getClassLoader());
    }
}
//输出结果
Hi,definedLoader
com.company.ClassLoaderTest@4554617c
sun.misc.Launcher$AppClassLoader@18b4aac2

问:使用自定义类加载器加载的类对象不能直接使用,必须使用Object接收,那怎么调用对象的方法呢?

答:通过反射的方式获取对象的方法,然后再执行,以此来避开AppClassLoad加载器

双亲委派怎么实现的?打破双亲委派?

首先我们要知道双亲委派机制是怎么实现的?分析源码我们可以看到

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
    	//类加载过程是会加锁的,避免同时加载
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //如果上层加载器不为空
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //因为BootstrapClassLoadder不存在上层加载器,所以单独考虑
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    //如果上面没能成功加载,调用自己的findClass尝试加载
                    c = findClass(name);
                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

上面就是双亲委派的实现原理,下面我们尝试打破双亲委派,从上面的源码可以很清楚的看到双亲委派主要是 loadClass 方法再起作用,所以如果我们想要打破这个机制,需要重写 loadClass 方法,代码如下

package com.company;

import java.io.*;

public class CLassLoadDemo2 extends ClassLoader {

    @Override
    public Class<?> loadClass(String name)
            throws ClassNotFoundException{
        //通过重写load方法,加载自定义类加载里的findClass打破了系统原有的双亲委派机制
        Class<?> c  =  new ClassLoaderTest().findClass(name);
        return c;
    }

    public static void main(String[] args) throws ClassNotFoundException {
        CLassLoadDemo2 classLoader = new CLassLoadDemo2();
        Class clazz =  classLoader.loadClass("com.company1.ClassDemo");
        Class clazz2 = classLoader.loadClass("com.company1.ClassDemo");
        System.out.println(clazz==clazz2);
    }
}

//输出
false

连接

验证

? 验证阶段也是检查阶段,检查字节码文件是否存在异常,文件格式是否正确等

准备

? 给静态变量赋默认值,例如 int 的默认值 0 等等,

解析

? 将类的符号引用(JVM问答中 #14 1.01 内容 com/company1/ClassCodeDemo )变成直接引用(内存地址)

初始化

? 将静态变量默认值赋初始值

.java -> .class

jvm一般使用的是混合模式编译java文件

? 技术图片

在这种模式下,一般使用解释器执行Java文件,当需要某一部分代码重复执行次数过高后(热点代码:设置属性 -XX: CompileThreshold 默认10000),会将这部分代码编译,提高执行效率,可以通过jvm参数指定mode

	-Xint 解释型
	-Xcomp 编译型

什么时候初始化呢?—— lazing init

在类加载过程我们知道了,一个类被加载到内存是有一系列的过程,那么这一系列的过程是以此完成的吗?答案很显然是否定的,因为在某些情况下,我们不需要初始化就可以使用类内对象了,例如,被 final 和 static 修饰的变量,在连接阶段中的准备阶段就完成赋值,因为这个值是确定的、不可变的。所以不需要初始化就可以赋值。可以得出类是有初始化时机的,总结如下

主动引用(发生初始化)

  • 使用new关键字创建新对象

    public class InitiaRef{
    	public static void main(String[] args){
    		A a = new A();
    	}
    }	
    
    class A{	
    	static {
    		System.out.println("static");
    	}
    }
    
    //结果
    D:\msbLearn\note\note-master\新建文件夹\viewjava>java InitiaRef
    static
    
  • 读取类的静态成员变量

    public class InitiaRef{
    	public static void main(String[] args){
    		int m = A.b;
    	}
    }	
    
    class A{	
    	static int b = 3;
    	static {
    		System.out.println("static");
    	}
    }	
    
    //结果
    D:\msbLearn\note\note-master\新建文件夹\viewjava>java InitiaRef
    static
    
  • 设置类的静态成员变量

    public class InitiaRef{
    	public static void main(String[] args){
    		A.b = 10;
    	}
    }	
    
    class A{	
    	static int b = 3;
    	static {
    		System.out.println("static");
    	}
    }
    
    //结果
    D:\msbLearn\note\note-master\新建文件夹\viewjava>java InitiaRef
    static
    
  • 调用静态方法

    public class InitiaRef{
    	static {
    		System.out.println("static");
    	}
    	public static void main(String[] args){
    	}
    }	
    
    //结果
    D:\msbLearn\note\note-master\新建文件夹\viewjava>java InitiaRef
    static
    
  • 子类初始化会初始化父类

    public class InitiaRef extends A{
    	public static void main(String[] args){
    	}
    }	
    
    class A{	
    	static {
    		System.out.println("static");
    	}
    }
    
    //结果
    D:\msbLearn\note\note-master\新建文件夹\viewjava>java InitiaRef
    static
    
  • 使用反射初始化

    public class InitiaRef{
    	public static void main(String[] args)throws Exception{
    		Class aa = Class.forName("A");
    	}
    }	
    class A{
    	static {
    		System.out.println("static");
    	}
    }
    
    //结果
    D:\msbLearn\note\note-master\新建文件夹\viewjava>java InitiaRef
    static
    

被动引用

  • 通过子类使用父类静态属性(方法和变量),子类不会初始化
//静态变量
public class InitiaRef {
	public static void main(String[] args)throws Exception{
		int m = B.aa;
	}
}	
class A{
	static int aa = 10;
	static {
		System.out.println("static A");
	}
}
class B extends A{
	static {
		System.out.println("static B");
	}
}

//静态方法
public class InitiaRef {
	public static void main(String[] args)throws Exception{
		B.display();
	}
}	
class A{
	static int aa = 10;
	static  void display()  {
		System.out.println("static A");
	}
}
class B extends A{
	static {
		System.out.println("static B");
	}
}

//结果
D:\msbLearn\note\note-master\新建文件夹\viewjava>java InitiaRef
static A
  • 使用数组

    public class InitiaRef {
    	public static void main(String[] args)throws Exception{
    		A[] aa = new A[10];
    	}
    }	
    class A{
    	static int aa = 10;
    	static  {
    		System.out.println("static A");
    	}
    }
    
    //结果 无输出
    
    
  • 最开始提到的无需初始化

    public class InitiaRef {
    	public static void main(String[] args)throws Exception{
    		System.out.println(A.aa);
    	}
    }	
    class A{
    	final static int aa = 10;
    	static  {
    		System.out.println("static A");
    	}
    }
    
    //结果
    D:\msbLearn\note\note-master\新建文件夹\viewjava>java InitiaRef
    10
    

JMM问答

DCL为什么要用volatile?什么是伪共享?缓存模型是什么样子的?缓存一致性怎么实现的?(MESI)

DCL 是什么

DCL意思是 Double Check Lock ,顾名思义,使用这种方式创建的单例模式需要两次检查,并且需要使用volatile关键字修饰类内对象。两次检查的问题我们放到 JUC 在讲,这里重点说一下为什么使用 volatile 关键字,关于这个问题,首先要知道的类初始化时的具体动作:

(1)给新建对象的实例分配内存
(2)调用对象的构造函数,初始化成员字段
(3)将新建对象指向分配的内存空间

背景

在计算机内部,各种元器件的速度差异巨大,例如CPU与加载内存数据等,由于该问题的存在,所以在CPU执行我们的程序的时候会发生指令重拍,简单理解就是CPU 会判定没有关系的两条语句进行优化,假设此时有两条指令,彼此没有引用关系,那么当第一条指令执行期间如果需要等待,例如加载数据之类,CPU会让第二条指令优先执行,用以提升计算机的运行效率

如此,便可以发现,如果上面 (2) ,(3) 两部发生了指令重拍,那么会导致新产生的对象没能成功引用到堆内对象,但是此时的引用已经不再为空,存在安全隐患。

内存模型

计算机内存模型:每个CPU有自己的 L1、L2 Cache 和共有的 L3 Cache ,三级缓存之外连接了内存

技术图片

参考资料:https://blog.csdn.net/weixin_43767015/article/details/104856899

伪共享是什么

了解伪共享首先要知道内存是怎么加载数据到缓存中的。假设我们需要一个元素A,但是CPU并不会只加载A到缓存,这个时候一般都是加载A以及A在内存相邻地址的数据进入缓存,如果存在其他CPU将同一块内存在数据读走,这是两个CPU修改自己的目的数据,但是都会导致对方的一部分缓存失效,最终导致两个CPU都需要频繁的去主存中刷新最新的数据,降低CPU执行效率。

缓存一致性

MESI 协议实现了各个高速Cache之间的一致性约束,协议内容如下:

首字母缩略词MESI中的字母表示可以标记高速缓存行的四种独占状态(使用两个附加位编码):

1、修改(M)

高速缓存行仅存在于当前高速缓存中,并且是脏的 - 它已从主存储器中的值修改(M状态)。在允许对(不再有效)主存储器状态的任何其他读取之前,需要高速缓存在将来的某个时间将数据写回主存储器。回写将该行更改为共享状态(S)。

2、独家(E)

缓存行仅存在于当前缓存中,但是干净 - 它与主内存匹配。它可以随时更改为共享状态,以响应读取请求。或者,可以在写入时将其改变为修改状态。

3、共享(S)

表示此高速缓存行可能存储在计算机的其他高速缓存中并且是干净的 - 它与主存储器匹配。可以随时丢弃该行(更改为无效状态)。

4、无效(I)

表示此缓存行无效(未使用)。

对于任何给定的高速缓存对,给定高速缓存行的允许状态如图1所示。

当块标记为M(已修改)时,其他高速缓存中块的副本将标记为I(无效)。

JVM看这一篇就够了

标签:demo   classpath   one   位置   ret   额外   指定   做什么   super   

原文地址:https://www.cnblogs.com/aierben/p/14686111.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!