JVM
运行在操作系统之上
java二进制字节码文件的运行环境
JVM的组成部分
java代码在编写完成后编译成字节码文件通过类加载器
来到运行数据区,主要作用是加载字节码到内存
包含
方法区/元空间 堆 程序计数器,虚拟机栈,本地方法栈等等
随后来到执行引擎,主要作用是翻译字节码为底层的系统指令
包含
解释器 即时编辑器 垃圾回收器GC
还有一部分是本地方法接口和本地库
使用原生的C和C++实现
程序计数器
线程私有的,内部保存的字节码的行号
用于记录正在执行的字节码指令的地址
程序计数器
线程私有
每个线程一份保存字节码的行号,用于记录正在执行的字节码指令的执行位置
当分片调度线程时,每次只需要重新从计数器记录的位置继续执行即可
JVM堆
主要用于保存对象的实例和数组等
当堆中内存空间满时,抛出OOM异常
Java内存结构
堆
分为年轻代和老年代
年轻代
分为Eden区 S0 S1
一开始对象会进入Eden区,在经历垃圾回收后依然存活就会移向s0,s1最终进入老年代
老年代
主要保存生命周期长的对象
永久代(java8之前)
功能同元空间
保存类信息,静态变量,常量,编译后的代码等
java8以后移至本地内存中,称为元空间
避免堆内存溢出
JVM栈
每个线程运行时所需要的内存,先进后出
每个线程的栈独立,线程安全
每个栈由多个栈帧组成,对应方法调用时占用的内存
每个线程只能有一个活动栈帧,即当前正在执行的方法对应的栈帧
垃圾回收不涉及栈内存,栈帧内存会在弹栈后自动释放
每个栈帧默认为1024k,栈内存大会导致线程数变少,因为每个线程都有自己独立的栈
如果方法内的局部变量没有逃逸出方法外(参数调用或者返回值),方法内的局部变量是线程安全的,因为不同线程调用同一个方法会创建不同栈帧执行
栈内存溢出问题
栈帧过多:递归调用等
栈帧过大
JVM方法区
运行时数据区的一部分
各个线程的共享内存区域
主要存储类的信息和运行时常量池
在hotspot虚拟机中,随虚拟机的开关创建释放
在jdk8之前存在堆中的永久代里
jdk8之后移到了本地内存(操作系统的内存)的元空间中
避免OOM
包括
class
classloader
运行常量池
类似于一张表
主要保存需要执行的类名,方法名,参数类型,字面量等信息
执行机器指令时,会根据符号地址去常量表进行查找得到需要的信息
常量池是.class文件中的,当类被加载时,就会将常量池信息存入运行时常量池
常量池和运行常量池
常量池存在.class字节码文件中,运行常量池
如果方法区中的内存无法满足分配请求,会抛出OOM异常
可以通过-XX:MaxMetaspaceSize=元空间大小m来设置元空间的最大容量
直接内存
不属于JVM中的内存结构,不由JVM进行管理
是虚拟机的系统内存,一般用于NIO操作时用于数据缓存区
分配和回收成本高,但读写性能好
NIO和BIO
BIO:
先切换到内核态,从磁盘中分批次读入系统缓冲区,再从系统缓冲区读入堆内存中的java缓冲区
NIO:
在系统内存中划出一块缓冲区(直接内存),系统和java都可以直接访问
避免了需要读入两次(两块缓冲区)的问题
类加载器
用于将字节码文件(.class文件)装载到运行数据区
主要分为四种
BootStrap ClassLoader
启动类加载器,C++编写,加载java核心库
ExtClassLoader
扩展类加载器,加载扩展jar包
AppClassLoader
应用类加载器,加载开发者自己编写的类
CustomizeClassLoader(不常用)
自定义类加载器,实现自定义类加载规则
类加载器的双亲委派模型
加载一个类时,会先委托上一级的加载器进行加载,如果上级加载器也有上级,就会继续向上委托.
如果顶级加载器(启动类加载器)无法加载此类,就会由子加载器进行加载
eg:
test -> AppClassLoader -> ExtClassLoader -> BootStrapClassLoader(无法加载)
回到AppClassLoader加载
String -> AppClassLoader -> ExtClassLoader -> BootStrapClassLoader(可加载)
由启动类加载器加载,其他加载器加载时直接执行即可
双亲委派机制的作用
避免类重复加载,保证唯一性
保证安全,类库API不会被修改
如果定义和核心库中相同的库和类,并自定义方法,就会出现报错
因为启动类加载器已经加载了同名类文件,如果重复加载就会对你自定义的同名类文件报错,防止恶意篡改核心API库