发布时间:2024-02-27 10:01
运行时数据区结构图:
从线程共享与否的角度来看运行时数据区的结构:
栈、堆、方法区之间的交互关系
官方文档:
方法区在哪里?
《Java虚拟机贵方》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择区进行垃圾收集或者进行压缩。”但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
所以,方法区看作是一块独立于Java堆的内存空间。
方法区的基本理解
例:启动以下代码,并使用JVisualVM查看方法区加载的类元信息
package com.atguigu.java;
/**
* 测试设置方法区大小参数的默认值
*
* jdk7及以前:
* -XX:PermSize=100m -XX:MaxPermSize=100m
*
* jdk8及以后:
* -XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m
* @author shkstart shkstart@126.com
* @create 2020 12:16
*/
public class MethodAreaDemo {
public static void main(String[] args) {
System.out.println("start...");
// try {
// Thread.sleep(1000000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
System.out.println("end...");
}
}
HotSpot中方法区的演进
例:
package com.atguigu.java;
/**
* 测试设置方法区大小参数的默认值
*
* jdk7及以前:
* -XX:PermSize=100m -XX:MaxPermSize=100m
*
* jdk8及以后:
* -XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m
* @author shkstart shkstart@126.com
* @create 2020 12:16
*/
public class MethodAreaDemo {
public static void main(String[] args) {
System.out.println("start...");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end...");
}
}
使用JDK7运行:
使用JDK8及以上版本运行上边的例子:
举例方法区内存溢出:
《深入理解Java虚拟机》中的例子:
自己实现的例子:
package com.atguigu.java;
import com.sun.xml.internal.ws.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
/**
* jdk6/7中:
* -XX:PermSize=10m -XX:MaxPermSize=10m
*
* jdk8中:
* -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
*
* @author shkstart shkstart@126.com
* @create 2020 22:24
*/
public class OOMTest extends ClassLoader {
public static void main(String[] args) {
int j = 0;
try {
OOMTest test = new OOMTest();
for (int i = 0; i < 10000; i++) {
//创建ClassWriter对象,用于生成类的二进制字节码
ClassWriter classWriter = new ClassWriter(0);
//指明版本号,修饰符,类名,包名,父类,接口
classWriter.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
//返回byte[]
byte[] code = classWriter.toByteArray();
//类的加载
test.defineClass("Class" + i, code, 0, code.length);//Class对象
j++;
}
} finally {
System.out.println(j);
}
}
}
JDK8中返回的结果:设置MetaspaceSize=10m,MaxMetaspaceSize=10m
JDK7返回的结果:设置PermSize=5m,MaxPermSize=5m
如何解决这些OOM?
方法区(Method Area)存储什么?
《深入理解Java虚拟机》书中对方法区(Method Area)存储内容描述如下:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
类型信息
对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:
域(Field)信息
方法(Method)信息
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
例:
package com.atguigu.java;
import java.io.Serializable;
/**
* 测试方法区的内部构成
* @author shkstart shkstart@126.com
* @create 2020 23:39
*/
public class MethodInnerStrucTest extends Object implements Comparable,Serializable {
//属性
public int num = 10;
private static String str = "测试方法的内部结构";
//构造器
//方法
public void test1(){
int count = 20;
System.out.println("count = " + count);
}
public static int test2(int cal){
int result = 0;
try {
int value = 30;
result = value / cal;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
@Override
public int compareTo(String o) {
return 0;
}
}
执行javap命令查看字节码文件,-p参数表示要把权限比较小(private)的字段或方法的信息显示出来,> test.txt表示将结果保存在当前路径的test.txt文件
字节码文件经过类加载器加载到运行时数据区,里边的信息都会加载方法区
下图是构造方法,即使没有显示声明也有默认构造方法,在方法区,构造方法也是方法
agrs_size=1表示参数大小,由于这个方法没有参数,所以只有一个默认参数this,locals=2表示局部变量表长度
代码举例:
non-final的类变量
例:
package com.atguigu.java;
/**
* non-final的类变量
* @author shkstart shkstart@126.com
* @create 2020 20:37
*/
public class MethodAreaTest {
public static void main(String[] args) {
Order order = null;
order.hello(); // 不会发生空指针异常
System.out.println(order.count);
}
}
class Order {
public static int count = 1;
public static final int number = 2;
public static void hello() {
System.out.println("hello!");
}
}
补充说明:全局常量
被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。
prepare阶段会对静态变量进行初始化,如int类型就复制为0,在初始化阶段(
运行时常量池VS常量池
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
如下:
一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池(Constant Pool Table),包括各种字面量和对类型、域和方法的符号引用。
为什么需要常量池?
一个java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池,之前有介绍。
比如:如下的代码:
虽然只有194字节,但是里面却使用了String、System、、PrintStream及Object等结构。这里代码量其实已经很小了。如果代码多,引用到的结构会更多!这里就需要常量池了!
可以说是将一些常用的公共的类型、值做为符号出现,然后根据符号引用,在使用的时候将符号引用转换为直接引用。
下图即为符号引用示例:
小结:
常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。
运行时常量池
字节码文件的常量池经过类加载器子系统加载到方法区就是运行时常量池
package com.atguigu.java1;
/**
* @author shkstart shkstart@126.com
* @create 2020 14:28
*/
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);
}
}
编译后通过javap -v -p MethodAreaDemo.class > test.txt 查看字节码文件
执行流程:
后续的找ppt不全,用文字说明即可
永久代为什么要被元空间替换:
StringTable字符串常量池
StringTable为什么要调整?
jdk7中将StringTble放到了堆空间中。因为永久代的回收效率很低,在full gc的时候才会触发。而full gc是老年代空间不足、永久代不足时才会触发。这就导致StringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放在堆里,能及时回收内存。
静态变量放在哪里?
看下面两段代码:
package com.atguigu.java1;
/**
* 结论:
* 静态引用对应的对象实体始终都存在堆空间
*
* jdk7:
* -Xms200m -Xmx200m -XX:PermSize=300m -XX:MaxPermSize=300m -XX:+PrintGCDetails
* jdk 8:
* -Xms200m -Xmx200m -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails
* @author shkstart shkstart@126.com
* @create 2020 21:20
*/
public class StaticFieldTest {
private static byte[] arr = new byte[1024 * 1024 * 100];//100MB
public static void main(String[] args) {
System.out.println(StaticFieldTest.arr);
// try {
// Thread.sleep(1000000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}
JDK7:static变量指向的对象实体是new出来的,所以存在堆中
JDK8:同样放在老年代
package com.atguigu.java1;
/**
* 《深入理解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();
}
}
有些人认为方法区(如HotSpot虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK 11时期的ZGC收集器就不支持类卸载)。
一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前Sun公司的Bug列表中,层出现过若干个严重的Bug就是由于低版本HotSpot虚拟机堆此区域未完全回收而导致内存泄漏。
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。
常见面试题