Java从编码到执行
Java是一种编译与解释都有的编程语言,其中大部分的代码时通过编译为字节码执行的,而一些特定的次数较多的代码将使用JIT做本地编译,直接交给操作系统调用。
编译器可以修改编译模式,共三种:
-Xmixed
,混合模,开始解释执行,启动速度较快,对热点代码实时检测和编译
什么是JVM
JVM是一种跨语言的平台,是一种规范。可以在JVM上运行的语言多大一百多种。JVM本身不是跨平台的,每种操作系统都有对应的实现。只要编程语言可以编译为class文件,就可以运行在JVM中。
可以使用 java -version
命令查看当前使用的jvm类型,目前市面上主要的集中jvm实现有:
Jrockit,BEA公司,曾号称最快的JVM,现被Oracle收购,合并到Hotspot中
LiquidVM,普通虚拟机运行于操作系统上,而LiquidVM直接运行在物理机上,作为一个操作系统
zing,azul 公司,商业软件,垃圾回收业界标杆(Hotspot参考了该虚拟机,提供了新的ZGC)
Class文件格式
查看Class文件的16进制形式,使用IDEA插件BinEd,打开.class文件并使用如下操作:
有如下类:
复制 package apache;
public class Main {
public Main() {
}
}
他的字节码信息如下;
复制 CA FE BA BE 00 00 00 34 00 10 0A 00 03 00 0D 07 00 0E 07 00 0F 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 12 4C 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65 01 00 04 74 68 69 73 01 00 0D 4C 61 70 61 63 68 65 2F 4D 61 69 6E 3B 01 00 0A 53 6F 75 72 63 65 46 69 6C 65 01 00 09 4D 61 69 6E 2E 6A 61 76 61 0C 00 04 00 05 01 00 0B 61 70 61 63 68 65 2F 4D 61 69 6E 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 00 21 00 02 00 03 00 00 00 00 00 01 00 01 00 04 00 05 00 01 00 06 00 00 00 2F 00 01 00 01 00 00 00 05 2A B7 00 01 B1 00 00 00 02 00 07 00 00 00 06 00 01 00 00 00 07 00 08 00 00 00 0C 00 01 00 00 00 05 00 09 00 0A 00 00 00 01 00 0B 00 00 00 02 00 0C

使用javap命令查看class文件信息
复制 javap -help
用法: javap <options> <classes>
其中, 可能的选项包括:
-help --help -? 输出此用法消息
-version 版本信息
-v -verbose 输出附加信息 #(常用)#
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类
和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的
系统信息 (路径, 大小, 日期, MD5 散列)
-constants 显示最终常量
-classpath <path> 指定查找用户类文件的位置
-cp <path> 指定查找用户类文件的位置
-bootclasspath <path> 覆盖引导类文件的位置
复制 $ javap -v Main.class
Classfile /C:/Users/xxx/Documents/GitHub/notes-java/zookeeper/target/test-classes/apache/Main.class
Last modified 2022-3-28; size 251 bytes
MD5 checksum 85c5689a1e68f826681fab80150694ee
Compiled from "Main.java"
public class apache.Main
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#13 // java/lang/Object."<init>":()V
#2 = Class #14 // apache/Main
#3 = Class #15 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 LocalVariableTable
#9 = Utf8 this
#10 = Utf8 Lapache/Main;
#11 = Utf8 SourceFile
#12 = Utf8 Main.java
#13 = NameAndType #4:#5 // "<init>":()V
#14 = Utf8 apache/Main
#15 = Utf8 java/lang/Object
{
public apache.Main();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lapache/Main;
}
SourceFile: "Main.java"
使用IDEA Show ByteCode 查看class文件信息
在IDEA内部配置javap工具
重启IDEA:
类加载机制
一个class文件被初始化到JVM通常要经过下面几个过程:
Linking,链接
Preparation,将class文件的静态变量赋默认值(注意不是代码设置的初始值)
Resolution,class文件中使用的常量池的符号引用转换为内存地址
Initializing,静态变量赋代码设置的初始值,调用静态代码块
1. Loading:ClassLoader
加载Class文件到内存的步骤由ClassLoader来完成:
可以在java代码中通过如下方式查询类加载器之间的关系,以及当前类是由哪个类加载器加载进来的:
复制 HelloWorld h = new HelloWorld();
// 三种类加载器之间的关系
System.out.println(h.getClass().getClassLoader()); //sun.misc.Launcher$AppClassLoader@e2f2a
System.out.println(h.getClass().getClassLoader().getParent()); //sun.misc.Launcher$ExtClassLoader@e7d9f1
System.out.println(h.getClass().getClassLoader().getParent().getParent()); // null,因为BootstrapClassLoader是由C语言实现的
// 获取三种类加载器的加载路径
System.out.println(System.getProperty("sun.boot.class.path")); // BootstrapClassLoader 扫描路径
System.out.println(System.getProperty("java.ext.dirs")); // ExtensionClassLoader 扫描路径
System.out.println(System.getProperty("java.class.path")); // AppClassLoader 扫描路径
双亲委派机制
当一个class需要被load到内存时,会自顶向上寻找class是否已经被load到内存了:CustomClassLoader、AppClassLoader、ExtensionClassLoader、BootstrapClassLoader的顺序
如果某个ClassLoader已经加载这个类,那么会直接返回
如果直到BootstrapClassLoader都没有加载这个类,那么Bootstrap会尝试load这个class
这个load的操作会自顶向上的进行,会以 BootstrapClassLoader、ExtensionClassLoader、AppClassLoader、CustomClassLoader的顺序进行load
双亲委派就是从子到父的查找过程,又有一个从父到子的load过程
为什么需要双亲委派?
主要是为了安全问题:如果没有双亲委派,假设第三方代码重写JDK中的核心类,对引用第三方代码的java程序造成破坏
次要问题:可以解决类加载缓存的问题,放置二次加载损耗资源
使用类加载器
什么时候需要手动加载一个类:
动态代理生成了一个新的class,此时需要程序手动load
目标class在网络上,需要先下载,再手动load
使用加载器加载一个资源为流:
复制 InputStream xx = Main.class.getClassLoader().getResourceAsStream("xx");
loadClass的源码
findInCache -> parent.loadClass -> findClass()
复制 protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 检查目标是否已经被加载,这个过程是调用的native方法
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();
// findClass是一个protected,不同的类加载器有不同的实现
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;
}
}
protected final Class<?> findLoadedClass(String name) {
if (!checkName(name))
return null;
return findLoadedClass0(name);
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
自定义类加载器
继承ClassLoader
类并重写loadClass
方法, 参考类sun.misc.Launcher.AppClassLoader
:
复制 public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) {
String myPath = "file:///Users/xxx/Desktop/" + name.replace(".","/") + ".class";
System.out.println(myPath);
byte[] cLassBytes = null;
Path path = null;
try {
path = Paths.get(new URI(myPath));
cLassBytes = Files.readAllBytes(path);
} catch (IOException | URISyntaxException e) {
e.printStackTrace();
}
Class clazz = defineClass(name, cLassBytes, 0, cLassBytes.length);
return clazz;
}
}
2. Linking
2.1 Verification
验证文件是否符合JVM规定
2.2 Preparation
静态成员变量赋默认值
复制 public class A {
public static int count = 2; // 在该阶段,count的值为0
public static T t = new T(); // 在该阶段,t的值为null
private T(){
count++;
}
}
2.3 Resolution
将类、方法、属性等符号引用解析为直接引用 常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用
3. Initialzing
调用初始化代码
复制 public class A {
public static int count = 2; // 在该阶段count的值为3
public static T t = new T(); // 在该阶段t的值已经被初始化为对象
private T(){
count++;
}
}
指令重排序
有如下代码:
复制 public class Main implements Serializable {
public static void main(String[] args) {
Main m = new Main();
}
}
他的main方法生成的指令如下:
复制 0: new #2 // class apache/Main
3: dup
4: invokespecial #3 // Method "<init>":()V 调用构造器
7: astore_1 // 将内存地址赋予给变量 m
8: return
因为有可能发生指令重排序,他的字节码指令有可能是如下这样的(astore_1 和 invokespecial有可能被重排):
复制 0: new #2 // class apache/Main
3: dup
7: astore_1 // 将内存地址赋予给变量 m
4: invokespecial #3 // Method "<init>":()V 调用构造器
8: return
所以也就有了双重检查锁的问题:
复制 public class S5_DoubleCheckLockImplement {
private static S5_DoubleCheckLockImplement INSTANCE;
private S5_DoubleCheckLockImplement() {
}
public static S5_DoubleCheckLockImplement getInstance() {
if (INSTANCE == null) {
synchronized(S5_DoubleCheckLockImplement.class) {
if (INSTANCE == null) {
INSTANCE = new S5_DoubleCheckLockImplement();
// astore_1 先执行该指令,此时instance已经不是null了
// 其他线程进入该方法,判断INSTANCE != null,直接返回了一个没有调用构造的空对象
// invokespecial 最后调用构造
// 所以需要 volatile 关键字,防止指令重排序
}
}
}
return INSTANCE;
}
}
JMM
Java Memory Model,Java内存模型。
硬件层的数据一致性
因为CPU、内存、磁盘的读取速度各不相同,所以存储器按照他们的速度将其划分为了6个层次。当CPU想要读取某个数据时,会先从寄存器开始读取,依次向下查找;如果找到了,会从下到上依次缓存到更高级别的缓存。是一种多级缓存的机制。