JVM结构
从左图可知,JVM主要包括四个部分:
1.类加载器(ClassLoader):在JVM启动时或者在类运行时将需要的class加载到JVM中。(右图表示了从java源文件到JVM的整个过程,可配合理解。
2.执行引擎:负责执行class文件中包含的字节码指令(执行引擎的工作机制,这里也不细说了,这里主要介绍JVM结构);
3.内存区(也叫运行时数据区):是在JVM运行的时候操作所分配的内存区。运行时内存区主要可以划分为5个区域,如图:
方法区(Method Area):用于存储一个类的所有结构信息(类的class)的地方,包括final标注的常量池、静态变量、静态方法、构造函数、成员变量、成员方法等。虽然JVM规范把方法区描述为堆的一个逻辑部分, 但它却有个别名non-heap(非堆),所以大家不要搞混淆了。方法区还包含一个运行时常量池。
java堆(Heap):存储java实例或者对象的实体信息(包含这个对象的变量和方法信息,成员变量会默认初始化,方法储存了地址值,指向了方法区)的地方。这块是GC的主要区域(后面解释)。从存储的内容我们可以很容易知道,方法区和堆是被所有java线程共享的。
java栈(Stack):java栈总是和线程关联在一起,每当创建一个(对象)线程时,JVM就会为这个线程创建一个对应的java栈。在这个java栈中又会包含多个栈帧(栈帧之间是隔离的),每运行一个方法就创建一个栈帧(所需内存大小在编译期间就确定了,栈帧代表方法处理逻辑),用于存储对象的引用,基本数据类型,局部变量表(方法内局部变量)、操作栈、方法返回值等。每一个方法从调用直至执行完成的过程,就对应一个栈帧在java栈中入栈到出栈的过程。所以java栈是现成私有的。
程序计数器(PC Register):用于保存当前线程执行的内存地址。由于JVM程序是多线程执行的(线程轮流切换),所以为了保证线程切换回来后,还能恢复到原先状态,就需要一个独立的计数器,记录之前中断的地方,可见程序计数器也是线程私有的。
本地方法栈(Native Method Stack):和java栈的作用差不多,只不过是为JVM使用到的native方法服务的。
本地方法接口:主要是调用C或C++实现的本地方法及返回结果。
java栈的内存图
当jvm启动,加载所需要对象的class类,执行类的main()方法,此时jvm会为对象创建一个java栈,main()里包含的方法,就是当前java栈的栈帧(一个方法就是一个栈帧,代表方法处理逻辑),线程2,线程3代表其它类的线程…
一个对象的内存图
方法区中常量池:
方法区也是所有线程共享。主要用于存储类的信息、常量池、方法数据、方法代码等。
方法区是JVM 的规范,永久代(PermGen space)是HotSpot对这种规范的实现。
在 JDK 1.8 中, HotSpot 已经没有 “PermGen space”这个区间了,取而代之的是 Metaspace(元空间)。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:
-XX:MetaspaceSize 初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize最大空间,默认是没有限制的。
除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
内存分配
Java的内存分配原理与C/C++不同,C/C++每次申请内存时都要malloc进行系统调用,而系统调用发生在内核空间,每次都要中断进行切换,这需要一定的开销,而Java虚拟机是先一次性给java类分配一块较大的空间,然后每次为类new对象时都在该空间上进行分配和释放,减少了系统调用的次数,节省了一定的开销,这有点类似于内存池的概念;二是有了这块空间过后,如何进行分配和回收就跟GC机制有关了。
java一般内存申请有两种:静态内存和动态内存。很容易理解,编译时就能够确定的内存就是静态内存,即内存是固定的,系统一次性分配,比如int类型变量;动态内存分配就是在程序执行时才知道要分配的存储空间大小,比如java对象的内存空间。根据上面我们知道,java栈、程序计数器、本地方法栈都是线程私有的,线程生就生,线程灭就灭(JVM相当于是一个进程,所加载的对象,就是一个线程),栈中的栈帧随着方法的结束也会撤销,内存自然就跟着回收了。所以这几个区域的内存分配与回收是确定的,我们不需要管的。但是java堆和方法区则不一样,我们只有在程序运行期间才知道会创建哪些对象,所以这部分内存的分配和回收都是动态的。一般我们所说的垃圾回收也是针对的这一部分。
总之Stack(栈)的内存管理是顺序分配的,而且定长,不存在内存回收问题(栈是线程私有的,随着执行结束就会被销毁);而Heap(堆) 则是为java对象的实例随机分配内存,不定长度,所以存在内存分配和回收的问题;