JVM内存模型深度剖析和优化
Java语言的跨平台性
问题
: Java语言的跨平台性是如何做到的- 通过不同操作系统平台的JVM版本,Linux和Windows的JVM版本各不相同
- Java是跨平台解释性语言,可以在不同的操作系统运行,
JVM从软件层面屏蔽不同操作系统在底层硬件和指令的差别
JVM内存模型
详细链接: https://www.processon.com/view/link/61e31579f346fb06cb9afec9
- JVM的组成部分主要分为两个子系统和两个组件
- 两个子系统 就是 类加载子系统 和 字节码执行引擎
- 类加载子系统 主要是负责将字节码文件加载到JVM内存中去
- 字节码执行引擎 主要是负责GC、执行字节码文件中的指令以及修改程序计数器中的值等
- 两个组件 就是 本地接口 和 运行时数据区
- 本地接口 主要是与本地库打交道,都知道JVM是C++写的,所以需要一些本地库的支持
- 运行时数据区 就是我们常说的JVM内存,主要分为线程共享和线程独享两大块
- 线程共享 有 堆 和 方法区
- 堆
- 一般new出来的对象都会存放在堆中
- 方法区(元空间)
- 方法区在JDK 7时处于堆中,在JDK 8中,方法区叫元空间,从堆中移除而放到直接内存中,主要是因为直接内存对IO操作具有更高的性能并且减少中间步骤的开销(虚拟内存到直接内存的重复开销)
- 堆
- 线程独享 有 栈、本地方法栈 和程序计数器
- 栈
- 栈主要是存放栈帧的,一个方法对应一个栈帧,栈帧主要分为局部变量表、操作数栈、方法出口、动态链接
- 局部变量表
- 局部变量表 类似于一个table,存放编译期间的变量或对象的内存地址
- 在编译期间,this会作为隐式参数放在局部变量表的第一位
- 局部变量表 类似于一个table,存放编译期间的变量或对象的内存地址
- 操作数栈
- 操作数栈 主要是用来进行一些操作数运算,运算完之后再赋值到局部变量表
- 方法出口
- 方法出口 就是通过它进行定位方法被调用的位置
- 动态链接
- 动态链接 就是引用类型变量与堆中实际对象的关联关系,主要是用来定位堆中实际对象的
- 局部变量表
- 栈主要是存放栈帧的,一个方法对应一个栈帧,栈帧主要分为局部变量表、操作数栈、方法出口、动态链接
- 本地方法栈 与 栈基本一致,只不过是用来处理本地方法
- 程序计数器 相当于代码执行位置的标识
- 栈
- 线程共享 有 堆 和 方法区
- 两个子系统 就是 类加载子系统 和 字节码执行引擎
- 整体流程就是 类加载子系统会将字节码文件加载到JVM内存中,而字节码文件不能直接被操作系统识别,所以需要通过字节码执行引擎去解析成操作系统能识别的指令,然后将指令交给CPU去执行,而这个过程需要本地接口与本地库的交互
JVM参数设置
-
分析
: 方法区如果内存使用达到21M(默认21M),也会触发FullGC,FullGC不但会回收堆也会回收方法区 -
推荐
: JVM调参java -Xms2048M -Xmx2048M -Xmn1024M -Xss512K -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -jar a.jar
-
方法区的参数
- -XX:MaxMetaspaceSize: 设置元空间最大值,默认-1(表示不受限制),会进行动态扩容,只受限于本地内存大小
- -XX:MetaspaceSize: 指定元空间触发Fullgc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M左右,达到该值就会触发full gc进行类型卸载, 同时收集器会对该值进行调整
- 如果释放了大量的空间, 就适当降低该值
- 如果释放了很少的空间, 那么在不超过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值,这个跟早期jdk版本的-XX:PermSize参数意思不一样,-XX:PermSize代表永久代的初始容量
-
堆参数
- -Xms: 堆空间初始大小
- -Xmx: 堆最大空间
- -Xmn: 年轻代大小
- -Xss: 栈空间大小
- -XX:NewRatio: 默认2表示年轻代占老年代的1/2,占堆的1/3
- -XX:SurvivorRatio: 默认8表示一个Survivor区占Eden区的1/8,占堆的1/10
-
调优
: 在设置JVM参数时,建议将方法区也设置一个值比如256M,避免在项目启动时,因为方法区太小而导致频繁FullGC
常见问题
什么样的对象会被移动到老年代
- 长期存活的对象,比如静态变量引用对象、对象池、缓存对象、Spring容器中的Bean对象等
MinorGC和FullGC都会触发STW吗
- 都会,但MinorGC触发STW时间比较短,用户几乎无感知,但FullGC触发时间比较长
为什么要设置STW机制
-
什么是STW
- STW就是Stop The World,停止所有用户线程。字节码执行引擎会开启后台线程去进行垃圾回收,就比如电商系统,用户发起下单请求对应的后端会有用户线程进行处理,而相对应的会产生大量的对象,需要及时的进行垃圾回收,这时候会进行STW,对于用户的体验就是卡顿一下,如果STW时间过长对于用户的体验是非常不好的
-
当通过gcroots去找可回收的对象时,假如当前确认A对象此时没有被引用着,如果不停止所有用户线程,可能会出现有其他线程再次引用A对象,但gcroots不知道A对象又被引用了,可能就直接把该对象回收掉了,造成这条引用断裂,这样会出大问题,为了避免这种情况的产生,JVM干脆停止所有用户线程去进行回收
-
可以举个例子,在使用serial和parNew进行垃圾回收时,因为采用的是复制算法,如果不暂停所有用户线程,那么复制到Survivor区可能有存活的对象和垃圾对象,这样肯定是不行的
什么时候会报错StackOverflowError
- 比如当递归调用方法的时候很有可能出现该错误,因为
线程栈被栈帧放满
- -Xss设置越小,说明一个线程栈里能分配的栈帧也就越小,但是对JVM整体来说能开启的线程数也就越多,为什么这么说呢
- 因为方法区和栈都是直接使用操作系统的直接内存的,栈内存越小,理论上操作系统就能开更多的线程
JVM优化原则是什么
- 尽可能让对象
都在年轻代里被分配和回收
,尽量别让太多对象频繁进入老年代
,避免频繁对老年代进行垃圾回收,同时给系统充足的内存
大小,避免年轻代频繁的进行垃圾回收
解释下Java里面的直接内存
- 直接内存有一种叫法
堆外内存
- 直接内存指的是Java应用程序直接从操作系统申请到的内存,运行时数据区的内存都是虚拟的内存
为什么JDK8将方法区从堆上移动到堆外内存
- 因为直接使用直接内存,IO操作上具有更高的性能,跟
零拷贝的原理有点雷同
有哪些方式可以直接使用直接内存
- Java的Unsafe类,做了一些本地内存的操作
- Netty的直接内存(Direct Memory),底层会调用操作系统的malloc函数
- JNI或者JNA程序,直接操纵了本地内存,比如一些加密库