1.JVM是什么?
JVM:Java Virtual Mechinal(Java虚拟机).它是一个虚构的计算机,是通过在实际的计算机上模拟各种功能来实现的。JVM的主要工作是解释自己的指令集(字节码,如java源码编译成class文件在虚拟机上运行)并映射到本地的CPU指令集或OS的系统调用。Java语言跨平台的本质就是不同的操作系统使用不同的JVM映射规则,使其与操作系统无关,从而实现跨平台。
2.JVM的内存结构是什么样子?
Java虚拟机在运行Java程序的时候,会把它所管理的内存划分为若干个不同的数据区域,如图:
3.那么接下来每个数据区域都是做什么的呢?
(1).程序计数器:是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器。尤其在多线程的情况下,尤为重要。Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,即在任何一个确定时刻,一个处理器只会执行一条线程,当线程切换后就需要恢复到正确位置,因此,程序计数器要实现线程隔离,每个线程都有自己的专属的计数器。值得注意的是:此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
(2).堆:此内存区域是Java虚拟机管理的最大一块内存,同时也是线程共有的,在虚拟机启动时创建。它用来存储Java中的对象实例(无论成员变量,局部变量还是类变量,它们指向的对象都存储在堆内存中),几乎所有的对象实例都在这分配内存。同时这里也是GC的主要区域。从内存回收的角度来看,由于收集器基本都采用分代收集法,所以在Java堆中还可以细分为:新生代和老年代(可以理解为不用代的对象内存位置是不同的);再细致可分为:Eden空间,From Survivor空间,To Survivor空间(8:1:1)。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。划分的目的是为了更好地回收内存或更快的分配内存。
注意:Java堆可以处在物理上不连续的内存空间中,只要逻辑上连续即可。
(3).虚拟机栈:它是线程私有的,生命周期与线程相同。三部分组成:局部变量区、操作数栈、帧数据区。(文章后面会详细介绍)
(4).本地方法栈:和虚拟机栈很类似,他们的区别不过是前者为虚拟机执行Native方法服务,后者为虚拟机执行Java方法服务。但不同虚拟机的实现不同,如Sun HotSpot虚拟机直接就把两栈合二为一。
(5).方法区:线程共享的内存区域,作用是存储Java类的结构信息。当我们创建对象实例后,对象的类型信息存储在方法区中;实例数据存放在堆中;实例数据指的是Java中创建的各种实例对象以及他们的值,类型信息指的是定义在Java代码中的常量、静态变量以及在类中声明的各种方法、方法字段等;同时可能包括即时编译器编译后产生的代码数据。
4.深入理解栈:
(1).让我们在来总结一下堆和栈的区别:
①.功能不同:栈内存用来存储局部变量和方法调用;而堆用来存储对象实例。
②.共享不同:栈是线程私有;而堆是线程共有。
③.异常错误不同:栈空间不足时为StackOverFlowError;堆空间不足时为OutOfMemoryError。
④.空间大小不同:堆远远大于栈的内存大小。
(2).我们都知道栈的三部分组成:局部变量区、操作数栈和帧数据区。其中局部变量区和操作数栈要视对应的方法大小而定,是按字节计算的。但调用一个方法时,它从类型信息中得到此方法局部变量区和操作数栈大小并分配栈内存,然后压入栈中。
◆局部变量区:
是一个以字节为单位、从0开始计数的数组,类型为short、byte、char的值存入数组前要转为int值,long和double占连续的俩字节,但访问long和double时只需要取出连续的第一项索引即可。(可以理解为方法中局部变量根据类型的不同占了不同大小的内存,并有自己的索引下标,refrence占一个字节)
◆操作数栈:
同局部变量区一样,即以字节为单位的数组。但不同的是它不是通过索引来访问,而是通过出栈和入栈来访问。可以把操作数栈理解为存储计算时的临时数据的地方。
◆帧数据区:
除了上述两个部分,Java栈帧还需要一些数据来支持常量池解析、正常方法返回以及异常派发机制。这些数据都储存在帧数据区中。当JVM执行到需要常量池中的数据时,它会通过帧数据区中指向常量池的指针来访问它。除此之外,帧数据区中的数据还要处理Java方法的正常结束和异常终止:如果通过return正常结束,当前栈帧弹栈,若有返回值,则把此值压入发起调用方法的操作数栈;如果异常中止,帧中保存了一个对此方法异常引用表的引用,有异常时,JVM找catch代码块中的代码,若没有则方法立即中止,帧中的信息恢复发起调用的方法的帧,再发起调用方法的上下文重新抛出异常。
5.什么是直接内存?
(1).直接内存也可以称为堆外内存,和堆内内存相对应,堆内内存=新生代+老年代+持久代,完全遵守JVM虚拟机的内存管理机制;而堆外内存就是把内存对象分配到虚拟机的堆以外的内存,直接受操作系统管理。
JDK的ByteBuffer(Java.nio.ByteBuffer)类提供了一个接口allocateDirect(int capacity),通过调用进行对外内存的申请,底层通过unsafe.allocateMemory(size)实现,如图:
(3).申请后如何释放内存呢?
JDK使用DirectByteBuffer对象来表示堆外存,每个DirectByteBuffer对象在初始化时,都会创建一个对应的Cleaner对象,用于保存堆外存的元信息(开始地址、大小和容量等),当DirectByteBuffer对象被GC回收后,Cleaner对象被放入RefrenceQueue中,然后由ReferenceHandler守护线程调用unsafe.freeMemory(address),回收对外内存。
主动回收:对于Sun JDK,调用Cleaner(sun.misc.Cleaner)的clean()方法就行;
基于GC回收:如上述,DirectByteBuffer对象被GC时,会调用Cleaner回收其堆外的引用。在于YGC只会只会回收新生代中的不可达对象,如果有大量DirectByteBuffer对象进入Old区,而又一直没有FGC,物理内存会被慢慢耗光,导致OOM。
(4).为什么DirectByteBuffer对象被GC,Cleaner对象会进入ReferenceQueue中呢?
原因:Cleaner对象关联了一个PhantomReference引用(jvm四种引用之一),如果GC过程中某个对象有且只有PhantomReference对它进行引用,那将会把这个引用放到java.lang.ref.Reference.pending队列里,GC完毕后,则会通过上述的守护线程调用方法,回收堆外内存。
(5).堆外内存默认大小:(-Xmx)-(1个survivor大小)
(6).堆外内存溢出异常:java.lang.OutOfMemoryError:Direct buffer memory
(7).优点:减少了垃圾回收;加快了复制速度(堆内存在flush到远程时,会先复制到直接内存,然后再发送)
6.System.gc():
(1).此方法其实是调用的Runtime.getRuntime().gc();再进入gc()则看到是native方法。运行此方法表明:java虚拟机扩展努力回收未使用的对象,以便内存可以快速复用。
(2).作用:
●做一次full gc
●执行后会暂停整个进程
●此方法可以禁掉,使用-XX:+DisableExplicitGC
●最常见的场景时RMI/NIO下的堆外内存分配
7.注意点:
(1).Java中的基本数据类型不一定存储在栈中。栈内存用来存储局部变量和方法调用,如果该局部变量是基本数据类型,则存储在栈中;如果局部变量是一个对象(int[] arr = new int[]{1,2} ),则存储在堆中。
(2).方法区,有人叫“永久代”,本质不等价,只是因为把GC分代收集扩展至方法区。在该区域进行内存回收的目的主要是对常量池的回收和类型(内存数据)的卸载,但回收效率很低。运行时常量池是方法区的一部分,用来存储java类文件常量池中的符号信息。
(3).HotSpot方法区的变化:JDK1.2-6,使用永久带实现方法区,使用GC分代实现方法区;JDK7 Oracle HotSpot 移除永久代,JDK 7 中的符号表被移动到Native Heap中,字符串常量池和类引用被移动到Java Heap中;JDK 8 永久代已完全被元空间取代。