JVM跨平台原理
不同操作系统上运行的JVM是不一样的,这才是JVM跨平台性的本质!
不同的操作系统都可以执行通一份字节码文件,字节码是什么呢?
就是把java文件翻译成了另一个格式,为什么要编译成class再给jvm呢,因为这样就让jvm当解释器,解释型语言了,运行起来就比较慢。如果我们提前已经编译好了,jvm运行起来就会非常快
如上图,别的语言也是可以编译成字节码给jvm运行的
JVM结构
1、类加载子系统
会把class文件加载到内存中的方法区中,然后验证(不可能随便一个方法区都处理他),准备阶段(为static属性分配一个0值,后面初始化才会赋值),解析(将类的名字解析为方法区对应的地址,那个class对象的地址),最后初始化阶段(给类里面static属性赋值)
类加载器分类
双亲委派机制(重点)
双亲委派机制(Parent Delegation)是Java中的一种类加载机制。在Java中,类加载器负责将类加载到Java虚拟机中,而双亲委派机制就是一种类加载器之间的协作方式,通过这种方式可以保证Java程序中的类加载的顺序和一致性。
根据双亲委派机制,当一个类加载器需要加载一个类时,它首先会将这个任务委托给它的父类加载器去完成,如果父类加载器还存在父类加载器,那么它会继续委托给它的父类加载器,直到委托到最顶层的启动类加载器为止,如果启动类加载器无法加载这个类,那么这个任务会回到子类加载器中去,由子类加载器自己去加载这个类。
这种机制可以保证Java程序中的类的一致性,也可以避免类的重复加载,提高了类加载的效率。同时,通过双亲委派机制,也可以实现对Java程序中的类的访问权限的控制,防止恶意类的加载和使用。
tomcat为什么要自定义类加载器
主要原因就是防止隔离,因为如果用jvm默认的类加载器,那么加载a应用的class和b应用class如果同名就不会再加载了,因为判断是类名+类加载器实例,名字相同所有相同,所有我们要对a和b设置单独的类加载器,webappclassloader。
2、运行时数据区
蓝色是多个线程共享的,绿色是线程私有的
程序计数器
记录下一条指令的地址
程序控制流的指示器,循环、if else、异常处理、线程恢复都依赖它来完成
解释器工作是就是通过他来获取下一跳执行的字节码指令得到
唯一一个不会发生内存溢出的区域,因为只会记录一个数据
虚拟机栈(java栈、java方法栈)
线程私有的,因为执行完就出栈,所以虚拟机栈不需要垃圾回收
线程太多 和 单个栈帧态度都都会发送异常的,可以通过参数来设置虚拟机栈的大小的
栈帧
一个栈帧其实就是对应一个方法,方法里面就会定义局部变量,右边这个slot就是一个个局部变量
操作数栈是用来执行字节码指令过程中用来计算的(就是辅助计算的一个栈,比如a=10,b=10,算a+b,先10进操作数栈然后进局部变量表a=10,然后b也进操作栈然后进局部表,然后从局部变量表中取出a和b进操作栈计算结果)
本地方法栈
跟java的虚拟机栈是差不多的,只是方法是本地方法
堆
jvm规范中规定所有对象和数组都应该放在堆中,在执行字节码就会创建对象放入堆中,对象对应的引用地址放入虚拟机栈的栈帧,不过当方法执行完之后,刚刚所有创建的对象不会立马回收,而是等jvm后台执行GC才回收
新生代和老年代默认比是1:2,新生代占三分之一,可以参数调整
默认下伊甸园和s0和s1的比是8:1:1,可以参数调整
假如来了个非常大的对象,伊甸园存活下来,这个对象要超过了s0和s1,那么将直接进入老年代
如图假如是这么个非常大对象,连伊甸园都进不了,直接进入老年代
垃圾回收
1、为什么要垃圾回收
垃圾是指JVM没有任何应用指向的对象,如果不清理这些垃圾对象,那么他们就一直占内存,而不能给其他对象使用,最终垃圾对象越来越多,就OOM
2、垃圾标记阶段(找垃圾)
JVM(主要堆中)有哪些垃圾对象,有引用技术法和可达性分析法
引用技术法
可达性分析法
GC Roots是一组引用,包括:
3、垃圾回收算法
标记-清除算法
效率不高,而且会产生内存碎片
复制算法
因为复制算法要复制,所以垃圾越多越好,留下来的越少,移动的就越少,这样效率越高,所以很适合新生代,这个地方垃圾很多,所以s0和s1是不断交换的
标记整理算法
效率是非常低的,但是没有内存碎片
对比总结
4、垃圾收集器
CMS垃圾回收器
并发标记清楚垃圾回收期,特点是低暂停
地暂停就是让STW的时间变短,而且在垃圾回收过程中大部分时间用户线程还在执行,用户体验非常好,但是整个垃圾回收过程长了,吞吐量也更低(单位时间内执行用户线程更少)
初始阶段: 先STW一下,暂停所有工作线程,然后标记GC Root能直接可达的对象(找第一层就ok,所以STW非常快)一旦标记完恢复所有线程工作
并发标记:从上个阶段标记出对象,开始遍历整个老年代,标记出所有可达对象,耗时比较长,但不用STW,用户一起执行,三色标记
重新标记:解决上一阶段产生的误差,需要修正,需要STW但是不长
并发清理:删除垃圾对象,由于不需要移动,这个阶段也可以和用户一起执行,不需STW
G1垃圾回收器
从原本物理连续的新生老年代变成了逻辑上的新生老年代,全部都放一起了,然后大对象超过一个region50%就用humongous区
橙色代表垃圾回收线程,满橙色代码STW,前面两个STW阶段耗时都是非常少的,最后一个是自己设置手动设置能接受的实际来回收
扩展
当前的类加载器找不到需要加载的类怎么办
检查类路径:类路径指定了JVM在哪里搜索类,如果类路径没有正确设置,JVM就无法找到需要加载的类。可以通过命令行或IDE配置正确的类路径。
检查类名:如果类名拼写错误,或者包名和目录结构不匹配,JVM也无法找到需要加载的类。可以检查类名是否拼写正确,以及类名是否和文件名一致,是否在正确的包中。
检查依赖项:如果需要加载的类依赖其他类,而这些依赖项没有被正确加载,JVM也会找不到需要加载的类。可以检查类依赖项是否已经正确加载,并且是否存在冲突。
检查类加载器:如果当前类加载器找不到需要加载的类,可以考虑使用其他类加载器。例如,可以使用父类加载器或扩展类加载器来加载需要的类。
检查权限:如果当前类加载器没有足够的权限加载需要的类,JVM也会找不到需要加载的类。可以检查类加载器是否有足够的权限,以及是否需要增加相应的权限。
综上所述,当当前的类加载器找不到需要加载的类时,可以从类路径、类名、依赖项、类加载器和权限等多个方面入手,以找到解决问题的方案。
如何打破双亲委派机制
在Java中,类加载器采用双亲委派模型,不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。
但是有时候,我们需要打破双亲委派机制,例如在某些应用场景下,需要加载同名但不同版本的类,或者需要动态修改已加载的类等。为了打破双亲委派机制,可以使用以下两种方式:
自定义类加载器:可以继承ClassLoader类,重写loadClass方法,在该方法中实现自定义的加载逻辑。在加载类时,可以先通过自定义的类加载器加载类,如果找不到,则使用原有的双亲委派机制去加载。这样可以达到打破双亲委派机制的目的。
使用线程上下文类加载器:在Java 2之后,引入了线程上下文类加载器的概念。在加载类时,可以使用当前线程的上下文类加载器去加载类,如果找不到,则使用原有的双亲委派机制去加载。这样可以实现类加载器的灵活切换,达到打破双亲委派机制的目的。
需要注意的是,打破双亲委派机制可能会引起类加载器的混乱和冲突,可能会导致类的版本不一致,从而引发各种问题。因此,打破双亲委派机制应该谨慎使用,只在必要的情况下使用,并且需要做好相关的测试和风险评估。