jvm原理以及优化(持续更新)(关键词记忆)

本文采用关键词记忆写法。阅读全文过后,通过回忆关键词代表的那一部分的内容,来快速将这一部分知识记忆在脑海中。
总关键词:
职责、目的、原因、HotSpot、五步、牛反击主动、子引父静、数组类、常量、买房子、四个规范、final特殊、引用的改变、clinit、三固定一自定、报上去批下来、一区一堆两栈一计数器、记行数、四个内存区、native方法、分代收集、不太管、反汇编

关键词:职责
JVM的职责:
类加载(加载字节码文件)
运行时期的内存管理(字节码文件加载到内存中运行)

关键词:目的
学习JVM的目的是为了“优化”

关键词:原因
1、虚拟机帮我们创建对象管理对象等等,把很多底层的东西隐藏了,所以如果要深入学习JAVA,学习JVM是必不可少的。
2、了解JVM是为了写出更适合JVM自优化的代码。

关键词:HotSpot
虚拟机并不是单纯的一款虚拟机,sun公司仅仅公布了虚拟机的规范,市面上还是有很多各种特色的虚拟机的,当你在cmd输入:java –v,会得到下面这个图:
在这里插入图片描述
底下的HotSpot就是比较常见的一种虚拟机。

关键词:五步
类加载机制:
jvm把“描述类的数据”从class文件加载进内存,对数据校验、解析、初始化,最终成为能被jvm用的java类型。
对上面那句话拆解理解:
“class文件加载进内存”==》文件内容转成“流”进入JVM。
“数据校验、解析、初始化”==》让“流”的信息能被处理,到下一步“被使用”。
比如你写了一个“Person p = new Person();”那么这段代码要先“流”进JVM,然后“处理”,最后才能new出一个新对象,也叫作“被使用”。
一般而言一个加载机制要有下面的五步:
加载==》连接(数据验证、解析、默认)==》初始化==》使用==》卸载

关键词:牛反击主动(new、反射、继承、主类、动态语言)
初始化时机:
jvm定义了5种时机需要初始化:
1、关键字new。(在字节码文件中,其实叫做new指令),另外遇到getstatic、putstatic、invokestatic这四条指令时,如果还没有初始化,会触发初始化。
一句话,就是碰到new或者调用静态方法或者字段的时候,会触发初始化。
所以可以类名点静态调用数据。
2、使用反射时如果没初始化先初始化。
3、继承时,初始化一个类,如果父类没初始化,先初始化父类。接口不用,接口用到哪个初始化哪个,不会立马全初始化。
4、最先初始化主类。
5、java7的动态语言支持。一个MethodHandle在解析时,没初始化,先初始化。

关键词:子引父静、数组类(用数组定义类)、常量
上面这5种叫做“主动引用”,还有“被动引用”:
1、通过子类引用父类静态字段,不会导致子类初始化。
举例,孩子没钱,父亲有,孩子花的钱是父亲的,所以钱在用的时候,虽然经过的是孩子的手,但是实际上是属于父亲的,自然不需要孩子“初始化”。
2、通过数组定义引用类,不会触发此类的初始化。
比如“Person[] persons = new Person[10];”这个会加载Person这个类,但是呢生成的是Person的数组,所以并没有初始化这个类。
3、常量(加final)会在编译器,就进入常量池,可以理解成不在类中了,自然不会触发类的初始化。
比如“public static final int count1 = 0;”那么再通过这个类名点count1调用数据,不会初始化这个类。

关键词:买房子
类加载过程:
1、根据类全限定名,比如com.myth.test.Haha,将这个字节码转化为二进制字节流。(有钱买房子)
2、这个流中的数据结构(方法+字段),放进方法区。(买房子)
3、内存中生成一个com.myth.test.Haha.Class对象,用于访问这个类各个数据的入口。(发钥匙)

可以看出,上面只是要求将字节码转化为二进制流,并没有限制这个流来自哪里,怎么获取这个流,这就是“开放性”,比如你可以从war包获取字节流,从动态代理技术获得等等。
但是呢,不可能随便一个字节码文件就能转化为二进制流用吧,这就需要去“验证”,所以上面的“五步”,加载和连接中的验证是交替运行的。

关键词:四个规范
连接——验证:
作用:确定class文件的字节流符合jvm规范,不会危害jvm。
根据jvm规范,验证分为下面4个部分:
1、文件格式验证
2、元数据验证(语法等)
3、字节码验证
4、符号引用验证
jvm参数: -Xverify:none 关闭不必要校验,用编程工具一般可以关闭。

关键词:final特殊、引用的改变
连接——准备和解析
准备阶段就是为“类变量在方法区分配内存并赋默认初始值”,比如int默认先0,后面初始化再编程你定义的。
有一个例外就是用final修饰的,在这个阶段就直接赋值了。
解析阶段就是将常量池中的符号引用替换为直接引用。相当于int a = 1;在之前只是有a这个符号,但是这个阶段后,a就相当于1了。

关键词:clinit
初始化阶段:
真正开始执行java程序代码。
这个阶段要执行jvm的clinit方法,这个方法里放的是这个类的所有变量的赋值动作以及静态代码块。
根据源文件顺序放。如果先赋值再定义比如:

	static {
		i = 0;
		System.out.println(i);
	}
	static int i;

这样只会在打印的时候报错。
因为clinit 包含变量赋值,并且子类初始化前父类先初始化,所以父类中的静态代码块肯定是优于子类的变量赋值的。
这个clinit不是必须的,如果没有静态代码块,就不会生成。
为保证clinit在多线程中一个类只发生一次,jvm已经为我们加锁了等等。只要有一个线程去初始化了这个类,那么其他线程会阻塞。

上面的都了解了,开始一个思考题:

class JvmEg1 {
	public static int count1;
	public static int count2 = 0;
	/**
	 * 简单的单例
	 */
	private static JvmEg1 singTon = new JvmEg1();
	private JvmEg1() {
		count1 ++;
		count2 ++;
	}
	public static JvmEg1 getJvmEg1() {
		return singTon;
	}
	
}
/**
 * 同上面那种只是调换了一下private static JvmEg1 singTon = new JvmEg1();的顺序
 * 但是结果完全不同。
 * 这就涉及到了"clinit"了
 */
//class JvmEg1 {
//	/**
//	 * 简单的单例
//	 */
//	private static JvmEg1 singTon = new JvmEg1();
//	public static int count1;
//	public static int count2 = 0;
//	private JvmEg1() {
//		count1 ++;
//		count2 ++;
//	}
//	public static JvmEg1 getJvmEg1() {
//		return singTon;
//	}
//}

	public static void main(String[] args) {
		//创建
		JvmEg1.getJvmEg1();
		System.out.println(JvmEg1.count1);
		System.out.println(JvmEg1.count2);
	}

第一大段代码注释与未注释的代码,就一行代码不同就是private static JvmEg1 singTon = new JvmEg1();的位置。但是仅仅换了个位置,却是两个结果。
按照上面的“五步”开始分析。
第一步加载:
加载成流,好的,不用管。
第二步:连接。连接分三步,第一小步:验证:
都没问题,下一步
第二小步:解析
好的我把变量都拿出来并设置默认值。
然后进入初始化阶段。
初始化阶段,要用到clinit方法
先看未注释的,clinit方法会将count、count1、 singTon依次放进去初始化,
然后count和count1都是0,singTon初始化,要调用构造,也就是两个++,自然count和count1都变成1了。
再看注释的,clinit会将singTon、count、count1依次放入并依次初始化。(注意,顺序变了)
所以是先对count、count1这俩++,然后都变成1,然后count自己初始化,由于没有设置初始化值,所以还是1,但是count1设置了为0,自然就变为0了,最后结果就是1,0了。

关键词:三固定一自定
类加载器的分类
首先了解什么是类加载器:类加载过程的第一步“根据类全限定名,比如com.myth.test.Haha,将这个字节码转化为二进制字节流”,这一步拿出来,让代码控制,这个代码就是类加载器。
类加载器的分类:内部加载器(启动类加载器),外部加载器(独立于虚拟机外部,继承自java.lang.ClassLoader)

启动类加载器:将在<JAVA_HOME>/lib下的jar等启动。
拓展类加载器:<JAVA_HOME>/lib/ext下的(有的没有这个文件夹)。
应用程序加载器:负责加载用户类路径上指定的类库,开发者能直接用的。
用户自定义加载器:用户自己定义的需要加载的类。

这么多加载器,加载同一个类就会浪费资源,并且一个类被不同加载器加载会产生不同的对象,自然就不安全了。所以有了“双亲委派模型”。

关键词:报上去批下来
双亲委派模型:
举例说明:A类要在用户自定义加载器中加载,用户自定义加载器不会立马加载,而是将A类告诉应用程序加载器,应用程序加载器再将A告诉拓展类加载器,拓展类加载器再将A告诉启动类加载器,至此,已经到了顶层。启动类加载器,一扫描,A不归我加载,所以我不加载,我给拓展类加载器,拓展类一看,也不行,再给应用程序加载器,也不行,最后回到用户自定义加载器,ok,行,加载,这样就保证了只有一次加载。如果上面能“截胡”加载,就不会再往下传,自然也不会重复加载。

可以看一下加载器的源码:
ClassLoader.class:

    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 {
                        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();
                    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;
        }
    }

开始加锁:synchronized
下一行注释:First, check if the class has already been loaded,就是先看这个类有没有被加载
if (c == null) 就是没被加载
if (parent != null)就是当前加载器如果还有“父加载器”,那么让父加载器再调用一下loadClass这个方法,然后一直这样递归,自然就能到顶层加载器了。
后面else就是到了顶层加载器了,ok,尝试加载一下就是findBootstrapClassOrNull方法
加载不行,下面的if (c == null)执行,就相当于父类加载器没加载,子类加载器调用findClass方法。

如果想破坏双亲委任机制,就是想按照自己的想法去加载类(不常见),有以下两种方式:
自定义加载器,然后复写loadClass方法。
使用线程的上下文加载器对象。

关键词:一区一堆两栈一计数器
运行时数据区
jvm管理当前应用的内存(内存分配和内存销毁)
在这里插入图片描述
线程隔离的数据区的意思就是:每一个线程都有他的虚拟机栈、本地方法栈、程序计数器。
jvm其实就是模拟的计算机的环境。jvm会去管理这个应用的内存。

关键词:记行数
程序计数器:
可以简单理解成执行的代码的行数。就比如在线程中,A线程执行到第5行,B线程抢占,B线程执行完,A要从第6行开始。这个行数的计算,就是靠程序计数器。
这个区域很小,所以没规定oom,不会内存溢出。

关键词:四个内存区
虚拟机栈:
描述的是方法执行的内存管理。
会放这个方法的局部变量表(局部变量和方法的参数)、操作数栈(对数据的操作比如写入或者提取,就是出入栈)、动态链接(比如Person p;p.work();这个p就相当于一个符号引用,用这个p去调用work方法,至于这个p其实本身是没有意义的,jvm会将这个符号引用转为直接引用,到时就相当于直接指到work方法上了)、方法出口(方法返回)等。
栈里的东西,用完就出栈(销毁),所以垃圾回收不考虑这里。对执行引擎来说,无论你栈中有多少方法,只有位于栈顶的是有效的(所以方法用完就出栈)。
编译代码时,栈大小确定了,后期不会因为变量改变而改变栈大小。

关键词:native方法
本地方法栈:
服务于虚拟机使用的native方法(java调用非java代码的接口)

关键词:分代收集
堆:
jvm管理的内存的最大的一块。主要放对象和数组。堆里面的块块可以不连续。
回收:分代收集算法(新生代、年老代、永久代)。

关键词:不太管
方法区:
存类信息、常量、静态变量、即时编译后的代码(比较常用的代码)
java8之后,字符串常量池移动到堆里。因为分代收集算法将方法区看做“永久代”。永久代其实相当于对这个区域,jvm不怎么管。这里的回收比较少。
运行时常量池:
属于方法区一部分,存放编译期(运行时)各种字面量和符号引用。内存有限。

直接内存:
非虚拟机运行时数据区的部分。

关键词:反汇编
javap –c(这个命令能将class文件反汇编)
javap –c空格然后拖进来要反汇编的class文件

我们先创建一个简单的java文件:

public class JvmEg2 {
	public int name() {
		int a = 100;
		int b = 200;
		int c = 300;
		return (a + b) * c;
	}
}

然后去项目的lib里面找到其class文件。
然后用javap –c空格然后拖进来要反汇编的class文件

即可出现:

Compiled from "JvmEg2.java"
public class 虚拟机.JvmEg2 {
  public 虚拟机.JvmEg2();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method java/lang/Object."<init>":
()V
       4: return

  public int name();
    Code:
       0: bipush        100
       2: istore_1
       3: sipush        200
       6: istore_2
       7: sipush        300
      10: istore_3
      11: iload_1
      12: iload_2
      13: iadd
      14: iload_3
      15: imul
      16: ireturn
}

第一行:Compiled from “JvmEg2.java”
代表从JvmEg2.java编译来的这个class文件
第二三行就是类名和构造器。
剩下的要读,直接搜索“虚拟机字节码指令”。
比如:想知道 sipush的意思,一对照就知道这个是“将一个常量加载到操作数栈”。
最终这样看:

在这里插入图片描述
第一个“栈顶”指的是“操作数栈”。因为这是方法的code。
后面的“第二个本地变量”是因为每个局部变量表中,第一个永远是“this”。
后面的所有变量都要从第二个开始。

(后面对照下图看)第一行就是在右边操作数栈,栈顶放一个100
然后左边第2个存一个100
然后右边栈顶放个200,100去下面
然后左边第3个存个200
然后右边栈顶放个300,那俩去下面
然后左边第4个存个300
然后将左边第2个和第3个放到右边栈顶第1位和第2位(因为这俩要先计算)
然后将两个数相加后的300放到右边栈顶
然后将左边第4个放到右边栈顶
然后两个300相乘得到90000放到右边栈顶
然后返回这个9000。

上面的执行完应该这样:
在这里插入图片描述
由上也能看出,所有的对数据的操作是在操作数栈的,而局部变量表一旦定义完就是放在那边用于取数据的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值