程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机中,字节码解释器工作时就是通过改变计数器值来选取下一条执行的指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 在任何时刻,一个处理器(内核)都只会执行一条线程中的指令。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,这类内存区域为 线程私有的内存 。 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
Java虚拟机栈也是线程私有的,它的生命周期与线程相同。 每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口 等信息。一个方法从调用到执行完成就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。 虚拟机栈中的局部变量表存放着编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double),对象引用(reference类型),returnAddress类型(指向了一条字节码指令的地址)。 StackOverflowError异常:线程请求的栈深度大于虚拟机所允许的深度。 OutOfMemoryError异常:虚拟机栈可以动态扩展无法申请到足够的内存。
本地方法栈与虚拟机栈的作用是类似的,只不过虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。 有的虚拟机(例如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。
Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。堆的唯一目的就是 存放对象实例 ,几乎所有的对象实例都在这里分配内存。 Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。 OutOfMemoryError异常:堆中没有内存完成实例分配,并且堆也无法再扩展。
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域, 它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 在Hotspot虚拟机上,很多人更愿意把方法区称为“永久代”。 方法区和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对来说垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。 这区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
在jdk7之前,HotSpot虚拟机中将GC分代收集扩展到了方法区,使用永久代来实现了方法区。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,但是在之后的HotSpot虚拟机实现中,逐渐开始将方法区从永久代移除。jdk7中已经将运行时常量池从永久代移除,在Java堆(Heap)中开辟了一块区域存放运行时常量池。而在jdk8中,已经彻底没有了永久代,将方法区直接放在一个与堆不相连的本地内存区域,这个区域叫元空间。
Metaspace(元空间) 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过参数来指定元空间的大小。
运行时常量池(Runtime Constant Pool) 是方法区的一部分 。Class文件中除了有类的字段,版本等信息描述外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
对象创建在虚拟机中是非常频繁的行为,在并发情况下也并不是线程安全的。解决这个问题有两种方案:一种是对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。 3. 接下来虚拟机要对对象进行必要的设置,,如对象的哈希码,GC的分代年龄等,这些信息存放在对象的对象头中。 4. 当上面的步骤走完后,从虚拟机的角度看一个新的对象已经产生了,但从java程序来看,这个对象才刚刚开始---- init
方法还没有执行。 当执行new指令之后会接着执行 init
方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,锁状态标志等。另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
对齐填充并不是必然存在的,仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是说对象的大小必须是8字节的整数倍,不足就由对象填充来补齐。
我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。 目前主流的访问方式有使用句柄和直接指针两种。
使用句柄访问,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。 句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。
使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。 直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。
Hotpost虚拟机使用的是直接指针访问的方式。
内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。 JVM得拓展参数在注释中。
public class HeapOOM { /** *java堆溢出 * * -verbose:gc -Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError */ public static void main(String[] args) throws InterruptedException { ArrayList<OOMObject> oomObjects = new ArrayList<>(); while (true){ oomObjects.add(new OOMObject()); } } static class OOMObject{} } 复制代码
输出结果如下:
Java堆内存的OOM异常是实际应用中常见的内存溢出异常情况。当出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap space”。
关于虚拟机栈在Java虚拟机规范中描述了两种异常: 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
public class JavaVmStackSOF { private int stackLength = 1; public void stackLeak(){ stackLength++; stackLeak(); } /** *虚拟机栈溢出 * -Xss128k */ public static void main(String[] args) { JavaVmStackSOF javaVmStackSOF = new JavaVmStackSOF(); try{ javaVmStackSOF.stackLeak(); }catch (Throwable t){ System.out.println("stack length:"+javaVmStackSOF.stackLength); throw t; } } } 复制代码
运行结果:
-Xms20M 表示设置JVM启动内存的最小值为20M,必须以M为单位 -Xmx20M 表示设置JVM启动内存的最大值为20M,必须以M为单位。将-Xmx和-Xms设置为一样可以避免JVM内存自动扩展。大的项目-Xmx和-Xms一般都要设置到10G、20G甚至还要高 -verbose:gc 表示输出虚拟机中GC的详细情况 -Xss128k 表示可以设置虚拟机栈的大小为128k -Xoss128k 表示设置本地方法栈的大小为128k。不过HotSpot并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说这个参数是无效的 -XX:PermSize=10M 表示JVM初始分配的永久代的容量,必须以M为单位 -XX:MaxPermSize=10M 表示JVM允许分配的永久代的最大容量,必须以M为单位,大部分情况下这个参数默认为64M -Xnoclassgc 表示关闭JVM对类的垃圾回收 -XX:+TraceClassLoading 表示查看类的加载信息 -XX:+TraceClassUnLoading 表示查看类的卸载信息 -XX:NewRatio=4 表示设置年轻代:老年代的大小比值为1:4,这意味着年轻代占整个堆的1/5 -XX:SurvivorRatio=8 表示设置2个Survivor区:1个Eden区的大小比值为2:8,这意味着Survivor区占整个年轻代的1/5,这个参数默认为8 -Xmn20M 表示设置年轻代的大小为20M -XX:+HeapDumpOnOutOfMemoryError 表示可以让虚拟机在出现内存溢出异常时Dump出当前的堆内存转储快照 -XX:+UseG1GC 表示让JVM使用G1垃圾收集器 -XX:+PrintGCDetails 表示在控制台上打印出GC具体细节 -XX:+PrintGC 表示在控制台上打印出GC信息 -XX:PretenureSizeThreshold=3145728 表示对象大于3145728(3M)时直接进入老年代分配,这里只能以字节作为单位 -XX:MaxTenuringThreshold= 表示对象年龄大于1,自动进入老年代 -XX:CompileThreshold=1000 表示一个方法被调用1000次之后,会被认为是热点代码,并触发即时编译 -XX:+PrintHeapAtGC 表示可以看到每次GC前后堆内存布局 -XX:+PrintTLAB 表示可以看到TLAB的使用情况 -XX:+UseSpining 开启自旋锁 -XX:PreBlockSpin 更改自旋锁的自旋次数,使用这个参数必须先开启自旋锁 复制代码