一、前言
转眼间也过完了最后一个暑假,最近忙于校招,一直在复习以前学过的一些基础知识,今天就顺便总结一下最近复习的JVM相关的知识。
(想自学习编程的小伙伴请搜索圈T社区,更多行业相关资讯更有行业相关免费视频教程。完全免费哦!)
二、JVM内存结构
JVM的总体内存结构如下图所示:
大致分为下面几个重点的内容,本篇文章我们主要分析运行时数据区的几个结构
- 类装载器(ClassLoader)(用来装载.class文件)
- 执行引擎(执行字节码,或者执行本地方法)
- 运行时数据区(方法区、Java堆、Java栈、程序计数器、本地方法栈)
2.1 程序计数器
程序计数器是线程私有的区域,每个线程当然得有个计数器记录当前执行到哪个指令。占用的内存空间小,可以把它看成是当前线程所执行的字节码的行号指示器。
如果线程在执行Java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址;如果执行的是Native方法,这个计数器的值为空(Undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程回复等基础功能都需要依赖这个计数器来完成。
每条线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储,我们称之为线程私有的区域。
2.2 Java堆
对于大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。该区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。该区域也是垃圾收集器进行垃圾回收的主要区域。
堆的内存空间既可以固定大小,也可以在运行时动态地调整,通过如下参数设定初始值和最大值,比如 -Xms256M -Xmx1024M,其中 -X表示它是JVM运行参数,ms是memorystart的简称,mx是memory max的简称,分别代表最小堆容量和最大堆容量。
可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
java -Xms1M -Xmx2M HackTheJava
它是JVM用来存储对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配,堆中的对象的内存需要等待GC进行回收。
Java堆是垃圾收集器管理的主要区域。由于现在的收集器基本上采用的都是分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法,可以将堆分成两块:
- 新生代(Young Generation)
- 老年代(Old Generation)
细致分就是把新生代分为:Eden空间、From Survivor空间、To Survivor空间。
堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
(1) 堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的
(2) 所有新创建的Object 都将会存储在新生代Yong Generation中。如果Young Generation的数据在一次或多次GC后存活下来,那么将被转移到老年代Old Generation。新的Object总是创建在Eden Space。
2.3 Java虚拟机栈
JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈,生命周期与线程相同。JVM栈中存放的为当前线程中局部基本类型的变量(java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double)、部分的返回结果以及栈帧(Stack Frame),非基本类型的对象在JVM栈上仅存放一个指向堆上的地址。
虚拟机栈描述的是Java方法执行的内存模型:每个Java方法在执行的同时会创建一个栈帧(Stack Frame)用于存储局部变量表、操纵数栈、常量池引用、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机中入栈到出栈的过程。
可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小:
java -Xss512M HackTheJava
局部变量表中存放了编译期可知的各种:
- 基本数据类型(boolen、byte、char、short、int、 float、 long、double)
- 对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)
- returnAddress类型(指向了一条字节码指令的地址)
其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
2.4 本地方法栈
本地方法栈也是一个线程私有的区域,本地方法栈与Java虚拟机栈的作用相似,两者的区别在于虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为虚拟机用到的Native方法服务,此区域用于存储每个Native方法调用的状态。
与虚拟机栈一样,本地方法栈也会抛出StackOverflowError(栈溢出)和OutOfMemoryError(内存不足)异常。
2.5 方法区
方法区是各个线程共享的内存区域,方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。
HotSpot 虚拟机把它当成永久代来进行垃圾回收。但是很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
(1)在Sun JDK中这块区域对应的为PermanetGeneration,又称为持久代。
(2)方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在一定的条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。
三、常用参数
一些jvm中常见的参数配置如下:
- -Xms64m 最小堆内存 64m.
- -Xmx128m 最大堆内存 128m.
- -XX:NewSize=30m 新生代初始化大小为30m.
- -XX:MaxNewSize=40m 新生代最大大小为40m.
- -Xss=256k 线程栈大小。
- -XX:+PrintHeapAtGC 当发生 GC 时打印内存布局。
- -XX:+HeapDumpOnOutOfMemoryError 发送内存溢出时 dump 内存。
新生代和老年代的默认比例为 1:2,也就是说新生代占用 1/3的堆内存,而老年代占用 2/3 的堆内存。
可以通过参数 -XX:NewRatio=2 来设置老年代/新生代的比例。
默认的,新生代中各个区域的大小比例为Edem : from : to = 8 : 1 : 1 (可以通过参数 –XX:SurvivorRatio 来设定)。
四、总结
本篇介绍了JVM虚拟机中运行时数据区的五个内存区域,这些地方也是我们平时开发中最常接触到的地方,所以对其有所掌握了解还是很有必要的,也有助于我们在服务出现告警时进行相关的问题排查。
有任何问题还请各位指正,互相讨论学习。
部分参考自:《深入理解Java虚拟机》