java内存区域
1、运行时数据区域
java虚拟机管理的内存包括以下几个区域 ( >=jdk8版本 )
与1.8版本不同的是,在1.8版本前,方法区使用的是“永生代”。
- 1.6或者更早的版本,常量池都是分配在永久代中,当常量池溢出时,会报出“PermGen space”错误提示信息,说明运行时常量池:的确属于方法区(永久代)。
- 在1.7版本起,字符串常量池被移动到了java堆中,溢出时提示“Java heap space”。
- 1.8元空间替代了永久代,永久代是有大小限制的,如果类太多,还是会报出“PermGen space”,而元空间使用的是本地内存。
注:方法区的常量池为运行时常量池
补充:
-
常量池:本质是一张表,根据表找到要加载的类名、方法名,类参数类型,字面量等等。
-
运行时常量池:类加载到内存后,会将class常量池中的内容放到运行时常量池。
-
字符串常量池:属于运行时常量池分出的一部分,类加载到内存后,字符串会到字符串常量池中。不会重复创建字符串对象。StringTable类似hashTable结构,如果项目中常量值较多,可以调整StringTable的桶个数;如果有大量的数据且有重复值,可以尝试把数据放入串池中(intern方法),减少内存使用
说到字符串常量池,浅唠一下
String a = "a";
String b = "b";
String c = "ad";
// false d != a + b;
// a、b 存放位置为字符串常量池。c 存放位置为堆,操作时使用了new StringBuilder()
String d = a + b;
// true
// e存在区域同为字符串常量池,
// 编译时结果已经确认为“ab”,不会被更改。但是d拼接的为变量,结果值是不能被确定的,运行期间动态的去拼接
String e = "a" + "b"
2、程序计数器(寄存器)
计数器可以看作当前线程所执行的字节码的行号指示器。
计数器是“线程私有”,各线程间的计数器互不影响,独立存储。此区域也是唯一一个没有规定任何OutOfMemoryError情况的区域。
图中解释器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
3、java虚拟机栈
同样为“线程私有”,生命周期与线程相同。后进先出
每个方法被执行时,java虚拟机都会同步创建一个栈帧,用于存储局部变量、操作数栈、动态连接、方法出口信息等。
如果栈帧过多,超出栈内存,会抛出StackOverflowError异常(代码可尝试无限递归),如果无法申请到足够内存会抛出OutOfMemoryError异常(代码可尝试不断创建线程)。
注:
- 垃圾回收机制不会涉及栈内存,因为栈中方法每一次执行完成后会自动被回收。
- 并不是栈内存越大越好,栈内存越大线程数越小,如总内存500MB,一个栈占2MB,那么只能运行250个线程。
- 线程私有并不等于线程安全,如果局部变量逃离了方法作用范围,线程不安全,反之。
本地方法栈
为虚拟机使用到的本地方法服务,非java代码编写,和虚拟机栈一样,会在栈深度溢出或栈拓展失败时分别抛出StackOverflowError和OutOfMemoryError异常。
如Object的clone方法,通过本地(Native)方法服务间接的调用C/C++的实现。
java堆
java堆是虚拟机管理内存中最大的一块,是被所有线程共享的内存区域,虚拟机启动时创建,唯一目的就是存放对象实例(通过new关键字创建的对象都会使用堆内存)。
此区域也是垃圾收集器管理的内存区域(GC堆)。
java堆中又分为很多区域,比如新生代、老年代、Eden等,具体有机会会在新文章解释。
当java堆中没有内存完成实例分配且无法再扩展时,会抛出OutOfMemoryError异常(代码可尝试不断生成新对象,且避免被垃圾回收机制清除),具体提示“Java heap space”。
方法区
与java堆一样,为线程共享,用于存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的部分,堆可以选择不实现垃圾收集。
《java虚拟机规范》中把方法区描述为堆的一个逻辑部分,称为“非堆”。
写到方法区,不得不再提一下“永久代”概念,1.8为界,1.8以前使用的为永久代,占用堆内存,1.8以后包括1.8使用“元空间”,元空间使用的为本地内存。 理论上系统可使用的内存有多大,元空间就有多大,所以很难出现永久代存在时的内存溢出问题。
如果方法区无法满足新的内存分配需求会抛出OutOfMemoryError异常。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,属于系统内存的一部分。但是这部分也被频繁使用。
- 常用于NIO操作,用于数据缓冲区。
- 分配回收成本较高,但是读写性能高。
- 不受JVM内存回收管理。直接内存的回收原理:java中有个底层Unsafe对象,来管理直接内存的分配与回收,需要java主动调取回收,通过源码可以看到
通过ByteBuffer的方法进入源码,可以看到DirectByteBuffer,它是NIO的一个实现。点进它的构造方法,发现红色框里Unsafe完成了对直接内存的分配,在点进Cleaner关联的一个回调,会发现释放了直接内存。
使用NIO效率远远大于普通的IO操作,注意:java本身不具备磁盘读写的能力,需要调用操作系统提供的方法(Native)方法。原因大概了解一下:
左面为普通IO操作,右面为NIO操作。NIO使用了直接内存,要比普通IO少了一层缓冲操作,速度就得到了提升。
直接内存也会抛出OutOfMemoryError异常,提示“Direct buffer memory”。
原文掘金地址