- 方法区也存在垃圾回收。
运行时数据期结构图
- 线程共享与否角度
栈、堆、方法区交互关系
代码上:
栈堆方法结构角度:
- 栈 -> 堆中实例数据指针 -> 方法区中对应对象类型数据
- 另一个情况
- 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。
- 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
- 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误: java. lang. outofMemoryError:PermGen space或者java.lang. outofMemoryError: Metaspace
- 关闭JVM就会释放这个区 域的内存。
方法区
方法区存储:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
-
jdk7以前习惯把方法区称永久代,jdk8开始,使用元空间取代永久代
-
jdk8完全废弃永久代概念
-
元空间的本质和永久代类似,都是对JVM规范中万法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存。
-
永久代、元空间二者并不只是名字变了,内部结构也调整了。根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OOM异常。
设置方法区大小与oom
jdk7以前:
-xx:PermSize 来设置永久代初始分配空间。默认值是20.75M
-XX:MaxPermSize 来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M
jdk8后:
-XX:Netaspacesize=10om
-XX:MaxMetaspacesize=100m 值是-1 = 没有限制
方法区内存结构
- 它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
类型信息
对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:
- 这个类型的完整有效名称(全名-包名.类名)
- 这个类型直接父类的完整有效名(对于interface或是java.lang.0bject,都没有父类)
- 这个类型的修饰符(public, abstract, final的某个子集)
- 这个类型直接接口的一个有序列表
域(Field)信息
- JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
- 域的相关信息包括:域名称、域类型、域修饰符(public, private,protected,static,final, volatile, transient的某个子集)
方法(Method)信息
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
- 方法名称
- 方法的返回类型(或void)·方法参数的数量和类型(按顺序)
- 方法的修饰符(public, private, protected,static, final,synchronized, native, abstract的一个子集)
- 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
- 异常表( abstract和native方法除外)
- 每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
non-final的类变量
- 静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分。
- 类变量被类的所有实例共享,即使没有类实例时你也可以访问它。
补充说明:全局常量: static final
- 被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。
运行时常量池
-
方法区,内部包含了运行时常量池。
-
字节码文件,内部包含了常量池。
-
要弄清楚方法区,需要理解清楚classFile,因为加载类的信息都在方法区。
-
要弄清楚方法区的运行时常量池,需要理解清楚classFile中的常量池。
字节码-常量池 方法区-运行时常量池 字节码-加载-方法区
常量池
一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外还包含一项信息那就是常量池表(Constant Pool Table),包括各种字面量和对类型、域和方法的符号引用。
总结:常量池可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。
程序执行过程
public class MethodAreaDemo {
public static void main(String[] args) {
int x = 500;
int y = 100;
int a = x / y;
int b = 50;
System.out.println(a + b);
}
}
/*
译后字节码 stack=3,locals5, args_size=1
*/
pc寄存器存储当前线程下一条指令。
程序计数器为栈帧中记录当前执行指令,已其他栈帧切回来时可以知道执行到哪
方法区演进
版本 | 说明 |
---|---|
jdk1.6前 | 有永久代(permanent generation), 静态变量存放在永久代上 |
jdk1.7 | 有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中 |
jdk8及后 | 无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆 |
永久代即方法区
- jdk1.6
- jdk1.8
方法区垃圾回收
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。
- HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。
判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
- 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如osGi、JSP的重加载等,否则通常是很难达成的。
- 该类对应的java.lang.class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
对象实例
创建方式
创建步骤
实例化的过程
- 加载类元信息
- 为对象分配内存
- 处理并发问题
- 属性的默认初始化(零值初始化
- 设置对象头的信息
- 属性的显式初始化、代码块中初始化、构造器中初始化
内存布局
public class Customer{
int id = 1001;
String name;
Account acct;
{
name = "匿名客户";
}
public Customer(){
acct = new Account();
}
}
class Account{
}
public class CustomerTest {
public static void main(String[] args) {
Customer cust = new Customer();
}
}
访问定位
jvm如何通过栈帧中对象引用访问起到其内部对象实例?
访问方式:
- 句柄 (实例栈空间地址稳定一些)
- 直接 (效率好一些)
直接内存
元空间使用本地直接内存
概述
-
不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
-
直接内存是在Java堆外的、直接向系统申请的内存区间。
-
来源于NIO,通过存在堆中的DirectByteBuffer操作Native内存通常,访问直接内存的速度会优于Java堆。即读写性能高。
- 因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存。、
- Java的NIo库允许Java程序使用直接内存,用于数据缓冲区
-
可能导致outofMemoryError异常
-
由于直接内存在Java堆外,因此它的大小不会直接受限于-Xmx指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。
-
缺点
- 分配回收成本较高
- 不受JVM内存回收管理
-
直接内存大小可以通过MaxDirectMemorySize设置如果不指定,默认与堆的最大值-xmx参数值一致
Unsafe类
通过Unsafe类可以分配内存,可以释放内存;
类中提供的3个本地方法allocateMemory、reallocateMemory、freeMemory分别用于分配内存,扩充内存和释放内存,与C语言中的3个方法对应。
可以定位对象某字段的内存位置,也可以修改对象的字段值,即使它是私有的;
public native long allocateMemory(long l);
public native long reallocateMemory(long l, long l1);
public native void freeMemory(long l);
简单理解
java process memory = java heap + native memory
java进程内存 = 堆内存 + 本地内存