[TOC]
1. 大纲
1、根据Java虚拟机规范,Java虚拟机所管理的内存包括方法区、虚拟机栈、本地方法栈、堆、程序计数器等。
2、我们通常认为JVM中运行时数据存储包括堆和栈。这里所提到的栈其实指的是虚拟机栈,或者说是虚拟栈中的局部变量表。
各区域介绍
栈中存放一些基本类型的变量数据(int/short/long/byte/float/double/Boolean/char)和对象引用。
堆中主要存放对象,即通过new关键字创建的对象。(变量存在栈中(即对象引用), 创建的对象在堆中), 见下图
但没有存储所有的对象, 如果逃逸分析后发现没有逃逸, 则对象则会在栈上分配
堆可细分为 新生代/老年代/永久代(jdk8后替换成元空间), 新生代又细分为 Eden + From Survivor + To Survivor区。
字符串常量池: 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。保存的是字符串(key)和 字符串对象的引用(value)的映射关系,字符串对象的引用指向堆中的字符串对象。
程序计数器: 是用来控制代码的执行执行, 比如接下来是循环/判断/跳转回主线程等等
方法区: 存放类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
- 运行时常量池: 存放编译期生成的各种字面量(Literal)和符号引用(Symbolic Reference) 。
字面量是源代码中的固定值的表示法,即通过字面我们就能知道其值的含义。字面量包括整数、浮点数和字符串字面量。常见的符号引用包括类符号引用、字段符号引用、方法符号引用、接口方法符号。
本地方法栈: 本地方法栈描述Native方法的执行;比如unsafe下的cas方法(就是操作c语言的方法)
hotspot jdk8中移除了永久代以后的内存结构
JVM结构及堆的划分 - 光何 - 博客园 (cnblogs.com)
2. 方法区(元空间/永久代) 怎么回收对象?
《Java虚拟机规范》 中提到过可以不要求虚拟机在方法区中实现垃圾收集, 事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK 11时期的ZGC收集器就不支持类卸载)
方法区的垃圾收集主要回收两部分内容: 废弃的常量和不再使用的类型
回收废弃常量与回收Java堆中的对象非常类似。 举个常量池中字面量回收的例子, 假如一个字符串“java”曾经进入常量池中, 但是当前系统又没有任何一个字符串对象的值是“java”, 换句话说, 已经没有任何字符串对象引用常量池中的“java”常量, 且虚拟机中也没有其他地方引用这个字面量。 如果在这时发生内存回收, 而且垃圾收集器判断确有必要的话, 这个“java”常量就将会被系统清理出常量池。 常量池中其他类(接口) 、 方法、 字段的符号引用也与此类似
判定一个类型是否属于“不再被使用的类”的条件需要同时满足下面三个条件:
- 该类所有的实例都已经被回收, 也就是Java堆中不存在该类及其任何派生子类的实例。
- 加载该类的类加载器已经被回收, 这个条件除非是经过精心设计的可替换类加载器的场景, 如OSGi、 JSP的重加载等, 否则通常是很难达成的。
- 该类对应的java.lang.Class对象没有在任何地方被引用, 无法在任何地方通过反射访问该类的方法
Java虚拟机被允许对满足上述三个条件的无用类进行回收, 这里说的仅仅是“被允许”, 而并不是和对象一样, 没有引用了就必然会回收 ,关于是否要对类型进行回收 , HotSpot虚拟机提供了-Xnoclassgc
参数进行控制
在大量使用反射、 动态代理、 CGLib等字节码框架, 动态生成JSP以及OSGi这类频繁自定义类加载器的场景中, 通常都需要Java虚拟机具备类型卸载的能力, 以保证不会对方法区造成过大的内存压力。
方法区和永久代以及元空间是什么关系呢?
方法区和永久代以及元空间的关系很像 Java 中接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区,也就是说永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。并且,永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现变成了元空间。
3. 堆的分代
Java虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为新生代、老年代和永久代(对HotSpot虚拟机而言),这就是JVM的内存分代策略。
Java虚拟机将堆内存划分为新生代、老年代和永久代,永久代是HotSpot虚拟机特有的概念(JDK1.8之后为metaspace替代永久代),它采用永久代的方式来实现方法区,其他的虚拟机实现没有这一概念,永久代主要存放常量、类信息、静态变量等数据,与垃圾回收关系不大,新生代和老年代是垃圾回收的主要区域。
3.1 新生代(Young Generation)
新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。 HotSpot将新生代划分为三块,一块较大的Eden(伊甸)空间和两块较小的Survivor(幸存者)空间,默认比例为8:1:1。划分的目的是因为HotSpot采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。 GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的(作为保留区域)。GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,要么移到老年代要么复制到To survivor区
年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header中)的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor区。接着清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区。
对象的年龄都存在对象中, 存放在对象头中
接着, From Survivor区和To Survivor区会交换它们的角色,也就是说新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区,总之,不管怎样都会保证To Survivor区在一轮GC后是空的。GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。
“对象从新生代进入到老年代”的四种情况
- Minor GC/Young GC 时,To Survivor 区不足以存放存活的对象,对象会直接进入到老年代。
- 经过多次 Minor GC/Young GC 后,如果存活对象的年龄达到了设定阈值,则会晋升到老年代中。
- 动态年龄判定规则,To Survivor 区中相同年龄的对象,如果其大小之和占到了 To Survivor 区一半以上的空间,那么大于此年龄的对象会直接进入老年代,而不需要达到默认的分代年龄。
- 大对象:由
-XX:PretenureSizeThreshold
启动参数控制,若对象大小大于此值,就会绕过新生代,直接在老年代中分配。 (默认值是0 , 表示所有对象都放eden区, 超过eden区剩余大小就放老年代; 这个参数只对 Serial 和ParNew的单线程版收集器有效)
-XX:PretenureSizeThreshold 的默认值和作用 - 简书 (jianshu.com)
3.2 老年代(Old Generationn)
在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
这里会发生Full GC , 产生STW(stop the world)
新生代和老年代的空间配比是 1 : 2
什么时候会触发Full GC(建议收藏) - 知乎 (zhihu.com)
3.3 永久代(Permanent Generationn)
永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。
在jdk8后, 用元空间代替了永久代, 永久代直接使用了堆外内存, 不再占用jvm内存
JVM结构及堆的划分 - 光何 - 博客园 (cnblogs.com)
4. java对象分配流程
栈上分配: 如果对象经过逃逸分析后若没有发生逃逸, 则会直接在栈上分配, (如果栈内存不够, 也会继续往下)
逃逸分析: 其实就是判断我们将这个user对象会不会return出去,出去了的话,这时候我们对于这个对象来说就不会在栈上分配,因为后续的代码可能还需要使用这个对象实例,可以说只要是多个线程共享的对象就是逃逸对象,
这样的话, 不需要GC介入去回收这个对象,出栈即释放资源,可以提高性能
TLAB: 全称Thread Local Allocation Buffer, 即:线程本地分配缓存。这是一块线程专用的内存分配区域。TLAB占用的是eden区的空间。在TLAB启用的情况下(默认开启),JVM会为每一个线程分配一块TLAB区域。