JVM内存模型和GC算法分析
JVM运行时数据区
JVM在运行过程中会把它所管理的内存划分成若干不同的数据区域。
- 线程私有:程序计数器、虚拟机栈、本地方法栈 (
主要存放指令
) - 线程共享:堆、方法区 (
主要存放数据
)
一、程序计数器
程序计数器是用于存放下一条指令所在单元的地址的地方。
我们可以随意拿一个class
文件进行反编译,看看其结构。
如下,JvmDemo.class
文件:
cafe babe 0000 0033 0045 0a00 1000 2608
0027 0900 0f00 2808 0029 0900 0f00 2a0a
002b 002c 0900 2d00 2e07 002f 0a00 0800
260a 0008 0030 0a00 0800 310a 0032 0033
...
TIP
: javap
是 Java class文件分解器,可以反编译(即对javac编译的文件进行反编译),也可以查看java编译器生成的字节码。用于分解class文件。
执行javap -v ./JvmDemo.class > JvmDemo.txt
,将JvmDemo.class
文件分解为JvmDemo.txt
:
其左侧的序号就是所谓的程序计数器
,用于确定代码的执行顺序。
二、虚拟机栈
存储当前线程
运行方法所需的数据、指令、返回地址。
1、虚拟机栈
栈
这个数据结构的特性就是先进后出
。虚拟机栈主要存放的是栈帧
。
2、栈帧
类中每一个方法对应一个栈帧。通俗的说,可以将程序调用的一个方法看做一个栈帧
。
栈帧可以划分为四个结构:
- 局部变量表
- 操作数栈
- 动态连接
- 返回地址
3、虚拟机栈的*Error
当线程调用一个方法,就会产生一个栈帧
,存储在栈中。若是方法的内部还有方法调用的话,那就会有新的栈帧
存放在栈中,成为栈顶元素
。
如线程对下面方法的调用:
public void A(){
B();
...其他操作
}
public void B(){
C();
...其他操作
}
}
public void C(){
...其他操作
}
那么其虚拟机栈
的情况如下:
从结构上来看,在栈帧C未出栈时,A和B方法是无法取出的。所以A和B方法必须等待C方法的执行。
从上图也可以看出,若是有某个方法是无限递归
的话,那么虚拟机栈
只有入栈,没有出栈。就会导致栈内存溢出。(栈默认大小是 1M
)
如下无限递归调用recursion()
方法:
/**
* @author wangcw
* @create 2019-05-08 21:16
* @description:测试栈溢出
**/
public class JvmDemo {
private static int stackLength = 0;
public static void recursion(){
stackLength++;
recursion();
}
/* 测试递归调用,让 栈内存溢出 */
public static void main(String[] args) {
try{
recursion();
} catch (Throwable e){
System.out.println("出现异常,递归调用次数(即栈的长度):" + stackLength);
e.printStackTrace();
}
}
}
默认情况下调用结果:
出现异常,递归调用次数(即栈的长度):11907
java.lang.*Error
尝试修改一下栈的大小为2M( -Xss2M
),再启动:
出现异常,递归调用次数(即栈的长度):27266
java.lang.*Error
可以看出,当出现栈溢出的错误时,一般不是更改栈大小能解决的,通常情况下都是循环递归代码导致的。
一般是递归死循环,导致栈帧过多,虚拟机棧已经存放不下去了,就会出现栈溢出的情况。
4、虚拟机栈的OutOfMemoryError
不同于*Error,OutOfMemoryError指的是当整个虚拟机栈内存耗尽,并且无法再申请到新的内存时抛出的异常。JVM未提供设置整个虚拟机栈占用内存的配置参数。虚拟机栈的最大内存大致上等于“JVM进程能占用的最大内存。
当虚拟机栈能够使用的最大内存被耗尽后,便会抛出OutOfMemoryError,可以通过不断开启新的线程来模拟这种异常。
**
* java栈溢出OutOfMemoryError
* JVM参数:-Xss2m
*/
public class JavaVMStackOOM {
private void dontStop() {
while (true) {
}
}
//通过不断的创建新的线程使Stack内存耗尽
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(() -> dontStop());
thread.start();
}
}
public static void main(String[] args) {
JavaVMStackOOM oom = new _03_JavaVMStackOOM();
oom.stackLeakByThread();
}
}
设置单个线程虚拟机栈的占用内存为2m并不断生成新的线程,最终虚拟机栈无法申请到新的内存,抛出异常:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
5、虚拟机栈的优化
高并发,多线程项目中。栈默认大小 1M - - >
1000线程同时运行 * 1M = 1G 。需要占用1G的空间,相当浪费。
通常情况下对虚拟机栈的优化都是将其调小
。大部分情况下,栈大小128K
基本就够了。
三、本地方法栈
本地方法栈的功能和特点类似于虚拟机栈。不同的是,本地方法栈服务的对象是JVM执行的native
方法,而虚拟机栈服务的是JVM执行的java
方法。
四、JMM(JVM内存模型)
1、分代的思想
1.1、堆 (分布如下:)
新生代
- Eden空间
- From Survivor空间
- To Survivor空间
老年代
1.2、方法区
永久代
(当JDK >= 1.8成为元空间
)
2、JMM对象内存分配
- 对象优先在Eden分配
- 长期存活的对象将进入老年代(
GC多次仍然存在,存活时间长
) - 大对象直接进入老年代(
大小在新生代放不下,因为新生代默认情况仅占有堆内存的1/3
) - 动态对象年龄判定
而方法区,即永久代
存放的内容:
- 1.类的全限定名(类的全路径名)。
- 2.类的直接超类的权全限定名(如果这个类是Object,则它没有超类)。
- 3.类的类型(类或接口)。
- 4.类的访问修饰符,public,abstract,final等。
- 5.类的直接接口全限定名的有序列表。
- 6.常量池(字段,方法信息,静态变量,类型引用(class))等
3、JMM中对GC算法的选择
3.1 新生代采用的是复制回收算法
(Minor GC
)
复制回收算法的一个缺点就是每次都需要保留50%的内存空间不使用。
按照很多互联网公司的数据统计,新生代一般有90%的对象是不需要垃圾回收的。程序员能自己处理好,仅大概有10%的对象需要去做垃圾回收。(复制回收算法
)。
上述结论也能推理出新生代中 Eden区
、From Survivor区
、To Survivor区
在新生代中的比例是 8:1:1
的原因。其中10%的From区
或者To区
是做预留空间使用的。
3.2 老年代采用的是标记清除算法
或者标记整理算法
(Full GC
)
标记-清除算法
的缺点是清除完,内存不连续。标记-整理算法
比标记-清除算法
多了一个整理的步骤,虽然内存空间连续了,但是效率相对要低一些。一般老年代的GC还是会优先选择标记-清除算法
。
4、关于堆的内存溢出 OutOfMemoryError
内存溢出
:指的是GC已经回收不了,通常是对象过多或者过大,已经超出了启动时设置的最大堆内存,就会导致内存溢出。
尝试一下堆内存溢出,设置启动参数:-Xms5m -Xmx5m
。
-Xms:设置初始分配大小,默认为物理内存的“1/64”
-Xmx:最大分配内存,默认为物理内存的“1/4”
HeapOOM.java
/**
* @author wangcw
* @create 2019-05-08 21:16
* @description:测试堆内存溢出
**/
public class HeapOOM {
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
int i = 0;
while (true){ //无限循环
System.out.println(i++);
list.add(new Object());
}
}
}
运行后会得到结果:
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
5、关于堆的内存泄漏
内存泄漏
:一般是指某个对象,JVM回收不走(根据可达性分析),认为其有用,而实际上该对象是没用的,就一直占用的内存空间,导致JVM的内存空间减小,这就是内存泄漏。
Java的内存泄露
多半是因为对象存在无效的引用,对象得不到释放,如果发现Java应用程序占用的内存出现了泄露的迹象,那么我们一般采用下面的步骤分析:
- 用工具生成java应用程序的
heap dump
(如jmap
) - 使用
Java heap
分析工具(如MAT),找出内存占用超出预期的嫌疑对象 - 根据情况,分析嫌疑对象和其他对象的引用关系。
- 分析程序的源代码,找出嫌疑对象数量过多的原因。
五、JVM常用问题处理方式
保存堆栈快照日志
分析内存泄漏
调整内存设置
控制垃圾回收频率
选择适合的垃圾回收器
TIP:在不了解JVM内存模型的情况下不要随意更改class文件,容易造成OOM。