字节码
多语言混合编程
就是我们编写系统的时候,可以使用好多的语言,比如python了,java了什么的,但是最后这些语言都会被翻译成字节码,无论是什么语言,最终翻译成的字节码都是一样的,最后这些字节码再由java虚拟机运行。
虚拟机与Java虚拟机的介绍
所谓虚拟机(Virtual Machine), 就是一台虚拟的计算机、它是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机和程序虛拟机。
java虚拟机作用
Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行。
每一条Java指令,Java虚拟机规范中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里。
java虚拟机特点

类装载器子系统把字节码文件装载到内存中,并生成Class对象,涉及到加载,链接,初始化。
多个线程共享方法区与堆,而至于Java栈,本地方法栈,程序计数器,都是每个线程私有的。
操作系统只能解释机器指令,但是我们的字节码并不等同于机器指令,还必须要把字节码翻译成机器指令,操作系统才能识别。而我们的执行引擎的作用主要就是把字节码翻译成机器指令。

只要让字节码文件符合jvm规范,就可以在jvm上解释执行,即使它不是Java语言开发的,只要这个语言最后生成的字节码是正确的就可以,如下图:

JVM架构模型
Java编译器输入的指令流基本上是一种基于栈的指令集架构,另外一种指令集架构则是基于寄存器的指令集架构。Hotspot虚拟机只有一种寄存器:pc寄存器,所以它是基于栈的指令集架构。具体来说:这两种架构之间的区别:
栈式架构是零地址指令,每八位进行对齐,每八位为基本单位。指令集更小,但是指令更多;寄存器架构它是每16位为基本单位(双字节方式进行对齐,也就是每16位对齐)
基于栈的计算流程(以Java虚拟机为例)
下面有个2+3的java代码,如下:
public class StackStruTest {//这是java源文件,字节码文件我们程序员看不懂,需要进行反编译
public static void main(String[] args) {
int i = 2;
int j = 3;
int k = i + j;
}
}
在idea中运行StackStruTest.java类生成字节码文件StackStruTest.class,生成的字节码文件会被保存到target目录里面,如下图:

然后打开test路径对应的终端,如下图:

最后需要对字节码文件StackStruTest.class进行反编译,反编译的命令是javap -v 字节码文件名字,如下图:

反编译结果如下图:

基于寄存器的计算流程
只需要两行代码,如下:
mov eax,2 //将eax寄存器的值设为2
add eax,3 //使eax寄存器的值加3
总结
由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
虚拟机的启动
java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的
具体实现指定的。(由jvm规范定义的,具体实现由具体的jvm虚拟机实现)。
虚拟机的执行
虚拟机的退出
有如下几种情况:

热点代码:当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会把这些代码认定为“热点代码”,为了提高其执行效率,在运行时,虚拟机会把这些代码编译成与本地平台相关的机器码,并放在方法区缓存起来。

SUN公司的HotSpot VM
HotSpot历史
最初由一家名为“Longview Technologies”的小公司设计。
1997年, 此公司被Sun收购;2009年,Sun公司被甲骨文收购。
JDK1.3时, HotSpot VM成为默认虚拟机
目前Hotspot占有绝对的市场地位,称霸武林。
不管是现在仍在广泛使用的JDK6,还是使用比例较多的JDK8中, 默认的虚拟机都是HotSpot。
Sun/Oracle JDK和OpenJDK的默认虚拟机。
因此本课程中默认介绍的虚拟机都是HotSpot,相关机制也主要是指HotSpot的GC机制。(比如其他两个商用虚拟机都没有永久代的概念)
从服务器、桌面到移动端、嵌入式都有应用。
名称中的HotSpot指的就是它的热点代码探测技术。
通过计数器找到最具编译价值代码,触发即时编译或栈上替换。
通过编译器与解释器协同工作,在最优化的程序响应时间与最佳执行性能中取得平衡。
在jdk7及以前,习惯上把方法区称为永久代,jdk8开始,使用元空间取代了永久代。不过元空间与永久代最大的区别在于:元空间不再虚拟机设置的内存中,而是使用本地内存。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。

内存结构三层总体划分图,如下图:

三层划分结构细节,如下图:

首先来看张图片,如下图:

类加载器ClassLoader角色
如下图:

类加载过程
public class HelloLoader {
public static void main(String[] args) {
System.out.println("谢谢 ClassLoader 加载我....");
System.out.println("你的大恩大德,我下辈子再报!");
}
}
HelloLoader 的加载过程:
如下图:

完整的类的加载过程
加载----->链接(验证----->准备------>解析)------>初始化
如下图:

加载过程:
加载.class文件的方式
链接过程 (分为三个子阶段:验证—> 准备----->解析)
验证(Verify):
目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

所有的能够被java虚拟机识别的字节码文件,它的开头全都是CAFE BABE。
准备(Prepare):
为类变量分配内存并且设置该类变量的默认初始值,即零值。
这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
例 变量a在准备阶段会赋初始值,但不是1,而是0,在初始化阶段会被赋值为 1
public class HelloApp {
private static int a = 1; //prepare:a = 0 ---> initial : a = 1
public static void main(String[] args) {
System.out.println(a);
}
}
解析(Resolve):
初始化
注意这里的初始化并不是指我们类中的构造方法,而是字节码反编译之后的构造器方法,如下图:

public class DeadThreadTest {
public static void main(String[] args) {
Runnable r = () -> {
System.out.println(Thread.currentThread().getName() + "开始");
DeadThread deadThread = new DeadThread();
System.out.println(Thread.currentThread().getName() + "结束");
};
Thread t1 = new Thread(r, "线程1");
Thread t2 = new Thread(r, "线程2");
t1.start();
t2.start();
}
}
class DeadThread {
static {
if(true) {
System.out.println(Thread.currentThread().getName() + "初始化当前类");
while(true) {
}
}
}
}
输出结果如下:

可以发现只有线程1可以初始化我们的DeadThread类,这是因为当线程1初始化我们的类的时候,有一个死循环;而由于虚拟机必须保证一个类的()方法在多线程下被同步加锁,因此这个时候其它线程就不能再初始化DeadThread类。这个点开发中注意,一定要避免,否则可能会有隐含的错误。
字节码反编译软件Bytecode Viewer
如下图:

首先在idea里面运行ClassInitTest类生成字节码文件,如下图:

接着把字节码文件拖到Bytecode Viewer反编译字节码的软件中,可以看到字节码的反编译结果,如下图:

idea中自带的字节码反编译插件

首先先编译我们的java文件,如下图:

编译以后再View的地方会看到一个反编译字节码的标签,如下图:

然后就能够看到我们的字节码的反编译的结果了,如下图:


代码示例,如下:
public class ClassLoaderTest {
public static void main(String[] args) {
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader); //sun.misc.Launcher$AppClassLoader@18b4aac2
//获取系统类加载器的上层加载器(注意这里只是上层而不是父类没有继承关系):扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader); //sun.misc.Launcher$ExtClassLoader@1b6d3586
//获取其上层,这里获取不到,因为扩展类加载器的上层是Bootstrap引导类加载器,但是引导类加载器并没有继承ClassLoader抽象类
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader); //null
//对于用户自定义类来说:当加载这个类的字节码的时候,默认是使用系统类加载器进行加载
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader); //sun.misc.Launcher$AppClassLoader@18b4aac2
//String类使用引导类加载器进行加载的。 ---> Java核心类库都是使用引导类加载器进行加载的
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1); //null
}
}
运行结果如下图:

几种加载器类型
1)启动类加载器也叫做引导类加载器Bootstrap ClassLoader
2)扩展类加载器Extension ClassLoader
3)应用程序类加载器System ClassLoader
代码如下:
public class ClassLoaderTest1 {
public static void main(String[] args) {
System.out.println("************启动类加载器************");
//获取BootstrapClassLoader能够加载类的路径
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL element : urLs) {
System.out.println(element.toExternalForm());
}
//获取扩展类加载器能够加载类的路径
System.out.println("************扩展类加载器************");
String extDirs = System.getProperty("java.ext.dirs");
System.out.println(extDirs);
}
}
运行结果如下图:

用户自定义的类加载器
在Java的日常应用程序开发中,类的加载几乎是由上节3种类加载器(引导、扩展和系统类加载器)相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。
为什么要自定义类加载器?
用户自定义类加载器实现步骤:
代码如下:
public class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
//将路径下的文件以流的形式存入到内存中
byte[] result = getClassFromCustomPath(name);
if (result == null) {
throw new FileNotFoundException();
} else {
//defineClass和findClass搭配使用
return defineClass(name, result, 0, result.length);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
throw new ClassNotFoundException(name);
}
//自定义流的获取方式
private byte[] getClassFromCustomPath(String name) {
//从自定义路径中加载指定类:细节略
//如果指定路径的字节码文件进行了加密,则需要在此方法中进行解密操作。
return null;
}
}
关于ClassLoader
ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器),以下这些方法都不是抽象方法,可以具体的实现,如下图:

关于获取ClassLoader的途径,如下图:

我们可以根据代码示例体会看一下,如下:
public class ClassLoaderTest2 {
public static void main(String[] args) {
try {
//1.
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
System.out.println(classLoader);
//2.
ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
System.out.println(classLoader1);
//3.
ClassLoader classLoader2 = ClassLoader.getSystemClassLoader().getParent();
System.out.println(classLoader2);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
//运行结果如下:
null
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1540e19d
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。
而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
我们使用一个案例引入这个双亲委派机制,我们在自己的src路径下创建自己的java.lang.String类。
public class String {
//
static{
System.out.println("我是自定义的String类的静态代码块");
}
}
这时我们在创建一个新的Test类来引用它,并且看看他的加载器是什么,代码如下:
public class StringTest {
public static void main(String[] args) {
java.lang.String str = new java.lang.String();
System.out.println("hello,atguigu.com");
StringTest test = new StringTest();
System.out.println(test.getClass().getClassLoader());
}
}
//运行结果如下:
hello,atguigu.com
sun.misc.Launcher$AppClassLoader@18b4aac2
我们发现程序并没有输出我们静态代码块中的内容,可见仍然加载的是 JDK 自带的 String 类。
这时我们将代码进行修改一下,再来运行起来看看是怎么样的输出结果,代码如下:
package java.lang;
public class String {
//
static{
System.out.println("我是自定义的String类的静态代码块");
}
//错误: 在类 java.lang.String 中找不到 main 方法
public static void main(String[] args) {
System.out.println("hello,String");
}
}
//运行结果如下:
错误:在类java.lang.String中找不到main方法,请将main方法定义为:
public static void main (String[] args)
否则JavaFX 应用程序类必须扩展javafx.application.Application
由于双亲委派机制一直找父类,所以最后找到了Bootstrap ClassLoader,Bootstrap ClassLoader找到的是 JDK 自带的 String 类,在那个String类中并没有 main() 方法,所以就报了上面的错误。
双亲委派机制原理
为什么要有双亲委派机制呢?因为否则可能会影响我们之前编写的代码,比如说之前我们的程序中有很多String类,它使用的都是JDK自带的类。而现在我们自定义了一个String类,如果没有双亲委派机制,我们直接使用了系统类加载器加载,那么之前所有的String类就会出错,因为它就变成了我们自定义的String类了。如果没有双亲委派机制原理我们就会破坏核心的源代码程序。
沙箱安全机制
当我们运行自定义String类main方法的时候出现了报错,这种其实就是沙箱安全机制,不允许你在程序中破坏核心的源代码程序。·