JVM是Java的核心和关键!了解JVM的内存结构,更有利于我们对Java有着更清晰的认识!
1、字节码文件
类加载的是字节码文件。字节码文件可以来自于jar包、war包、或者网络等来源。
字节码文件中包含了字节码对应的类的版本号、父类的全限定名、属性信息(属性名字、属性类型、属性权限(public、private))、方法信息(方法名字、方法参数、方法中的代码、方法异常、方法局部变量表等)、运行时常量池等。对应着这个类的全部信息。
你们可能好奇为什么没有存储属性的值信息呢?因为值信息对应着赋值代码。静态变量赋值代码会在类初始化的时候执行,而普通变量赋值代码会在对象实例化的时候执行。这时因为每个对象的普通变量的值都不一样,所以每个对象分别对应一个。
运行时常量池:
当源代码中属性被经常修改,某一个方法被经常调用的时候,这时候字节码如果原封不动的把属性名字和方法名字编译过来的话,会产生冗余,特别是当属性名字和方法名字占用的字节特别大的时候。
所以字节码文件采用了运行时常量池,每个常量只在内存中保留一份,为每个常量提供了一个索引,在方法信息、属性信息中用索引来替换原来的常量,当使用的时候用索引去常量池中寻找。这也叫做符号引用。
常量池中常量有很多种:
1、字符串常量:表示一个字符串,代表类的全限定名、方法名、属性名等。
2、属性常量:属性常量包括属性所属的类全限定名、属性的名字和属性的类型。名字和类型等都指向一个字符串常量。
3、方法常量: 方法常量包括方法所属的类全限定名、方法名字、标志着方法的形参类型和返回值类型的字符串。所有信息都指向一个字符串常量
4、类常量:类的全限定名,指向一个字符串常量。
2、类的加载
1、加载
读取class字节码文本文件到内存中,在方法区创建klass对象对应着类文件的元信息,klass是类文件对应的C++对象。然后再堆区创建class对象,class对象为klass的镜像,防止Java程序来读取klass,只将class对象暴露给Java程序用于反射之类的。
2、链接
验证的话其实在加载的时候就验证了,验证字节码是否符合规范。
准备的话,就是为类中的静态变量赋默认值,final变量直接赋值。
解析的话,不一定会发生,一般是当代码执行的时候才会解析,将符号引用转化为对应的直接引用,也就是指针。
3、初始化
字节码文件会将源代码中的静态变量赋值语句和静态代码块合并到一块,并存在clinit方法中。初始化的时候就是执行clinit方法。
类加载的时机:
1、创建类的实例:new、反射等
2、读取类的静态字段(final修饰除外)和执行类的静态方法
3、当初始化一个子类的时候,会先初始化其父类
JVM内存模型图
虚拟机栈、本地方法栈、程序计数器是每个线程都单独拥有的。
堆和方法区是线程之间共享的。
1、虚拟机栈
虚拟机栈是Java程序执行方法的背后结构。栈由栈帧构成,顶部的栈帧是对应着当前执行的方法。栈帧中存放的数据有:
局部变量表,用来存放局部的变量引用、
操作栈,用来对数据执行运算操作、
动态链接,用来链接到运行时常量池中,用来解析方法 本身对应的符号引用,以及方法中的属性的符号引用解析。
方法返回地址:当前方法执行完毕后,需要返回的地址。
2、本地方法栈
和虚拟机栈一样,只不过本地方法栈对应的是调用的本地方法。
3、程序计数器
程序计数器用来标记当前执行的代码的地址,当切换方法的时候,需要保存当前方法的程序计数器信息,给程序计数器赋予被切换方法的地址信息,当被切换方法执行完毕后,需要恢复之前的程序计数器信息。
4、堆
用来存放实例化对象、字符串常量池、class对象等
5、方法区
用来存放klass对象,klass对象存放了类的元数据,包含了类中的所有信息。
还存放运行时常量池、JIT热点代码
在JDK1.8后,方法区被移到了直接内存,也就时JVM的内存之外,属于操作系统中的内存。
从对象创建过程看JVM
接下来我们详细分析一下对象的创建过程:
1、首先判断对象对应的类是否加载,如果加载过,则不用 重复加载,如果没有加载,则执行加载过程。
2、字节码文件会将static代码段和static变量赋值语句自动封装为clinit方法,将普通变量赋值语句和构造方法语句自动封装为init方法。
3、加载过程:利用类加载器字节码文本文件到内存中,在内存中映射了一个klass对象,用来存放类的元信息,包括版本信息、父类信息、接口信息、方法信息、属性信息等全部信息,klass对象被存储在方法区。接着klass创建了一个class对象,将class对象存放在堆中,class对象中存放是类的静态变量信息,初始化类对象时会执行clinit方法来初始化静态变量。
4、当初始化类之后,就会开始实例化对象。实例化对象被分配在堆上。实例化对象分为对象头和实例数据两部分。对象头又分为管理信息和类型指针,管理信息主要包括hashcode、锁、分代信息等管理信息,类型指针指向方法去的klass对象,来表明对象对应的类。实例数据中主要包括类中的非静态变量,当实例化对象的时候,会执行类的init方法,来初始化非静态变量。对象的引用存在了虚拟机栈中的局部变量表里。
ClassLoader和双亲委派模型
ClassLoader也就是我们熟知的类加载器,类加载的作用是根据类的全限定名来加载字节码文件,将字节码文件加载到内存中。
ClassLoader分为四种:
1、Bootstrap ClassLoader 引导类加载器,负责加载%JAVA_HOME%/lib路径下的jar包中的类,属于系统类。引导类加载器是由C++编写的,Java程序中无法获取其对象。
2、Extension ClassLoader 扩展类加载器,负责加载%JAVA_HOME%/jre/lib/ext路径下的jar包中的类,属于扩展类。
3、App ClassLoader 应用程序类加载器,负责加载CLASSPATH路径下的类,默认的为应用程序当前路径,属于应用类。
4、自定义加载器,通过继承ClassLoader类,重写findClass方法自定义加载类。
package base;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
public class MyClassLoader extends ClassLoader{
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
File file = new File("D:/"+name+".class");
byte[] data = null;
try {
data = getClassBytes(file);
} catch (Exception e) {
e.printStackTrace();
}
Class c = defineClass(name,data,0,data.length);
return c;
}
private byte[] getClassBytes(File file) throws Exception
{
// 这里要读入.class的字节,因此要使用字节流
FileInputStream fis = new FileInputStream(file);
FileChannel fc = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel wbc = Channels.newChannel(baos);
ByteBuffer by = ByteBuffer.allocate(1024);
while (true){
int i = fc.read(by);
if (i == 0 || i == -1)
break;
by.flip();
wbc.write(by);
by.clear();
}
fis.close();
return baos.toByteArray();
}
}
如图所示,从上到下依次层次降低,只不过不是通过继承方式实现,而是通过parent指针来实现
双亲委派模型
双亲委派模型的具体做法是:
1、当加载一个类的时候,首先看加载器本身是否已经加载过这个类,如果加载过了,直接返回可用的类。
2、如果还未加载,就到双亲加载器中寻找,如果双亲加载器加载过,就直接返回可用的类。
3、就这样依次往上查找是否已经加载过,如果到了最顶端,则依次从上往下尝试加载。
4、如果到了最底部都无法加载,则报ClassNotFoundException
双亲委派的好处:
1、防止类重复加载
2、防止系统核心类被自定义的类篡改。
双亲委派的缺点:
应用类加载器可以获取系统类,而引导类加载器却无法获取应用类。通过SPI(Servcie Provide Interface)解决这一问题。
总结
符号引用和直接引用:
当加在类文件的时候,如果当前类文件引用了其他类文件,而其他类文件当前还未加载,无法确定其地址,所以这时给其赋予一个唯一的符号引用。待其他类文件加载完成后,将符号引用改为直接引用。直接引用也就是地址。
Klass和class对象的区别:
Klass存放着Java类对应的基本元信息,放在元空间里;class对象存放类的静态变量,放在堆区里。
类的静态变量
类的静态变量存在class对象中,每个实例都需要区class对象中读取和更改。实例中只存储非静态变量。
常量池
运行时常量池:字节码解析的时候出现的常量
字符串常量池:Java程序运行时出现的字符串池
运行时常量池存储在方法区
字符串常量池存储在堆区
局部基本数据变量是直接存在虚拟机栈中的。
类非静态基本数据变量是直接存在对象实例中的。
类静态基本数据变量是直接存在class对象实例中的。
JVM中不存在基本数据类型的常量池的。
我的猜测是如果要将变量存储在常量池中,还需要额外一次的访问内存。因为基本类型占据的空间比较小,所以直接存储在对象中或者虚拟机栈中,用着方便。