转自:简书 https://www.jianshu.com/p/a60d6ef0771b
文章目录
内存划分
java虚拟机按照运行时内存使用区域划分如图:
一、程序计数器(Program Counter Register)
程序计数器是一块较小的内存空间,他可以看做是记录当前线程执行程序的位置,改变计数器的值来确定执行的下一条指令,比如循环、分支、方法跳转、异常处理,线程恢复都是依赖程序计数器来完成。
Java虚拟机多线程是通过线程轮流切换并分配处理器执行时间的方式实现的。为了线程切换能恢复到正确的位置,每条线程都需要一个独立的程序计数器,所以它是线程私有的。
- 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
- 如果正在执行的是Native方法,这个计数器值则为空(Undefined)。
- 如果此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
二、java虚拟机栈(VM Stack)
java虚拟机栈是线程私有,生命周期与线程相同。创建线程的时候就会创建一个java虚拟机栈。
虚拟机执行java程序的时候,每个方法都会创建一个栈帧,栈帧存放在java虚拟机栈中,通过压栈出栈的方式进行方法调用。
栈帧又分为一下几个区域:局部变量表、操作数栈、动态连接、方法出口等。
平时我们所说的变量存在栈中,这句话说的不太严谨,应该说局部变量存放在java虚拟机栈的局部变量表中。
局部变量表存放各种编译期可知的8中基本数据类行和对应应用,以及returnAdress类型(指向了一条字节码地址)。double和long类型占用2个局部变量空间,其他类型占用1个空间(slot)。局部变量内存空间在编译期间分配完成、
注意:
- 当用户请求web服务器,每个请求开启一个线程负责用户的响应计算(每个线程分配一个虚拟机栈空间),如果并发量大时,可能会导致内存溢出(OutOfMemoneyError),可以适当的把每个虚拟机栈的大小适当调小一点,减少内存的使用量来提高系统的并发量。
- 当栈空间调小以后,又会引发方法调用深度的的问题。因为,每个方法都会生成一个栈帧,如果方法调用深度很深就意味着,栈里面存放大量的栈帧,可能导致栈内存溢出(StackOverFlowError)。
Java Virtual Machine定义了两种异常:OutOfMemoryError与StackOverflowError。
- OutOfMemoryError:Java启动一个线程时,JVM没有足够的内存空间为该线程分配Java栈,JVM抛出OutOfMemoryError异常。
- StackOverflowError:Java启动一个线程时,JVM为它分配一个栈,Java栈以帧为单位保持线程运行状态,当线程调用一个方法时,JVM压入一个新的栈帧到这个线程的栈中,只要这个方法没返回,这个栈帧就存在。如果方法的嵌套调用深度太深(如深度很深的递归),随着Java栈中的帧增多,最终导致线程的栈中的所有栈帧的大小总和大于-Xss设定的值,即产生StackOverflowError,栈溢出异常。
三、本地方法栈(Native Method Stack)
本地方法栈 为虚拟机使用到本地方法服务(native)。本地方法栈为线程私有,功能和虚拟机栈非常类似。线程在调用本地方法时,来存储本地方法的局部变量表,本地方法的操作数栈等等信息。
JVM规范中对本地方法栈的实现没有具体规定,各个虚拟机实现可自由实现。早SUN的HotSpot虚拟机就将本地方法栈和虚拟机方法栈合二为一。
四、JAVA堆(Heap):
Java 中的堆是 JVM 所管理的最大的一块内存空间,被所有线程共享,在虚拟机启动时创建。
堆的大小可以通过参数 –Xms、-Xmx 来指定。
堆中不存放基本类型和对象引用,只存放对象本身,几乎所有的对象实例和数组都在堆中分配。
在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。 这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。
- 新生代:Young Generation,主要用来存放新生的对象。
- 老年代:Old Generation或者称作Tenured Generation,主要存放应用程序声明周期长的内存对象。(经过多次新生代的垃圾收集,默认是15次的对象)。
堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。
默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。
因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
当堆中分配的对象实例过多,且大部分对象都在使用,就会报内存溢出异常(OutOfMemoneyError)。
五、方法区
永久代
(Java8之前)方法区是被所有线程共享区域,用于存放已被虚拟机加载的类信息,常量,静态变量等数据。被Java虚拟机描述为堆的一个逻辑部分。习惯是也叫它永久代(permanment generation)
永久代也会垃圾回收,主要针对常量池回收,类型卸载(比如反射生成大量的临时使用的Class等信息)。
常量池用于存放编译期生成的各种字节码和符号引用,常量池具有一定的动态性,里面可以存放编译期生成的常量;运行期间的常量也可以添加进入常量池中,比如string的intern()方法。
当方法区满时,无法在分配空间,就会抛出内存溢出的异常(OutOfMemoneyError)。
JDK1.7中JVM把String常量区从方法区中移除了;
JDK1.8中JVM把String常量池移入了堆中,同时取消了“永久代”,改用元空间代替(Metaspace)
常量池
常量池,用于存放编译期生成的各种字面量和符号引用。
运行时常量池与Class文件常量池区别
- JVM对Class文件中每一部分的格式都有严格的要求,每一个字节用于存储那种数据都必须符合规范上的要求才会被虚拟机认可、装载和执行;但运行时常量池没有这些限制,除了保存Class文件中描述的符号引用,还会把翻译出来的直接引用也存储在运行时常量区
- 相较于Class文件常量池,运行时常量池更具动态性,在运行期间也可以将新的变量放入常量池中,而不是一定要在编译时确定的常量才能放入。最主要的运用便是String类的intern()方法
- 在方法区中,常量池有运行时常量池和Class文件常量池;但其中的内容是否完全不同,暂时还未得知
String.intern()
检查字符串常量池中是否存在String并返回池里的字符串引用;若池中不存在,则将其加入池中,并返回其引用。
这样做主要是为了避免在堆中不断地创建新的字符串对象
元空间
JAVA8之后HotSpot溢出了永久代,使用元空间。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小。
原先永生代中类的元信息会被放入本地内存(元数据区,metaspace),将类的静态变量和内部字符串放入到java堆中。
为什么从永久代切换到元空间?
1)字符串存在永久代中,容易出现性能问题和内存溢出。
2)类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
3)永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
JDK1.4加的NIO中,ByteBuffer有个方法是allocateDirect(int capacity) ,这是一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。