java虚拟方法_Java虚拟机——方法区

🌳方法区(Method Area)并不是所谓的存储方法的区域,而是供各线程共享的运行时内存区域。它存储了已被虚拟机加载的类信息、方法信息、字段信息、常量(final修饰)、静态变量、即时编译器编译后的代码缓存等。

方法区也是一种规范,在不同虚拟机里头实现是不一样的,最典型的实现就是HotSpot虚拟机Java8之前的永久代(PermGen space)和Java8的元空间(Metaspace)。

1. 设置方法区大小

方法区的大小决定了系统可以加载多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机则会抛出java.lang.OutOfMemoryError: PermGen space(Java 7)或者java.lang.OutOfMemoryError: Metaspace(Java 8)内存溢出错误。

以Java8版本为例,我们可以使用-XX:MetaspaceSize=size设置元空间初始大小,-XX:MaxMetaspaceSize=size设置元空间最大值。默认情况下,在windows平台上,-XX:MetaspaceSize值为21M,-XX:MaxMetaspaceSize值为-1,即没有限制,所以极端情况下如果不断地加载类,虚拟机会耗尽所有可用的系统内存。

下面举个元空间OOM的例子:

import com.sun.org.apache.bcel.internal.util.ClassLoader;

import com.sun.xml.internal.ws.org.objectweb.asm.ClassWriter;

import jdk.internal.org.objectweb.asm.Opcodes;

public class Test extends ClassLoader {

public static void main(String[] args) {

int count = 0;

try {

Test test = new Test();

for (int i = 0; i < 10000; i++) {

String className = "Class" + i;

// 创建ClassWriter对象,用于生成类的二进制字节码

ClassWriter classWriter = new ClassWriter(0);

// 指定版本号、修饰符、类名、包名、父类和接口

classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, className, null, "java/lang/Object", null);

byte[] bytes = classWriter.toByteArray();

// 加载类

test.defineClass(className, bytes, 0, bytes.length);

count++;

}

} finally {

System.out.println(count);

}

}

}

上面例子中,我们尝试加载10000个类,通过参数-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m将元空间大小设置为固定大小10M,运行上面的程序控制台输出:

b1d158cfd15eab2a6d31534d1fcb9933.png

2. 方法区、堆、栈关系

方法区和堆、栈的关系如下图所示:

public class Bird {

public static void main(String[] args) {

Bird bird = new Bird();

}

}

0f8cb1380fea9fad4daf5e132d35d241.png

3. 方法区内部结构

方法区内部主要存储了以下内容(不同JDK版本内容有所不同,具体参考下面“方法区演进”):

3.1 类型信息

对每个加载的类型(类class、接口 interface、枚举enum、注解 annotation),JVM必须在方法区中存储以下类型信息:

是类还是接口;

这个类的全限定名(包名.类名);

这个类型直接父类的全限定名(interface和java.lang.Object没有父类);

这个类的修饰符(public,abstract,final);

这个类型直接接口的一个有序列表(一个类可以实现多个接口)。

3.2 方法信息

方法信息包含了这个类的所有方法信息(包括构造器),这些信息和其声明顺序一致:

方法名称;

方法的返回值类型(没有返回值则是void);

方法参数的数量和类型(有序);

方法的修饰符(public,private,protected,static,final,synchronized,native,abstract);

方法的字节码、操作数栈、局部变量表及其大小(abstract和native方法除外);

异常表(abstract和native方法除外)。

3.3 域信息

域Field我们也常称为属性,字段。域信息包含:

域的声明顺序;

域的相关信息,包括名称、类型、修饰符(public,private,protected,static,final,volatile,transient)。

3.4 JIT代码缓存

3.5 运行时常量池

🌳运行时常量池(Runtime Constant Pool),它是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到常量池中。

运行时常量是相对于常量来说的,它具备一个重要特征是:动态性。当然,值相同的动态常量与我们通常说的常量只是来源不同,但是都是储存在池内同一块内存区域。Java语言并不要求常量一定只能在编译期产生,运行期间也可能产生新的常量,这些常量被放在运行时常量池中。

我们知道类字节码反编译后,会有一个constant pool的结构,俗称为常量池,所有的变量和方法引用都作为符号引用保存在class文件的常量池里。虚拟机栈的动态链接就是将符号引用(这些符号引用的集合就是常量池)转换为直接引用(符号引用对应的具体信息,这些具体信息的集合就是运行时常量池,存在方法区中)的过程。

常量池包含的内容有:

直接常量:

CONSTANT_INGETER_INFO整型直接常量池public final int CONST_INT=0;

CONSTANT_String_info字符串直接常量池 public final String CONST_STR="CONST_STR";

CONSTANT_DOUBLE_INFO浮点型直接常量池

等等各种基本数据类型基础常量池

方法名、方法描述符、类名、字段名,字段描述符的符号引用

3.6 静态变量

静态变量就是使用static修饰的域信息。静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分。静态变量也成为类变量,类变量被类的所有实例共享,即使没有类实例时你也可以访问它:

public class Test {

private static String hello = "hello";

private static void hello() {

System.out.println("hello");

}

public static void main(String[] args) {

Test test = null;

test.hello();

System.out.println(test.hello);

}

}

上面程序运行并不会报空指针异常。

通过final修饰的静态变量我们俗称常量。常量在编译的时候就会被分配具体值:

public class Test {

private static String hello = "hello";

private static final String HELLO = "hello";

}

通过javap -v -p Test.class查看其字节码:

14ce07c181a21c7f0c5d080f086c6924.png

通过上面的学习我们知道,静态变量(类变量)在类加载过程的初始化阶段才会被赋值。

3.7 演示方法区内部结构

下面通过字节码内容来查看上面这些信息,现有如下代码:

public class Test extends Object implements Cloneable, Serializable {

private static String hello = "hello";

private static final String HELLO = "hello";

public int a = 0;

public void method1() {

System.out.println("method1");

}

public static String method2(String name) {

try {

int a = 1;

int b = a / 0;

} catch (Exception e) {

e.printStackTrace();

}

return name;

}

}

通过javap -v -p Test.class查看其字节码:

Classfile /Users/mrbird/idea workspace/JVM-Learn/target/classes/cc/mrbird/jvm/learn/Test.class

Last modified 2019-4-01; size 1016 bytes

MD5 checksum ab0309674b0f0b5fbd0766af035efe0a

Compiled from "Test.java"

// 类型信息

public class cc.mrbird.jvm.learn.Test implements java.lang.Cloneable,java.io.Serializable

minor version: 0

major version: 52

// 类的修饰符

flags: ACC_PUBLIC, ACC_SUPER

// 常量池

Constant pool:

#1 = Methodref #11.#38 // java/lang/Object."":()V

#2 = Fieldref #10.#39 // cc/mrbird/jvm/learn/Test.a:I

#3 = Fieldref #40.#41 // java/lang/System.out:Ljava/io/PrintStream;

#4 = String #27 // method1

#5 = Methodref #42.#43 // java/io/PrintStream.println:(Ljava/lang/String;)V

#6 = Class #44 // java/lang/Exception

#7 = Methodref #6.#45 // java/lang/Exception.printStackTrace:()V

#8 = String #14 // hello

#9 = Fieldref #10.#46 // cc/mrbird/jvm/learn/Test.hello:Ljava/lang/String;

#10 = Class #47 // cc/mrbird/jvm/learn/Test

#11 = Class #48 // java/lang/Object

#12 = Class #49 // java/lang/Cloneable

#13 = Class #50 // java/io/Serializable

#14 = Utf8 hello

#15 = Utf8 Ljava/lang/String;

#16 = Utf8 HELLO

#17 = Utf8 ConstantValue

#18 = Utf8 a

#19 = Utf8 I

#20 = Utf8

#21 = Utf8 ()V

#22 = Utf8 Code

#23 = Utf8 LineNumberTable

#24 = Utf8 LocalVariableTable

#25 = Utf8 this

#26 = Utf8 Lcc/mrbird/jvm/learn/Test;

#27 = Utf8 method1

#28 = Utf8 method2

#29 = Utf8 (Ljava/lang/String;)Ljava/lang/String;

#30 = Utf8 e

#31 = Utf8 Ljava/lang/Exception;

#32 = Utf8 name

#33 = Utf8 StackMapTable

#34 = Class #44 // java/lang/Exception

#35 = Utf8

#36 = Utf8 SourceFile

#37 = Utf8 Test.java

#38 = NameAndType #20:#21 // "":()V

#39 = NameAndType #18:#19 // a:I

#40 = Class #51 // java/lang/System

#41 = NameAndType #52:#53 // out:Ljava/io/PrintStream;

#42 = Class #54 // java/io/PrintStream

#43 = NameAndType #55:#56 // println:(Ljava/lang/String;)V

#44 = Utf8 java/lang/Exception

#45 = NameAndType #57:#21 // printStackTrace:()V

#46 = NameAndType #14:#15 // hello:Ljava/lang/String;

#47 = Utf8 cc/mrbird/jvm/learn/Test

#48 = Utf8 java/lang/Object

#49 = Utf8 java/lang/Cloneable

#50 = Utf8 java/io/Serializable

#51 = Utf8 java/lang/System

#52 = Utf8 out

#53 = Utf8 Ljava/io/PrintStream;

#54 = Utf8 java/io/PrintStream

#55 = Utf8 println

#56 = Utf8 (Ljava/lang/String;)V

#57 = Utf8 printStackTrace

{

// 域信息

private static java.lang.String hello;

descriptor: Ljava/lang/String;

flags: ACC_PRIVATE, ACC_STATIC

// 域信息

private static final java.lang.String HELLO;

descriptor: Ljava/lang/String;

flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL

ConstantValue: String hello

// 域信息

public int a;

descriptor: I

flags: ACC_PUBLIC

// 方法信息

public cc.mrbird.jvm.learn.Test();

descriptor: ()V

flags: ACC_PUBLIC

Code:

stack=2, locals=1, args_size=1

0: aload_0

1: invokespecial #1 // Method java/lang/Object."":()V

4: aload_0

5: iconst_0

6: putfield #2 // Field a:I

9: return

LineNumberTable:

line 5: 0

line 9: 4

LocalVariableTable:

Start Length Slot Name Signature

0 10 0 this Lcc/mrbird/jvm/learn/Test;

// 方法信息

public void method1();

descriptor: ()V

flags: ACC_PUBLIC

Code:

// 操作数栈大小,局部变量表大小,参数个数

stack=2, locals=1, args_size=1

0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;

3: ldc #4 // String method1

5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

8: return

LineNumberTable:

line 12: 0

line 13: 8

LocalVariableTable:

Start Length Slot Name Signature

0 9 0 this Lcc/mrbird/jvm/learn/Test;

// 方法信息

public static java.lang.String method2(java.lang.String);

descriptor: (Ljava/lang/String;)Ljava/lang/String;

flags: ACC_PUBLIC, ACC_STATIC

Code:

stack=2, locals=3, args_size=1

0: iconst_1

1: istore_1

2: iload_1

3: iconst_0

4: idiv

5: istore_2

6: goto 14

9: astore_1

10: aload_1

11: invokevirtual #7 // Method java/lang/Exception.printStackTrace:()V

14: aload_0

15: areturn

// 异常表

Exception table:

from to target type

0 6 9 Class java/lang/Exception

LineNumberTable:

line 17: 0

line 18: 2

line 21: 6

line 19: 9

line 20: 10

line 22: 14

// 局部变量表

LocalVariableTable:

Start Length Slot Name Signature

2 4 1 a I

10 4 1 e Ljava/lang/Exception;

0 16 0 name Ljava/lang/String;

StackMapTable: number_of_entries = 2

frame_type = 73 /* same_locals_1_stack_item */

stack = [ class java/lang/Exception ]

frame_type = 4 /* same */

static {};

descriptor: ()V

flags: ACC_STATIC

Code:

stack=1, locals=0, args_size=0

0: ldc #8 // String hello

2: putstatic #9 // Field hello:Ljava/lang/String;

5: return

LineNumberTable:

line 7: 0

}

SourceFile: "Test.java"

4. 方法区的演进

随着JDK的迭代升级,Hotspot中方法区的存储的内容发生了如下变化(上面介绍的方法区的内部结构是经典情况下的,具体还是需要看JDK是什么版本):

版本

描述

jdk1.6及之前

有永久代(permanent generation),静态变量存放在永久代上

jdk1.7

有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中

jdk1.8及之后

无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍然保存在堆中

上面说的静态变量在JDK1.6之前存放在永久代,JDK1.7后移动到堆空间指的是变量本身,变量对应的对象实例一直都是在堆空间分配的。举个例子:

public class StaticObjTest {

static class Test {

static ObjectHolder staticObj = new ObjectHolder();

ObjectHolder instanceObj = new ObjectHolder();

void foo() {

ObjectHolder localObj = new ObjectHolder();

}

}

private static class ObjectHolder {

}

}

这个例子中,三个new ObjectHolder()的创建,都是在堆中分配的,localObj是方法foo内的局部变量,存放在虚拟机栈的局部变量表中;instanceObj为成员变量,随着对象实例的创建也分配在堆中;静态变量staticObj根据JDK版本的不同存放位置也不同,JDK1.6及之前,存放在永久代中,JDK1.7及之后存放到堆中。

永久代为什么会被元空间替代?因为永久代的大小是很难确定的,如果一个程序动态加载的类过多就很容易触发永久代的Full GC(Full GC代价大,耗时长,影响程序性能)甚至OOM,程序直接奔溃;而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存,这样元空间就基本不会因为触发Full GC和OOM了。

字符串常量池(StringTable)为什么要放到堆中?因为如果将StringTable放在永久代的话回收效率很低,在Full GC的时候才会触发。而Full GC是老年代的空间不足、永久代不足时才会触发。这就导致 StringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。

5.方法区垃圾回收

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

本文参考:https://mrbird.cc/JVM-Learn.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值