黄色的是所有线程共享数据,存在垃圾回收。
灰色的是线程之间数据私有,不存在垃圾回收。
通过类装载子系统把class装载到运行时数据区。
类装载方式有两种 :
1.装载:查找和导入class文件;
2.连接:(细分为:检查,准备,解析)
检查:检查载入的class文件数据的正确性;
准备:为类的静态变量分配存储空间;
解析:将符号引用转换成直接引用(可选的)
3.初始化:初始化静态变量,静态代码块。
注意: 在Java中类的加载是动态的,它不会一次性记载所有类然后运行,而是先把保证程序能运行的基类先加载到JVM中,其他类则是在需要时再加载,这样就加快了加载速度,而且节约了程序运行过程中内存的开销。
作用:将class文件加载进内存。
虚拟机自带的类加载器:
自定义的类加载器:
三个类加载器的关系:
public class Demo {
public static void main(String[] args) {
//获取AppClassLoader
ClassLoader classLoader = Demo.class.getClassLoader();
System.out.println(classLoader);
//获取ExtClassLoader
ClassLoader classLoader1 = Demo.class.getClassLoader().getParent();
System.out.println(classLoader1);
//获取BootstrapClassLoader
ClassLoader classLoader2 = Demo.class.getClassLoader().getParent().getParent();
System.out.println(classLoader2);
}
}
下图展示了"类加载器"的层次关系,这种关系称为类加载器的"双亲委派模型"。
双亲委派机制的一个显而易见的好处是:Java的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如:java.lang.Object。它存放在rt.jar中。无论哪一个类加载器要加载这个类,最终都是委派给处于顶端的"启动类加载器"进行加载,因此java.lang.Object类在程序的各种类加载器环境中都是同一个类。
相反,如果没有"双亲委派机制",如果用户自己编写了一个java.lang.Object,那么当我们编写其它类时,这种隐式的继承使用的将会是用户自己编写的java.lang.Object类,那将变得一片混乱。
做一个演示:
这里我编写了一个类,包名是java.lang,类名是String。
但是看到异常中的错误提示说的是找不到main方法,分析一下原因:
图中的String是我们自定义的类,加载时会从AppClassloader往上找,先会询问ExtClassLoader中有没有这个类,显然它没有String这个类,又会向上找父级加载器BootStrapClassLoader,显然这里面可以加载java.lang.String,但是它里面的这个String是官方提供的类,里面没有main方法,所以会报这个异常。
总结:每个自定义的类在加载时,都会一级一级向上询问父加载器中有没有这个类,父类加载器中有的话就用父类加载器中的类,如果实在找不到就用你自定义的这个类,这样就保证了Class只会加载一次,防止了我们的代码与源代码冲突,这就是双亲委派机制。
沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。
用于运行带有native的方法。
带有native的方法只有声明,没有实现,它的实现是C语言编写的。
带有native的方法不是java官方编写的,是C语言编写的代码,我们可以简单的理解为,带有native的方法就是调用了第三方函数库或者叫做调用了C语言函数库来实现功能的。
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址),由执行引擎读取下一条命令,占内存空间非常小,可以忽略不计。
它是当前线程所执行的字节码的行号指示器,通过不断的改变这个计数器的值来选取下一条要执行的字节码指令。
如果执行的是native的方法,那么这个计数器是空的。
总结:类似于指针,意思就是当前方法执行完,下一个该执行那个方法,就需要用pc寄存器来做标记,实质上pc寄存器存储的就是下一个要运行的方法的地址。
1.7之前叫做永久代,1.7以后叫做元空间,方法区逻辑上是堆的一部分,但是实际堆中只有年轻代和老年代。
永久代:存储一个类的结构信息,运行时期的常量池,方法数据,构造,成员变量等。
元空间:存储一个类的结构信息,运行时期的常量池、方法数据、方法代码、符号引用等。
二者都是JVM规范中方法区的实现,不过最大的区别是元空间并不在虚拟机中,而是使用本地内存。
常量池:类,方法,接口等中的常量,也包括字符串常量,如String s = "java"这种申明方式的。
注意:方法区只是一个规范,它在不同的虚拟机里面实现是不一样的,最经典的就是永久代(PermGen space)和元空间(Meta space)。
实例变量在堆内存中,与方法区无关。
总结:方法区存储了一个类的结构信息,也就是Class(大Class),就是类的模板信息。
先了解几个概念:
堆管存储,栈管运行。
程序 = 框架 + 业务逻辑。
队列:先进先出,后进后出。
栈:先进后出,后进先出。
运行普通方法的地方。
单线程的情况下,第一个方法会压栈执行,如果在该方法内又调用了另外一个方法,第二个方法会进入栈在第一个方法的上面,直到第二个方法执行完毕弹栈后,第一个方法才会得到执行,当所有方法弹栈之后,该程序就执行完毕了。
栈中存储着:8中基本数据类型 + 对象的引用变量(等号左边的对象引用) + 方法都是在栈内存中分配着。
方法在栈内存中叫栈帧
在java层面就是叫方法
StackOverflowError 栈内存溢出(错误)
凡是new出来的都在堆内存。
OutOfMemoryError (Java heap space)堆内存溢出
拓展:内存泄漏问题
当长生命周期的对象持有短生命周期的对象的引用,就很可能发生内存泄漏。尽管短生命周期的对象已经不再需要,但是长生命周期的对象一直持有它的引用导致其无法被回收。
例如:1. 一个map中我们存储了多个对象,该map一直被其他对象所引用,但是map中的个别对象很长时间未被引用,由于该对象在map中,这个对象一直被map引用,不会被回收。
例如:2. 如果一个外部类的实例对象的方法返回了一个内部类的实例对象, 这个内部类对象被长期引用了,即使那个外部类实例对象不再被使 用,但由于内部类持久外部类的实例对象,这个外部类对象将不会 被垃圾回收,这也会造成内存泄露。