JVM
JDK、JRE、JVM之间的区别?
JDK : Java标准开发包,提供了编译、运行所需的各种工具和资源,包括Java编译器,Java运行时环境,以及常用的Java类库等
JRE : Java运行环境,用于运行Java字节码,包含了JVM以及JVM工作所需要的类库,所以普通用户安装JRE就可以运行Java程序,开发者则必须安装JDK来编译、调试程序。
JVM : Java虚拟机,时JRE的一部分,他可以运行字节码文件,不同系统的虚拟机实现是Java实现跨平台的最核心的部分。
JVM内存模型
线程共享
堆
存储了对象。
字符串常量池
是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
方法区
存储常量,静态变量,类信息。如果静态变量是对象,那么对象还是存储在堆中,方法区存储的是对象的引用(内存地址)。
运行时常量池
Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic Reference)的 常量池表(Constant Pool Table) 。
常量池表会在类加载后存放到方法区的运行时常量池中。
线程私有
栈
一个线程开启后,会从栈中分配内存区域,为自己的线程栈。
本地方法栈
被 native 修饰的方法就是本地方法,比如 Thread.start() 源码中调用的 start()就是本地方法。
调用被 native 修饰的方法时,就会从本地方法栈中获取一块内存来运行本地方法。
也就是和线程栈一样,每个线程调用本地方法都有自己独立的内存空间。
程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
栈的详解
一个线程开启后,会从栈中分配内存区域,为自己的线程栈。
当线程调用方法后,会从自己的线程栈中获取一些内存,用来存储方法中产生的数据,比如成员变量,所以方法内的成员变量有效区是方法内。
栈帧
一个方法会对应一个专属的内存区域,这就是栈帧内存区域,是用来隔离不同的方法所占用的内存区域。
线程栈也是一个存储结构,它存储栈帧,规则是先进后出。
在上图中就是一个线程栈运行了main()和compute()两个方法后的状态。
main()先被调用,先进栈,compute()后被调用,所以在main()的上面。
最后出栈时也是compute()先出栈,出栈后,compute方法所占用的内存也被释放了。
局部变量表
局部变量表用来存放方法中产生的各种变量,如果局部变量是对象,那么存储的是对象的引用(内存地址)
操作栈数
操作数栈是在程序运行过程中,用于存储操作数的临时内存空间。
如 int a = 1;
首先在局部变量表中创建a,然后操作数栈中压入一个1,然后这个1会出栈,存入a在局部变量表中所在的内存空间。
如 int a = 3 * 10;
首先在局部变量表中创建a,然后操作数栈中完成3*10这个操作,得到结果30,然后将30出栈,存入a在局部变量表中所在的内存空间。
动态链接
图中调用了compute()方法,这个方法是存储在方法区的,是线程共享的,动态链接就是compute()在方法区的入口地址,通过这个入口地址,就可以在方法区找到compute()的指令代码。
方法出口
保存了compute()方法在main()方法中的位置,用于解决执行完compute()方法后,执行main()方法中哪一行代码的问题。
比如方法的返回值,就是通过方法出口,返回到main方法中去的。
垃圾收集
堆空间基本结构
新生代:Eden、s0、s1
老年代:Tenured
模型的默认内存分配
新生代占用1/3,老年代占用2/3
新生代中 Eden、s0、s1的内存占用比例为8:1:1
回收机制
JVM垃圾回收器判断对象是否存活的基本算法是可达性分析算法
可达性分析算法:
将“GC Roots”对象作为起点,从这些节点向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象。
GC Roots根节点:线程栈的本地变量,静态变量、本地方法栈的变量等等。
最开始对象都在Eden中,触发minor gc后,主要是从方法区、栈、本地方去栈中的GC Root找到它们引用的对象,一直找到最后一个引用对象,这个对象的成员变量不在引用其他对象了。
这条GC Root链条上的所有对象都在被引用与使用,所以标记为非垃圾对象。
那么这些非垃圾对象都会被复制到一个空的Survivor中,Eden中其他对象都会被回收掉。
每次minor gc都会回收 Eden和Survivor,如果Survivor里面的对象依旧存活,那么会复制到另外一个空的Survivor中,比如s0复制到s1中。
每次回收,对象存活,它的分代年龄会+1,当达到15后,会被移动到老年代中。
老年代存放满了,不会直接触发OMM,会触发full gc,如果触发full gc没有回收到足够的空间,那么就会触发OMM
注意:
如果Survivor的一个区,如s0放不下s1和Eden中的对象,那么会直接放到老年代中。
对象动态年龄判断机制:
minor gc后,需要复制到Survivor区的这一批对象总大小超过了Survivor一个区内存大小的50%,那么这批对象会直接进入老年代。
JVM为什么要调优?
STW: Stop-The-World: 是在垃圾回收算法执⾏过程当中,将JVM内存冻结应用程序停顿的⼀种状态。
minor gc 时间非常短,full gc时间较长。
minor gc 和 full gc都会触发STW,那么用户在使用的时候可能会出现卡顿等情况,等STW过去后就会恢复正常。
调优就是为了减少STW的出现,主要为了减少full gc的出现,当然minor gc出现太频繁也不行。
假如给堆分配的是3G的内存空间。
那么根据堆的内存模型分配,新生代占用1/3,占用到1G.
Eden、s0、s1比例是8:1:1,那么s0与s1都是100M
对象生成都进入了Eden区,触发minor gc后,还剩下70M的对象,大于了s0的50%,触发了对象动态年龄判断机制,那么对象会直接进入老年代。
一般情况下,没有很高的数据量,不会产生这么大的对象,可以正常使用。
但是项目有秒杀,大促等等功能,用户量足够,可能活动期间内,每秒有很高的数据量。
每秒都可能有70M的对象存入老年代,老年代满了后就会触发full gc,消耗时间长。
触发STM后用户体验会很差。
如何优化?
根据这个秒杀和大促的业务需求,订单数据量大,订单完成支付就结束了,所以对象实际会过期的很快,那么老年代可以不需要那么大的内存,新生代则需要很大的内存,至少s0与s1要大于预估值2倍以上,比如预估是每秒70M的存活对象,那么要大于70M*2,尽量调高增加容错。
3G的堆内存,可以手动设置老年代占用1G,Eden1.6G,s1 200MB,s2 200MB
这样可以支持100MB以下的存活对象。
其他情况还是根据业务和项目功能进行分析调整。
JVM参数
调优堆栈内存
JVM相关文章
调优工具
1.Arthas(推荐)(阿里2018-09 开源的Java诊断工具)
m