JVM
全称是 Java Virtual Machine
,中文称为 Java
虚拟机 。
JVM
是Java
程序运行的底层平台,与Java
支持库一起构成了Java
程序的执行环境。分为JVM
规范和JVM
实现两个部分。简单来说,Java
虚拟机就是指能执行标准Java
字节码的虚拟计算机。
现在的JDK
、JRE
和JVM
一般是整套出现的。
JDK
= JRE
+ 开发调试诊断工具
JRE
= JVM
+ Java
标准库
常见的JDK
厂商包括:
Oracle
公司,包括 Hotspot
虚拟机、GraalVM
;分为OpenJDK
和OracleJDK
两种
版本。IBM
公司,J9
虚拟机, 用在IBM
的产品套件中Azul Systems
公司,高性能的Zing
和开源的Zulu
Dragonwell
是阿里开发的OpenJDK
定制版Corretto OpenJDK
Red Hat
公司的OpenJDKAdopt
OpenJDK
此外,还有一些开源和试验性质的JVM
实现,比如Go.JVM
各种版本的JDK
一般来说都会符合Java
虚拟机规范。
两者的区别一般来说包括:
JDK
提供的工具套件略有差别,比如jmc
等有版权的工具。JRE
中某些私有的API
不一样。选择哪个版本需要考虑研发团队的具体情况:比如机器的操作系统,团队成员的掌握情况,兼顾遗留项目等等。
当前Java最受欢迎的长期维护版本是Java8
和Java11
。
Java8
是经典LTS
版本,性能优秀,系统稳定,良好支持各种CPU
架构和操作系统平台。Java11
是新的长期支持版,性能更强,支持更多新特性,而且经过几年的维护已经很稳定。有的企业在开发环境使用OracleJDK
,在生产环境使用OpenJDK
,也有的企业恰好相反,在开发环境使用OpenJDK
,在生产环境使用OracleJDK
,也有的公司使用同样的打包版本。
开发和部署时只要进行过测试就没问题。
一般来说。 测试环境、预上线环境的JDK
配置需要和生产环境一致。
Java
中的字节码,是值 Java
源代码编译后的中间代码格式,一般称为字节码文件。
字节码文件中,一般包含以下部分:
可以这么说,大部分信息都是通过常量池中的符号常量来表述的。
常量是指不变的量,比如字母 'A'
或者数字 1024
在UTF8
编码中对应到对应的二进制格式都是不变的。同样地,字符串在Java
中的二进制表示也是不变的, 比如 "AA"
。
在Java
中需要注意的是, final
关键字修饰的字段和变量,表示最终变量,只能赋值1
次,不允许再次修改,由编译器和执行引擎共同保证。
在Java
中,常量池包括两层含义:
class
文件中的一个部分,里面保存的是类相关的各种符号常量。根据 JVM规范,标准的JVM
运行时数据区包括以下部分:
具体的JVM
实现可根据实际情况进行优化或者合并,满足规范的要求即可。
堆内存是指由程序代码自由分配的内存,与栈内存作区分。在Java
中,堆内存主要用于分配对象的存储空间,只要拿到对象引用,所有线程都可以访问堆内存。
以Hotspot
为例,堆内存(HEAP
)主要由GC
模块进行分配和管理, 可分为以下部分:
其中,新生代和存活区一般称为年轻代。
除堆内存之外,JVM
的内存池还包括非堆(NON_HEAP
),对应于JVM
规范中的方法区,常量池等部分:
内存溢出(OOM
)是指可用内存不足。
程序运行需要使用的内存超出最大可用值,如果不进行处理就会影响到其他进程,所以现在操作系统的处理办法是:只要超出立即报错,比如抛出内存溢出错误 。
就像杯子装不下,满了要溢出来一样,比如一个杯子只有500ml
的容量,却倒进去600ml
,于是水就溢出造成破坏。
内存泄漏(Memory Leak
)是指本来无用的对象却继续占用内存,没有再恰当的时机释放占用的内存。
不使用的内存,却没有被释放,称为内存泄漏 。 也就是该释放的没释放,该回收的没回收。
比较典型的场景是: 每一个请求进来,或者每一次操作处理,都分配了内存,却有一部分不能回收(或未释放),那么随着处理的请求越来越多,内存泄漏也就越来越严重。
在Java
中一般是指无用的对象却因为错误的引用关系,不能被GC
回收清理。
如果存在严重的内存泄漏问题,随着时间的推移,则必然会引起内存溢出。
内存泄漏一般是资源管理问题和程序BUG
,内存溢出则是内存空间不足和内存泄漏的最终结果。
在64
位的系统中,如下:
public class Test{
private long orderId;
private long userId;
private byte state;
private long createMillis;
}
一般来说,Test
类的每个对象会占用40
个字节。怎么算的,往下看
64
位JVM
中,对象头占用12
字节,但是以8
字节对齐,所以一个空类的实例(空对象)至少占用16
字节。在32
位JVM
中,对象头占用8
个字节,以4
的倍数对齐(不是说4
字节)。所以new
出来的很多简单对象,甚至是new Object()
,都会占用不少内容。
计算方式为:
12
字节。long
类型的字段占用8
字节,3
个long
字段占用24
字节。byte
字段占用1
个字节。37
字节,而64
位JVM
是8
字节对齐,则实际占用40
个字节。对象头中一般包含两个部分:
8
字节。8
个字节。32
位JVM
,以及内存小于-Xmx32G
的64
位JVM
上(默认开启指针压缩),一个引用占用的内存默认是4
个字节。所以,64
位的JVM
一般需要消耗更多堆内存。所以前面的计算中,对象头占用12
字节。
如果是数组,对象头中还会多出一个部分:
int
值,占用4
字节。如果是包装类型,那么比原生数据类型消耗的内存要多:
Integer
:占用16
字节(头部8+4=12
,数据4
字节),因为 int
部分占4
个字节,所以使用 Integer
比原生类型int
要多消耗 300%
的内存。Long
:一般占用24
个字节(头部8+4
+数据长度8
字节=20
字节,再对齐),当然,对象的实际大小由底层平台的内存对齐确定,具体由特定 CPU
平台的 JVM
实现决定。 看起来一个 Long
类型的对象,比起原生类型 long
多占用了8
个字节(也多消耗200%
)。截止目前,JVM
可配置参数已经达到1000
多个,其中GC
和内存配置相关的JVM
参数就有600
多个。
但在绝大部分业务场景下,常用的JVM
配置参数也就10
来个。
例如:
# JVM启动参数不换行
# 设置堆内存
‐Xmx4g ‐Xms4g
# 指定GC算法
‐XX:+UseG1GC ‐XX:MaxGCPauseMillis=50
# 指定GC并行线程数
‐XX:ParallelGCThreads=4
# 打印GC日志
‐XX:+PrintGCDetails ‐XX:+PrintGCDateStamps
# 指定GC日志文件
‐Xloggc:gc.log
# 指定Meta区的最大值
‐XX:MaxMetaspaceSize=2g
# 设置单个线程栈的大小
‐Xss1m
# 指定堆内存溢出时自动进行Dump
‐XX:+HeapDumpOnOutOfMemoryError
‐XX:HeapDumpPath=/usr/local/
此外,还有一些常用的属性配置:
# 指定默认的连接超时时间
‐Dsun.net.client.defaultConnectTimeout=2000
‐Dsun.net.client.defaultReadTimeout=2000
# 指定时区
‐Duser.timezone=GMT+08
# 设置默认的文件编码为UTF‐8
‐Dfile.encoding=UTF‐8
# 指定随机数熵源(Entropy Source)
‐Djava.security.egd=file:/dev/./urandom
需要根据系统的配置来确定,要给操作系统和JVM
本身留下一定的剩余空间。
推荐配置系统或容器里可用内存的 70~80%
最好。
比如说系统有 8G
物理内存,系统自己可能会用掉一点,大概还有 7.5G
可以用,那么建议配置 ‐Xmx6g
。
说明:
7.5G*0.8 = 6G
,如果知道系统里有明确使用堆外内存的地方,还需要进一步降低这个值。
JVM
总内存=栈+堆+非堆+堆外+Native
一般来说,JDK8
及以下版本通过以下参数来开启GC
日志:
‐XX:+PrintGCDetails ‐XX:+PrintGCDateStamps ‐Xloggc:gc.log
如果是在JDK9
及以上的版本,则格式略有不同:
‐Xlog:gc*=info:file=gc.log:time:filecount=0
java ‐XX:+UseG1GC
‐Xms4g
‐Xmx4g
‐Xloggc:gc.log
‐XX:+PrintGCDetails
‐XX:+PrintGCDateStamps
Hello
Java8
版本的Hotspot JVM
,默认情况下使用的是并行垃圾收集器(Parallel GC
)。其他厂商提供的JDK8
基本上也默认使用并行垃圾收集器。
Java9
之后,官方JDK
默认使用的垃圾收集器是G1
。
常见的垃圾收集器包括:
‐XX:+UseSerialGC
‐XX:+UseParallelGC
‐XX:+UseConcMarkSweepGC
‐XX:+UseG1GC
就是只有单个worker
线程来执行GC
工作。
并行垃圾收集,是指使用多个GC worker
线程并行地执行垃圾收集,能充分利用多核 CPU
的能力,缩短垃圾收集的暂停时间。 除了单线程的GC
,其他的垃圾收集器,比如 PS
,CMS
, G1
等新的垃圾收集器都使用了多个线程来并行执行GC
工作。
并发垃圾收集器,是指在应用程序在正常执行时,有一部分GC
任务,由GC
线程在应 用线程一起并发执行。 例如 CMS/G1
的各种并发阶段。
G1
的堆内存不再单纯划分为年轻代和老年代,而是划分为多个(通常是 2048
个)可以存放对象的小块堆区域(smaller heap regions
)。
每个小块,可能一会被定义成 Eden
区,一会被指定为 Survivor
区或者 Old
区。 这样划分之后,使得 G1
不必每次都去回收整个堆空间,而是以增量的方式来进行处 理: 每次只处理一部分内存块,称为此次 GC
的回收集(collection set
)。
下一次GC
时在本次的基础上,再选定一定的区域来进行回收。增量式垃圾收集的好处 是大大降低了单次GC
暂停的时间。
年轻代是分来垃圾收集算法中的一个概念,相对于老年代而言,年轻代一般包括:
Eden
区。GC
时,用存活区来保存活下来的对象。 存活区也是年轻代 的一部分,但一般有2
个存活区,所以可以来回倒腾。 因为GC
过程中,有一部分操作需要等所有应用线程都到达安全点,暂停之后才能执行,这时候就叫做GC
停顿,或者叫做GC
暂停。
这两者一般可以认为就是同一个意思。
缺乏经验的话,针对当前问题,往往需要使用不同的工具来收集信息,例如:
CPU
,内存,磁盘IO
,网络等等)GC
日志一般根据APM
监控来排查应用系统本身的问题。
有时候也可以使用Chrome
浏览器等工具来排查外部原因,比如网络问题。
可量化的3个性能指标:
TPS
;这些指标。可以具体拓展到单机并发,总体并发,数据量,用户数,预算成本等等。
这个问题请根据实际情况回答,比如Linux
命令,或者JDK
提供的工具等。
可以使用 ps ‐ef
和 jps ‐v
等等。
比如: free ‐m
, free ‐h
, top
命令等等。
一般先使用 jps
命令, 再使用 jstack ‐l
一般使用 jmap
工具来获取堆内存快照。
根据实际情况来看,获取内存快照可能会让系统暂停或阻塞一段时间,根据内存量决定。
使用jmap
时,如果指定 live
参数,则会触发一次FullGC
,需要注意。
示例:
jmap ‐dump:format=b,file=3826.hprof 3826
JVM
有一个内置的分析器叫做HPROF
, 堆内存转储文件的格式,最早就是这款工具定义的。
一般使用 Eclipse MAT
工具,或者 jhat
工具来处理。
上网搜索是比较笨的办法,但也是一种办法。另外就是,各种JDK
工具都支持 ‐h
选项来查看帮助信息,只要用得比较熟练,即使忘记了也很容易根据提示进行操作。