<JVM上篇:内存与垃圾回收篇>07-方法区(四)

简介: <JVM上篇:内存与垃圾回收篇>07-方法区

7.6. 方法区的演进细节

  1. 首先明确:只有 Hotspot 才有永久代。BEA JRockit、IBMJ9 等来说,是不存在永久代的概念的。原则上如何实现方法区属于虚拟机实现细节,不受《Java 虚拟机规范》管束,并不要求统一
  2. Hotspot 中方法区的变化:

image.png

c2c6ca1d8758ff4defcae60e76e87516.jpg7.6.1. 为什么永久代要被元空间替代?


官网地址:JEP 122: Remove the Permanent Generation (java.net)


0afb129e23c47f92d361429af0f0eb2e.png


随着 Java8 的到来,HotSpot VM 中再也见不到永久代了。但是这并不意味着类的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫做元空间(Metaspace)。


由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。


这项改动是很有必要的,原因有:


为永久代设置空间大小是很难确定的。在某些场景下,如果动态加载类过多,容易产生 Perm 区的 oom。比如某个实际 Web 工 程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。

"Exception in thread 'dubbo client x.x connector' java.lang.OutOfMemoryError:PermGen space"

而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。 因此,默认情况下,元空间的大小仅受本地内存限制。


对永久代进行调优是很困难的。

有些人认为方法区(如 HotSpot 虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java 虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如 JDK 11 时期的 ZGC 收集器就不支持类卸载)。 一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前 Sun 公司的 Bug 列表中,曾出现过的若干个严重的 Bug 就是由于低版本的 HotSpot 虚拟机对此区域未完全回收而导致内存泄漏


方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型


7.6.2. StringTable 为什么要调整位置?

jdk7 中将 StringTable 放到了堆空间中。因为永久代的回收效率很低,在 full gc 的时候才会触发。而 full gc 是老年代的空间不足、永久代不足时才会触发。


这就导致 StringTable 回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。


7.6.3. 静态变量存放在那里?


示例1

/**
 * 静态引用对应的对象实体始终都存在堆空间
 * jdk7:
 * -Xms200m -Xmx200m -XX:PermSize=300m -XX:MaxPermSize=300m -XX:+PrintGCDetails
 * jdk8:
 * -Xms200m -Xmx200m-XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails
 */
public class StaticFieldTest {
    private static byte[] arr = new byte[1024 * 1024 * 100];
    public static void main(String[] args) {
        System.out.println(StaticFieldTest.arr);
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

37968dafb40c9f37f5e57d6568aa5b43.png


37f2ecefb708cfa92a7ab14dad476a27.png

结论:静态引用对应的对象实体始终都存在堆空间

  • 示例2
/**
 * 《深入理解Java虚拟机》中的案例:
 * staticObj、instanceObj、localObj存放在哪里?
 * @author shkstart  shkstart@126.com
 * @create 2020  11:39
 */
public class StaticObjTest {
    static class Test {
        static ObjectHolder staticObj = new ObjectHolder();
        ObjectHolder instanceObj = new ObjectHolder();
        void foo() {
            ObjectHolder localObj = new ObjectHolder();
            System.out.println("done");
        }
    }
    private static class ObjectHolder {
    }
    public static void main(String[] args) {
        Test test = new StaticObjTest.Test();
        test.foo();
    }
}

使用 JHSDB 工具进行分析,这里细节略掉


e8ced63603ee4a62c9eb7d4c48ae94dd.png


**三个变量的引用:**staticobj 随着 Test 的类型信息存放在方法区,instanceobj 随着 Test 的对象实例存放在 Java 堆,localobject 则是存放在 foo()方法栈帧的局部变量表中。


e7c6b3dd5175466ee891da9e84577418.png


测试发现:三个对象的数据在内存中的地址都落在 Eden 区范围内,所以结论:只要是对象实例必然会在 Java 堆中分配。


接着,找到了一个引用该 staticobj 对象的地方,是在一个 java.lang.Class 的实例里,并且给出了这个实例的地址,通过 Inspector 查看该对象实例,可以清楚看到这确实是一个 java.lang.Class 类型的对象实例,里面有一个名为 staticobj 的实例字段:


从《Java 虚拟机规范》所定义的概念模型来看,所有 Class 相关的信息都应该存放在方法区之中,但方法区该如何实现,《Java 虚拟机规范》并未做出规定,这就成了一件允许不同虚拟机自己灵活把握的事情。JDK7 及其以后版本的 HotSpot 虚拟机选择把静态变量与类型在 Java 语言一端的映射 class 对象存放在一起,存储于 Java 堆之中,从我们的实验中也明确验证了这一点


备注:前面已经说过了,没有栈上分配这码事,如果未逃逸,做标量替换,把一个对象分解为多个成员变量,也还是存在堆上。


7.7. 方法区的垃圾回收


有些人认为方法区(如 Hotspot 虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java 虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如 JDK11 时期的 zGC 收集器就不支持类卸载)。


一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前 sun 公司的 Bug 列表中,曾出现过的若干个严重的 Bug 就是由于低版本的 HotSpot 虚拟机对此区域未完全回收而导致内存泄漏。


方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。


先来说说方法区内常量池之中主要存放的两大类常量:字面量和符号引用。字面量比较接近 Java 语言层次的常量概念,如文本字符串、被声明为 final 的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量:


类和接口的全限定名

字段的名称和描述符

方法的名称和描述符

HotSpot 虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。


回收废弃常量与回收 Java 堆中的对象非常类似。


判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:


该类所有的实例都已经被回收,也就是 Java 堆中不存在该类及其任何派生子类的实例。


加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP 的重加载等,否则通常是很难达成的。


该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。


Java 虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot 虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class 以及 -XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看类加载和卸载信息


在大量使用反射、动态代理、CGLib 等字节码框架,动态生成 JSP 以及 OSGi 这类频繁自定义类加载器的场景中,通常都需要 Java 虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。


总结


fe10760d98064f0b14075c484e5ef8f2.jpg

关于MiniorGC和Full GC的区别. MinorGC是老年代的GC,FullGC是整个堆的GC

栈帧中的动态链接指向的是方法区中的方法,代表当前操作的是哪个方法


常见面试题


百度:


说一下 JVM 内存模型吧,有哪些区?分别干什么的?


蚂蚁金服:


Java8 的内存分代改进 JVM 内存分哪几个区,每个区的作用是什么?


一面:JVM 内存分布/内存结构?栈和堆的区别?堆的结构?为什么两个 survivor 区?


二面:Eden 和 survior 的比例分配


小米:


jvm 内存分区,为什么要有新生代和老年代


字节跳动:


二面:Java 的内存分区


二面:讲讲 vm 运行时数据库区 什么时候对象会进入老年代?


京东:


JVM 的内存结构,Eden 和 Survivor 比例。


JVM 内存为什么要分成新生代,老年代,持久代。


新生代中为什么要分为 Eden 和 survivor。


天猫:


一面:Jvm 内存模型以及分区,需要详细到每个区放什么。


一面:JVM 的内存模型,Java8 做了什么改


拼多多:


JVM 内存分哪几个区,每个区的作用是什么?


美团:


java 内存分配 jvm 的永久代中会发生垃圾回收吗?


一面:jvm 内存分区,为什么要有新生代和老年代?


相关文章
|
3天前
|
存储 Java
深入理解Java虚拟机:JVM内存模型
【4月更文挑战第30天】本文将详细解析Java虚拟机(JVM)的内存模型,包括堆、栈、方法区等部分,并探讨它们在Java程序运行过程中的作用。通过对JVM内存模型的深入理解,可以帮助我们更好地编写高效的Java代码,避免内存溢出等问题。
|
6天前
|
算法 Java Go
Go vs Java:内存管理与垃圾回收机制对比
对比了Go和Java的内存管理与垃圾回收机制。Java依赖JVM自动管理内存,使用堆栈内存并采用多种垃圾回收算法,如标记-清除和分代收集。Go则提供更多的手动控制,内存分配与释放由分配器和垃圾回收器协同完成,使用三色标记算法并发回收。示例展示了Java中对象自动创建和销毁,而Go中开发者需注意内存泄漏。选择语言应根据项目需求和技术栈来决定。
|
4天前
|
存储 机器学习/深度学习 Java
【Java探索之旅】数组使用 初探JVM内存布局
【Java探索之旅】数组使用 初探JVM内存布局
13 0
|
8天前
|
存储 自然语言处理 算法
【JVM】内存模型全面解读
【JVM】内存模型全面解读
12 0
|
9天前
|
存储 监控 Java
三万字长文:JVM内存问题排查Cookbook
本文主要系统性地整理了排查思路,为大家遇到问题时提供全面的排查流程,不至于漏掉某些可能性误入歧途浪费时间。
|
9天前
|
算法 Java 应用服务中间件
Tomcat性能优化及JVM内存工作原理
Tomcat性能优化及JVM内存工作原理
|
9天前
|
存储 缓存 Java
[JVM] 浅谈JMM(Java 内存模型)
[JVM] 浅谈JMM(Java 内存模型)
|
7天前
|
Linux
Linux rsyslog占用内存CPU过高解决办法
该文档描述了`rsyslog`占用内存过高的问题及其解决方案。
30 4
|
1月前
|
移动开发 运维 监控
掌握Linux运维利器:查看CPU和内存占用,轻松解决性能问题!
掌握Linux运维利器:查看CPU和内存占用,轻松解决性能问题!
|
1月前
|
监控 Python
【python】实现cpu/内存监控的功能(非常简单)
【python】实现cpu/内存监控的功能(非常简单)