JVM
JVM概述
1. 什么是JVM
Java Virtual Machine - java 程序的运行环境(java 二进制字节码的运行环境)
2. 好处与作用
-
一次编写,到处运行
JVM是Java能够实现跨平台的原因。
-
自动内存管理,垃圾回收功能
JVM有自动垃圾回收机制,可以回收内存中无用的对象。
-
数组下标越界检查
JVM内部有对数组下标越界的检查,可以避免发生修改其他数组内存的情况发生。因为数组的空间是连续的,当数组越界之后可能会修改别的数组的内存对应的数值。
-
多态
3. jvm、jre、jdk之间的关系
JDK:Java Development Kit: Java 开发环境;由 jre + 运行开发环境 组成。
JRE:Java Runtime environment: Java 运行环境;由 jvm + 核心类库 组成。
JVM:Java virtual machine: java 虚拟机。
三者关系:jvm < jre < jdk
4. 常见的JVM
JVM只是一套规范,不同的厂商可以制定不同的规范。
-
Hotspot:官网上下的基本都是这个,免费的。(目前国内主流的)
-
J9t:IBM的,商用的需要和IBM的其他软件绑定,比如webSphere。
-
Zing VM:这是收费的,垃圾回收做的很厉害(可以在 10ms 内回收 TB级别 的内存)
-
JRockitt:比较老的JVM
-
Microsoft JVMt:这个没有了
一、 JVM内存结构(JDK1.8)
- JVM由:程序计数器、虚拟机栈、本地方法栈、堆、方法区(元空间)组成。
1. 程序计数器
- 作用:是记住下一条jvm指令的执行地址
- 特点:
- 是线程私有的,每个线程有属于的自己的程序计数器
- 不会存在内存溢出,因为只记录下一条指令的地址,指向完指令之后就会刷新,所以不存在内存溢出。
2. 虚拟机栈
- 虚拟机栈的数据结构和数据结构中的栈一致,先进后出,后进先出。
- 每个线程运行时所需要的内存,一个线程对应一个虚拟机栈,是一对一关系。
- 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存;即每调用一个方法就会产生一个栈帧。
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
- 当方法执行完之后,方法对应在虚拟机栈创建的栈帧会被弹出。
1. 垃圾回收是否涉及栈内存
不涉及,因为当方法执行完之后,方法对应在虚拟机栈创建的栈帧会被弹出。不需要GC,栈帧会自己销毁。
2. 栈内存分配越大越好吗
不是,因为一个线程对应一个虚拟机栈。当虚拟机栈的内存分配变大时,整个应用程序能运行的线程数将会变少。
3. 方法内的局部变量是否线程安全?
- 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的,即在当前方法内。
- 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全。
- 局部变量是以参数方式传递到方法中
- 局部变量当做方法返回值进行返回
4. 栈内存溢出
-
java.lang.StackOverflowError
-
栈帧过多导致栈内存溢出,即执行的的方法过多导致创建过多的方法,从而超出栈内存。
- 例如,递归调用,没有设置合适的终止条件。
-
栈帧过大导致栈内存溢出,一个或者几个方法中需要的内存直接超出栈的最大内存。
- 例如,单个方法创建的时需要的栈帧内存需要过多或者方法过多导致栈内存溢出。
5. 栈内存参数设置
- 设置256k
-Xss256k
6. 线程运行诊断案例
案例1: cpu 占用过多
linux系统定位:
-
用top定位哪个进程对cpu的占用过高找到 pid 进程号
-
ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
-
jstack 进程id
可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号
案例2:程序运行很长时间没有结果
查看对应的代码中是否存着锁,可以通过JDK自带的 jvisualvm 查看,可以查看是否存在死锁。
使用方式:直接在命令行输入 jvisualvm 回车出现界面,在左边选择需要查看的线程。
3. 本地方法栈
- Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
- 本地方法栈,也是线程私有的。
- 本地方法是使用C语言实现的。
- 它的具体做法是 Native Method stack中登记 native方法,在Execution Engine执行时加载本地方法库。
- 对于一个运行中的Java程序而言,它还可能会用到一些跟本地方法相关的数据区。当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。本地方法可以通过本地方法接口来访问虚拟机的运行时数据区,但不止如此,它还可以做任何它想做的事情。
- 并不是所有的JVM都支持本地方法。
- Hotspot 中的本地方法,例如:object中的方法 hashCode()....
4. 堆 Heap
-
通过 new 关键字,创建对象都会使用堆内存
-
虚拟机栈中的栈帧中创建的对象也在堆中,虚拟机栈中的栈帧只保留指向堆中变量的内存地址。
-
需要当一个new出来的局部变量属于某个方法时,且该变量没有的作用范围没有跳出当前方法
即,指向当前变量的方法只有一个,那么此变量也就不存在线程安全问题,只有当指向这个变量的引用
不止一个时会存在线程安全问题(变量被当做返回值返回,或者是以参数形式传入方法)
-
-
堆线程共享的,堆中对象都需要考虑线程安全的问题
-
堆有垃圾回收机制
1. 堆内存溢出
- 异常:java.lang.OutOfMemoryError:Java heap space
- 例如:for循环对字符串 String 进行拼接,每次都会在堆中创建出新的对象,最终会导致堆内存溢出。
2. 堆内存参数设置
- 分配8m
-Xmx8m
3. 堆内存诊断工具
1. jps
-
查看当前系统有多少java进程在运行中,显示java进程的进程id和进程名
jps
2. jmap
-
查看堆内存占用情况,记录的是瞬间的堆内存参数,可以多次抓取结果进行比对。
jmap - heap 进程id
3. jconsole
-
图形界面的,多功能的监测工具,可以连续监测,动态变化的界面。
jconsole
4. jvisualvm
-
图形界面的,多功能的监测工具,可以连续监测比jconsole更强大
jvisualvm
4. 堆内存诊断案例
1. GC后,内存占用仍然很高
-
GC回收的只是已经没有被引用指向的对象,如果一个对象一直有引用指向,那么此对象就不会被回收。
如果堆中存在一个很大的对象并且该产生了死锁,那么此对象不会被GC,只有死锁不被打破,
那么会一直占用堆内存。解决办法是设置死锁自动释放时间,或者认为干预进行打破。
-
可以使用jvisualvm工具的堆dump功能对当前线程进行快照抓取,然后分析快照内容再排除相应代码。
5. 方法区
1. 官方给出的定义:
Java虚拟机中有一个被所有jvm线程共享的方法区。方法区有点类似于传统编程语言中的编译代码块或者操作系统层面的代码段。它存储着每个类的构造信息,譬如运行时的常量池,字段,方法数据,以及方法和构造方法的代码,包括一些在类和实例初始化和接口初始化时候使用的特殊方法。方法区在jvm启动时候被创建。虽然方法区在逻辑层面上是堆的一部分,但是就简单实现来说既不会被回收也不会被压缩。这个规范并不强制指定方法区存放的位置也不会对编译过的代码有管理策略的限制。方法区可能有一个固定的大小或者也可以通过计算大小去扩展也可以在不需要的时候被压缩。方法区的内存也不需要是连续的。Jvm虚拟机实现可以提供给编程人员或者用户初始化方法区的大小,同时在方法区可变大小的情况下,控制这个方法区的最大值和最小值。
下面这种异常情况是和方法区有关联的:
如果方法区满足不了构造所需要的内存,jvm就会抛出 OutOfMemoryError
2. 特点
-
所有jvm线程共享
-
存储着每个类的构造信息:运行时的常量池,字段,方法数据,以及方法和构造方法
-
方法区在jvm启动时候被创建
-
不同厂商实现的jvm方法区会有所不同
-
JDK1.8将方法区移动到了操作系统本地内存中,称为元空间,但是其中的stringTable保留在了堆中。
在1.8之后,因为生成的放置类的方法区是在操作系统本地中,且默认没有设置内存上限,所有一般很少
看见有方法区内存溢出的情况。
3. 内存溢出
1. JDK1.8之前及参数设置
- 方法区(永久代)内存溢出
异常:java.lang.OutOfMemoryError: PermGen space
参数设置(设置8m):-XX:MaxPermSize=8m
2. JDK1.8之后及参数设置
- 元空间内存溢出
异常:java.lang.OutOfMemoryError: Metaspace
参数设置(设置8m):-XX:MaxMetaspaceSize=8m
4. 实际场景
-
Spring中的aop
aop使用过程中会使用cglib动态代理的方式在内存中创建一个代理类,如果创建的类达到一定数量将内存溢出。
-
mybatis
mybatis使用cglib动态代理的方式在内存中创建一个代理类mapper接口的实现类。
5. 常量池及运行时常量池
- 常量池:是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
- 运行时常量池:常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
6. StringTable的特点
-
常量池中的字符串仅是符号,第一次用到时才变为对象
-
利用串池的机制,来避免重复创建字符串对象
-
字符串变量拼接的原理是 StringBuilder (1.8)
String s1 = "a"; String s2 = "b"; String s3 = "ab"; // s1 + s2 实际编译结果:new StringBuilder().append("a").append("b").toString() // 等价与:new String("ab"),将产生一个新的对象在堆中不在字符串常量池中 String s4 = s1 + s2;
-
字符串常量拼接的原理是编译期优化
// javac 在编译期间的优化,结果已经在编译期确定为ab, // 直接使用ab字符串到池中进行寻找是否有ab存在 String s5 = "a" + "b";
-
可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
-
JDK1.8 将这个字符串对象尝试放入串池,如果存着则不放入,如果没有则放入串池, 会把串池中的对象返回
-
JDK1.6 将这个字符串对象尝试放入串池,如果存着则不放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
-
注意点(JDK1.8)
-
new String("ab"),产生两个对象
- new 产生的在堆中
- 如果字符串常量池中不存在ab,将产生 ab
-
String c = new String("a") + new String("b"),
-
此时字符串常量池中有 "a" 和 "b" ,没有拼接得到的 "ab"
-
变量拼接产生的字符串不会存储到字符串常量池
-
如果需要变量拼接产生的字符串放到字符串常量池中需要调用 intern 方法
c.intern() // 将字符串对象尝试放入字符串常量池
-
-
7. StringTable的位置
1. JDk1.6及内存参数设置
StringTable放置在方法区的常量池中,常量池中的垃圾回收机制只有当 full gc 时才会触发垃圾回收,回收效率不高。
内存参数设置(设置10m):-XX:MaxPermSize=10m
2. JDK1.8及内存参数设置
StringTable放置在对中,在堆中只要发生min gc 就可以进行 StringTable 的垃圾回收。
内存参数设置(设置10m)
- 设置对内存:-Xmx10m
- 注意:这个参数是用于关闭一些校验(可不设置):-XX:-UseGCOverheadLimit
3. StringTable会发生垃圾回收
4. StringTable的性能调优
-
底层数据结构
StringTable的底层使用的是哈希表实现的。
-
调优思路
-
主要是设置哈希表的大小,使得hash表充分利用。尽量减少hash碰撞,提高插入和查找的时间。
// 设置2000个 -XX:StringTableSize=桶个数 -XX:StringTableSize=2000
-
考虑将字符串对象是否入池
-
二、垃圾回收
1. 判断对象为垃圾的几种算法
1. 引用计数法
-
每个被创建的对象被引用一次就加 1 ,解除引用就减 1 ,当被引用的次数为 0 时,将被标记为垃圾,在进行垃圾回收时进行清除。但是此算法无法解除循环引用问题。
-
此算法已经不用使用,因为无法解决循环引用问题。
-
循环引用:创建了 A 和 B 两个对象,A 引用了 B 对象,B 引用了 A 对象。当没有其他对象直接A和B两个对象时,此时A和B已经是垃圾,但是引用计数还是 1 ,垃圾收集器无法清除,这样将成为永远的垃圾,无法被清除一直存在。
2. 可达性分析算法
- Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
- 扫描堆中的对象,看是否能够沿着 GC Root对象 为起点的引用链找到该对象,找不到,表示可以回收。
- 常见的 GC Root :
- System级的对象即JDK JVM自身的对象。
- Native 本地方法对象。
- 线程对象:每开启应该线程产生一个线程GC Root对象,是垃圾回收主要对象。
- 同步锁根对象:被同步锁锁住的对象。