本文是《深入理解Java虚拟机(周志明)》这本书的重点摘要。
本笔记仅作为复习,不过多的对内容进行讲解。
本笔记按照书的目录进行,如遇到需要细看的,可以到书中找对应内容。
本笔记并不是按照书中原话进行摘要,而是根据自己的理解使用大白话进行记录,同时进行了少部分扩展。如有错误欢迎指出。
由于内容较多,一共分为三篇:
篇幅 | 链接 |
---|---|
深入理解Java虚拟机(周志明)(1)第一部分 走进Java(2)第二部分 自动内存管理机制 | https://blog.csdn.net/zhaohongfei_358/article/details/134927759 |
深入理解Java虚拟机(周志明)(3)第三部分 虚拟机执行子系统(4)第四部分 程序编译与代码优化 | https://blog.csdn.net/zhaohongfei_358/article/details/135067398 |
深入理解Java虚拟机(周志明)(5)第五部分 高效并发 | https://blog.csdn.net/zhaohongfei_358/article/details/135111650 |
无重点
Java虚拟机只和字节码(也就是class
文件)打交道。因此,JVM可以运行其他语言,例如:Groovy、Jython、Scala、Kotlin等。
它们都是先将各自的代码编译成class文件,然后交给JVM运行即可。
这也是JVM的语言无关性。
而字节码可以拿到任何平台运行,例如Windows、Mac、Linux等,只要该平台有JVM即可。这是Java语言的平台无关性,即“一次编译,处处运行”。
无重点
第6章后续讲解了如何看懂字节码,这部分比较高级,且大多数人用不到,感兴趣可以自行阅读。
虚拟机把class文件记载到内存,并进行校验、解析、初始化等,最终形成可以被虚拟机直接使用的java类,这就是虚拟机的类加载机制。
class文件不一定非要是文件,从网络或其他地方加载的二进制流也可以。
类的生命周期:
加载、连接的发生时机:各JVM实现自行决定
初始化的发生时机:当需要用到该类时(包括访问其属性和方法),主要场景:
new
对象时类加载包括如下动作:
java.lang.Class
对象。(注意:加载过程和后面的验证、准备等动作是交叉进行的,并不是整个做完加载才进行后续验证)类加载可以允许用户在运行时加载类。用户可以实现自己的类加载器。
一个类可以被多个类加载器加载,每个类加载器有自己的独立的类名称空间。这也意味着两个类来源于同一个Class文件,只要加载它们的类加载器不同,那这两个类就必定不相等。
Java提供了三种类加载器:
<JAVA_HOME>/lib
下的类,用户无法直接使用。<JAVA_HOME>/lib/ext
中的类。类加载器的双亲委派模型:下游的类加载应该将类加载动作尽可能的委派给父类,如果父类不能加载(搜索范围内找不到该类),那么子类再做加载工作。(这里上下游并不是继承关系,而是组合关系)。这么做的目的是:一个类尽可能被一个类加载器加载,否则就会出现上面提到的“同一个class文件存在两个不相等的类”的问题。
双亲委派模型只是一个建议,并不强制。
有些情况确实无法遵守双亲委派模型。例如:
无重点
程序运行时,每当进入一个新方法,就会在“栈”中创建一个“栈帧(Stack Frame)”,并放置在栈的最顶层。也就说,栈中最顶层的栈帧存储就是当前运行的方法的数据。
一个栈帧存储了如下数据:
+-x÷
时,用于存放操作数的。例如:执行1+2
的步骤就是,先1,2,3分别入栈,执行+操作,从栈顶取出1,2进行相加操作。(感兴趣可以百度“计算器栈实现”)在Java中,由于部分方法(重写或重载的方法)具体调用哪个方法是在运行时决定的。因此,方法调用是指:确定应该调用哪个方法。
部分方法调用在编译期就可以唯一确定,这类方法的调用称为解析(Resolution)。
符合“编译期可知,运行期不变”要求的方法,主要包括静态方法和私有方法两大类。
对于多态方法(重载和重写),虚拟机确定其调用有两种方式:静态分派和动态分派。
静态分派:对于静态方法的重载多态,会在编译期给调用的参数确定一个静态类型,以确定一个确定的方法。
样例:
public class Test {
static abstract class Human {}
static class Man extends Human {}
static class Woman extends Human {}
public static void sayHello(Human human) {
System.out.println("Hello, human");
}
public static void sayHello(Man man) {
System.out.println("Hello, man");
}
public static void sayHello(Woman woman) {
System.out.println("Hello, woman");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
sayHello(man);
sayHello(woman);
}
}
输出:
Hello, human
Hello, human
上述代码中的Human
称为静态类型(Static Type)(也称为外观类型(Apparent Type)),Man
则称为实际类型(Actual Type)。
在编译期,静态类型已经可以确定,因此代码编译时该方法的调用也选择sayHello(Human human)
进行调用,而不理会它的实际类型。
也就是说,对于静态分派,要调用哪个方法在编译期就已经确定下来了。
动态分派:如果是非静态方法的重写,那就需要在运行期动态确定应该调用哪个方法。
样例:
public class Test2 {
static abstract class Human {
void sayHello() {
System.out.println("Hello, human");
}
}
static class Man extends Human {
@Override
void sayHello() {
System.out.println("Hello, man");
}
}
static class Woman extends Human {
@Override
void sayHello() {
System.out.println("Hello, woman");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
}
}
输出:
Hello, man
Hello, woman
由于调用的是非静态方法,因此会在运行期来确定这两个对象实际是什么类型,然后调用其对应的方法。
动态语言:在运行期对类型进行检查,例如:Javascript,python等。例如,js一个变量可以是任何类型 var a = new Object(); a=3;
,但Java就不行。优点:动态语言灵活性高,开发效率高;缺点:错误不容易在编译期发现,导致代码稳定性差,扩展性也差。
静态语言:在编译期对类型进行检查,例如:Java,C++等。例如:java中Object a = new Object(); a=3
就是非法的。优点:代码稳定性好,很多错误在编译期就可以被发现。缺点:代码不够灵活和简洁。
Java1.7之后,在虚拟机层面对动态语言类型进行了支持,可以使用java.lang.invoke
包来实现。
无重点(较高级)。
虽然类加载大部分过程用户无法通过Java控制(都是由JVM自行完成的),但字节码生成和类加载器用户可以操作,利用这两点就可以玩出很多花样。
多个应用程序会部署在一个Tomcat的服务器上(不过现在都用Springboot了,都是一个程序一个tomcat)。
如果Tomcat只使用一个类加载器可能会引发:两个程序相同路径且相同名称类只加载了一份,但两个类代码又不一样出现问题。
如果Tomcat为每一个应用程序使用完全独立的类加载又会引发:两个程序都使用了Spring3.0,如果将Spring的类加载两份,又太浪费内存资源。
因此,Tomcat采用的方案如下(非常符合上面提到的“双亲委派模型”):
灰色部分为Java提供的类加载器,白色部分为Tomcat自己实现的类加载器。
即:对于公共类(例如:Spring等框架的类),都使用公共的CommonClassLoader
。而对应用程序自己的类,都使用各自独立的WebAppClassLoader
,这样就完美解决了上面两个问题。
OSGi中,没有使用双亲委派模型的“树状结构”,而是采用了更复杂的“网状结构”。
其将代码分成了各个模块(Bundle),每个Bundle有自己的类加载器,Bundle之间会互相依赖。
例如:
例如,该例子中,BundleB模块就依赖BundleA和BundleC中的代码。
在Spring中有大量的代理类,其可以在原类发放的执行前后增加一些逻辑。
使用Java生成代理类的简单样例:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class DynamicProxyTest {
interface IHello {
void sayHello();
}
static class Hello implements IHello {
@Override
public void sayHello() {
System.out.println("hello world");
}
}
// 定义动态代理类
static class DynamicProxy implements InvocationHandler {
Object originalObj; // 原对象
Object bind(Object originalObj) {
this.originalObj = originalObj;
// 返回代理类
return Proxy.newProxyInstance(originalObj.getClass().getClassLoader(),
originalObj.getClass().getInterfaces(),
this);
}
// 使用代理类时,执行的其实是这个方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.print("welcome! "); // 在原来的方法前面增加逻辑
return method.invoke(originalObj, args); // 执行原始方法
}
}
public static void main(String[] args) {
IHello hello = (IHello) new DynamicProxy().bind(new Hello()); // 给Hello绑定代理类
hello.sayHello();
}
}
输出:
welcome! hello world
Java逆向移植(Java Backporting Tools):将高版本JDK编写的代码放到低版本的JDK环境下部署。
Retrotranslator技术就是Java逆向移植工具中较为出色的一个。
无重点,感兴趣可以自己看原文。
编译期优化就是在编译期做的优化操作,例如很多Java新特性都是靠编译期优化实现的,其底层的JVM并没有做任何改变。
Javac编译器是用Java实现的。
原文讲解了调试方法和编译过程。
Java中的泛型只存在编译前,用于对类型进行约束(同时也方便了程序员,不用老是强制类型转换了),使代码更加安全,避免运行时产生类型转换错误。
但是,在编译后泛型会被擦除。
例如:
编译前代码:
List<String> list = new ArrayList<>();
list.add("hello");
String str = list.get(0);
编译后代码:
List list = new ArrayList();
list.add("hello");
String str = (String)list.get(0);
编译后泛型被擦除了。
因此,下面这段代码是无法编译:
public class GenericTypesTest {
public static void method(List<String> list) {} // 编译报错,报存在相同的方法
public static void method(List<Integer> list) {}
}
Java的每一个基本数据类型对应了一个包装类,例如:int
对应Integer
。
当给包装类进行赋值时可以使用基本类型,即编译器会自动将基本数据类型转为包装类,称为“自动装箱”。例如:Integer a = 1;
包装类对象可以像基本数据类型那样使用,例如四则运算。此时会自动将包装类型转为基本数据类型,称为“自动拆箱”。
包装类比较时,遵循如下规则:① 包装类==包装类
,引用比较。② 包装类==基本数据类型
,值比较。
例如:
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
System.out.println(c == 3); // true 值比较
System.out.println(c == d); // true 引用比较(但由于小于255,因此都是一个引用)
System.out.println(e == f); // false 引用比较。不是同一对象
System.out.println(c == (a + b)); // true 值比价,因为a+b后被自动拆箱成了int
System.out.println(c.equals(a + b)); // true 值比较。
System.out.println(g == (a + b)); // true 值比较。(a+b)隐含个一个自动转long
System.out.println(g.equals(a + b)); // Long不能和int比较,因此为false
编译器对if
和循环做了优化。
例如:
int a = 0;
if (true) { a = 1; }
else { a = 2; }
编译后:
int a = 1;
在例如:
while (false) {} // 编译报错,报“语句不可达”。
无重点
该节实战内容:对Javac进行改造,增加了一个对驼峰式命名的校验,若不符合,则编译失败。
JVM运行代码通常都是依赖解释器。
但,JVM中还有一个即时编译器(Just In Time Compiler,JIT),简称为JIT编译器。
它的作用是:将“热点代码”(也就是运行频繁的代码)编译成本地机器码,并进行进一步优化。
本章就是讲这个的。
无重点
优化可能会对你的并发结果产生影响,因此了解怎么优化的还是有必要的。
对于下面这段代码的最后四行,会进行四步优化
static class B {
int value;
final int get() {
return value;
}
}
public void foo() {
int y, z, sum;
B b = new B();
y = b.get();
// ... do something...
z = b.get();
sum = y + z;
}
方法内联优化,优化后:
...
y = b.value; // 这里被优化了
// ... do something...
z = b.value; // 这里被优化了
sum = y + z;
...
公共子表达式消除(Common Subexpression Elimination) 优化:
y = b.value;
// ... do something... // 假设这里没有对b.value进行修改
z = y; // 这里被优化了
sum = y + z;
复写传播(Copy Propagation) 优化:
y = b.value;
// ... do something...
y = y; // 这里被优化了,消除了z
sum = y + y; // 这里被优化了
无用代码消除(Dead Code Elimination) 优化:
y = b.value;
// ... do something...
// y = y; // 这行被删除了
sum = y + y;
公共子表达式消除:若一个计算表达式中出现了公共的表达式,则不会重复计算。
例如:
int d = (c*b) * 12 + a + (a + b*c)
会被优化成:
int E = b*c ; // 这里只是这样写便于理解,但实际并不会真的声明E
int d = E * 12 + a + (a + E); // 即 对于公共表达式“b*c”不会被重复计算两次
// 甚至部分JVM还会进一步优化成:
int d = E * 13 + a * 2;
Java在使用数据取值时,JVM会判断数组是否越界,若越界则报错。但如果每次都检查,那么性能就难免受影响。因此,如果编译时能够确定下来这段不会越界,那么就会不进行越界检查。
方法内联:如果编译期可以根据上下文确定调用的方法,JVM有可能会直接把该方法的内容内联到调用的地方,即不真正的调用方法。
例如:
优化前:
public static void sayHello(String name) {
System.out.println("Hello, " + name);
}
public static void test() {
String name = "张三";
sayHello(name);
// ...
}
优化后:
```java
public static void sayHello(String name) {
System.out.println("Hello, " + name);
}
public static void test() {
String name = "张三";
System.out.println("Hello, " + name); // 这里被优化了
// ...
}
逃逸分析:一种分析手段,为其他优化提供依据。主要就是用来分析一个变量或对象是不是只在某个作用域下有效,这样就可以逃脱掉一些处理,进而优化速度或内存。
主要场景:
public void method() {
Person p = new Person();
p.name = "Amy";
System.out.println(p.name);
}
此时,根据标量替换,就会被优化成:public void method() {
String name = "Amy"; // 并没有创建Person对象
System.out.println(name);
}
JVM的即时编译器和C/C++的静态编译器相比,有如下劣势: