JVM探究
常见的面试题:
- 谈谈你对JVM的理解?java8虚拟机和之前的变化更新?
- 什么是OOM,什么是栈溢出StackOverFlowError?怎么分析?
- JVM常用的调优参数有哪些?
- 内存快照如何抓取,怎么分析Dump文件?知道吗?
- 谈谈JVM中,类加载器的认识?
1. JVM的位置
2. JVM的体系结构
注意:
Java栈、本地方法栈、PC寄存器没有垃圾回收,用完了就自动弹出或清除。
3. 类加载器
- 虚拟机自带的加载器
- 启动类(根)加载器(Bootstrap ClassLoader)
- 扩展类加载器(ExtClassLoader)
- 应用程序加载器(AppClassLoader)
4. 方法区
方法区是所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说所有定义的方法的信息都保存在该区域,此区域属于共享区间。
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关。
5. 栈
栈是一种数据结构。
栈:先进后出、后进先出。可以理解为一个桶。
队列:先进先出(FIFO:First Input First OutPut)。可以理解为一个管道。
栈内存,主管程序的运行,生命周期和线程同步;
线程结束,栈内存也就释放。
每当创建一个新的线程时,JVM会为这个线程创建一个Java栈,同时会为这个线程分配一个PC寄存器,并且这个PC寄存器会指向这个线程的第一行可执行代码。每当调用一个新方法时会在这个栈上创建一个新的栈帧数据结构,这个栈帧会保留这个方法的一些元信息,如在这个方法中定义的局部变量、一些用来支持常量池的解析、正常方法返回及异常处理机制等。
6. 堆
Heap,堆。
一个JVM只有一个堆内存,堆内存的大小是可以调节的。
堆内存中还要细分为三个区域:
- 新生区(伊甸园区)
- 养老区 Tenured Gen (老年代)
- 永久区 Perm Gen
GC 垃圾回收,主要是在伊甸园区和养老区。
如果堆内存满了,会触发OOM异常。
在JDK8以后,永久存储区改了个名字叫元空间。
永久区
这个区域不存在垃圾回收,关闭JVM就会释放这个区域的内存。
- jdk1.6之前:永久代,常量池在方法区;
- jdk1.7:永久代,但是慢慢退化了,去永久代提出,常量池在堆中;
- jdk1.8:无永久代,常量池在元空间
7. 新生区、永久区、堆内存调优
public class MemoryDemo {
public static void main(String[] args) {
//虚拟机试图使用的最大内存
long max = Runtime.getRuntime().maxMemory();
//jvm的初始化内存
long total = Runtime.getRuntime().totalMemory();
System.out.println("max=" + max + "字节"+"," + max/(double)1024/1024 +"M");
System.out.println("total=" + total + "字节"+"," + total/(double)1024/1024 +"M");
}
}
默认情况下:
分配的总内存是电脑内存的1/4,而初始化的内存是总内存的1/64
打印结果:
max=3769630720字节,3595.0M
total=255328256字节,243.5M
修改VM参数:
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
-XX:+PrintGCDetails可以打印GC的信息。
打印结果:
max=1029177344字节,981.5M
total=1029177344字节,981.5M
Heap
PSYoungGen total 305664K, used 15729K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
eden space 262144K, 8% used [0x00000000eab00000,0x00000000ebf7afb8,0x00000000fab00000)
from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)
to space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)
ParOldGen total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)
Metaspace used 3234K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 354K, capacity 388K, committed 512K, reserved 1048576K
8. 使用jProfiler 工具分析OOM问题
在程序启动时增加dump的参数:
-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
示例
import org.omg.Messaging.SyncScopeHelper;
import java.util.ArrayList;
/**
* jProfiler的使用
*/
public class MemoryDemo2 {
public static void main(String[] args) {
ArrayList<MemoryDemo2> list = new ArrayList<>();
int count = 0;
try {
while (true){
list.add(new MemoryDemo2());
count++;
}
} catch (Error e) {
System.out.println("count:" + count);
e.printStackTrace();
}
}
}
打印结果:
java.lang.OutOfMemoryError: GC overhead limit exceeded
Dumping heap to java_pid17468.hprof ...
Heap dump file created [12537441 bytes in 0.090 secs]
count:326989
*** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message can't create byte arrau at JPLISAgent.c line: 813
java.lang.OutOfMemoryError: GC overhead limit exceeded
at MemoryDemo2.main(MemoryDemo2.java:14)
启动程序后发现项目所在目录多了一个文件:java_pid17468.hprof
在安装了jProfiler工具后,直接可以双击打开这个文件。
从线程这里可以查看到问题代码的位置:
从当前对象后面可以查看到有哪些大对象:
常用的VM参数:
-Xms80m -Xmx80m -XX:+PrintGCDetails
-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
//-Xms 设置初始化内存分配大小 默认1/64
//-Xmx 设置最大分配内存,默认1/4
//-XX:+PrintGCDetails 打印GC垃圾回收信息
//-XX:+HeapDumpOnOutOfMemoryError oom Dump
9. GC
-
GC的区域
- PSYoungGen (新生区)
eden (伊甸园区)
from (幸存0区)
to(幸存1区)
- ParOldGen(老年区)
-
GC的种类
- 轻GC(普通的GC)
- 重GC(Full GC)
-
GC有哪些算法
-
标记清除法
优点:不需要额外的空间
缺点:两次扫描,严重浪费时间,会产生内存碎片
-
-
标记压缩整理
通过再次扫描,向一端移动存活的对象,防止内存碎片产生
-
复制算法
新生代主要用的是复制算法。
好处:没有内存碎片
坏处:浪费内存空间,有一半空间永远都是空的(to 区)
-
引用计数器
每次GC都会将Eden区活的对象移到幸存区中,一旦Eden区被GC后,就会是空的。
当一个对象经历了15次(默认值),还没有死,就会被移到老年区,这个参数值可以修改:
-XX:MaxTenuringThreshold=99
总结:
内存效率:复制算法>标记清除算法>标记压缩算法
内存整齐度:复制算法=标记压缩算法>标记清除算法
内存利用率:标记压缩算法=标记清除算法>复制算法
思考一个问题:难道没有最优算法吗?
答案:没有, 没有最好的算法,只有最合适的算法
GC :分代收集算法
年轻代: 存活率低 复制算法!
老年代: 区域大:存活率高 标记清除(内存碎片不是太多)+标记压缩混合 实现