一、虚拟机架构图
二、类加载过程
类加载器的作用:负责把class文件加载到内存中
类加载过程:
- 加载:
- 通过类的全限定名获取此类的二进制字节流
- 文件的编码结构---->运行时的内存结构
- 内存中生成一个class对象
- 链接:
- 验证:保证被加载类的一个正确性
- 准备:为类变量分配内存 设置类变量初始值(不会为final修饰的变量以及实例变量赋值)
- 解析:相当于一个翻译过程
- 初始化: 初始化阶段执行类加载方法() 的过程,()不同于类的构造器。若该类具有父类,JVM 会保证子类的()执行前,父类的该方法已经执行完。多线程下被同步加锁。
三、类加器的分类
- 虚拟机自带的类加载器:
- 启动类加载器:由c/c++语言实现,嵌套在JVM内部,不继承自java.lang.ClassLoader,没有父加载器,只加载java、javax、sun等开头的类
- 扩展类加载器:派生于ClassLoader类,父类加载器为启动类加载器。
- 应用类加载器:默认的类加载器,一般来说,java应用的类都是由它完成加载。派生于ClassLoader类,父类加载器为扩展类加载器。通过ClassLoader.getSystemClassLoader()方法获取该类加载器。
- 自定义类加载器:
- 好处:隔离加载类
- 修改类加载的方式
- 扩展加载源
- 防止源码泄露
双亲委派机制:
好处: 避免类的重复加载、保护程序安全,防止核心API被篡改。
沙箱安全机制: 保护原生JDK的安全。
四、内部结构
1、PC 寄存器: 用来存储下一条即将执行的指令地址,指令由执行引擎执行。
使用PC寄存器存储字节码指令地址有什么用?/ 或为什么使用PC寄存器记录当前线程的执行地址?
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
2、本地方法:(native修饰的)与java环境外交互、与操作系统交互。本地方法栈:用来管理本地方法的结构 线程私有
3、虚拟机栈:
概念:栈是运行时的单位、栈解决程序运行时的问题,即程序如何执行,或者如何处理数据。
栈帧:一个内存区块,栈中数据以栈帧格式存在,每个方法对应一个栈帧。
局部变量表:存储方法参数和定义在方法体内的局部变量,数组结构,建立在线程的栈上,线程私有不存在数据安全问题。容量大小在编译期就已确定,随栈帧的销毁而销毁。
运行原理:先进后出
4、堆空间
概述:
- 一个JVM实例对应一个进程实例,一个JVM实例有一个运行时数据区。
- 一个Runtime就有一个独立的方法区和堆
- 一个进程有多个线程,多个线程共享一个方法区和堆空间
- 一个线程拥有自己独立的程序计数器、本地方法栈、虚拟机栈
- 为了解决多个线程访问出现线程不安全问题—>TLAB(线程私有空间)
- 垃圾回收只会在堆(方法区)当中进行回收
堆内存细分:
基本划分:新生代+老年代+元空间
比例:新生代:老年代=1:2
新生代=Eden:from:to=8:1:1
创建对象在Eden区
内存分配策略
- 默认对象分配在Eden区
- 如果一个对象回收超过阈值次数还存活就把它放入老年代
- 大对象分配在老年代
- 对于体积不大的对象优先分配在Eden区的TLAB区
- 对象还有可能分配在栈空间
TLAB区(Thread Local Allocation Buffer)
为什么要有该区域?
堆空间是线程共享的区域,在高并发的场景下分配内存空间,会出现线程不安全的问题,采用加锁虽然可以避免此问题但是会影响效率。
TLAB是线程私有的一块区域,即使多个线程同时分配也不会有线程安全的问题,提高吞吐量,快速分配,JVM会将TLAB作为内存分配的首选
五、逃逸分析
1、为什么存在逃逸分析
如果对象在堆内存分配–可能引起GC–导致STW–应用程序卡顿,而逃逸分析可以减少此类现象的发生
2、什么情况在栈上分配
如果一个对象没有发生逃逸,就可以在栈上分配,随着方法的结束对象的出栈,不涉及GC有效提高性能
3、判断对象是否发生逃逸
new出来的对象是否被外部方法调用,调用了就代表逃逸了。新建对象尽量是局部变量
4、逃逸分析目前还不是很成熟
六、方法区(元空间)
线程共享的区域,此区域大小决定了系统可以加载多少个类
堆栈方法区三者的关系:
内部结构:
- 类的信息:类、接口、枚举等
- 域信息:包的public、protected、private等
- 方法信息:方法名称、返回类型
- 常量信息
- 静态变量/类变量
方法区的垃圾回收:
必要又难以让人满意,主要回收常量池里面不常使用常量和类型
七、垃圾回收
垃圾:在程序运行过程中没有任何指针指向该对象
意义: 不进行垃圾回收内存迟早会消耗完,导致其他对象无法分配内存,没有GC则无法保证应用程序的正常进行。
回收区域: 只有方法区和堆、频繁收集新生代、较少收集老年代、基本不动元空间/方法区
八、垃圾回收算法
判断对象是否存活的两种算法:引用计数法和可达性分析算法
1、引用计数算法
对于一个对象被引用则加1,引用失效就减1,当计数器为0时则表示该对象为垃圾。
缺点:无法解决循环依赖的问题
2、可达性分析算法:
以根对象为起始点从上往下搜索根对象所链接的对象是否可达,搜索走过的路径被称为引用链,不可达对象称为垃圾,
判定一个对象是否可回收,至少要经历两次标记过程。
GC Roots包含元素
- 方法区中常量引用对象
- 同步sync关键字持有的对象
- 静态类变量
3、标记清除算法
标记: 从引用根节点开始遍历,标记所有被引用的对象。一般是对象的header中记录为可达对象
清除: 从头到尾进行遍历,如果某个对象在其header中没有标记为可达对象,则将其回收,清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表,下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够就存放。
缺点: 产生空间碎片,还需要维护一个空闲列表
4、标记压缩算法(老年代)
缺点:移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址,移动过程中,需要全程暂停用户应用程序即STW。
5、复制算法(新生代)
核心思想:将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
在Eden区空间用完并且程序需要再创建对象时触发Minor GC 在GC后,如果对象仍然存活,将会被移到Survior区。
再次触发GC的时候Eden区和from区两者会作为回收区域
在Eden和from回收存活的对象复制到to之后要做三件事情
1、清空Eden和from区
2、把原先from变为to原先to变为from
3、对象d年龄加1(年龄达到设定值—>老年代)
优点:没有标记和清除过程,高效、不会产生空间碎片
缺点:需要两倍活着对象的空间大小
6、分代回收算法
新生代和老年代回收算法
7、增量回收算法
垃圾收集线程每次只收集一部分空间,接着切换到应用程序,反复执行,可避免长时间STW
缺点:线程来回切换造成上下文开销,降低吞吐量
8、分区回收算法
把一个内存区域划分为多个内存空间,每次只回收若干小区域内存
9、总结
没有最好的回收算法,只有最合适的,目前用的最多的是复合算法
九、MinorGC/MajorGC/FullGC的对比
a.MinorGC
只回收新生代
新生代空间不足的时候,该区域有个特点 对象大部分是朝生夕死
会触发STW 暂停其他用户线程 垃圾收集结束 用户线程才恢复
b.MajorGC
回收老年代
回收速度比MinorGC慢10倍以上 STW时间更长
c.FullGC
回收整个堆与方法区
更应该尽量避免