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参数

调优堆栈内存

参数

说明

示例

-Xmx

设置最大堆大小

-Xmx3072m,设置JVM最大可以哦那个内存为3072MB

-Xms

设置最小堆大小

-Xms3072m,设置JVM初始内存为3072MB。

建议与 -Xmx 相同,避免每次垃圾回收后,JVM重新分配内存。

-Xmn

设置年轻代大小

-Xmn2g,设置年轻代大小为2GB。

整个JVM内存大小为年轻代+老年代+持久代。

持久代大小一般固定为64MB。

年轻代增大后,会减小老年代大小。

Sun官方推荐配置为整个堆的3/8。

-Xss

设置线程栈大小

-Xss128k,设置每个线程栈大小为128KB

JDK5.0以前每个线程栈大小为1MB。

JDK5.0后每个线程栈大小为256KB。

在相同物理内存下,减小该值可以生成更多的线程。

但操作系统堆进程内的线程上线有一定限制,一般是3000~5000。

-XX:NewRation=n

设置值年轻代和老年代的占用内存比值

-XX:NewRation=4,设置年轻代(Eden+Survivor)与老年代的比值。

设置为4,那么年轻代:老年代为1:4。

年轻代占用堆栈的1/5。

-XX:SurvivorRation=n

设置Survivor与Eden的占用内存比值

-XX:SurvivorRation=4,设置Survivor(S0与S1)与Eden的比值。

设置为4,那么Survivor(S0与S1各占1):Eden为2:4

一个Survivor区占整个年轻代的1/6。

-XX:MaxTenuringThreshold=n

设置最大分代年龄

-XX:MaxTenuringThreshold=0,设置最大分代年龄为0。

设置为0,年轻代对象不会经过Survivor区,直接进入老年代,对老年代较多的应用,提升了效率。

设置较大的值,那么年轻代对象会在S0与S1中进行多次复制,增加了对象在年轻代的活动时间,增加了在年轻代被回收的概率。

JVM相关文章

JVM 参数配置说明 - 阿里云官方文档

JVM 内存配置最佳实践 - 阿里云官方文档

调优工具

1.Arthas(推荐)(阿里2018-09 开源的Java诊断工具)

官网:https://arthas.aliyun.com/

m