运行时数据区
Java虚拟机在执行java程序过程中会将它所管理的内存分为几块不同的区域,方便对对象的管理。每个区域都有对应的管理对象。
上图就是java运行时内存的模型图。
程序计数器
程序计数器占用的内存并不多,它就类似于线程执行的中控台,它用来标记线程需要执行的下一行字节码指令,在多线程环境下,每一个线程都有单独的程序计数器,在单核处理器的时候,每一个时刻只会又一个线程执行,那么就会进行线程切换,而每一个线程切换完成后需要回到他正确的执行位置,程序计数器就帮忙干这个事情。
如果线程执行的java方法,那么它记录的就是虚拟机字节码指定的地址,如果是native方法计数器则为空(undefined),由于每一个线程都有单独的程序计数器,所以它是线程私有的。
Java虚拟机栈
Java虚拟机栈也是线程私有的,每个方法被执行时都会同步创建一个栈帧用于存储局部变量表,那么在这个方法执行完之后就会出栈。
虚拟机栈主要存放基本数据类型和引用类型,如果线程请求的深度大于栈的高度就会抛出StackOverflowError异常。栈也是会自动扩容的,当扩容的时候发现申请不到足够的内存了就会抛出OutOfMemoryError。
本地方法栈
本地方法栈的功能和Java虚拟机栈作用是类似的,只不过Java虚拟机栈是为Java服务的,本地方法栈为本地方法服务(非Java语言,比如c,c++等)。
堆
关于堆大家可能更加熟悉一点,平时我们new(关键字)创建的对象就在这里,这里也是垃圾收集器主要工作的地方。堆是属于线程共享的。堆中的内存也有着自己的划分,主要划分为新生代,老年代(永久代在jdk8以后被废除,用元空间就行代替)。新生代和老年代针对对象的大小会进入到这两个地方,新生代中被分为Eden区和From Survivor 和To Survivor,Eden区和两个存活区的默认大小是8:1。新创建的对象如果占用内存不是很高就会进入新生代的Eden,而如果创建的对象占用内存很大就会直接进入老年代。为什么这么设计呢,其实和垃圾收集器有关,大家都知道java主流收集器都是采用的分代收集算法,针对新生代和老年代的垃圾收集是不一样,这个后面再细讲,对于新生代的收集效率肯定是比老年代高的,因为新生代普通占用内存少,而且新生代的对象的引用也是少于老年代,为了使尽量压榨垃圾收集器的劳动力就采用了这种收集算法。
方法区
方法区用于存储类加载的信息,常量,静态变量,即时编译器编译后的代码缓存等。
这里需要提到一下永久代的概念,很多人会把方法区和永久代混为一谈,jdk8之前,设计者为了方法区能够将堆中的分代收集直接运用到方法区中,这样就不用单独为方法写一个垃圾收集的方法,采用了永久代来实现方法区,但其实后来发现这个策略是极其不好的,更容易发生内存内存溢出,于是虚拟机的开发团队,在jdk6的时候就开始开发替代永久代的方法,在jdk8的时候就完全放弃的永久代这个概念,改用元空间就行代替。
运行时常量池
这个是方法区的一部分,除了有类的版本,字段,方法,接口等描述信息外,还有一项重要的信息就是常量池表。常量池表的作用是为了存放编译期生成的各种字面量和符号的引用,这部分内容在类加载完成后悔放入运行时常量池中。
.1.字面量
在java中,声明为final的int、long、double、char等基本类型的常量值,特殊的字符串文本"abc",abc也是字面量。字面量一出生,其内容、大小全部固定,不会发生改变。
1.符号
符号引用以一组符号来描述所引用的目标,符号引用可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。
比如一个类中有着另一个对象的引用,但是由于此时是编译器,并不知道这个对象的实际地址,所以用一个符号来进行代替。
直接内存
直接内存并不属于运行时数据区,但是使用率也是非常高,他是直接属于操作系统级别的内存不受虚拟机堆大小的控制,也是可能发生OutOfMemoryError(内存溢出) 的,我们使用的NIO类就是使用的直接内存。
元空间
前面提到了jdk8中永久代被元空间代替了,元空间和直接内存一样都属于本地缓存直接向操作系统申请内存,并不归虚拟机管。只所以用元空间替代,因为永久代是属于堆的,我们无法准确的预计出永久代的大小,而且永久代的对象占用空间都比较大更加容易产生Full GC,而Full GC就意味着会暂停用户线程,影响系统使用。所以基于效率和内存大小就将元空间代替了永久代,而元空间的垃圾收集更加优化,它不会暂停用户线程,而且理论上可申请的容量大小是基于机器的实际物理内存的。