目录
堆
定义
- 通过new关键字,创建的对象的存储区域
- 是虚拟机所管理的内存中最大的一块区域,目的是存放对象实例。
特点
- 是线程共享的一块区域,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
- 既可以被实现成固定大小,又可以是扩展的,主流通过-Xmx 及-Xms(-Xms 堆内存的初始大小,默认为物理内存的1/64,-Xmx 堆内存的最大大小,默认为物理内存的1/4
)进行配置。
方法区
定义
- 与Java堆一样,是各个线程共享的内存区域,用于存储已经被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据。
-
方法去逻辑上是堆的组成部分,具体的实现不同,HotSpot虚拟机1.8以前,其具体实现永久代在JVM内存中,1.8及1.8以后,其实现元空间在本地内存(操作系统中)
特点
- 方法区在虚拟启动时被创建
- 方法区逻辑上是堆的组成部分,但是不同版本的JVM对齐实现不同,比如永久代(jdk8之前的实现,使用的堆内存)和元空间(jdk8的实现,使用的是操作系统内存)
- 方法区无法满足新的内存分配需求时,将抛出OOM异常(动态生成JSP文件,不断新建字符串等情况会触发以上情况)
运行时常量池
定义
运行时常量池(Runtime Constant Pool)是方法区的一部分,Class文件中除了有类的版本、字节、方法、接口等描述信息外,还有一项是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池。并把里面的符号地址变为真实地址。
- 1.6中运行时常量池表放在永久代中
- 1.8中运行时常量池被移入了堆内存中
先来了解二进制字节码下的常量池
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
对以上文件执行 javap -v HelloWorld.class后得到以下字节码指令
Classfile /Users/lizhijian/IdeaProjects/Demo/out/production/Demo/jvm/HelloWorld.class
Last modified 2021-6-16; size 541 bytes
MD5 checksum 2857962a7a60aa95f2f6642d6b02afad
Compiled from "HelloWorld.java"
public class jvm.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
// (2)获取#21 #22指向的信息
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // hello world (6)指向hello world
#4 = Methodref #24.#25 // (9) java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // jvm/HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Ljvm/HelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System (3)
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream; (4)
#23 = Utf8 hello world //(7)
#24 = Class #31 // (10)java/io/PrintStream
#25 = NameAndType #32:#33 // (11)println:(Ljava/lang/String;)V
#26 = Utf8 jvm/HelloWorld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public jvm.HelloWorld();
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 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ljvm/HelloWorld;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
// (1)获取常量池中编号为2的常量
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
// (5)将#3 指向的常量加载到操作数栈
3: ldc #3 // String hello world
5: invokevirtual #4 // (8)Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 11: 0
line 12: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"
可以看到Constant pool 下面的部分是常量池, 就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。(1)~(11)即为加载常量池数据的过程。
运行时常量池:当类被加载时,类的常量池信息就会放入运行时常量池中,并且把里面的符号地址(#1,#2)换为真实地址。
StringTable(字符串常量池)
针对下面代码进行反编译
public class Demo1_22 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
}
}
编译后字节码:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=1
0: ldc #2 // String a 从常量池#2号位置加载a
2: astore_1 // 存入到LocalVariableTable变量表1号槽位
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: return
LineNumberTable:
line 12: 0
line 13: 3
line 14: 6
line 15: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
3 7 1 s1 Ljava/lang/String;
6 4 2 s2 Ljava/lang/String;
9 1 3 s3 Ljava/lang/String;
常量池中的信息,都被加载到运行时常量池中,这时a b ab都是符号还没有变成字符串对象,要等到具体执行到String s1 = "a"这句话的时候,才去把a符号变为“a”字符串。过程如下:
ldc #2 会把a符号变为”a“字符串(懒加载)。并在准好的的stringTable[ ] 中查找是否有相同的字符串,如果没有则放入串池。
字符串变量拼接
再来看一个demo
//StringTable["a", "b", "ab"]
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; //new StringBuilder().append(s1).append(s2).toString();
System.out.println(s3 == s4);
}
编译后字节码:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=5, args_size=1
0: ldc #2 // String a
// 将a存入局部变量表
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
// 创建StringBuilder对象
9: new #5 // class java/lang/StringBuilder
12: dup
// 调用StringBuilder的构造方法
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
// 加载局部变量表1号位置元素
16: aload_1
// 调用局部变量表中的append方法
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
// 调用局部变量表中的append方法
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
// 调用局部变量表中的toString方法
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
// 将toString结果存入4号局部变量表中
27: astore 4
29: return
LineNumberTable:
line 12: 0
line 13: 3
line 14: 6
line 15: 9
line 16: 29
LocalVariableTable:
Start Length Slot Name Signature
0 30 0 args [Ljava/lang/String;
3 27 1 s1 Ljava/lang/String;
6 24 2 s2 Ljava/lang/String;
9 21 3 s3 Ljava/lang/String;
29 1 4 s4 Ljava/lang/String;
可以看到, String s4 = s1 + s2;<==> new StringBuilder().append(s1).append(s2).toString();
而toString()的源码是
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
在堆中新建一个字符串,综上所述System.out.println(s3 == s4); 输出false.
字符串编译器优化
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
String s5 = "a" +"b";
因String s3 = “ab” 已经在串池中创建(s3 先去常量池加载,加载完以后将“ab”添加到StringTable[ ]),jvm认为“a”和“b”都是常量,不会改变,因此在编译期做出优化,使String s5 = "a" + "b",直接在串池中找到“ab”字符串,并没有重新创建对象。即System.out.println(s3 == s5) <==> true
延迟实例化
public static void main(String[] args) {
System.out.println("1");
System.out.println("2");
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
System.out.println("8");
System.out.println("9");
System.out.println("1");
System.out.println("2");
System.out.println("3");
}
从上图中可以看到,随着代码的执行,StringTable中对应的字符串个数一直在增加,直到System.out.println("9")执行完。当第二次执行System.out.println("1");时数量没有再增加,因为“1”~“9”已经在串池中生成过。
intern()方法
概念
intern()作用是将这个字符串对象放入串池,如果有则不会放入,如果没有则放入串池,同时会把串池中的对象返回(这里需要注意,不管串池中之前存不存在,都会把串池对象返回)
- 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串 池中的对象返回
- 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份, 放入串池, 会把串池中的对象返回
1.8下下看几个🌰:
- demo1
public static void main(String[] args) {
// new String("a")中 "a"是常量被放入串池 "b"是常量被放入串池
// new String("a")又在堆中生成了新的对象
String s1 = new String("a") + new String("b"); // 这里因为是动态拼接的,”ab“并不放入串池中
String s2 = new String("a") + new String("b");
System.out.println(s1 == "ab"); //false
System.out.println(s2.intern() == "ab"); // true
}
执行到new String("a")时,会在串池中放入”a“,同时在堆中生成新对象,new String("b")同理。但是当执行到String s1 = new String("a") + new String("b");时,因为是动态拼接的,所以”ab“并不会放到串池中,只会在堆中创建对象。因此s1==="ab"输出false,而s2.intern()返回的是串池对象,因此s2.intern()==”ab“输出true。
- demo2
public static void main(String[] args) {
String s = new String("a") + new String("b");// 堆对象
String s2 = s.intern(); // "ab"在串池中不存在,把s对象放入串池,同时把结果赋值给s2
String x = "ab";
System.out.println(s2 == "ab"); // true
System.out.println(s == x); // true
}
--输出
true
true
执行到String s2 = s.intern();时,发现串池中并没有"ab",则在串池中创建”ab“,注意:这里需要理解为把s对象放入到了串池!!!!!!!!,可以看到,s2拿到的是"ab"在串池中的地址,因此输出true,而串池中的"ab"本身就是用的s的地址,因此s==x(ab)输出true。
- demo3
如果是“ab”先创建,则输出如下。
public static void main(String[] args) {
String x = "ab"; // 串池对象
String s = new String("a") + new String("b");// 堆对象
String s2 = s.intern(); // "ab"在串池中已经存在,s.intern()返回串池对象
System.out.println(s2 == x); // true
System.out.println(s == x); // false
}
--输出
true
fasle
可以看到变量s返回的是堆中地址,而s.intern()返回的是串池地址,即s==x 输出false(因为串池中的”ab“最初生成时并没有依赖变量s,所以s和"ab"没有直接联系),s2==x 输出true。
1.6下🌰:
- demo1
字符串先创建
public static void main(String[] args) {
String x = "ab";
String s = new String("a") + new String("b");// 堆对象
String s2 = s.intern();
System.out.println(s2 == x);
System.out.println(s == x);
}
--输出
true
false
”ab“先在串池中创建,s2 = s.intern(),为s2返回的是串池地址,s本身指向堆中(串池已经有"ab"了,即不用依赖s进行再次创建),所以,s2==x 输出true,s==x 输出false。
- demo2
字符串后创建
public static void main(String[] args) {
String s = new String("a") + new String("b");// 堆对象
String s2 = s.intern(); // 1.6中s.intern()拷贝一份放入串池,理解为"ab"的创建不直接依赖s
String x = "ab";
System.out.println(s2 == x);
System.out.println(s == x);
}
-- 输出
true
false
从代码中可以分析出,s2=s.intern(),返回的是串池对象,因此s2==x输出true,s.intern()是拷贝一份“ab”到串池中,即“ab”并不直接依赖s,因此s==x,输出false。
总结:
- 1.6/1.8中s.intern()返回的都是串池对象
- 字符串变量s本身是否指向串池中的“ab”,需要看“ab”的创建是否依赖于s。
直接内存
应用场景
- 常见于 NIO 操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受 JVM 内存回收管理
- 传统文件操作
操作文件代码如下:
static final String FROM = "E:\\xx.mp4";
static final String TO = "E:\\a.mp4";
static final int _1Mb = 1024 * 1024;
public static void main(String[] args) {
io(); // io 用时:1535.586957 1766.963399 1359.240226
directBuffer(); // directBuffer 用时:479.295165 702.291454 562.56592
}
private static void directBuffer() {
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel();
) {
ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
while (true) {
int len = from.read(bb);
if (len == -1) {
break;
}
bb.flip();
to.write(bb);
bb.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
}
private static void io() {
long start = System.nanoTime();
try (FileInputStream from = new FileInputStream(FROM);
FileOutputStream to = new FileOutputStream(TO);
) {
byte[] buf = new byte[_1Mb];
while (true) {
int len = from.read(buf);
if (len == -1) {
break;
}
to.write(buf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("io 用时:" + (end - start) / 1000_000.0);
}
--输出
io 用时:2921.12
directBuffer 用时:892.55
可以看到,利用直接内存的方式性能上要大幅度高于传统IO方式
原理分析
- 传统文件操作
传统文件操作会先从磁盘中将数据加载到系统内存,再将数据从系统内存加载到堆内存的缓冲区中,经历两步操作。
- 利用直接内存
如上图所示,利用直接内存,在磁盘中开辟了一段空间使系统内存和Java堆内存都能直接访问,进而大幅提高了文件的读写性能。
参考资料:《深入理解Java虚拟机》