前语
这次所讲述的是运转时数据区的最终一个部分
从线程共享与否的角度来看
ThreadLocal:怎么确保多个线程在并发环境下的安全性?典型应用便是数据库连接办理,以及会话办理。
栈、堆、办法区的交互关系
下面就触及了目标的拜访定位
Persion persion = new Persion();
- Person:寄存在元空间,也能够说办法区
- person:寄存在 Java 栈的局部变量表中
- new Person():寄存在 Java 堆中
在 person 目标中,有个指针指向办法区中的 Person 类型数据,表明这个 person 目标是用办法区中的 Person 类 new 出来的。
办法区的了解
办法区在哪里?
官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4
。
《Java 虚拟机标准》中清晰阐明:「尽管一切的办法区在逻辑上是归于堆的一部分,但一些简略的完成或许不会挑选去进行废物搜集或许进行紧缩。」但关于 HotSpotJVM 而言,办法区还有一个别名叫做 Non-Heap(非堆),意图便是要和堆分开。
所以,办法区看作是一块独立于 Java 堆的内存空间。
办法区首要寄存的是 Class,而堆中首要寄存的是实例化的目标。
-
办法区(Method Area)与 Java 堆相同,是各个线程共享的内存区域
-
办法区在 JVM 发动的时分被创立,并且它的实际的物理内存空间中和 Java 堆区相同都能够是不连续的
-
办法区的巨细,跟堆空间相同,能够挑选固定巨细或许可扩展
-
办法区的巨细决定了体系能够保存多少个类,假如体系界说了太多的类,导致办法区溢出,虚拟机同样会抛出内存溢出过错:
java.lang.OutofMemoryError:PermGen space
或许
java.lang.OutOfMemoryError:Metaspace
- 加载大量的第三方的 jar 包
- Tomcat 部署的工程过多(30 ~ 50 个)
- 大量动态的生成反射类
-
封闭 JVM 就会开释这个区域的内存
代码示例:
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...");
}
}
上方简略的程序,却加载了 1600 多个类
HotSpot中办法区的演进
在 JDK7 及曾经,习惯上把办法区称为永久代。JDK8 开端,运用元空间代替了永久代。
办法区是 JVM 标准中的一部分,并不是实际的完成,实际的完成是永久代或是元空间,所以能够当作等价。
- JDK 1.8 后,元空间寄存在堆外内存中
实质上,办法区和永久代并不等价。仅是对 Hotspot 而言的。《Java 虚拟机标准》对怎么完成办法区,不做一致要求。例如:BEAJRockit / IBM J9 中不存在永久代的概念。
现在来看,当年运用永久代,不是好的 ideal。导致 Java 程序更简单 OOM(超过 -XX:MaxPermsize 上限)
而到了 JDK8,总算彻底抛弃了永久代的概念,改用与 JRockit、J9 相同在本地内存中完成的元空间(Metaspace)来代替
元空间的实质和永久代相似,都是对 JVM 标准中办法区的完成。不过元空间与永久代最大的差异在于:元空间不在虚拟机设置的内存中,而是运用本地内存。
永久代、元空间二者并不仅仅名字变了,内部结构也调整了。
依据《Java 虚拟机标准》的规定,假如办法区无法满意新的内存分配需求时,将抛出 OOM 反常。
设置办法区巨细与OOM
办法区的巨细不必是固定的,JVM 能够依据应用的需求动态调整。
JDK7及曾经
- 经过
-XX:Permsize
来设置永久代初始分配空间。默许值是 20.75M -
-XX:MaxPermsize
来设定永久代最大可分配空间。32 位机器默许是 64M,64 位机器模式是 82M - 当 JVM 加载的类信息容量超过了这个值,会报反常 OutofMemoryError:PermGen space
JDK8今后
元数据区巨细能够运用参数 -XX:MetaspaceSize
和 -XX:MaxMetaspaceSize
指定
默许值依赖于渠道 Windows 下,-XX:MetaspaceSize
约为 21M,-XX:MaxMetaspaceSize
的值是 -1,即没有束缚。
与永久代不同,假如不指定巨细,默许状况下,虚拟机会耗尽一切的可用体系内存。假如元数据区产生溢出,虚拟机相同会抛出反常 OutOfMemoryError:Metaspace
。
-XX:MetaspaceSize
:设置初始的元空间巨细。关于一个 64 位的服务器端 JVM 来说,其默许的 -XX:MetaspaceSize
值为 21MB。这便是初始的高水位线,一旦触及这个水位线,Full GC 将会被触发并卸载没用的类(即这些类对应的类加载器不再存活)然后这个高水位线将会重置。新的高水位线的值取决于 GC 后开释了多少元空间。假如开释的空间缺乏,那么在不超过 MaxMetaspaceSize 时,适当进步该值。假如开释空间过多,则适当下降该值。
假如初始化的高水位线设置过低,上述高水位线调整状况会产生很屡次。经过废物收回器的日志能够观察到 Full GC 屡次调用。为了防止频频地 GC,主张将 -XX:MetaspaceSize
设置为一个相对较高的值。
办法区OOM
举例:
代码:OOMTest 类承继 ClassLoader 类,取得 defineClass() 办法,可自己进行类的加载
/**
* jdk6/7 中:
* -XX:PermSize=10m -XX:MaxPermSize=10m
*
* jdk8 中:
* -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
*
*/
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_8, 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);
}
}
}
不设置元空间的上限
运用默许的 JVM 参数,元空间不设置上限。
输出成果:
10000
设置元空间的上限
JVM 参数
-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
输出成果:
8531
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at com.youngkbt.java.OOMTest.main(OOMTest.java:29)
怎么处理这些OOM
-
要处理 OOM 反常或 heap space 的反常,一般的手法是首要经过内存映像剖析东西(如 Ec1ipse Memory Analyzer)对 dump 出来的堆转储快照进行剖析,重点是确认内存中的目标是否是必要的,也便是要先分清楚到底是呈现了内存走漏(Memory Leak)仍是内存溢出(Memory Overflow)
- 内存走漏便是 有大量的引证指向某些目标,可是这些目标今后不会运用了,可是因为它们还和 GC ROOT 有相关,所以导致今后这些目标也不会被收回,这便是内存走漏的问题
-
假如是内存走漏,可进一步经过东西检查走漏目标到 GC Roots 的引证链。于是就能找到走漏目标是经过怎样的路径与 GCRoots 相相关并导致废物搜集器无法自动收回它们的。把握了走漏目标的类型信息,以及 GCRoots 引证链的信息,就能够比较准确地定位出走漏代码的方位
-
假如不存在内存走漏,换句话说便是内存中的目标确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx 与 -Xms),与机器物理内存比照看是否还能够调大,从代码上检查是否存在某些目标生命周期过长、持有状态时刻过长的状况,测验削减程序运转期的内存耗费
办法区的内部结构
《深化了解 Java 虚拟机》书中对办法区(Method Area)存储内容描绘如下:它用于存储已被虚拟机加载的 类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
类型信息
对每个加载的类型(类 class、接口 interface、枚举 enum、注解 annotation),JVM 必须在办法区中存储以下类型信息:
- 这个类型的完好有效称号(全名 = 包名.类名)
- 这个类型直接父类的完好有效名(关于 interface 或是
java.lang.object
,都没有父类) - 这个类型的润饰符(public,abstract,final 的某个子集)
- 这个类型直接接口的一个有序列表
域信息
JVM 必须在办法区中保存类型的一切域的相关信息以及域的声明次序。
域的相关信息包含:域称号、域类型、域润饰符(public,private,protected,static,final,volatile,transient 的某个子集)。
办法(Method)信息
JVM 必须保存一切办法的以下信息,同域信息相同包含声明次序:
- 办法称号
- 办法的回来类型(或 void)
- 办法参数的数量和类型(按次序)
- 办法的润饰符(public,private,protected,static,final,synchronized,native,abstract 的一个子集)
- 办法的字节码(bytecodes)、操作数栈、局部变量表及巨细(abstract 和 native 办法在外)
- 反常表(abstract 和 native 办法在外)
每个反常处理的开端方位、完毕方位、代码处理在程序计数器中的偏移地址、被捕获的反常类的常量池索引。
内部结构代码举例
/**
* 测验办法区的内部构成
*/
public class MethodInnerStrucTest extends Object implements Comparable<String>,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;
}
}
反编译字节码文件,并输出值文本文件中,便于检查。参数 -p 确保能检查 private 权限类型的字段或办法。
指令:javap -v -p MethodInnerStrucTest.class > test.txt
Classfile /F:/IDEAWorkSpaceSourceCode/JVMDemo/out/production/chapter09/com/youngkbt/java/MethodInnerStrucTest.class
Last modified 2020-11-13; size 1626 bytes
MD5 checksum 0d0fcb54854d4ce183063df985141ad0
Compiled from "MethodInnerStrucTest.java"
// 类型信息
public class com.youngkbt.java.MethodInnerStrucTest extends java.lang.Object implements java.lang.Comparable<java.lang.String>, java.io.Serializable
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #18.#52 // java/lang/Object."<init>":()V
#2 = Fieldref #17.#53 // com/youngkbt/java/MethodInnerStrucTest.num:I
#3 = Fieldref #54.#55 // java/lang/System.out:Ljava/io/PrintStream;
#4 = Class #56 // java/lang/StringBuilder
#5 = Methodref #4.#52 // java/lang/StringBuilder."<init>":()V
#6 = String #57 // count =
#7 = Methodref #4.#58 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#8 = Methodref #4.#59 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
#9 = Methodref #4.#60 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#10 = Methodref #61.#62 // java/io/PrintStream.println:(Ljava/lang/String;)V
#11 = Class #63 // java/lang/Exception
#12 = Methodref #11.#64 // java/lang/Exception.printStackTrace:()V
#13 = Class #65 // java/lang/String
#14 = Methodref #17.#66 // com/youngkbt/java/MethodInnerStrucTest.compareTo:(Ljava/lang/String;)I
#15 = String #67 // 测验办法的内部结构
#16 = Fieldref #17.#68 // com/youngkbt/java/MethodInnerStrucTest.str:Ljava/lang/String;
#17 = Class #69 // com/youngkbt/java/MethodInnerStrucTest
#18 = Class #70 // java/lang/Object
#19 = Class #71 // java/lang/Comparable
#20 = Class #72 // java/io/Serializable
#21 = Utf8 num
#22 = Utf8 I
#23 = Utf8 str
#24 = Utf8 Ljava/lang/String;
#25 = Utf8 <init>
#26 = Utf8 ()V
#27 = Utf8 Code
#28 = Utf8 LineNumberTable
#29 = Utf8 LocalVariableTable
#30 = Utf8 this
#31 = Utf8 Lcom/youngkbt/java/MethodInnerStrucTest;
#32 = Utf8 test1
#33 = Utf8 count
#34 = Utf8 test2
#35 = Utf8 (I)I
#36 = Utf8 value
#37 = Utf8 e
#38 = Utf8 Ljava/lang/Exception;
#39 = Utf8 cal
#40 = Utf8 result
#41 = Utf8 StackMapTable
#42 = Class #63 // java/lang/Exception
#43 = Utf8 compareTo
#44 = Utf8 (Ljava/lang/String;)I
#45 = Utf8 o
#46 = Utf8 (Ljava/lang/Object;)I
#47 = Utf8 <clinit>
#48 = Utf8 Signature
#49 = Utf8 Ljava/lang/Object;Ljava/lang/Comparable<Ljava/lang/String;>;Ljava/io/Serializable;
#50 = Utf8 SourceFile
#51 = Utf8 MethodInnerStrucTest.java
#52 = NameAndType #25:#26 // "<init>":()V
#53 = NameAndType #21:#22 // num:I
#54 = Class #73 // java/lang/System
#55 = NameAndType #74:#75 // out:Ljava/io/PrintStream;
#56 = Utf8 java/lang/StringBuilder
#57 = Utf8 count =
#58 = NameAndType #76:#77 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#59 = NameAndType #76:#78 // append:(I)Ljava/lang/StringBuilder;
#60 = NameAndType #79:#80 // toString:()Ljava/lang/String;
#61 = Class #81 // java/io/PrintStream
#62 = NameAndType #82:#83 // println:(Ljava/lang/String;)V
#63 = Utf8 java/lang/Exception
#64 = NameAndType #84:#26 // printStackTrace:()V
#65 = Utf8 java/lang/String
#66 = NameAndType #43:#44 // compareTo:(Ljava/lang/String;)I
#67 = Utf8 测验办法的内部结构
#68 = NameAndType #23:#24 // str:Ljava/lang/String;
#69 = Utf8 com/youngkbt/java/MethodInnerStrucTest
#70 = Utf8 java/lang/Object
#71 = Utf8 java/lang/Comparable
#72 = Utf8 java/io/Serializable
#73 = Utf8 java/lang/System
#74 = Utf8 out
#75 = Utf8 Ljava/io/PrintStream;
#76 = Utf8 append
#77 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#78 = Utf8 (I)Ljava/lang/StringBuilder;
#79 = Utf8 toString
#80 = Utf8 ()Ljava/lang/String;
#81 = Utf8 java/io/PrintStream
#82 = Utf8 println
#83 = Utf8 (Ljava/lang/String;)V
#84 = Utf8 printStackTrace
{
// 域信息
public int num;
descriptor: I
flags: ACC_PUBLIC
private static java.lang.String str;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE, ACC_STATIC
// 办法信息
public com.youngkbt.java.MethodInnerStrucTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 10
7: putfield #2 // Field num:I
10: return
LineNumberTable:
line 10: 0
line 12: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/youngkbt/java/MethodInnerStrucTest;
public void test1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=2, args_size=1
0: bipush 20
2: istore_1
3: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
6: new #4 // class java/lang/StringBuilder
9: dup
10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
13: ldc #6 // String count =
15: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
18: iload_1
19: invokevirtual #8 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
22: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
28: return
LineNumberTable:
line 17: 0
line 18: 3
line 19: 28
LocalVariableTable:
Start Length Slot Name Signature
0 29 0 this Lcom/youngkbt/java/MethodInnerStrucTest;
3 26 1 count I
public static int test2(int);
descriptor: (I)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 30
4: istore_2
5: iload_2
6: iload_0
7: idiv
8: istore_1
9: goto 17
12: astore_2
13: aload_2
14: invokevirtual #12 // Method java/lang/Exception.printStackTrace:()V
17: iload_1
18: ireturn
// 反常表
Exception table:
from to target type
2 9 12 Class java/lang/Exception
LineNumberTable:
line 21: 0
line 23: 2
line 24: 5
line 27: 9
line 25: 12
line 26: 13
line 28: 17
LocalVariableTable:
Start Length Slot Name Signature
5 4 2 value I
13 4 2 e Ljava/lang/Exception;
0 19 0 cal I
2 17 1 result I
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 12
locals = [ int, int ]
stack = [ class java/lang/Exception ]
frame_type = 4 /* same */
public int compareTo(java.lang.String);
descriptor: (Ljava/lang/String;)I
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=2
0: iconst_0
1: ireturn
LineNumberTable:
line 33: 0
LocalVariableTable:
Start Length Slot Name Signature
0 2 0 this Lcom/youngkbt/java/MethodInnerStrucTest;
0 2 1 o Ljava/lang/String;
public int compareTo(java.lang.Object);
descriptor: (Ljava/lang/Object;)I
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: checkcast #13 // class java/lang/String
5: invokevirtual #14 // Method compareTo:(Ljava/lang/String;)I
8: ireturn
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/youngkbt/java/MethodInnerStrucTest;
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #15 // String 测验办法的内部结构
2: putstatic #16 // Field str:Ljava/lang/String;
5: return
LineNumberTable:
line 13: 0
}
Signature: #49 // Ljava/lang/Object;Ljava/lang/Comparable<Ljava/lang/String;>;Ljava/io/Serializable;
SourceFile: "MethodInnerStrucTest.java"
non-final的类变量
静态变量(static 润饰的变量)和类相关在一起,跟着类的加载而加载,他们成为类数据在逻辑上的一部分。
类变量被类的一切实例共享,即便没有类实例时,你也能够拜访它。
/**
* non-final 的类变量
*/
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!");
}
}
如上代码所示,即便咱们把 order 设置为 null,也不会呈现空指针反常。
输出成果:
hello!
1
大局常量
大局常量便是运用 static final 进行润饰。
被声明为 final 的类变量的处理办法则不同,每个大局常量在编译的时分就会被分配了。
class Order {
public static int count = 1;
public static final int number = 2;
// ......
}
检查上面代码,这部分的字节码指令
public static int count;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
public static final int number;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 2 // 阐明编译的时分就现已赋值为 2
能够发现 staitc 和 final 一起润饰的 number 的值在编译上的时分现已写死在字节码文件中了。
运转时常量池VS常量池
运转时常量池,便是运转时的常量池。
- 办法区,内部包含了运转时常量池
- 字节码文件,内部包含了常量池
- 要弄清楚办法区,需求了解清楚 ClassFile,因为加载类的信息都在办法区
- 要弄清楚办法区的运转时常量池,需求了解清楚 ClassFile 中的常量池
常量池
常量池也叫静态常量池,呈现在 Class 文件的字节码中。
一个有效的字节码文件中除了包含类的版别信息、字段、办法以及接口等描绘符信息外,还包含一项信息便是常量池表(Constant Pool Table),包含各种字面量和对类型、域和办法的 符号引证。
字面量相似与咱们往常说的常量,首要包含:
- 文本字符串:便是咱们在代码中能够看到的字符串,例如
String a = "aa"
。其间 aa 便是字面量 - 被 final 润饰的变量
为什么需求常量池
一个 Java 源文件中的类、接口,编译后产生一个字节码文件。而 Java 中的字节码需求数据支撑,一般这种数据会很大以至于不能直接存到字节码里,换另一种方式,能够存到常量池,这个字节码包含了指向常量池的引证。在动态链接的时分会用到运转时常量池,之前有介绍。
如下的代码:
public class SimpleClass {
public void sayHello() {
System.out.println("hello");
}
}
尽管上述代码只有 194 字节,可是里边却运用了 String、System、PrintStream 及 Object 等结构。这儿的代码量其实很少了,假如代码多的话,引证的结构将会更多,这儿就需求用到常量池了。
常量池中有什么
- 数量值
- 字符串值
- 类引证
- 字段引证
- 办法引证
例如下面这段代码
public class MethodAreaTest2 {
public static void main(String args[]) {
Object obj = new Object();
}
}
将会被翻译成如下字节码
new #2
dup
invokespecial
#2 等等这些带 # 的,都是引证了常量池。
小结
常量池、能够看做是一张表,虚拟机指令依据这张常量表找到要执行的类名、办法名、参数类型、字面量等类型。
运转时常量池
运转时常量池(Runtime Constant Pool)是办法区的一部分,是由常量池(静态常量池)进入办法区后的称号。
常量池表(Constant Pool Table)是 Class 文件的一部分,用于寄存编译期生成的各种字面量与符号引证,这部分内容将在类加载后寄存到办法区的运转时常量池中。(运转时常量池便是常量池在程序运转时的称号)
运转时常量池,在加载类和接口到虚拟机后,就会创立对应的运转时常量池。
JVM 为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项相同,是经过索引拜访的。
运转时常量池中包含多种不同的常量,包含编译期就现已清晰的数值字面量,也包含到运转期解析后才能够取得的办法或许字段引证。此时不再是常量池中的符号地址了,这儿换为实在地址。
运转时常量池,相关于 Class 文件常量池的另一重要特征是:具有动态性。(或许会多加一些 Class 文件没有的字面量)
运转时常量池相似于传统编程语言中的符号表(symbol table),可是它所包含的数据却比符号表要更加丰富一些。
当创立类或接口的运转时常量池时,假如结构运转时常量池所需的内存空间超过了办法区所能提供的最大值,则 JVM 会抛 OutofMemoryError 反常。
字符串常量池
详细请看 JVM – StringTable字符串常量池。
字符串常量池 是运转时常量池中的一小部分,字符串常量池的方位在 JDK 不同版别下,有一定差异。
JDK1.6 及之前:有永久代,运转时常量池包含字符串常量池。
JDK1.7 有永久代,但现已逐渐「去永久代」,字符串常量池从永久代里的运转时常量池分离到堆里.
JDK1.8 及之后无永久代,运转时常量池在元空间,字符串常量池里依然在堆里。
办法区运用举例
如下代码
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);
}
}
字节码:
public class com.youngkbt.java1.MethodAreaDemo
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#24 // java/lang/Object."<init>":()V
#2 = Fieldref #25.#26 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #27.#28 // java/io/PrintStream.println:(I)V
#4 = Class #29 // com/youngkbt/java1/MethodAreaDemo
#5 = Class #30 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 LocalVariableTable
#11 = Utf8 this
#12 = Utf8 Lcom/youngkbt/java1/MethodAreaDemo;
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 args
#16 = Utf8 [Ljava/lang/String;
#17 = Utf8 x
#18 = Utf8 I
#19 = Utf8 y
#20 = Utf8 a
#21 = Utf8 b
#22 = Utf8 SourceFile
#23 = Utf8 MethodAreaDemo.java
#24 = NameAndType #6:#7 // "<init>":()V
#25 = Class #31 // java/lang/System
#26 = NameAndType #32:#33 // out:Ljava/io/PrintStream;
#27 = Class #34 // java/io/PrintStream
#28 = NameAndType #35:#36 // println:(I)V
#29 = Utf8 com/youngkbt/java1/MethodAreaDemo
#30 = Utf8 java/lang/Object
#31 = Utf8 java/lang/System
#32 = Utf8 out
#33 = Utf8 Ljava/io/PrintStream;
#34 = Utf8 java/io/PrintStream
#35 = Utf8 println
#36 = Utf8 (I)V
{
public com.youngkbt.java1.MethodAreaDemo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/youngkbt/java1/MethodAreaDemo;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=5, args_size=1
0: sipush 500
3: istore_1
4: bipush 100
6: istore_2
7: iload_1
8: iload_2
9: idiv
10: istore_3
11: bipush 50
13: istore 4
15: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
18: iload_3
19: iload 4
21: iadd
22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
25: return
LineNumberTable:
line 9: 0
line 10: 4
line 11: 7
line 12: 11
line 13: 15
line 14: 25
LocalVariableTable:
Start Length Slot Name Signature
0 26 0 args [Ljava/lang/String;
4 22 1 x I
7 19 2 y I
11 15 3 a I
15 11 4 b I
}
SourceFile: "MethodAreaDemo.java"
字节码执行进程展现:
首要现将操作数 500 放入到操作数栈中
然后存储到局部变量表中
然后重复一次,把 100 放入局部变量表中,最终再将变量表中的 500 和 100 取出,进行操作
将 500 和 100 进行一个除法运算,再把成果入栈
在最终便是输出流,需求调用运转时常量池的常量
最终调用 invokevirtual(虚办法调用),然后回来
回来时
程序计数器一直核算的都是当前代码运转的方位,意图是为了便利记载办法调用后能够正常回来,或许是进行了 CPU 切换后,也能回来到本来的代码进行执行。
办法区的演进细节
首要清晰:只有 Hotspot 才有永久代。BEA JRockit、IBMJ9 等来说,是不存在永久代的概念的。原则上怎么完成办法区归于虚拟机完成细节,不受《Java 虚拟机标准》管束,并不要求一致。
Hotspot 中办法区的改动:
版别 | 改动 |
---|---|
JDK1.6 及曾经 | 有永久代,静态变量存储在永久代上 |
JDK1.7 | 有永久代,但现已逐渐「去永久代」,字符串常量池,静态变量移除,保存在堆中 |
JDK1.8 | 无永久代,类型信息,字段,办法,常量保存在本地内存的元空间,但字符串常量池、静态变量仍然在堆中 |
JDK6 的时分
JDK7 的时分
JDK8 的时分,元空间巨细只受物理内存影响
为什么永久代要被元空间代替?
官方文档:openjdk.java.net/jeps/122
JRockit 是和 HotSpot 融合后的成果,因为 JRockit 没有永久代,所以他们不需求装备永久代。
跟着 Java8 的到来,HotSpot VM 中再也见不到永久代了。可是这并不意味着类的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫做元空间(Metaspace)。
因为类的元数据分配在本地内存中,元空间的最大可分配空间便是体系可用内存空间,这项改动是很有必要的,原因有:
- 为永久代设置空间巨细是很难确认的
在某些场景下,假如动态加载类过多,简单产生 Perm 区的 OOM。比如某个实际 Web 工程中,因为功能点比较多,在运转进程中,要不断动态加载许多类,经常呈现丧命过错。
Exception in thread‘dubbo client x.x connector'java.lang.OutOfMemoryError:PermGen space
而元空间和永久代之间最大的差异在于:元空间并不在虚拟机中,而是运用本地内存。
因而,默许状况下,元空间的巨细仅受本地内存束缚。
-
对永久代进行调优是很困难的
首要是为了下降 Full GC
有些人以为办法区(如 HotSpot 虚拟机中的元空间或许永久代)是没有废物搜集行为的,其实不然。《Java 虚拟机标准》对办法区的束缚对错常宽松的,提到过能够不要求虚拟机在办法区中完成废物搜集。事实上也确实有未完成或未能完好完成办法区类型卸载的搜集器存在(如 JDK11 时期的 ZGC 搜集器就不支撑类卸载)。
一般来说这个区域的收回效果比较难令人满意,尤其是类型的卸载,条件适当苛刻。可是这部分区域的收回有时又确实是必要的。曾经 Sun 公司的 Bug 列表中,曾呈现过的若干个严峻的 Bug 便是因为低版别的 HotSpot 虚拟机对此区域未彻底收回而导致内存走漏。
办法区的废物搜集首要收回两部分内容:常量池中抛弃的常量和不在运用的类型。
StringTable为什么要调整方位
JDK7 中将 StringTable 放到了堆空间中。因为永久代的收回功率很低,在 Full GC 的时分才会触发。而 Full GC 是老时代的空间缺乏、永久代缺乏时才会触发。
这就导致 StringTable 收回功率不高。而咱们开发中会有大量的字符串被创立,收回功率低,导致永久代内存缺乏。放到堆里,能及时收回内存。
静态变量寄存在哪里?
静态引证对应的目标实体一直都存在堆空间。如代码所示:
/**
* 定论:
* 1、静态引证对应的目标实体(也便是这个 new byte[1024 * 1024 * 100])一直都存在堆空间
* 2、仅仅那个变量(适当于下面的 arr 变量名)在 JDK6,JDK7,JDK8 寄存方位中有所改动
* 3、arr 是目标,new byte[1024 * 1024 * 100] 是目标实体
*
* jdk7:
* -Xms200m -Xmx200m -XX:PermSize=300m -XX:MaxPermSize=300m -XX:+PrintGCDetails
* jdk 8:
* -Xms200m -Xmx200m -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails
*/
public class StaticFieldTest {
private static byte[] arr = new byte[1024 * 1024 * 100]; // 100MB
public static void main(String[] args) {
System.out.println(StaticFieldTest.arr);
}
}
JDK6环境下
JDK7 环境下
JDK8 环境
这个问题需求用 JHSDB 东西来进行剖析,这个东西是 JDK9 开端自带的(JDK9 曾经没有),在 bin 目录下能够找到。
package com.youngkbt.java1;
/**
* 《深化了解 Java虚拟机》中的事例:
* staticObj、instanceObj、localObj 寄存在哪里?
*/
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();
}
}
staticObj 跟着 Test 的类型信息寄存在办法区,instanceObj 跟着 Test 的目标实例寄存在 Java 堆,localObject 则是寄存在 foo() 办法栈帧的局部变量表中。
注意,目标实体、目标实例都是
new Xxx()
,如上方代码的staticObj
是目标,而new ObjectHolder()
是目标实体、目标实例。
测验发现:三个目标的数据在内存中的地址都落在 Eden 区范围内,所以定论:只需是目标实例必定会在 Java 堆中分配。
接着,找到了一个引证该 staticObj 目标的当地,是在一个 java.1ang.Class
的实例里,并且给出了这个实例的地址,经过 Inspector 检查该目标实例,能够清楚看到这确实是一个 java.lang.Class
类型的目标实例,里边有一个名为 staticObj 的实例字段:
从《Java 虚拟机标准》所界说的概念模型来看,一切 Class 相关的信息都应该寄存在办法区之中,但办法区该怎么完成,《Java 虚拟机标准》并未做出规定,这就成了一件答应不同虚拟机自己灵敏把握的事情。JDK7 及其今后版别的 HotSpot 虚拟机挑选把静态变量与类型在 Java 语言一端的映射 Class 目标寄存在一起,存储于 Java 堆之中,从咱们的试验中也清晰验证了这一点。
办法区的废物收回
有些人以为办法区(如 Hotspot 虚拟机中的元空间或许永久代)是没有废物搜集行为的,其实不然。《Java 虚拟机标准》对办法区的束缚对错常宽松的,提到过能够不要求虚拟机在办法区中完成废物搜集。事实上也确实有未完成或未能完好完成办法区类型卸载的搜集器存在(如 JDK11 时期的 ZGC 搜集器就不支撑类卸载)。
一般来说这个区域的收回效果比较难令人满意,尤其是类型的卸载,条件适当苛刻。可是这部分区域的收回有时又确实是必要的。曾经 Sun 公司的 Bug 列表中,曾呈现过的若干个严峻的 Bug 便是因为低版别的 HotSpot 虚拟机对此区域未彻底收回而导致内存走漏。
办法区的废物搜集首要收回两部分内容:常量池中抛弃的常量和不再运用的类型。
先来说说办法区内常量池之中首要寄存的两大类常量:字面量和符号引证。字面量比较挨近 Java 语言层次的常量概念,如文本字符串、被声明为 final 的常量值等。而符号引证则归于编译原理方面的概念,包含下面三类常量:
- 类和接口的全限定名
- 字段的称号和描绘符
- 办法的称号和描绘符
HotSpot 虚拟机对常量池的收回策略是很清晰的,只需常量池中的常量没有被任何当地引证,就能够被收回。
收回抛弃常量与收回 Java 堆中的目标十分相似。(关于常量的收回比较简略,重点是类的收回)
断定一个常量是否「抛弃」仍是相对简略,而要断定一个类型是否归于「不再被运用的类」的条件就比较苛刻了。需求一起满意下面三个条件:
- 该类一切的实例都现已被收回,也便是 Java 堆中不存在该类及其任何派生子类的实例
- 加载该类的类加载器现已被收回,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP 的重加载等,否则一般是很难达成的。
- 该类对应的 java.lang.C1ass 目标没有在任何当地被引证,无法在任何当地经过反射拜访该类的办法
Java 虚拟机被答应对满意上述三个条件的无用类进行收回,这儿说的仅仅是「被答应」,而并不是和目标相同,没有引证了就必定会收回。关于是否要对类型进行收回,HotSpot 虚拟机提供了 -Xnoclassgc
参数进行控制,还能够运用 -verbose:class
以及 -XX:+TraceClass-Loading
、-XX:+TraceClassUnLoading
检查类加载和卸载信息。
在大量运用反射、动态署理、CGLib 等字节码结构,动态生成 JSP 以及 OSGi 这类频频自界说类加载器的场景中,一般都需求 Java 虚拟机具有类型卸载的能力,以确保不会对办法区形成过大的内存压力。
总结
常见面试题
百度
三面:说一下 JVM 内存模型吧,有哪些区?别离干什么的?
蚂蚁金服
Java8 的内存分代改进。
JVM 内存分哪几个区,每个区的效果是什么?
一面:JVM 内存分布/内存结构?栈和堆的差异?堆的结构?为什么两个 survivor 区?
二面:Eden 和 survior 的份额分配
小米
JVM 内存分区,为什么要有新生代和老时代?
字节跳动
二面:Java 的内存分区。
二面:讲讲 JVM 运转时数据库区。
什么时分目标会进入老时代?
京东
JVM 的内存结构,Eden 和 Survivor 份额。
JVM 内存为什么要分红新生代,老时代,耐久代。新生代中为什么要分为 Eden 和 Survivor。
天猫
一面:JVM 内存模型以及分区,需求详细到每个区放什么。
一面:JVM 的内存模型,Java8 做了什么改动?
拼多多
JVM 内存分哪几个区,每个区的效果是什么?
美团
Java 内存分配。
JVM 的永久代中会产生废物收回吗?
一面:JVM 内存分区,为什么要有新生代和老时代?