JVM探究

198 阅读5分钟

JVM探究

常见的面试题:

  • 谈谈你对JVM的理解?java8虚拟机和之前的变化更新?
  • 什么是OOM,什么是栈溢出StackOverFlowError?怎么分析?
  • JVM常用的调优参数有哪些?
  • 内存快照如何抓取,怎么分析Dump文件?知道吗?
  • 谈谈JVM中,类加载器的认识?

1. JVM的位置

image.png

2. JVM的体系结构

image.png

注意:

Java栈、本地方法栈、PC寄存器没有垃圾回收,用完了就自动弹出或清除。

3. 类加载器

  1. 虚拟机自带的加载器
  2. 启动类(根)加载器(Bootstrap ClassLoader)
  3. 扩展类加载器(ExtClassLoader)
  4. 应用程序加载器(AppClassLoader)

4. 方法区

方法区是所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说所有定义的方法的信息都保存在该区域,此区域属于共享区间

静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关。

5. 栈

栈是一种数据结构。

:先进后出、后进先出。可以理解为一个桶。

队列:先进先出(FIFO:First Input First OutPut)。可以理解为一个管道。

栈内存,主管程序的运行,生命周期和线程同步;

线程结束,栈内存也就释放。

每当创建一个新的线程时,JVM会为这个线程创建一个Java栈,同时会为这个线程分配一个PC寄存器,并且这个PC寄存器会指向这个线程的第一行可执行代码。每当调用一个新方法时会在这个栈上创建一个新的栈帧数据结构,这个栈帧会保留这个方法的一些元信息,如在这个方法中定义的局部变量、一些用来支持常量池的解析、正常方法返回及异常处理机制等。

image.png

6. 堆

Heap,堆。

一个JVM只有一个堆内存,堆内存的大小是可以调节的。

堆内存中还要细分为三个区域:

  • 新生区(伊甸园区)
  • 养老区 Tenured Gen (老年代)
  • 永久区 Perm Gen

GC 垃圾回收,主要是在伊甸园区和养老区

如果堆内存满了,会触发OOM异常。

在JDK8以后,永久存储区改了个名字叫元空间

永久区

这个区域不存在垃圾回收,关闭JVM就会释放这个区域的内存。

  • jdk1.6之前:永久代,常量池在方法区;
  • jdk1.7:永久代,但是慢慢退化了,去永久代提出,常量池在堆中;
  • jdk1.8:无永久代,常量池在元空间

image.png

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工具后,直接可以双击打开这个文件。

从线程这里可以查看到问题代码的位置:

image.png

从当前对象后面可以查看到有哪些大对象:

image.png

常用的VM参数:

-Xms80m -Xmx80m -XX:+PrintGCDetails
-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError    
    
//-Xms 设置初始化内存分配大小 默认1/64
//-Xmx 设置最大分配内存,默认1/4
//-XX:+PrintGCDetails  打印GC垃圾回收信息
//-XX:+HeapDumpOnOutOfMemoryError oom Dump    
    

9. GC

  1. GC的区域

    • PSYoungGen (新生区)

    ​ eden (伊甸园区)

    ​ from (幸存0区)

    ​ to(幸存1区)

    • ParOldGen(老年区)
  2. GC的种类

    • 轻GC(普通的GC)
    • 重GC(Full GC)
  3. GC有哪些算法

    • 标记清除法

      优点:不需要额外的空间

      缺点:两次扫描,严重浪费时间,会产生内存碎片

image.png

  • 标记压缩整理

    通过再次扫描,向一端移动存活的对象,防止内存碎片产生

  • 复制算法

    新生代主要用的是复制算法。

    好处:没有内存碎片

    坏处:浪费内存空间,有一半空间永远都是空的(to 区)

  • 引用计数器

每次GC都会将Eden区活的对象移到幸存区中,一旦Eden区被GC后,就会是空的。

当一个对象经历了15次(默认值),还没有死,就会被移到老年区,这个参数值可以修改:

-XX:MaxTenuringThreshold=99

总结:

内存效率:复制算法>标记清除算法>标记压缩算法

内存整齐度:复制算法=标记压缩算法>标记清除算法

内存利用率:标记压缩算法=标记清除算法>复制算法

思考一个问题:难道没有最优算法吗?

答案:没有, 没有最好的算法,只有最合适的算法

GC :分代收集算法

年轻代: 存活率低 复制算法!

老年代: 区域大:存活率高 标记清除(内存碎片不是太多)+标记压缩混合 实现