目录
JVM
Java虚拟机即Java Virtual Machine。
跨平台特性:一次编写、到处运行
自动实现垃圾回收
Java源代码(.java)需要经过编译器编译成字节码(.class),JVM负责将字节码翻译成特定平台下的机器码并运行,也就是说,只要在不同的平台上安装对应的JVM,就可以运行字节码文件。
JVM内存结构
Java虚拟机(一般用的是HotSpot)
类加载器
类加载器的作用
就是将字节码文件(class文件)加载到JVM中。当一个类被使用的时候,才会加载到内存
类加载的执行过程(类的生命周期)其中准备、验证、解析3个部分统称为连接
- 加载:查找和导入class文件
- 验证:保证加载类的准确性
- 准备:为类变量分配内存并设置类变量初始值
- 解析:把类中的符号引用转换为直接引用
- 初始化:对类的静态变量,静态代码块执行初始化操作
- 使用:JVM 开始从入口方法开始执行用户的程序代码
- 卸载:当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象。
类加载器分类
- 启动类加载器:用来加载java核心类库,无法被java程序直接引用;
- 扩展类加载器:用来加载java的扩展库,java的虚拟机实现会提供一个扩展库目录,该类加载器在扩展库目录里面查找并加载java类;
- 应用程序类加载器:它根据java的类路径来加载类,一般来说,java应用的类都是通过它来加载的;
- 自定义类加载器:由java语言实现,继承自ClassLoader;
双亲委派机制
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。
如果父类的加载器还存在其父类加载器,则进一步向上委托,依次递归请求最终达到顶层的启动类加载器。
如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制。
- 优点
- 避免类的重复加载
- 保护程序安全,防止核心API被随意篡改
- 缺点
- 在某些场景下双亲委派制过于局限,所以有时候必须打破双亲委派机制来达到目的。例如:SPI机制
打破双亲委派机制
自定义类加载器
- 自定义类加载器加载一个类需要:继承ClassLoader,重写findClass,如果不想打破双亲委派模型,那么只需要重写findClass;如果想打破双亲委派模型,那么就重写整个loadClass方法,设定自己的类加载逻辑,想要打破 即重写的时候让自己去加载 不让父加载器去加载
线程上下文类加载器
- JNDI 通过引入线程上下文类加载器,可以在 Thread.setContextClassLoader 方法设置,默认是应用程序类加载器,来加载 SPI 的代码。有了线程上下文类加载器,就可以完成父类加载器请求子类加载器完成类加载的行为。打破的原因,是为了 JNDI 服务的类加载器是启动器类加载,为了完成高级类加载器请求子类加载器(即上文中的线程上下文加载器)加载类。
Tomcat,应用的类加载器优先自行加载应用目录下的 class,并不是先委派给父加载器,加载不了才委派给父加载器。
方法区(Method Area)
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单来说,所有定义的方法的信息都保存在该区域,此区域属于共享区间。
存放静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关。尤其是类信息,可以理解为实例化变量的模板化信息,因为user对象我们可以new很多个出来,但是用的类信息是User.class类,这种模板类信息按道理只需要加载一次的,所以这部分信息单独放在方法区。
Metaspace 元空间
元空间是方法区的实现。方法区的实现,JDK1.7之前是永久代,JDK1.8之后是元空间。
- 元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
堆(Heap)
一个JVM只有一个堆内存,堆内存大小可以调节
存放几乎所有的对象实例和数组。堆是 OOM 故障(内存溢出)最主要的发生区域。
GC: 主要的垃圾回收工作发生在堆中,回收无用的对象,释放内存空间。
从内存回收的角度,可划分为新生代和老年代,可进一步细分为Eden空间、From Survivor空间、To Survivor
- 新生代(1/3 堆空间)
- 伊甸区(Eden)(8/10)JAVA中的对象一般先分配到eden区,
- 幸存0区(from)(1/10)
- 幸存1区(to)(1/10)
- 老年代(2/3 堆空间)
虚拟机栈
对于每一个线程,JVM 都会在线程被创建的时候,创建一个单独的栈。也就是说虚拟机栈的生命周期和线程是一致。并且是线程私有的。Java 虚拟机栈中出栈入栈的元素就称为「栈帧」。
线程结束,栈内存也就释放,对于栈来说,不存在垃圾回收问题。
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
线程请求的栈深度超过虚拟机允许的最大深度,抛出StackOverFlowError异常(栈内存溢出)(递归)。
堆和栈的区别
1、申请方式的不同。栈由系统自动分配,而堆是人为申请开辟;
2、申请大小的不同。栈获得的空间较小,大小是固定的,而堆获得的空间较大,可以根据需要进行动态调整;
3、申请效率的不同。栈由系统自动分配,速度较快,而堆一般速度比较慢;
4、底层不同。栈是连续的空间,而堆是不连续的空间。
5、共享性的不同 。栈内存是线程私有的。 堆内存是所有线程共有的。
6、功能不同。栈内存用来存储局部变量和方法调用,而堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中。
7、存储内容的不同。栈在函数调用时,函数调用语句的下一条可执行语句的地址第一个进栈,然后函数的各个参数进栈,其中静态变量是不入栈的。堆一般是在头部用一个字节存放堆的大小,堆中的具体内容是人为安排;
Native
凡是带了native 关键字的,说明java的作用范围达不到了,会去调用底层C语言的库
会进入本地方法栈
调用本地方法本地接口(JNI)
JNI作用:扩展Java的使用,融合不同的编程语言为Java所用,通过JNI加载本地方法库中的方法
在内存中专门开辟了一块标记区域:本地方法栈(Native Method Stack),来登记Native方法
程序计数器
看作是当前线程所执行的字节码的行号指示器
记录一下方法执行到哪了