了解jvm
jvm跨平台原理
不同的操作系统上运行的jvm是不同的,但是功能,作用是相同的,所以只要将Java代码编译成字节码,各个操作系统的jvm都能将他执行成相同的效果。
字节码的作用是什么
hello.java----javac---->hello.class-----执行—>JVM
java代码-----编译----->字节码-----解释----->机器指令
作用:兼容各种语言,供不同平台的JVM解释成相应的机器指令
JVM整体结构
- hello.java
- hello.class
- 类加载子系统(类加载器)
- 加载
- 链接
- 验证:验证待加载class文件是否正确,比如验证,比如验证文件格式
- 准备:为static变量分配内存并赋零值
- 将符号引用解析为直接引用
- 初始化
- 运行时数据区(Runtime Data Area)
- 本地方法栈(native方法)
- Java方法栈(Java方法)
- 方法区
- 程序计数器(执行指令时,线程切换后又继续执行应该从哪一条指令继续执行)
- 堆
- 执行引擎
- 解释器
- JIT编译器
- 垃圾回收器
类加载器的分类
- 引导类加载器:BootStrapClassLoader
加载jre/lib目录下的包或类 - 自定义类加载器:继承实现ClassLoader类
- ExtClassLoader(扩展类加载器)
加载jre/lib/ext目录下的包或类 - AppClassLoader
加载classpath路径下的包和类 - WebAppClassLoader
- ExtClassLoader(扩展类加载器)
双亲委派(向上委派)
AppClassLoader------>ExtClassLoader------->BootStrapClassLoader
作用:
- 避免类的重复加载
- 防止核心API被篡改
Tomcat里面为什么要自定义类加载器
为了实现类加载的隔离
eg:
应用A和应用B存在类名相同的类(内容不同)需要被加载,但AppClassLoader只有一个,不能创建多个,所以有可能只能加载一个,在JVM中判断一个类是否已经被加载的逻辑是:类名+对应的类加载器实例,所以Tomcat自定义类加载器的原因是实现类加载的隔离。
运行时数据区
- 方法区(多线程共享):常量池,方法信息,类信息
- 堆(多线程共享):
- 新生代
- Eden
- S0
- S1
- 老年代
- 新生代
- java方法栈(线程私有):
- 栈帧:
- 局部变量表
- 操作数栈
- 动态链接
- 方法返回地址
- 栈帧:
- 本地方法栈(线程私有):native本地方法相关信息
- 程序计数器(线程私有):记录当前程序下一条执行的指令
程序计数器(PC Register)
- 是物理寄存器的抽象实现
- 用来记录待执行的下一条指令的地址
- 它是程序控制流的指示器,循环、if else、异常处理、线程恢复等都依赖它来完成
- 解释器工作时就是通过它来获取下一条需要执行的字节码指令的
- 它是唯一一个在JVM规范中没有规定任何OutOfMemoryError(内存溢出)情况的区域
虚拟机栈(Java栈,Java方法栈)
每个线程在创建时都会创建一个虚拟机栈,栈内会保存一个个的栈帧,每个栈帧对应一个方法。
- 虚拟机栈是线程私有的
- 一个方法开始执行栈帧入栈,方法执行完对应的栈就出栈,所以虚拟机栈不需要进行垃圾回收
- 虚拟机栈存在OutOfMemoryError和StackOverflowError
- 线程太多就有可能出现内存溢出,现车给创建时没有足够的内存去创建虚拟机栈了
- 方法调用层次太多,就可能会出现栈溢出
- 可以通过***-Xss***来设置虚拟机栈的大小
栈帧
- 局部变量表(多个slot)
记录局部变量在局部变量表中的位置和值 - 方法返回地址
- 操作数栈
用来在执行字节码指令过程中用来进行计算, - 动态链接
- 附加信息
本地方法栈
本地方法:native method,在Java中定义的方法,但由其他语言实现
虚拟机栈存的是Java方法调用过程的栈帧,本地方法栈存的是本地方法调用过程中的栈帧。是线程私有的,可能会出现内存溢出和栈溢出。
堆
-Xms:ms(memory start),指定堆的初始化内存大小,等价于-XX:initailHeapSize
-Xmx:mx(memory max),指定堆的最大内存大小,等价于-XX:MaxHeapSize
一般情况下会把ms和mx设置为一样大,这样JVM就不需要在GC后去修改堆的内存大小了,提高了效率,默认情况下,初始化内存大小=物理内存大小/64,最大内存大小=物理内存大小/4
可以通过-XX:NewRatio参数来配置新生代和老年代的比例,默认为2,表示新生代占1,老年代占2,也就是新生代占堆区总大小的1/3.
新生代中的eden,s0,s1的比例关系为8:1:1,可以通过-XX:SurvivorRatio来调整
新生对象创建后都会先存在eden区,当eden区满了就会触发MinorGC或YGC机制,回收一些垃圾对象,剩下的就会存入到S0区,并记录这些对象都经历过几次GC(YGC),后续eden区又满了,又会回收,然后eden转S0,S0剩下的转S1并记录2,表示这些对象经历过两次GC了,后面不断重复这个过程,当对象经历过的GC次数变成15时,下次再次执行时就会被放入老年代 ,当eden里面放了一个大对象时,S0,S1放不下就有可能从Eden直接到老年代,还有一种可能是Eden区都放不下的超大对象有可能直接放入老年代。
垃圾回收:
- Young GC/Minor GC:负责新生代的垃圾回收
- Old GC/Major GC:老年代垃圾回收
- Full GC:整堆回收
为什么要进行垃圾回收
垃圾是指在JVM中没有任何引用指向它的对象,如果不清理这些垃圾对象,它们就会一直占着内存,而不能给其他对象使用,最终垃圾对象越来越多,就会出现内存溢出(OOM)了。
垃圾标记阶段
- 引用计数法
每个对象都保存一个引用计数器属性,用于记录对象被引用的次数。
优点:实现简单,计数器为0时就代表是垃圾对象
缺点:需要额外空间存储引用和维护计数器,无法处理循环引用的问题 - 可达性分析法
以GC root(栈或方法区中存在的对象引用)为起始点,然后一层一层的找到所引用的对象,被找到的对象就是存活的对象,那么其他不可达对象就是垃圾对象。
标记清除算法
如果内存不足后,就会STW,暂停用户线程的执行,然后执行算法进行垃圾回收
- 标记阶段:从GC root开始遍历,找到可达对象,并在对象头中进行记录
- 清除阶段:堆内存空间进行线性遍历,如果发现对象头中没有记录是可达对象,则回收它
复制算法
将内存空间分为两块,每次只使用一块,在进行垃圾回收时,将可达对象复制到另外没有被使用的内存块中,然后再清除当前内存块中的所有对象,后续再按照同样的流程来进行垃圾回收,交换着来。
优点:
- 没有标记和清除阶段,通过GC root找到可达对象,直接复制,不需要修改对象头,效率高,不会出现内存碎片
缺点:
- 需要更多的内存,始终有一半内存空闲
- 如果可达对象比较多,那么复制过程中的开销就较大了
- 对象复制后,对象的内存地址发生了变化,需要修改维护引用地址
标记整理法
第一阶段和标记清除法一样,从GC root中找到并标记可达对象
第二阶段将所有存活对象移到内存的另一端
最后清理掉边界外所有的空间
垃圾清除算法对比总结:
标记-清除 | 标记-整理 | 复制 | |
---|---|---|---|
速度 | 中等 | 最慢 | 最快 |
空间开销 | 少(有碎片) | 少(无碎片) | 最多(空间换时间) |
移动对象 | 否 | 是 | 是 |
分代收集算法(理念)
新生代可以使用复制算法,老年代可以使用标记清除或者标记整理
常见的垃圾收集器
-XX:+PrintCommandLineFlags,查看使用过的垃圾收集器
-XX:+UseSerialGC,指定使用Serial GC和Serial Old GC
-XX:+UseParNewGC,指定新生代使用ParNew GC
-XX:+UseConMarkSweepGC,指定老年代使用CMS GC
-XX:+UseParallelGC,指定新生代使用ParParallelGC
-XX:+UseParallelSweepGC,指定老年代使用Parallel Old GC