1. java的编译
1.1 JAVA到底是编译执行还是解释执行?
java有即时编译器提供即时编译技术支持,被称为JIT(Java Just Time complier)。
默认的情况下,JAVA使用混合编译
模式:即混合使用解释器+热点代码编译。
JAVA首先会把class文件以解释模式来执行,在执行过程中发现有一些代码的执行频率比较高,会使用JIT把这块代码直接编译让CPU执行。这也是为什么我们有时候会发现程序第一次执行时很慢,但是第二次执行或者过一段时间之后就会变快的原因。
热点代码检测的机制:
- 多次被调用的方法(方法计数器)
- 多次被调用的循环(循环计数器)
1.2 JVM启动参数指定编译模式
| 参数 | 说明 | | —- | —- | | -Xint | 纯解释模式。启动很快,执行稍慢 | | -Xmixed | 混合模式。 启动适中,执行适中。 | | -Xcomp | 纯编译模式。启动很慢,执行很快。 | | -XX:CompileThreshold=10000 | 检测热点代码的执行次数阈值,超过此阈值则会被编译成为本地代码 |
1.3 为什么不直接使用执行效率最高的编译模式?
- 现在的JVM执行效率已经很高了。
纯解释模式在类多的时候执行太慢, 纯编译模式在类多的时候编译太慢, 所以JVM的默认编译模式取折中的混合编译模式。
1.4 小程序
分别制定不同的编译模式查看执行结果
public class T009_WayToRun {
public static void main(String[] args) {
long start = System.currentTimeMillis();
m();
long end = System.currentTimeMillis();
System.out.println(end - start);
long start1 = System.currentTimeMillis();
m();
long end1 = System.currentTimeMillis();
System.out.println(end1 - start1);
}
public static void m() {
for(long i=0; i<10000_0000L; i++) {
long j = i%3;
}
}
//纯解释模式结果-Xint
//210444 212055
//混合模式结果(-Xmixed -XX:CompileThreshold=100000)
//4816 2583
//纯编译模式结果 -Xcomp
//2712 2598
}
2.class加载
大概来讲class文件加载分为
loading
、linking
、initializing
三个过程。
1.1 Loading(双亲委派)
1.1.1 java加载器类别
Java的加载器类别自上而下共有4类加载器,分别为:
- BootstrapClassLoader(启动类加载器)
- ExtClassLoader (标准扩展类加载器)
- AppClassLoader(系统类加载器)
- CustomClassLoader(用户自定义类加载器)
|
BootstrapClassLoader
|c++
编写,加载java
核心库java.*
。比如String。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作 | | :—- | —- | |ExtClassLoader
|java
编写,加载扩展库 jre/lib/ext/*.jar | |AppClassLoader
| 加载classpath
下的内容, 我们一般自己写的代码都由AppClassLoader
加载 | |CustomClassLoader
|java
编写,用户自定义的类加载器,可加载指定路径的class
文件 |
1.1.2 双亲委派
java加载类的机制为双亲委派机制。大概流程如下
点击查看【processon】
由于加载器从低级到高级分别为BootstrapClassLoader、ExtClassLoader、AppClassLoader、CustomClassLoader。而且每个加载器所负责加载的类也不一样。因此不同类型的类需要每个加载器去加载。过程如下:
- 一个新的class请求被加载
- 首先由
CustomClassLoader
加载,CustomClassLoader
从自己缓存中找,找到则返回,找不到则委托自己的父级AppClassLoader
去加载。 AppClassLoader
先从缓存中找,找到则返回,找到则返回,找不到则委托自己的父级ExtClassLoader
去加载。ExtClassLoader
先从缓存中找,找到则返回,找到则返回,找不到则委托自己的父级BootstrapClassLoader去加载。BootstrapClassLoader
先从缓存中找,找到则返回,找到则返回,找不到则验证看看是不是属于自己负责加载的类。如果是则加载, 如果不是则委托自己的下一级ExtClassLoader
去加载。ExtClassLoader
验证看看是不是属于自己负责加载的类。如果是则加载, 如果不是则委托自己的下一级AppClassLoader
去加载。AppClassLoader
验证看看是不是属于自己负责加载的类。如果是则加载, 如果不是则委托自己的下一级CustomClassLoader
去加载。CustomClassLoader
验证看看是不是属于自己负责加载的类。如果是则加载, 如果不是则抛出NotClassFoundException。
需要注意的是: 这4个加载器并不存在继承关系。 也就是说:这里面说的子类加载器并不是从父加载器派生的。据个人猜测可能是源码里面有一个类似属性叫做 parent之类的吧。
public class TestClassLoader {
public static void main(String[] args) {
System.out.println(TestClassLoader.class.getClassLoader());
System.out.println(TestClassLoader.class.getClassLoader().getParent());
System.out.println(TestClassLoader.class.getClassLoader().getParent().getParent());
}
}
输出结果:
sun.misc.Launcher$AppClassLoader@18b4aac2 sun.misc.Launcher$ExtClassLoader@4554617c null
一般我们写程序都是放在classpath下面的, 所以我们的TestClassLoader类会被AppClassLoader加载。 多以第一个打印的是AppClassLoader, 第二个打印的是AppClassLoader的parent, 所以是ExtClassLoader。 第三个打印的是AppClassLoader的parent的parent,所以应该是BootstrapClassLoader,但是由于BootstrapClassLoader是C++实现的并非JAVA实现,所以结果是null。
从输出结果看, AppClassLoader和ExtClassLoader和BootstrapClassLoader都属于是sun.misc.Launcher类里面的。 我们打开sun.misc.Launcher看源码会发现这个:
1.1.3 双亲委派机制的作用
- 防止重复加载同一个
.class
。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。 - 保证核心
.class
不能被篡改。通过委托方式,不会去篡改核心.clas
,即使篡改也不会去加载,即使加载也不会是同一个.class
对象了。不同的加载器加载同一个.class
也不是同一个Class
对象。这样保证了Class
执行安全。想象一下,如果没有双亲委派机制, 自己写一个String.class
然后打成Jar包给友商用, 友商引入自己写的String.class
之后会吧他们自己机器的String.class
替换掉,这样太危险。1.1.4 自定义类加载器
自定义类加载器需要继承ClassLoader
类并重写findClass
方法:
先来一个待加载的Person类: ```java package com.wangfan.exercise;
public class Person { public void sayHello(){ System.out.println(“hello!”); } }
此时Person类里面只有一个sayHello方法。把这个类编译成class文件, Person.class, 存放路径为:
> /Users/wangfan/Desktop/com/wangfan/exercise/Person.class
自己写一个加载器并测试Person类:
```java
package com.wangfan.exercise;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* @Author:壹心科技BCF项目组 wangfan
* @Date:Created in 2020/10/28 12:19
* @Project:epec
* @Description:TODO
* @Modified By:wangfan
* @Version: V1.0
*/
public class CustomClassLoader extends ClassLoader {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
ClassLoader customClassLoader = new CustomClassLoader();
Class<?> clazz = customClassLoader.loadClass("com.wangfan.exercise.Person");
Object instance = clazz.newInstance();
Method sayHello = clazz.getMethod("sayHello");
sayHello.invoke(instance);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = null;
InputStream is = null;
ByteArrayOutputStream baos = null;
try {
String filePath = "/Users/wangfan/Desktop/"+name.replace(".","/")+".class";
is = new FileInputStream(new File(filePath));
System.out.println(filePath);
baos = new ByteArrayOutputStream();
int ch = 0;
while (-1 != (ch = is.read())) {
baos.write(ch);
}
data = baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
is.close();
baos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return this.defineClass(name, data, 0, data.length);
}
}
需要注意的是:
- 如果想要自己的Person.class文件生效,就不能把Person.class放在项目路径下面, 因为双亲委派机制的AppClassLoader会加载Person.class。
- 因为JVM规范的要求。Person.class文件要放在它声明import下面。也就是“com/wangfan/exercise” 路径下面。
使用场景:
- class文件加密,防止反编译。
有了自定义的class加载器,我们可以使用密文类型的class文件, 即将java代码便编译成为class文件后再把文件内容进行一次加密,然后我们再该类的自定义加载器里面把class文件解密后再definedClass, 这样可以更安全。保证源码不被反编译。
- 框架反射+代理。
1.2 linking
2.1.1 verification
校验阶段:校验.class是否符合JVM标准。2.1.2 preparation
准备阶段: 主要是给静态变量赋值默认值。2.1.3resolution
将类、方法、符号引用解析为直接引用。
常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用。1.3. initializing
调用类初始化代码, 给静态成员变量赋值初始值。
1.4 JAVA的懒加载与JVM加载规范。
JVM并没有规定java何时被加载,但是严格规定了类什么时候必须初始化。 因此很多JVM实现(比如Hostop)使用懒加载机制加载JAVA类。
JVM和初始化类的规范:
- 执行new、 getstatic、pustatic、invokestatic指令时必须初始化类,访问final变量除外。
java.lang.reflec
对类进行反射时必须初始化。- 初始化子类时父类必须先逐级向上初始化。
- 虚拟机启动时,被执行的主类必须先初始化。
- 动态语言支持
java.lang.invoke.MethodHandle
的解析结果为:REF_getstatic、REF_putstatic、REF_invokestatic的方法句柄时,该类必须初始化。
public class LazyLoading { //严格讲应该叫lazy initialzing,因为java虚拟机规范并没有严格规定什么时候必须loading,但严格规定了什么时候initialzing
public static class P {
final static int i = 8;
static int j = 9;
static {
//在类被加载后会自动执行linking和initicializing,而在initicializing阶段的会执行类的静态块, 所以可以认为执行只要类P被加载就一定会打印“P”
System.out.print("P");
}
}
public static class X extends P {
static {
System.out.print("X");
}
}
public static void main(String[] args) throws Exception {
//P p; //没有任何结果, 因为只是声明了一下, 没有访问任何p的变量或调用p的方法,因此虚拟机不会加载P
//X x = new X(); //结果:PX 因此使用new关键字, 所以需要先加载X的父类P
//System.out.println(P.i); //结果:8 因为使用了p的final属性。所以类P没有被初始化
//System.out.println(P.j); //结果:P9 访问了P的飞final的静态属性, 所以类P会被初始化。
//Class.forName("com.mashibing.jvm.c2_classloader.T008_LazyLoading$P");//结果:P 使用烦着一定会被初始化。
}
}
3. 一道面试题
下面的代码会输出什么?
public class A {
public static void main(String[] args) {
System.out.println(T.I);
}
}
class T {
public static int I = 2;
public static T t = new T();
T(){
I++;
}
}
结果: 3
下面的代码又会输出什么?
public class A {
public static void main(String[] args) {
System.out.println(T.I);
}
}
class T {
public static T t = new T();
public static int I = 2;
T(){
I++;
}
}
结果: 2
为什么T内部两行代码换一换位置输出的结果就不一样的? 构造器里面的函数到底什么时候执行?
原因:
从类的加载过程分析。
loading -> verification -> preparation -> solution -> initializing.
第一种情况:
loading
: 将class文件载入内存。verification
: 检查。preparation
:静态变量赋值默认值 因此,到此时:I = 0,t = null
。solution
:解析。initializing
:静态变量赋值初始值并调用构造方法,因此先I = 2
再I++
T.I = 3
第二种情况:
loading
: 将class文件载入内存。verification
: 检查。preparation
:静态变量赋值默认值 因此,到此时:t = null, I = 0
。solution
:解析。initializing
:静态变量赋值初始值并调用构造方法,因此先执行t = new T()
,会调用T的构造方法。因此此时 I = 1。再执行I的赋值初始值:I = 2
T.I = 2
由此可见, 构造器里面的代码在类初始化时执行。
4. 又一道面试题
下面的DCL单例模式:
//DCL单例(Double Check Lock)
public class DCLSingleton {
private static volatile DCLSingleton INSTANCE = null;//#1
private int i;
private DCLSingleton(){
i = 10;
}
public DCLSingleton getInstance(){
if (INSTANCE == null) { //#2
synchronized (DCLSingleton.class){ //#3
if (INSTANCE == null){ //#4
INSTANCE = new DCLSingleton();//#5
}
}
}
return INSTANCE;
}
}
请问? 上面的INSTANCE
私有静态变量需不需要加volatile
?
答案是需要加的。 为什么呢?
主要是因为CPU发生了指令重排序。 CPU的指令重排序
我们都知道:之所以双检锁单例只要是为了规避懒加载式单例时,有多个不同的线程以极快的速度同时进入 #1 内,造成生产多个实例的问题。
举个例子来详细说一下, 假设线程A先进来经过#1->#2->#3->#4 最终达到 #5 然后开始一些列的类加载与初始化的过程: loading -> verificition -> preparation -> resolution -> initializing。 当进行到preparation就已经分配好了对象的内存空间并赋值了默认值。
接下来initializing要做的就是2步:
- 调用调用类的初始化代码进行初始化。
- 把当前内存空间的指针指向
INSTENCE
很不幸的是, 这个时候CPU发生了指令重排序。所以结果变成了这样:
- 把当前内存空间的指针指向
INSTENCE
- 调用调用类的初始化代码进行初始化。
这个时候假如赶得特别巧,当线程A刚刚执行到第1步, 有一个线程B来了, 进入到了#2这个时候 INSTENCE
就已经不是null了。所以会直接把INSTENCE
返回给其他对象使用。 但是这个时候DCLSingleton由于还没有执行构造器代码,所有里面的变量i是默认值0。这样线程B就拿着0这个值去做运算了。 当然会出错。当然这种情况的出现的概率极低。
我们来总结一下如果出现这种情况需要满足什么条件?
- 线程A在initicalizing时恰好发生了指令重排序
- 线程A在进行initicalizing时恰好有一另一个线程B进入#2
- 线程B进入把半初始化的DCLSingleton.class 返回并使用里面为初始化的变量i进行计算。