1.堆
JVM堆是Java程序运行时内存管理的核心,它主要用于存储对象实例和数组。堆内存的特点是动态分配和回收,它允许对象的创建和销毁,同时也需要注意内存泄漏和性能问题。
1.1 堆的结构
JVM堆通常被分为三个主要部分:
1.1.1 新生代(Young Generation)
新生代用于存储刚刚被创建的对象。它被分为三个区域:Eden空间和两个Survivor空间(通常称为From和To空间)。新创建的对象首先被分配到Eden空间,经过一次垃圾回收后,仍然存活的对象会被移到Survivor空间。多次循环后,仍然存活的对象会被移到年老代。
1.1.2 年老代(Old Generation)
年老代用于存储生命周期较长的对象。在新生代中经过多次垃圾回收后,仍然存活的对象会被晋升到年老代。年老代中的对象一般需要经历更多的垃圾回收周期才会被回收。
1.1.3 永久代/元空间(Permanent Generation/Metaspace)
在早期的JVM版本中,永久代用于存储类的元数据、方法信息以及静态变量。然而,由于永久代容易导致内存泄漏和溢出问题,1.8后JVM引入了元空间来代替。元空间的元数据存储在本地内存中,不再受到固定大小的限制。
jdk1.8以及之后:在堆内存中,逻辑上存在,物理上不存在(元空间使用的是本地内存),如下图:
什么是永久代和元空间?
方法区是一种规范,不同的虚拟机厂商可以基于规范做出不同的实现,永久代和元空间就是出于不同jdk版本的实现。
方法区就像是一个接口,永久代与元空间分别是两个不同的实现类。
只不过永久代是这个接口最初的实现类,后来这个接口一直进行变更,直到最后彻底废弃这个实现类,由新实现类—元空间进行替代。
1.2 堆的内存溢出
使用如下代码:
public class a { public static void main(String[] args) { List<String> list=new ArrayList<>(); String a="hello"; while (true){ a=a+a; list.add(a); } } }
执行以上代码后就会发生堆内存溢出如下图:
Java堆的大小可以通过命令行参数来配置,主要参数包括:
-Xms:设置堆的初始大小。
-Xmx:设置堆的最大大小。
通常,将这两个参数设置为相同的值可以减少堆的动态调整,提高性能。例如:
java -Xms512m -Xmx512m -jar YourApp.jar
这将设置堆的初始大小和最大大小都为512兆字节。
1.3 堆内存诊断
1.3.1 jmap
首先jps找到java运行的进程,然后jmap -heap 进程id就可以查看堆内存了,如下图:
1.3.2 jvisualvm
执行下面的代码:
public class a { public static void main(String[] args) throws InterruptedException { Thread.sleep(30000); byte[] bytes = new byte[1024 * 1024 * 50]; System.out.println("-------"); Thread.sleep(30000); bytes=null; System.out.println("-------"); Thread.sleep(30000); } }
然后执行jvisualvm会得出如下图:当bytes=null后进行垃圾回收后,内存占用直接减少50M。如下图:
2. 方法区
方法区(Method Area)是JVM的另一个重要内存区域,它主要用于存储类的元数据、静态变量、常量池以及方法代码。
2.1 方法区的结构
方法区包含以下主要部分:
类的元数据
方法区存储了每个类的元数据,包括类名、父类、接口、字段、方法等信息。这些信息在程序运行时起到重要作用,例如方法的调用和字段的访问。
静态变量
静态变量属于类,不属于对象的实例。这些变量在类加载时初始化,存在于方法区中。
常量池
常量池包含了类中使用的常量,例如字符串、数字、类名等。它为类的运行时常量提供了存储空间。
方法代码
方法区存储了类中的方法代码,包括字节码指令和方法的字节码表示。这些代码在方法被调用时执行。
在早期的JVM版本中,方法区被实现为永久代。然而,由于永久代的内存泄漏和性能问题,JVM在较新的版本中引入了元空间来替代永久代。元空间的好处是不再受限于固定大小,避免了永久代引起的一些问题。如下图: