今日份学习:JVM和字节码
JVM与字节码原理
Java 1995. Sun micro systems
JLS与JVMS
- Java语言规范 Java Language Specification
- 定义Java语言的语法
- Java编译器:两者之间的桥梁
- Java虚拟机规范 Java Virtual Machine Specification
- 定义字节码如何在JVM中执行
- 定义字节码如何在JVM中执行
JVM的基本结构
- HotSpot JVM:Architecture
JVM的运行时内存区域
-
堆
- 所有对象都在堆上分配
- 只能操作对象的引用(地址)
- 只能控制对象的诞生,但是无法控制它的死亡
-
方法栈
- 线程私有
- 每当一个方法调用发生,方法就入栈一个栈帧
- 每当方法退出(返回或者异常),栈帧就被弹出
- 局部变量在栈帧中分配
- 当栈深度过大时,抛出StackOverflowError异常
-
本地方法栈
-
栈帧
- 局部变量表
- 操作数栈
字节码是如何工作的?
-
方法区
- 被征个虚拟机所共享的Class信息
- 运行时常量池
- Java7之前:永久代(PermGen)
- OutOfMemory:Perm gen
- Java8之后:元空间(Metaspace)
- 元空间和Native共享内存
字节码
字节码的结构
- 查看和阅读字节码
- javap
- 图形化工具:例如Classpy
- 反编译器
字节码的加载和执行
The Java® Virtual Machine Specification — Java SE 8 Edition
JVM看到的major_version比自己的高,会拒绝。UnsupportedClassVersionError:Unsupported major.minor version xxx.
类加载器
- 类加载器打通了代码和数据间的障碍
- 动态加载代码
- 动态生成代码
- 动态替换代码
- 使用场景
- Mock
- AOP
- 热部署/热替换
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Scanner;
class DynamicJson {
// 假设目录中有fastjson-1.2.61.jar和fastjson-1.2.62.jar
public static void main(String[] args)
throws MalformedURLException, IllegalAccessException, IllegalArgumentException, InvocationTargetException,
NoSuchMethodException, SecurityException, ClassNotFoundException, NoSuchFieldException {
System.out.println("输入想要使用的fastjson版本");
Scanner scanner = new Scanner(System.in);
String version = scanner.next();
System.out.println(version);
File jar = new File("fastjson-" + version + ".jar");
if (jar.isFile()) {
ClassLoader cl = new URLClassLoader(new URL[] { jar.toURI().toURL() });
// com.alibaba.fastjson.JSON.toJSONString(Object)
Class jsonClass = cl.loadClass("com.alibaba.fastjson.JSON");
System.out.println(jsonClass.getMethod("toJSONString", Object.class).invoke(null, new ArrayList()));
System.out.println(jsonClass.getField("VERSIION"));
} else {
System.out.println("Not Found");
}
}
}
双亲委派加载模型(Java8)
-
越核心的东西越是由父加载器(层次越高的加载器)加载的。
-
如果加载一个类的时候它的父类还没有被加载,那么先加载它的父类。
为什么需要由双亲委派加载模型?
-
1. 安全性
- 不希望加载一个恶意的字节码
问题:如果我们自己写的java,lang.object是不会执行还是会被已经加载的覆盖?
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
class MyBadClassloadere extends ClassLoader {
public static void main(String[] args) throws ClassNotFoundException {
new MyBadClassloadere().loadClass("text.java");
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
byte[] bytes = Files.readAllBytes(new File(name + ".class").toPath());
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("", e);
}
}
}
结果:
java.lang.SecurityException: Prohibited package name: java.lang
-
2. 正确性
- 类加载也被instanceof所检查
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
class MyBadClassloadere extends ClassLoader {
public static void main(String[] args) throws ClassNotFoundException {
// 这里会显示不能转换,证明不是同一种类
Test test = (Test) new MyBadClassloadere().loadClass(Test.class.getName()).newInstance();
// 这里也能证明它们不是同一种类
Object test = new MyBadClassloadere().loadClass(Test.class.getName()).newInstance();
System.out.println("instanceof test: " + (test instanceof Test));
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
if (name.contains("test")) {
try {
byte[] bytes = Files.readAllBytes(new File("test.class").toPath());
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("", e);
}
} else {
return super.loadClass(name);
}
}
}
JIT(Just in time) Compiler
外界和JVM内部的桥梁
两种执行方式:
-
解释执行
- 方便、跨平台
- 慢
-
编译执行
-
麻烦、不灵活、编译后的代码臃肿
-
快
-
-
选项-XX:+PrintCompilation可以打开编译日志,当JVM对方法进行编译的时候,都会打印一行信息,什么方法被编译了。
现在好像是
- 混合模式?
题外话:编译器的前端和后端
这里的“前端(Front End)”指的是编译器对程序代码的分析和理解过程。它通常只跟语言的语法有关,跟目标机器无关。而与之对应的“后端(Back End)”则是生成目标代码的过程,跟目标机器有关。