前言
- 前端编译,将java文件编译成class文件。
- 我们可以拿着这个文件到各种平台的jvm上运行,这就是java所谓的跨平台的语言。
- 而jvm却也因此可以称为跨语言的平台,因为jvm面对的是class文件,而不是java文件,这意味着任何语言不管kotlin还是scala等,只要能被编译成class文件,jvm就能运行。所以jvm和java可以说没什么关系。
简单的看下jvm的整体架构(取自网络)
Class文件
- 既然jvm面对的是class文件,就需要简单看下class文件的内容到底是什么,那是怎样的格式。
- 其实,class文件本身是一个二进制文件,里面的内容全是二进制的,但是它有固定的格式,比如最开始的两个字节表示模数(java是cafe babe,像其他文件比如jpg、png、txt等都有它们各自的模数,我们通过模数可以知道这是个什么文件),接下来的两个字节表示小版本号,再接下两个字节表示主版本号,我们可以根据这两个版本号可以知道目前我们使用的java版本是多少。还有其他的,比如类名、父类名、常量池信息、接口数、接口列表,方法数、方法列表等信息。
- 具体哪部分对应的是类名,哪部分对应的是接口列表,查官网文档即可。
- 一般我们使用jclasslib这个插件,可以可视化这些信息。而不用汇编、或二进制、十六进制去表现这些信息。也许你可能遇到jclasslib显示乱码,解决方案
方法区
方法区是逻辑上的概念,其落地实现在jdk1.8之前叫永久代,jdk1.8及其之后叫元空间metaspace
其保存在着被加载过的每一个类的信息;
类加载
三个阶段
- 加载loading
- 链接linking(准备、验证、解析)
- 初始化initalizing
- 加载阶段,就是将class文件加载到内存中,这里就要使用类加载器了。
- 链接阶段,分为三个小阶段(准备、验证、解析) ,准备阶段是检查class文件格式是否正确,验证阶段为静态变量赋默认值,一般为0,null,false等,解析阶段将符号引用替换为直接引用。
- 初始化阶段,为静态变量赋我们给的值,执行静态代码块。
有四种加载器,分别是:
- 启动类加载器,BootstrapClassLoader
- 扩展类加载器,ExtClassLoader
- 应用类加载器,AppClassLoader
图示(以下内容都为jdk1.8版本)
- 启动类加载器,可以加载rt.jar或charset.jar等lib里的类
- 扩展类加载器,则是可以加载ext文件夹下的类
- 注意,hot spot是懒加载,这表示不会一次性将加载器可以加载的类全都加载到内存中,而是使用需要用的时候才会进行加载。
图示 jdk1.8:
- 而应用类加载器则是加载在你项目里的输出文件夹下,即你项目编译好后的class文件存放的位置。
图示 java web 的项目
图示 普通java项目
双亲委派
在讲自定义类加载器之前,先说一下双亲委派机制。
- 有一个类需要加载,此时应用类加载器先看看该类有没有加载过,若没有,则先委托器父加载器(即扩展类加载器)加载,扩展类加载器也是同样的步骤,先看看该类有没有加载过,若没有,则先委托器父加载器(即启动类加载器)加载。
- 启动类加载器为顶级父加载器,他不会进行委托,先看看有没有加载,若没有,则尝试加载,然后发现需要加载的类在它搜索的范围即(lib文件夹下)没有,则让扩展类加载器自己加载,而扩展类加载器也没有在它搜索的范围即(/lib/ext文件夹下)搜索到该class文件,则让应用类加载器自己加载,应用类加载器在自己搜索的范围内找到则加载,若每找到则抛异常(ClassNotFount)
- 需注意的是父加载器,不是父类,它们之间没有继承关系,只不过有一个成员变量parent指向了其它类加载器,表示它是自己的父加载器。
- 自定义类加载器(所有类加载必须是ClassLoader的子类,或者其子类的子类等,嗯,启动类加载器是c++写的,不做讨论)
如何自定义类加载器,看一下ClassLoader源码
//当我们调用ClassLoader该方法时,传入类的完全限定名,将进入其重载方法。
public Class<?> loadClass(String name) throws ClassNotFoundException {
return this.loadClass(name, false);
}
//类加载器通过这个方法完成双亲委派模型机制的实现
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized(this.getClassLoadingLock(name)) {
//首先看看是否已经加载过该类了
Class<?> c = this.findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (this.parent != null) {
//若没有加载过,先交给其父加载器加载
c = this.parent.loadClass(name, false);
} else {
c = this.findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException var10) {
}
if (c == null) {
long t1 = System.nanoTime();
//若走到这一步,说明需要本加载器去加载,findClass方法是真正的加载类的方法
c = this.findClass(name);
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
this.resolveClass(c);
}
return c;
}
}
//自定义类加载器,需要做的就是继承ClassLoader类,重写这个方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
- 看看AppClassLoader是如何重写findClass方法,以及如何指定父加载器?
(嗯,看源码的话,AppClassLoader是Launcher的静态内部类,ExtClassLoader也是,所以打印它们的名称时会有个 ‘ $ ’,如sun.misc.Launcher$AppClassLoader@18b4aac2
)
AppClassLoader(URL[] urls, ClassLoader parent) {
//super构造方法指定parent
super(urls, parent, factory);
ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
ucp.initLookupCache(this);
}
- 嗯,AppClassLoader继承了URLClassLoader,而URLClassLoader则是实现了findClass,而AppClassLoader没有重写它。所以看看URLClassLoader是怎么重写ClassLoader的findClass方法的。
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
//找路径
String path = name.replace('.', '/').concat(".class");
//加载至内存,就需获取文件流一样
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
//自定义类加载器,重写方法的时候,必须调用该方法defineClass,它的作用是根据加载到内存中的数据,生成class对象。
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
注意defineClass有许多重载,看看其最终执行的defineClass
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
编译解释执行
接下来讨论以下java的编译执行和解释执行。
我们可以设置jvm纯编译执行(启动慢,因为要编译,但执行快),纯解释执行(启动快,但执行慢,因为要一条一条解释),混合执行(综合效率高,具体是将热点代码即时编译保存着,需要的时候直接执行即可,而出现次数少的代码一条一条解释执行即可,因为java解释器解释执行的速度比直接执行慢不了太多。)
怎么判断热点代码:
有个计数器,当每段代码在一段时间内执行的频率高过阈值,则进行即时编译。