作者:小傅哥  2020-12-31 07:57:25
云计算
虚拟化 当学习一个新知识不知道从哪下手的时候,最有效的办法是梳理这个知识结构的脉络信息,汇总出一整张的思维导出。接下来就是按照思维导图的知识结构,一个个学习相应的知识点,并汇总记录。

创新互联公司专注于桥东企业网站建设,响应式网站建设,商城网站定制开发。桥东网站建设公司,为桥东等地区提供建站服务。全流程按需定制开发,专业设计,全程项目跟踪,创新互联公司专业和态度为您提供的服务
本文转载自微信公众号「bugstack虫洞栈」,作者小傅哥 。转载本文请联系bugstack虫洞栈公众号。
目录
一、前言
学习,不知道从哪下手?
当学习一个新知识不知道从哪下手的时候,最有效的办法是梳理这个知识结构的脉络信息,汇总出一整张的思维导出。接下来就是按照思维导图的知识结构,一个个学习相应的知识点,并汇总记录。
就像JVM的学习,可以说它包括了非常多的内容,也是一个庞大的知识体系。例如:类加载、加载器、生命周期、性能优化、调优参数、调优工具、优化方案、内存区域、虚拟机栈、直接内存、内存溢出、元空间、垃圾回收、可达性分析、标记清除、回收过程等等。如果没有梳理的一头扎进去,东一榔头西一棒子,很容易造成学习恐惧感。
如图 24-1 是 JVM 知识框架梳理,后续我们会按照这个结构陆续讲解每一块内容。
图 24-1 JVM 知识框架
二、面试题
谢飞机,小记!,很多知识根本就是背背背,也没法操作,难学!
「谢飞机」:大哥,你问我两个JVM问题,我看看我自己还行不!
「面试官」:啊?嗯!往死了问还是?
「谢飞机」:就就就,都行!你看着来!
「面试官」:啊,那 JVM 加载过程都是什么步骤?
「谢飞机」:巴拉巴拉,加载、验证、准备、解析、初始化、使用、卸载!
「面试官」:嗯,背的挺好!我怀疑你没操作过! 那加载的时候,JVM 规范规定从第几位开始是解析常量池,以及数据类型是如何定义的,u1、u2、u4,是怎么个玩意?
「谢飞机」:握草!算了,告诉我看啥吧!
三、类加载过程描述
图 24-2 JVM 类加载过程
「JVM 类加载过程分为」,加载、链接、初始化、使用和卸载这四个阶段,在链接中又包括:验证、准备、解析。
四、写个代码加载下
JVM 之所以不好掌握,主要是因为不好实操。虚拟机是 C++ 写的,很多 Java 程序员根本就不会去读,或者读不懂。那么,也就没办法实实在在的体会到,到底是怎么加载的,加载的时候都干了啥。只有看到代码,我才觉得自己学会了!
所以,我们这里要手动写一下,JVM 虚拟机的部分代码,也就是类加载的过程。通过 Java 代码来实现 Java 虚拟机的部分功能,让开发 Java 代码的程序员更容易理解虚拟机的执行过程。
1. 案例工程
- interview-24
 - ├── pom.xml
 - └── src
 - └── main
 - │ └── java
 - │ └── org.itstack.interview.jvm
 - │ ├── classpath
 - │ │ ├── impl
 - │ │ │ ├── CompositeEntry.java
 - │ │ │ ├── DirEntry.java
 - │ │ │ ├── WildcardEntry.java
 - │ │ │ └── ZipEntry.java
 - │ │ ├── Classpath.java
 - │ │ └── Entry.java
 - │ ├── Cmd.java
 - │ └── Main.java
 - └── test
 - └── java
 - └── org.itstack.interview.jvm.test
 - └── HelloWorld.java
 
「以上」,工程结构就是按照 JVM 虚拟机规范,使用 Java 代码实现 JVM 中加载 class 文件部分内容。当然这部分还不包括解析,因为解析部分的代码非常庞大,我们先从把 .class 文件加载读取开始了解。
2. 代码讲解
2.1 定义类路径接口(Entry)
- public interface Entry {
 - byte[] readClass(String className) throws IOException;
 - static Entry create(String path) {
 - //File.pathSeparator;路径分隔符(win\linux)
 - if (path.contains(File.pathSeparator)) {
 - return new CompositeEntry(path);
 - }
 - if (path.endsWith("*")) {
 - return new WildcardEntry(path);
 - }
 - if (path.endsWith(".jar") || path.endsWith(".JAR") ||
 - path.endsWith(".zip") || path.endsWith(".ZIP")) {
 - return new ZipEntry(path);
 - }
 - return new DirEntry(path);
 - }
 - }
 
2.2 目录形式路径(DirEntry)
- public class DirEntry implements Entry {
 - private Path absolutePath;
 - public DirEntry(String path){
 - //获取绝对路径
 - this.absolutePath = Paths.get(path).toAbsolutePath();
 - }
 - @Override
 - public byte[] readClass(String className) throws IOException {
 - return Files.readAllBytes(absolutePath.resolve(className));
 - }
 - @Override
 - public String toString() {
 - return this.absolutePath.toString();
 - }
 - }
 
目录形式的通过读取绝对路径下的文件,通过 Files.readAllBytes 方式获取字节码。
2.3 压缩包形式路径(ZipEntry)
- public class ZipEntry implements Entry {
 - private Path absolutePath;
 - public ZipEntry(String path) {
 - //获取绝对路径
 - this.absolutePath = Paths.get(path).toAbsolutePath();
 - }
 - @Override
 - public byte[] readClass(String className) throws IOException {
 - try (FileSystem zipFs = FileSystems.newFileSystem(absolutePath, null)) {
 - return Files.readAllBytes(zipFs.getPath(className));
 - }
 - }
 - @Override
 - public String toString() {
 - return this.absolutePath.toString();
 - }
 - }
 
2.4 混合形式路径(CompositeEntry)
- public class CompositeEntry implements Entry {
 - private final List
 entryList = new ArrayList<>(); - public CompositeEntry(String pathList) {
 - String[] paths = pathList.split(File.pathSeparator);
 - for (String path : paths) {
 - entryList.add(Entry.create(path));
 - }
 - }
 - @Override
 - public byte[] readClass(String className) throws IOException {
 - for (Entry entry : entryList) {
 - try {
 - return entry.readClass(className);
 - } catch (Exception ignored) {
 - //ignored
 - }
 - }
 - throw new IOException("class not found " + className);
 - }
 - @Override
 - public String toString() {
 - String[] strs = new String[entryList.size()];
 - for (int i = 0; i < entryList.size(); i++) {
 - strs[i] = entryList.get(i).toString();
 - }
 - return String.join(File.pathSeparator, strs);
 - }
 - }
 
2.5 通配符类型路径(WildcardEntry)
- public class WildcardEntry extends CompositeEntry {
 - public WildcardEntry(String path) {
 - super(toPathList(path));
 - }
 - private static String toPathList(String wildcardPath) {
 - String baseDir = wildcardPath.replace("*", ""); // remove *
 - try {
 - return Files.walk(Paths.get(baseDir))
 - .filter(Files::isRegularFile)
 - .map(Path::toString)
 - .filter(p -> p.endsWith(".jar") || p.endsWith(".JAR"))
 - .collect(Collectors.joining(File.pathSeparator));
 - } catch (IOException e) {
 - return "";
 - }
 - }
 - }
 
2.6 类路径解析(Classpath)
启动类路径、扩展类路径、用户类路径,熟悉吗?是不经常看到这几句话,那么时候怎么实现的呢?
有了上面我们做的一些基础类的工作,接下来就是类解析的实际调用过程。代码如下:
- public class Classpath {
 - private Entry bootstrapClasspath; //启动类路径
 - private Entry extensionClasspath; //扩展类路径
 - private Entry userClasspath; //用户类路径
 - public Classpath(String jreOption, String cpOption) {
 - //启动类&扩展类 "C:\Program Files\Java\jdk1.8.0_161\jre"
 - bootstrapAndExtensionClasspath(jreOption);
 - //用户类 F:\..\org\itstack\demo\test\HelloWorld
 - parseUserClasspath(cpOption);
 - }
 - private void bootstrapAndExtensionClasspath(String jreOption) {
 - String jreDir = getJreDir(jreOption);
 - //..jre/lib/*
 - String jreLibPath = Paths.get(jreDir, "lib") + File.separator + "*";
 - bootstrapClasspath = new WildcardEntry(jreLibPath);
 - //..jre/lib/ext/*
 - String jreExtPath = Paths.get(jreDir, "lib", "ext") + File.separator + "*";
 - extensionClasspath = new WildcardEntry(jreExtPath);
 - }
 - private static String getJreDir(String jreOption) {
 - if (jreOption != null && Files.exists(Paths.get(jreOption))) {
 - return jreOption;
 - }
 - if (Files.exists(Paths.get("./jre"))) {
 - return "./jre";
 - }
 - String jh = System.getenv("JAVA_HOME");
 - if (jh != null) {
 - return Paths.get(jh, "jre").toString();
 - }
 - throw new RuntimeException("Can not find JRE folder!");
 - }
 - private void parseUserClasspath(String cpOption) {
 - if (cpOption == null) {
 - cpOption = ".";
 - }
 - userClasspath = Entry.create(cpOption);
 - }
 - public byte[] readClass(String className) throws Exception {
 - className = className + ".class";
 - //[readClass]启动类路径
 - try {
 - return bootstrapClasspath.readClass(className);
 - } catch (Exception ignored) {
 - //ignored
 - }
 - //[readClass]扩展类路径
 - try {
 - return extensionClasspath.readClass(className);
 - } catch (Exception ignored) {
 - //ignored
 - }
 - //[readClass]用户类路径
 - return userClasspath.readClass(className);
 - }
 - }
 
2.7 加载类测试验证
- private static void startJVM(Cmd cmd) {
 - Classpath cp = new Classpath(cmd.jre, cmd.classpath);
 - System.out.printf("classpath:%s class:%s args:%s\n", cp, cmd.getMainClass(), cmd.getAppArgs());
 - //获取className
 - String className = cmd.getMainClass().replace(".", "/");
 - try {
 - byte[] classData = cp.readClass(className);
 - System.out.println(Arrays.toString(classData));
 - } catch (Exception e) {
 - System.out.println("Could not find or load main class " + cmd.getMainClass());
 - e.printStackTrace();
 - }
 - }
 
这段就是使用 Classpath 类进行类路径加载,这里我们测试加载 java.lang.String 类。你可以加载其他的类,或者自己写的类
「测试结果」
- [-54, -2, -70, -66, 0, 0, 0, 52, 2, 28, 3, 0, 0, -40, 0, 3, 0, 0, -37, -1, 3, 0, 0, -33, -1, 3, 0, 1, 0, 0, 8, 0, 15, 8, 0, 61, 8, 0, 85, 8, 0, 88, 8, 0, 89, 8, 0, 112, 8, 0, -81, 8, 0, -75, 8, 0, -47, 8, 0, -45, 1, 0, 0, 1, 0, 3, 40, 41, 73, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 59, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 3, 40, 41, 86, 1, 0, 3, 40, 41, 90, 1, 0, 4, 40, 41, 91, ...]
 
这块部分截取的程序运行打印结果,就是读取的 class 文件信息,只不过暂时还不能看出什么。接下来我们再把它翻译过来!
五、解析字节码文件
JVM 在把 class 文件加载完成后,接下来就进入链接的过程,这个过程包括了内容的校验、准备和解析,其实就是把 byte 类型 class 翻译过来,做相应的操作。
整个这个过程内容相对较多,这里只做部分逻辑的实现和讲解。如果读者感兴趣可以阅读小傅哥的《用Java实现JVM》专栏。
1. 提取部分字节码
- //取部分字节码:java.lang.String
 - private static byte[] classData = {
 - -54, -2, -70, -66, 0, 0, 0, 52, 2, 26, 3, 0, 0, -40, 0, 3, 0, 0, -37, -1, 3, 0, 0, -33, -1, 3, 0, 1, 0, 0, 8, 0,
 - 59, 8, 0, 83, 8, 0, 86, 8, 0, 87, 8, 0, 110, 8, 0, -83, 8, 0, -77, 8, 0, -49, 8, 0, -47, 1, 0, 3, 40, 41, 73, 1,
 - 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 59, 1, 0, 20, 40, 41,
 - 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 3, 40, 41, 86, 1, 0, 3,
 - 40, 41, 90, 1, 0, 4, 40, 41, 91, 66, 1, 0, 4, 40, 41, 91, 67, 1, 0, 4, 40, 67, 41, 67, 1, 0, 21, 40, 68, 41, 76,
 - 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 4, 40, 73, 41, 67, 1, 0, 4};
 
java.lang.String 解析出来的字节码内容较多,当然包括的内容也多,比如魔数、版本、类、常量、方法等等。所以我们这里只截取部分进行进行解析。
2. 解析魔数并校验
很多文件格式都会规定满足该格式的文件必须以某几个固定字节开头,这几个字节主要起到标识作用,叫作魔数(magic number)。
例如;
- private static void readAndCheckMagic() {
 - System.out.println("\r\n------------ 校验魔数 ------------");
 - //从class字节码中读取前四位
 - byte[] magic_byte = new byte[4];
 - System.arraycopy(classData, 0, magic_byte, 0, 4);
 - //将4位byte字节转成16进制字符串
 - String magic_hex_str = new BigInteger(1, magic_byte).toString(16);
 - System.out.println("magic_hex_str:" + magic_hex_str);
 - //byte_magic_str 是16进制的字符串,cafebabe,因为java中没有无符号整型,所以如果想要无符号只能放到更高位中
 - long magic_unsigned_int32 = Long.parseLong(magic_hex_str, 16);
 - System.out.println("magic_unsigned_int32:" + magic_unsigned_int32);
 - //魔数比对,一种通过字符串比对,另外一种使用假设的无符号16进制比较。如果使用无符号比较需要将0xCAFEBABE & 0x0FFFFFFFFL与运算
 - System.out.println("0xCAFEBABE & 0x0FFFFFFFFL:" + (0xCAFEBABE & 0x0FFFFFFFFL));
 - if (magic_unsigned_int32 == (0xCAFEBABE & 0x0FFFFFFFFL)) {
 - System.out.println("class字节码魔数无符号16进制数值一致校验通过");
 - } else {
 - System.out.println("class字节码魔数无符号16进制数值一致校验拒绝");
 - }
 - }
 
「测试结果」
- ------------ 校验魔数 ------------
 - magic_hex_str:cafebabe
 - magic_unsigned_int32:3405691582
 - 0xCAFEBABE & 0x0FFFFFFFFL:3405691582
 - class字节码魔数无符号16进制数值一致校验通过
 
3. 解析版本号信息
刚才我们已经读取了4位魔数信息,接下来再读取2位,是版本信息。
魔数之后是class文件的次版本号和主版本号,都是u2类型。假设某class文件的主版本号是M,次版本号是m,那么完整的版本号可以表示成“M.m”的形式。次版本号只在J2SE 1.2之前用过,从1.2开始基本上就没有什么用了(都是0)。主版本号在J2SE 1.2之前是45,从1.2开始,每次有大版本的Java版本发布,都会加1{45、46、47、48、49、50、51、52}
- private static void readAndCheckVersion() {
 - System.out.println("\r\n------------ 校验版本号 ------------");
 - //从class字节码第4位开始读取,读取2位
 - byte[] minor_byte = new byte[2];
 - System.arraycopy(classData, 4, minor_byte, 0, 2);
 - //将2位byte字节转成16进制字符串
 - String minor_hex_str = new BigInteger(1, minor_byte).toString(16);
 - System.out.println("minor_hex_str:" + minor_hex_str);
 - //minor_unsigned_int32 转成无符号16进制
 - int minor_unsigned_int32 = Integer.parseInt(minor_hex_str, 16);
 - System.out.println("minor_unsigned_int32:" + minor_unsigned_int32);
 - //从class字节码第6位开始读取,读取2位
 - byte[] major_byte = new byte[2];
 - System.arraycopy(classData, 6, major_byte, 0, 2);
 - //将2位byte字节转成16进制字符串
 - String major_hex_str = new BigInteger(1, major_byte).toString(16);
 - System.out.println("major_hex_str:" + major_hex_str);
 - //major_unsigned_int32 转成无符号16进制
 - int major_unsigned_int32 = Integer.parseInt(major_hex_str, 16);
 - System.out.println("major_unsigned_int32:" + major_unsigned_int32);
 - System.out.println("版本号:" + major_unsigned_int32 + "." + minor_unsigned_int32);
 - }
 
「测试结果」
- ------------ 校验版本号 ------------
 - minor_hex_str:0
 - minor_unsigned_int32:0
 - major_hex_str:34
 - major_unsigned_int32:52
 - 版本号:52.0
 
4. 解析全部内容对照
按照 JVM 的加载过程,其实远不止魔数和版本号信息,还有很多其他内容,这里我们可以把测试结果展示出来,方便大家有一个学习结果的比对印象。
- classpath:org.itstack.demo.jvm.classpath.Classpath@4bf558aa class:java.lang.String args:null
 - version: 52.0
 - constants count:540
 - access flags:0x31
 - this class:java/lang/String
 - super class:java/lang/Object
 - interfaces:[java/io/Serializable, java/lang/Comparable, java/lang/CharSequence]
 - fields count:5
 - value [C
 - hash I
 - serialVersionUID J
 - serialPersistentFields [Ljava/io/ObjectStreamField;
 - CASE_INSENSITIVE_ORDER Ljava/util/Comparator;
 - methods count: 94
 ()V (Ljava/lang/String;)V ([C)V ([CII)V ([III)V ([BIII)V ([BI)V - checkBounds ([BII)V
 ([BIILjava/lang/String;)V ([BIILjava/nio/charset/Charset;)V ([BLjava/lang/String;)V ([BLjava/nio/charset/Charset;)V ([BII)V ([B)V (Ljava/lang/StringBuffer;)V (Ljava/lang/StringBuilder;)V ([CZ)V - length ()I
 - isEmpty ()Z
 - charAt (I)C
 - codePointAt (I)I
 - codePointBefore (I)I
 - codePointCount (II)I
 - offsetByCodePoints (II)I
 - getChars ([CI)V
 - getChars (II[CI)V
 - getBytes (II[BI)V
 - getBytes (Ljava/lang/String;)[B
 - getBytes (Ljava/nio/charset/Charset;)[B
 - getBytes ()[B
 - equals (Ljava/lang/Object;)Z
 - contentEquals (Ljava/lang/StringBuffer;)Z
 - nonSyncContentEquals (Ljava/lang/AbstractStringBuilder;)Z
 - contentEquals (Ljava/lang/CharSequence;)Z
 - equalsIgnoreCase (Ljava/lang/String;)Z
 - compareTo (Ljava/lang/String;)I
 - compareToIgnoreCase (Ljava/lang/String;)I
 - regionMatches (ILjava/lang/String;II)Z
 - regionMatches (ZILjava/lang/String;II)Z
 - startsWith (Ljava/lang/String;I)Z
 - startsWith (Ljava/lang/String;)Z
 - endsWith (Ljava/lang/String;)Z
 - hashCode ()I
 - indexOf (I)I
 - indexOf (II)I
 - indexOfSupplementary (II)I
 - lastIndexOf (I)I
 - lastIndexOf (II)I
 - lastIndexOfSupplementary (II)I
 - indexOf (Ljava/lang/String;)I
 - indexOf (Ljava/lang/String;I)I
 - indexOf ([CIILjava/lang/String;I)I
 - indexOf ([CII[CIII)I
 - lastIndexOf (Ljava/lang/String;)I
 - lastIndexOf (Ljava/lang/String;I)I
 - lastIndexOf ([CIILjava/lang/String;I)I
 - lastIndexOf ([CII[CIII)I
 - substring (I)Ljava/lang/String;
 - substring (II)Ljava/lang/String;
 - subSequence (II)Ljava/lang/CharSequence;
 - concat (Ljava/lang/String;)Ljava/lang/String;
 - replace (CC)Ljava/lang/String;
 - matches (Ljava/lang/String;)Z
 - contains (Ljava/lang/CharSequence;)Z
 - replaceFirst (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
 - replaceAll (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
 - replace (Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;
 - split (Ljava/lang/String;I)[Ljava/lang/String;
 - split (Ljava/lang/String;)[Ljava/lang/String;
 - join (Ljava/lang/CharSequence;[Ljava/lang/CharSequence;)Ljava/lang/String;
 - join (Ljava/lang/CharSequence;Ljava/lang/Iterable;)Ljava/lang/String;
 - toLowerCase (Ljava/util/Locale;)Ljava/lang/String;
 - toLowerCase ()Ljava/lang/String;
 - toUpperCase (Ljava/util/Locale;)Ljava/lang/String;
 - toUpperCase ()Ljava/lang/String;
 - trim ()Ljava/lang/String;
 - toString ()Ljava/lang/String;
 - toCharArray ()[C
 - format (Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
 - format (Ljava/util/Locale;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
 - valueOf (Ljava/lang/Object;)Ljava/lang/String;
 - valueOf ([C)Ljava/lang/String;
 - valueOf ([CII)Ljava/lang/String;
 - copyValueOf ([CII)Ljava/lang/String;
 - copyValueOf ([C)Ljava/lang/String;
 - valueOf (Z)Ljava/lang/String;
 - valueOf (C)Ljava/lang/String;
 - valueOf (I)Ljava/lang/String;
 - valueOf (J)Ljava/lang/String;
 - valueOf (F)Ljava/lang/String;
 - valueOf (D)Ljava/lang/String;
 - intern ()Ljava/lang/String;
 - compareTo (Ljava/lang/Object;)I
 ()V - Process finished with exit code 0
 
如果大家对这部分验证、准备、解析,的实现过程感兴趣,可以参照这部分用Java实现的JVM源码:https://github.com/fuzhengwei/itstack-demo-jvm
六、总结
学习 JVM 最大的问题是不好实践,所以本文以案例实操的方式,学习 JVM 的加载解析过程。也让更多的对 JVM 感兴趣的研发,能更好的接触到 JVM 并深入的学习。
有了以上这段代码,大家可以参照 JVM 虚拟机规范,在调试Java版本的JVM,这样就可以非常容易理解整个JVM的加载过程,都做了什么。
如果大家需要文章中一些原图 xmind 或者源码,可以添加作者小傅哥(fustack),或者关注公众号:bugstack虫洞栈进行获取。好了,本章节就扯到这,后续还有很多努力,持续原创,感谢大家的支持!
                标题名称:为了搞清楚类加载,竟然手撸JVM!
                
                本文链接:http://www.csdahua.cn/qtweb/news27/406277.html
            
网站建设、网络推广公司-快上网,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 快上网