@TOC
JVM体系结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QDjhfwUV-1630324826750)(jvm/jvm_simple.png)]
类加载器
负责加载class文件,class文件在文件开头由特定的标识,将class文件字节码加载到内存中,并将这些内容转换成__方法区__中的运行时数据结构并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
类加载器种类
- 虚拟机自带的类加载器:
- 启动类加载器 Bootstrap ,C++:
- 扩展类加载器 Extension, Java
- 应用程序类加载器 AppClassLoader,也叫系统加载器,加载当前应用的classpath中的所有类
- 用户自定义加载器
- Java.lang.ClassLoader的子类,用户可以定制类的加载方式
双亲委派机制
为了保证程序,引入双亲委派机制,防止用户自定义的类,污染java自带的类和源代码
所有的类加载过程都会,首先会让启动类加载器加载这个类,如果能加载,那么就加载这个类,如果不能,会让扩展类加载器加载,如果还不能才让应用程序类加载器加载。
比如用户自定义了java.lang.String这个类,但是只会由启动类加载器加载这个类,不会让应用程序加载器加载用户自定义的String类,这样就防止了JVM自带的String类被用户自定义的污染。
沙箱安全机制
将Java代码限定在虚拟机特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。
组成Java沙箱的基本组件
- 类加载体系结构
- class文件校验器
- 内置于java虚拟机的安全特性
- 安全管理器及Java API
Java安全模型的前三个部分——类加载体系结构、class文件检验器、Java虚拟机(及语言)的安全特性一起达到一个共同的目的:保持Java虚拟机的实例和它正在运行的应用程序的内部完整性,使得它们不被下载的恶意代码或有漏洞的代码侵犯。
相反,这个安全模型的第四个组成部分是安全管理器,它主要用于保护虚拟机的外部资源不被虚拟机内运行的恶意或有漏洞的代码侵犯。这个安全管理器是一个单独的对象,在运行的Java虚拟机中,它在对于外部资源的访问控制起中枢作用。
执行引擎
执行引擎负责解释命令,提交操作系统执行。
Native Interface
Java本地接口的作用是融合不同的编程语言为Java所用。标注了native关键字的方法,表示java已经无能为力了,需要借助操作系统和其他语言才能实现此功能。
它的具体做法是在Native Method Stack(本地方法栈)中登记native方法,在执行引擎执行时,加载native libraties。
PC寄存器
程序计数器 Program Counter Register
每个线程都有一个程序计数器,是一个线程是由的,就是一个指针,指向方法区中的方法字节码,用来存储指向下一条指令的地址,即将要指向的指令代码,由执行引擎读取下一条指令。是一个非常小的内存空间,几乎可以忽略不记。
如果要执行的是一个native方法,那么这个计数器时空的
用以完成分支、循环、跳转、异常处理、线程恢复等基础功能。不会发生内存溢出错误
方法区
供各个线程共享的运行时内存区域,它存储了每一个类的结构信息。例如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容。
方法区是规范,在不同的虚拟机中实现不一样,最定型的是永久代和元空间。
实例变量存储在堆内存中,和方法区无关
栈
栈管运行,堆管存储
栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程已结束该栈就Over。生命周期和线程一致,是线程私有的。
栈中主要存储什么?
8中基本类型的变量 + 对象的引用变量 +实例方法都是在栈内存中分配
放到栈空间的方法就是栈帧
栈帧中主要存储什么?
- 本地变量:输入参数和输出参数以及方法内部变量
- 栈操作:记录出栈、入栈操作
- 栈帧数据:包括类文件、方法等
堆Heap
一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。
堆结构
- 新生代
- 伊甸区
- 幸存者0区,别名from
- 幸存者1区,别名to
- 老年代
- 元空间/永久代
默认大小比
新生区:养老区 = 1:2
伊甸区:From区:To区 = 8 :1:1
工作流程
- 对象的创建、成长、和消亡发生在新生区
- 当对象快栈满新生区时,就会发生GC(Garbage Collection),也称为轻量级GC,伊甸区基本清空,伊甸区的的剩余对象移动到幸存0区
- 当伊甸区再次触发GC时,会扫描伊甸区和From区,堆这连个区域进行垃圾回收,经过这次还存活的对象直接复制到To区,同时这写对象的年龄+1。如果有对象的年龄达到了老年标准,则赋值到养老区
- 然后清空Eden和From区。from区和to区位置和名称不是固定的,每次GC过后都会交换,GC交换后,谁空谁时to区
- 这样,经历过很多次GC后,如果养老区也满了,这个时间将产生Full GC,也称重量级GC,进行养老区内存清
- 若养老区执行了多次重量级GC后,依然无法进行对象的保存,就会产生OOM错误
一个对象经历了15次GC,还没有死,这会被移入老年区
java hello.class -Xms1m -Xmx8m -XX:+PrintGCDetail # 指定内存大小为1M,运行并打印GC信息
java hello.class -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError # 如果发生堆内存溢出错误就报错
-XX:MaxTenuringThreshold=20 # 设置经历20次GC后,才可以进入老年代
元空间/永久代
方法区和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码等。虽然JVM规范将方法去描述为堆的一个逻辑部分,但它还有一个别名: Non-Heap非堆,目的就是要和堆分开
Java7之前,永久去是一个常驻内存区域,用于存放JDK自身锁携带的类和接口的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收掉的,关闭JVM才会释放此区域所占用的内存
Java8之后将最初的永久代去取消,由元空间取代,元空间本质和永久代类似。区别在于:永久代使用的是JVM的堆内存,而元空间并不再虚拟机中,而是使用本机物理内存
JProfiler 获取jvm内存快照进行分析
GC
Garbage Collection
分代回收策略:
- 频繁回收新生区
- 很少回收老年区
- 基本不动元空间
判断Java中对象存活的算法
- 引用计数法:每次对象赋值时都要维护一个计数器,当由地方引用这个对象的时候,计数器+1,当引用失效,计数器-1,当计数器为0时,jvm就认为对象不再被使用,是垃圾了
- 优点:实现简单,效率高
- 缺点:不能解决循环引用问题,比如A对象引用B对象,B对象引用A对象,但是A和B都不被其他任何对象引用
- 可达性分析,又称根搜索方法:通过一些GCRoots对象作为起点,从这些节点开始往下搜索,搜索通过路径称为引用链,当一个对象没有被GCRoots的引用链连接的时候,说明这个对象是不可用的
- GCRoots对象包括:
- 虚拟机栈中的引用的对象
- 方法区域中的类静态属性引用的对象
- 方法区域中常量引用的对象
- 方法栈中JNI(Native方法)的引用的对象
- GCRoots对象包括:
GC算法
- 复制算法:将内存分为两块,将其中一块上还活着的对象复制到另外空的一块上去。年轻代中就是采用的这种算法
- 优点:效率高,不会产生碎片
- 缺点:耗费空间,如果对象存活率高,不适合
- 标记清除:算法分为两个阶段,先标记出要回收的对象,然后统一回收这些对象
- 优点:不需要额外空间
- 缺点:会产生内存碎片;两次扫描,耗时严重,还会暂停整个应用
- 标记压缩:是对标记清除进行的优化,也分为两个阶段,先标记出要回收的对象,然后将存活的对象往一端滑动,老年代中使用的是这种算法
- 优点:没有内存碎片
- 缺点:需要移动对象
- 分代收集算法:根据对象存活周期的不同将内存划分为几块,对与不同区采用最适当的算法。