JVM内存模型与类加载机制
一. java虚拟机的内存模型如图:
补习一下jvm内存模型中的各个组成部分
堆:
- 我们new出来的对象全部放在堆中,他是jvm所能够动态分配的最大的一块空间
- 优点: 内存动态分配,生命周期不必事先告诉编译器,由JAVA的垃圾回收线程动态回收
栈:
- 存取的速度仅次于CPU的寄存器,但是存在栈里面的数据大小必须是提前定义的比如大家都知道int 4个字节
- jvm在调用某些方法时,方法的参数,以及局部变量就存放在栈里面,(因为它们符合内存空间已知的特性)
- 主函数在栈的最底部
- 栈是有线程特性的,每个线程拥有自己的栈,这也是多线程的情况下,方法内部的变量是绝对线程安全的,因为他们没有可能被共享
本地方法区
java8中,取消永久代,方法存放于元空间(Metaspace),元空间仍然与堆不相连,但与堆共享物理内存,逻辑上可认为在堆中
- 可以把本地方法区理解成是一个镜像,它通过java代码去调用c或者c++的程序,比如使用他们直接访问内存,常见的无锁的却线程安全的原子类就是用的本地方法CAS实现
程序计数器
- 每条线程都通过它获取自己将要执行的下一条代码在哪里
方法区(持久层):
我们讲的把类加载进内存,说的就把把类加载进了方法区,方法区里面存放着类的方法,常量,静态变量等等所有信息
下面的类加载机制,主要讲的就是这个阶段
异常:
- 栈: Stack Over Flow Error
- 出现这个异常说明栈空间溢出,方法的层级调用太多了,八成是因为我们的代码出现了递归调用,而且这个递归还没有结束的条件
- 堆: Out Of Memory Error
- 程序中的对象太多,还不满足垃圾回收的条件(没有任何引用指向对象时,被回收)导致堆空间溢出
如下四种情况,java虚拟机会结束自己的生命周期:
- System.exit(0) 0表示正常退出,其他异常
- 程序正常执行结束
- 程序在执行的过程中遇到了异常或者错误而异常终止(程序有了异常之后,如果不去捕捉,异常一一层一层的往外throw,最终会throw给main方法,而main方法throw给虚拟机之后,虚拟机结束运行)
- 操作系统出现出现错误
二. 类加载机制
这个错误号称是所有程序员都犯过,我也错了,原因就是类加载的过程不清楚,通过这次学习,搞懂了咋个过程,自然可以搞懂结果
代码引入
/**
*
@Data
public class text {
private static text text = new text();
public static int count1;
public static int count2 = 0;
/*
3 类被加载内存,把静态的变量全部附默认值, 这就是 18-20 分别为 null 0 0
4 再然后 初始化 首先, 调用构造方法18行, 得到 text 顺道 count1 = 1 count2 = 1 ,
5 往下执行19 20 分别为 1 0
*/
public text(){
count1++;
count2++;
}
public static text getSingleton(){
return text;
}
}
class mytext{ //1 启动类
public static void main(String[] args) {
text singleton = text.getSingleton(); //2 主动使用
System.out.println(singleton.count1);
System.out.println(singleton.count2);
}
}
类的加载连接,连接和初始化
1.加载:
类的加载是将类的.class文件中的二进制数据,(由硬盘)加载到内存中,并将其放在运行时的数据区的方法区,然后在堆中创建一个java.lang.Class对象,用来封装整个类在方法区中的数据结构
- 类加载的最终产品就是位于堆中的Class对象
这也是Class对象是反射的入口的原因,也可以看到,一旦类被加载进虚拟机,Class就会被虚拟机创建出来
- 加载.class文件的方式
- 从本地系统中直接加在
- 通过网络加载
- 从.zip .jar等归档文件中加载
- 从专有数据库中提取
- 将原文件编译为.class文件
2.连接
- 2.1 验证: 确保被加载的类的正确性(绝大多数都是通过javac来编译生成的字节码文件,但是不排出有用户是通过其他手段编译生成字节码文件,如果不符合jvm的规范,会报错)
- 2.2 准备: 为类的静态变量分配内存,并将其初始化为其默认值(整型0,对象null,布尔flase)(注意这里是静态变量,是class文件一经加载到内存,静态变量就会被分配空间(静态变量属于的是本类而不属于哪个实例)!而实例变量的内存是在new的时候动态分配的)
- 2.3 解析:把类中的符号引用转换为直接引用
- 2.4 初始化: 为类的静态变量赋予正确的初始值
注意一个地方,上面的第二步中的阶段二,准备阶段,以及第三步:初始化似乎有些重复,但是实际上是两回事准备阶段:初始化默认值整型0,对象为null,布尔为false但是初始化:他是将用户指定的初始化的值覆盖默认值
其实总体上看,这就是类加载的整个过程,下面是细化很多知识点,比如什么时候触发类的加载,
java程序对类的使用分为两种
-
主动使用(六种)
-
被动使用(除了上面的六种,其他都是被动)
-
所有的java虚拟机的实现必须在每个类或者接口被java程序"首次主动使用"时,才初始化他们
什么是主动使用?
- 创建类的实例 (new ...)
- 访问某个类,或者接口的静态变量,或者对静态变量进行赋值
- 调用类的静态方法(static method)
- 反射(Class.forName("com.XXX.text")
- 初始化一个类的子类
- java虚拟机启动时,被标明为启动类的类(比如我在一个文件同时写了多个类c1,c2....,java c1 ,这里的c1就是我主动使用到类)
除了上面的六种,其他都是被动
除了上面的六种,其他都是被动,而被动使用都不会导致类的初始化(也就是图1的第三步)
类加载器
1. java虚拟机自带的三个加载器
- 根类加载器(C++编写的,无法在java代码中获得该类)(BootStrap)
它没有父类加载器,他负责加载虚拟机的核心类库,java.lang.* 等.它从系统属性sun.boot.class.path所制定的目录中加载类库,它的实现依赖于底层的操作系统,并没继承java.lang.ClassLoader
后两种使用java代码实现
- 扩展类加载器(Extension)
它的父类加载器是根类加载器,它会从系统属性java.ext.dirs系统属性所指定的目录加载类库,或者从JDK的安装目录jre/lib/ext子目录(扩展目录)加载类库,(如果用户把自己的jar包放在=这个目录下,也会自动被Extension ClassLoader加载),这个类加载器本身是个纯java类,是java.lang.ClassLoader的子类
- 系统类加载器(也叫应用类加载器)(System)
它的父类加载器是扩展类加载器,它会从环境变量classpath(初学java的配置的环境变量)或者系统属性java.class.path所指定的目录去加载类,同时,他也是用户自定义的类加载器的默认父类(ClassLoader类的一个空参构造指明,如果我们不传递任何参数,它就会调用getSystemClassLoader()作为当前类的父类加载器),它本身同样也是一个纯java类,是java.lang.ClassLoader的子类
2. 用户自定义的类加载器
- java.lang.ClassLoader的子类
- 用户定制类的加载方式
提一下动态代理,第一个参数要求我们传递一个类加载器,使用这个类加载器,把我们的类加载进内存
- 有了类加载器,我们可以随心所欲的加载指定类,而不需要等到首次"主动使用"它的时候,再加载它
jvm规范:
类的加载:
- jvm有一套机制,允许类加载器在预料到某个类将要被使用的时候就去预先加载它,在这个预先加载的过程中,如果.class文件缺失,或者存在错误,它也不会告诉程序(因为可能他判断错了),直到程序第一次主动使用该类,类加载器直接报错(LinkageError)
类的验证:
- 类被加载后,就进入了连接阶段,这个阶段就是将已经读入内存的类的二进制数据合并到虚拟机的运行时环境中去
因为多个.class文件之间是没关系的,但是类之间可能存在调用关系,这个阶段会内存中处理好类之间的相互调用的复杂关系
类验证的内容:
- 类文件的结构检查
- 语义检查
- 字节码检验
- 二进制兼容性的验证
类的准备:
- 在准备阶段,jvm 为类的静态变量分配内存,并为他么附上默认值
比如下面: 为a 分配四个字节的内存空间,并初始化为0, 注意是0
为 b 分配8个字节的内存空间,并初始化为0;
public class A{
private static int a =1;
private static Long b;
}
类的解析
在解析阶段,jvm 会把类的二进制文件数据中的符号引用替换为直接引用,
例如:
public class worker{
public void driver(){
car.run();
}
}
在worker类的二进制文件中,包含了一个对car的run()方法的符号引用,在类的解析阶段,java虚拟机会把这个符号引用替换为一个指针,指向car类的run()方法,在方法区 的内存位置,这个指针就是直接引用
类的初始化
步骤:
- 假如这个类还没有被加载和连接,那就先进行加载和连接.
- 假如类,存在直接父类,并且这个父类还没有进行初始化,先初始化直接父类(一层层往上走,一直走到Object类)
- 假如类中有初始化语句,那就按照顺序,依次执行初始化语句
初始化时机:
- 上面的主动使用的六种情况
只有静态变量或静态方法,确实在当前类或者当前接口中定义时,才可以理解为,是对当前类或者当前接口的主动使用
public class parent {
static int a = 3;
static{
System.out.println("Parent static block");
}
}
class child extends parent{
static {
System.out.println("child static block");
}
}
class text4{
public static void main(String[] args) {
System.out.println(child.a);
}
}
结果
Parent static block
3
这段代码的结果很好的验证了类的加载时机之,只有静态变量或静态方法,确实在当前类或者当前接口中定义时,才可以理解为,是对当前类或者当前接口的主动使用
- 调用类加载器的去加载某个类的时候,不是主动使用,不会初始化类
public class textClassLoader {
static{
System.out.println("textClassLoader static block");
}
}
class text5{
public static void main(String[] args) throws ClassNotFoundException {
// 获取应用类加载器
ClassLoader loader = ClassLoader.getSystemClassLoader();
//把类加载进内存
loader.loadClass("com.tryjvm.textClassLoader");
System.out.println("---------");
//反射
Class.forName("com.tryjvm.textClassLoader");
}
}
- 静态变量赋初值**,java有两种初始化静态变量的途径
- 在声明的地方初始化
private static int a =1;
- 在静态代码块的初始化
private static int b;
static {
b=1;
}
- 若在此阶段没有进行初始化,则保持默认值0
编译时常量和运行时常量
第一段代码:
public class text3 {
public static final String x = "hello";
static{
System.out.println("world");
}
}
class text33{
public static void main(String[] args) {
System.out.println(text3.x);
}
}
结果:
hello
第二段代码:
public class text3 {
public static final String x =new String("hello");
static{
System.out.println("world");
}
}
class text33{
public static void main(String[] args) {
System.out.println(text3.x);
}
}
结果
hello
world
- 前者属于编译时常量,就是说,编译器在编译的时候,就知道了x的值,直接把他放在方法区的常量池里面,而不去初始化类
- 后者,x的值只有运行时才知道,会去初始化类,执行静态代码块
接口的初始化
- 在初始化一个类时,并不会先初始化它所实现的接口
- 在初始化一个接口时,并不会先初始化它的父接口
因此,父接口,并不会应为他的子接口,或者实现类的初始化而初始化,而是当程序首次使用接口的静态变量时,才会导致接口的初始化
父亲委托机制(Parent Delegation)
- 从jdk1.2开始,类的加载过程采用了父亲委托机制,除了根类加载器外,其他的类加载器,都有且只有一个父类,当ClassLoader1需要去加载某个类时,它首先委托给自己的父类加载器,父类还有父类继续往上委托,直到委托到根类加载器,如果从根类加载器加载不了,根的子类加载器尝试加载,直到某个父类加载器可以加载此类,由此类加载器加载(然后把此Class对象的引入往下传递),父类加载不了,当前类加载器加载,如果当前类也加载不了,抛出异常,ClassNotFoundException
- 若有一个类加载器可以成功的加载Sample类,那么这个类加载器,称为定义类加载器
- 所有能成功返回Class对象的引用的类加载器(包括定义类加载器),都被称作初始类加载器,也就是说,定义类加载器,包括他的所有子类加载类,都称作初始类加载器
这样做有个很明显的好处就是,java的核心类库,不会被用户自定义的类加载器加载, 用户自定义的类基本上也不会被高层的类加载器加载
强调
加载器之间的父子关系,不一定就是真真正正的继承关系,一对父子类加载器,可能是同一个加载器类的两个实例,具体谁是父,要看谁被包装,是一种包装关系
class MyClassLoader extends ClassLoader{
}
ClassLoader loader1 = new MyClassLoader();
//将loader1作为loader2的父类加载器
ClassLoader loader2 = new MyClassLoader(loader1);
安全
- 提高了平台的安全性,确保,一些本应该用父类加载的加载的核心类,不会被用户自定义的类加载器加载
命名空间
* 当前的这个类加载器和它所有的父类加载器所加载的类组成一个命名空间,在这个命名空间中,不会出现类的完整名字(包括类的包名)相同的 两个类, 而在不同的命名空间,就有可能出现这种情况
也就是说,假如有两个类加载器,并且他们没有仍和父子关系,那么,他们可以都去加载同一各类,加载进内存
运行时包
我们知道,在一个包下的不同类是可以相互访问的,那么假如我自己创建一个包叫 java.lang.SP ,我这个包里面的类,是否可以直接访问真正的java.lang.*里面的核心类呢? 答案是不可以的
- 引入了运行时包的概念,由同一个类加载加载的属于相同包的类,组成一个运行时包,这样就有两层保障,首先他们属于同一个包,其次,他们的定义类加载器相同,这样看,不可能存在用户自定义的类,冒充系统核心类库的类的情况
创建 用户自定义的类加载器
* 继承java.lang.ClassLoader类,重写 findClass(String name)方法, 根据制定的类的名字,返回CLass对象的引用