文章目录
前言一、方法区版本区别方法区内部结构方法区的演变
前言
堆和方法区和栈的关系
㤡
One one
=new One
其中One类会被加载进方法区
如果是在方法里面
new的对象,那么one变量就会存在虚拟机栈的局部变量表中,
而
new One ,
new的对象则会存在堆内存中,
局部变量表的one指向了堆内存中实例数据,
而该实例数据中包含了一个到对象数据类型的指针,而该指针指向了方法区的对象类型数据,
一、方法区
方法区与java堆一样,是各个线程共享的内存区域。
方法区在jvm启动的时候被创建,并且它的实际的物理内存空间中和java堆区一样都是不可连续的。
方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出
同样会抛出内存溢出的错误OutOfMemoryError: PermGen space或者 OutOfMemoryError:Metaspace
可能还有加载过多的jar包;在服务器上部署过多的项目;大量的动态生成反射
注意;方法区具体的落地实现在
1.7及以前是永久代,在
1.8开始改为元空间
元空间的本质和永久代类似,但是最大的区别在于,元空间不再虚拟机设置的内存中,而在本地内存中,
他们并不是名字改变了,内部结构也改变了
方法区大小不是固定的,可以调整大小
版本区别
1.7及以前
-XX
:PermSize来设置永久代初始分配空间,默认值是
20.75M
-XX
:MaxPermSize来设置永久代最大可分配空间,
32位机器默认是
64M
,64位默认
82M
当加载的类信息超过了容量,就会报oom
1.8及以后
元数据区大小可以使用参数
-XX
:MetaspaceSize 初始,默认 赋值是通过等号,如
-XX
:MetaspaceSize
=100M
-XX
:MaxMetaspaceSize 最大默认 一样,使用等号
默认值依赖于平台,因为元空间是存储在本地的内存中
Window下,初始默认是
21mB
,最大值是
-1,表示没有上限
与永久代不同的是,如果不指定最大的大小,默认情况下,
虚拟机会耗尽所有的可用系统内存,如果元数据区发生异常,一样抛出outofmemoryerror
Windows的对于
64位的虚拟机,默认初始值大小是
21M
,相当于是个初始的高水位线,
当触及到了这根线后,full gc会触发卸载没用的类(即这些类对应的类加载器将不再存活),
回收后,将会重置水位线,那么重置为多少,取决于释放了多少元空间,如果释放的少,
在不超过最大值时适当的提高,如果是释放的较多,那么将适当的降低值
因此可以知道,如果设置的初始值过低,那么会触发多次的full gc
,影响性能,
所以应该将初始值大小设置为一个较高的值
如何解决oom异常或heap space 异常 ,一般手段是首先通过内存映射分析工具对dump出来的堆转储快照进行分析,
重点是分清楚到底是出现了内存泄漏
(Memory leak
)还是内存溢出(Memory Overflow)
内存泄漏和溢出
。内存泄漏是指分配出去的内存无法回收了。
。内存溢出是指程序要求的内存,超出了系统所能分配的范围,从而发生溢出
如果不是内存溢出的问题,那么就应该看看参数是否需要改动,一些对象的生命周期是否过长
方法区内部结构
用于存储被虚拟机加载的类型信息
(类,接口,枚举,注解等
),常量
,静态变量,即时编译器编译后的代码缓存等
但并不是全都是这样,只可以说是一个经典的结构
类型信息:
Jvm必须在在方法区存储以下信息
。该类型的有效名称,权限定类名
。该类型的直接父类的权限定类名
。该类型的修饰符
。该类型直接接口的一个有序列表
域
(field
)信息
包括:域名称,域类型,域修饰符
Public
..static . . .final ..volatile等
方法信息
方法名称
返回类型
参数数量和类型,按照顺序
修饰符
方法字节码,操作数栈,局部变量表及大小
(native,
abstract除外
)
异常表
(native,
abstract除外
)
每个异常处理的开始位置,结束位置,代码处理在程序计数器中的偏移地址,被捕获的异常类的常量池索引
Static
final 修饰的常量
在编译期就已经确定的值
一个有效的字节码文件包含了类的版本信息,字段,方法以及接口等信息,
还包含了常量池表,包含个各种字面量,以及对类型,域和方法的的符号引用
常量池包含了 :数量值,字符串值,类引用,方法引用,字段引用
常量池是在
class文件中,编译期编译的,当类加载进内存时,会存放在方法区的运行时常量池里
运行时常量池,在加载类和接口道虚拟机后,就会创建对应的运行时常量池
Jvm为每个已加载的类型
(类或接口
)都维护一个常量池,池中的数组项通过索引访问的
运行时常量池包含了各种不同的常量,包括了编译器确定的数值字面量,
以及在运行期解析的方法的引用和字段的引用,但在运行时常量池里,不在是符号引用了,而是真实的地址
因此常量池和运行常量池的区别就是存放的符号引用和真实地址 ,
并且运行时常量池具 备动态性
String
.intern()方法
当创建类或接口的运行时常量池时,如果所需的空间大小超过了方法区的最大空间
,
就会报OutOfMemoryError异常
方法区的演变
只有hotsopt才有永久代,且方法区的实现没有统一的要求
1.6及之前 :有永久代,静态变量存放在永久代上,字符串常量池在运行时常量池里
运行时常量池存在方法区里,也就是永久代
1.7:有永久代,但已经去永久代,字符串常量池,静态变量移除,保存在堆中
1.8及以后:无永久代,类型信息,字段,方法,常量保存在本地内存中的元空间,
但是静态变量和字符串常量池仍然在堆中
运行时常量池一直都在方法区里
永久代被替换为元空间的原因:
1永久代设置空间大小是很难确定的,
在某些场景下,如果动态加载类过多,容易产生perm的oom区,
比如某个web中,如果要不断动态加载很多类,就很有可能出现OutOfMemoryerror
而最大的区别在于,元空间 不在虚拟机中,使用的是本地内存,只受限于本地内存大小
2对永久代的调优是很难的一件事
方法区的垃圾收集主要回收两部分,废弃的常量和不在使用的类
字符串常量池为什么移到堆内存中
StringTable为什么要调整
因为永久代的回收效率低,在full gc时才会触发,而触发的条件是老年代,或在永久代空间不足时触发,
还有其他原因触发,不详诉,
这就导致了字符串常量池回收效率不高,但在实际开发中会有大量的字符串被创建,回收效率低,
会导致永久代内存不足,而放到堆里,回收的次数较多
当在类的属性上创建一个对象,那么他的变量是跟着实例走的,实例在堆内存
如果是静态变量创建的对象,
1.7及之后选择将静态变量与类型在java语言一端的映射
class对象存放在一起,
存储于java堆中
注意,之前我们说的,在方法里面创建对象的话,虽然实例保存在堆内存中,
如果该变量没有发生逃逸
,那么就存在标量替换,变量存在该方法对应的栈中,存在局部变量表中
补充:注意,如果是静态方法,如果有参数的话,前面的槽存的是参数,而非静态方法,第一个参数是
this
Java虚拟机规范中,提到可以不要求在方法区进行垃圾回收,
事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在
判断一个类型是否属于“不在被使用的类”的条件需要同时满足三个条件:
1.该类所有的实例都已经被回收,也就是java堆中不存在该类及其任何派生类的实例
2.加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,否则很难达成
3.该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射获取到该类的方法
而且只是允许同时达成了这三个条件,才有可能回收,但不是必然的,而对象则是没有了引用就会回收,这是不一样的
-Xnoclassgc 可以通过该参数进行设置
-verbose
:class 及
-XX
:+TraceClass
-Loading
,-XX
:+TrancClassUnLoading 可查看类的加载与卸载信息
运行时常量池的回收目的明确,当没有被其他地方引用时,就会回收