Java内存划分主要分为以下几块:
程序计数器
线程私有,可以看做是当前线程所执行的字节码的行号,用于下一次线程切换的时候虚拟机定位到上一次执行的位置。
虚拟机栈
-
线程私有,生命周期与线程相同。描述的是方法执行的内存模型。
-
进入方法时对应入栈,方法结束的时候对应出栈。
-
该区域存储着局部变量,操作数,方法出口等信息。
方法区
线程共享。主要用来存储类的元信息。 在1.7和1.8之后的实现逻辑有所不同。
由于该区域大小一般较小,一般不会对该区域进行垃圾回收。所以在1.7之前的版本,有可能会因为字符串常量池过大导致该区域内存溢出(Permgen space out of memory error)。
永久代(PermGen)和方法区之间是什么关系?主要如下几点:
-
方法区是虚拟机规范的一部分,而永久代是hotspot虚拟机用来实现方法区提出的,其他虚拟机未必有永久代。
-
在1.7之前,永久代就是方法区,永久代的内存地址和堆地址是连续的。
-
在1.8之后,hotspot移除了永久代,将原先永久代中的静态变量和常量池并入堆中,将类的元信息放入元空间(metaspace)中。移除永久代的目的是因为PermGen常常会发生内存溢出, 引发恼人的
java.lang.OutOfMemoryError: PermGen
总结以上,归纳为两点:
- 存储位置不同,永久代物理上是堆的一部分(但是并不是垃圾回收的主要战场),和新生代,老年代地址是连续的,而元空间属于本地内存。
- 存储内容不同,元空间存储类的元信息,静态变量和常量池等并入堆中。相当于永久代的数据被分到了堆和元空间中。
下面介绍其中几项概念:
- 类型信息。包括类的完整名称,父类名称等,该类型信息是在类加载器加载类的时候从类文件中提取出来的。
- 类的静态变量(所以静态变量也被称为类变量)
- 常量池。包括实际定义的常量和对类型,域,方法的符号引用。它在java的动态链接中起到了核心的作用。这里面有个需要注意的地方,字符串常量池在1.7之后已经从永久代中移除,放入堆中。
注意此处的元空间metaspace , Java将其放在本地内存中, 默认只受本地内存大小的限制,也就是说本地内存剩余多少,理论上Metaspace就可以有多大。也可以使用参数 -XX:MaxMetaspaceSize 参数来指定 Metaspace 区域的大小。
直接内存(堆外内存)
不是虚拟机运行时数据区的一部分。主要是NIO库中一些直接操作本地内存的操作, 例如DirectByteBuffer。
其内存大小虽然不受堆最大内存的制约,但是也会受到操作系统最大内存的制约。
Java中NIO的核心缓冲就是ByteBuffer,所有的IO操作都是通过这个ByteBuffer进行的;
Bytebuffer有两种: HeapByteBuffer和DirectByteBuffer
//分配HeapByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(int capacity);
//分配DirectByteBuffer
ByteBuffer buffer = ByteBuffer.allocateDirect(int capacity);
两者区别:
DirectByteBuffer | HeapByteBuffer | |
---|---|---|
涉及到IO时拷贝情况 | 不需要拷贝,直接使用 | 需要拷贝到看是的HeapByteBuffer后再使用 |
创建开销 | 需要调用原生方法从系统申请内存, 所以创建开销较大。 不过一般应用否提前申请一大块内存, 然后自己实现内存管理机制, 例如netty | 从JVM堆上分配,速度很快,所以创建开销小 |
对于GC的影响 | 不存在与堆栈, 但是有冰山现象的问题 | 频繁申请新的对象会引发GC |
为啥要使用堆外内存?
- 可以在进程间共享,减少虚拟机间的复制
- 对垃圾回收停顿的改善:如果应用某些长期存活并大量存在的对象,经常会出发YGC或者FullGC,可以考虑把这些对象放到堆外。过大的堆会影响Java应用的性能。如果使用堆外内存的话,堆外内存是直接受操作系统管理( 而不是虚拟机 )。这样做的结果就是能保持一个较小的堆内内存,以减少垃圾收集对应用的影响。
- 在某些场景下可以提升程序I/O操纵的性能。少去了将数据从堆内内存拷贝到堆外内存的步骤。
堆
空间占比最大的一块区域。被所有线程共享。几乎所有的对象实例和数组都会存储在此地。垃圾收集主要是针对此处进行工作。堆具体介绍见下面分析。
主要分为新生代和老年代,见下图分配示意.
先讲述一下新生代的事。以下垃圾回收的算法其实也就是复制算法的实现。
新生代,顾名思义,Java中绝大部分对象都是在该区域被创建,存放新建创建的对象。其特点是对象更新速度快,因为Java中大多数对象都不需要存活很长时间(典型的就是局部变量)。该区域是进行垃圾回收频繁的区域,且进行的垃圾回收类型是Minor GC(GC发生的区域不是整个新生代,而是新生代中的Eden区).
新生代又被分为Eden区,S0区,S1区。默认参数是Eden区占新生代的绝大部分空间(8:1)。当一个对象被创建的时候,首先会在Eden区分配空间。当Eden区没有足够的空间时,会触发一次Minor GC,此时会将存活的对象移动到S0,再将Eden清空。若再次发生Minor GC,则将Eden,S0中存活的对象移动到S1,再将Eden,S0清空。
这样对象就会反复在新生代的三个区之间来回移动,随着对象的移动,其GC年龄也会不断增加,当GC年龄达到一个默认值(15)的时候,就会将该对象实例移动到老年代,如此,老年代的数据就出来了。所以,老年代的数据都是新生代中那些存活年龄很大的对象。
经过以上步骤,老年代已经呼之欲出了。当老年代空间不足时,也会触发一次GC,此时的GC又叫FULL GC,比新生代发生的Minor GC要慢得多。
几个问题:
- A a = new A(); 各个部分分别属于哪个内存区域?
答:new A()存储在堆上, 然后 a存储在该方法的虚拟机栈上,A 存储在方法区上
参考资料:
- https://zhuanlan.zhihu.com/p/161939673
- 深入理解Java虚拟机
ew A()存储在堆上, 然后 a存储在该方法的虚拟机栈上,A 存储在方法区上
参考资料:
- https://zhuanlan.zhihu.com/p/161939673
- 深入理解Java虚拟机
- https://www.sczyh30.com/posts/Java/jvm-metaspace/