JVM内存模型
# 内存结构
- 运行时数据区:
- 线程共享:堆(对象+字符串常量池)
- 线程私有:虚拟机栈+本地方法栈+程序计数器
- 本地内存:
- 元空间(运行时常量池)
- 直接内存
jdk1.7 方法区的实现为永久代,且在运行时数据区中;
jdk1.8之后用本地内存实现元空间;
# 程序计数器
作用:记住下一条jvm指令的执行地址 特点:
- 是线程私有的
- 不存在内存溢出
# 虚拟机栈
每个线程运行时所需要的内存,称为虚拟机栈
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存,都会产生一个新的栈帧
栈帧:参数,局部变量,返回地址
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
**q1:**栈内存越大越好?
栈内存越大,线程数越少,因为物理内存是一定的!
**q2:**方法内局部变量是否线程安全?
如果方法内局部变量没有逃离方法的作用范围是线程安全的,如果是局部变量引用了对象,并逃离方法的作用范围,则需要考虑线程安全!
public static StringBuilder m(){
StringBuilder sb = new StringBuilder();
sb.append(1);
return sb;//对象作为返回值逃离作用范围,不安全
}
2
3
4
5
栈内存溢出:
栈帧过多导致栈内存溢出
栈帧过大导致栈内存溢出
案例:@JsonIgnore
public class Demo {
public static void main(String[] args) throws JsonProcessingException {
Dept d = new Dept();
d.setName("Market");
Emp e1 = new Emp();
e1.setName("zhang");
e1.setDept(d);
d.setEmps(Arrays.asList(e1));
// { name: 'Market', emps: [{ name:'zhang', dept:{ name:'', emps: [ {}]} },] }
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(d));
}
}
class Emp {
private String name;
@JsonIgnore
private Dept dept;
}
class Dept {
private String name;
private List<Emp> emps;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
CPU占用过多:
- 用top定位哪个进程对cpu的占用过高
- ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
- jstack 进程id,可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号
# 本地方法栈
本地方法栈为虚拟机使用到的 Native 方法服务
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError
和 OutOfMemoryError
两种错误。
# 堆
特点:
- 线程共享,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
更改堆内存
VM options: -Xmx 8m
Terminal堆内存诊断:
- jps 工具 查看当前系统中有哪些 java 进程
- jmap 工具 查看堆内存占用情况 jmap - heap 进程id
- jconsole 工具 图形界面的,多功能的监测工具,可以连续监测
TLAB:
为什么要有TLAB?
- 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
- 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
- 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度
什么是TLAB?
- 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
- 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
1、每个线程都有一个TLAB空间
2、当一个线程的TLAB存满时,可以使用公共区域(蓝色)的
# 方法区
方法区是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。
当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
永久代&元空间:
方法区内存溢出:
/**
* 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
* -XX:MaxMetaspaceSize=8m
*/
public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码
public static void main(String[] args) {
int j = 0;
try {
Demo1_8 test = new Demo1_8();
for (int i = 0; i < 10000; i++, j++) {
// ClassWriter 作用是生成类的二进制字节码
ClassWriter cw = new ClassWriter(0);
// 版本号, public, 类名, 包名, 父类, 接口
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
// 返回 byte[]
byte[] code = cw.toByteArray();
// 执行了类的加载
test.defineClass("Class" + i, code, 0, code.length); // Class 对象
}
} finally {
System.out.println(j);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
spring和mybatis中cglib动态代理在运行期间生成大量类,可能导致永久代内存溢出,1.8之后使用元空间是系统内存,相对充裕很多,垃圾回收机制也是元空间自行管理,不会像永久代垃圾回收效率那么低
# 运行时常量池
out目录下的类反编译:javap -v xx.class
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
...
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // hello world
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // cn/itcast/jvm/t5/HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
- 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
# StringTable
例题:
// ldc #2 会把 a 符号变为 "a" 字符串对象
// ldc #3 会把 b 符号变为 "b" 字符串对象
// ldc #4 会把 ab 符号变为 "ab" 字符串对象
String s1 = "a"; // 懒惰的,执行到时才创建
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; //false
// new StringBuilder().append("a").append("b").toString() new String("ab"),在堆中
String s5 = "a" + "b"; //true
// javac 在编译期间的优化,结果已经在编译期确定为ab,在串池中找到了
2
3
4
5
6
7
8
9
10
// ["ab", "a", "b"]
public static void main(String[] args) {
String x = "ab";
String s = new String("a") + new String("b");
// 堆 new String("a") new String("b") new String("ab")
String s2 = s.intern();
// 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
System.out.println( s2 == x);//t
System.out.println( s == x );//f
}
2
3
4
5
6
7
8
9
10
// ["ab", "a", "b"]
public static void main(String[] args) {
String s = new String("a") + new String("b");
// 堆 new String("a") new String("b") new String("ab")
String s2 = s.intern();
// 1.6将堆中对象s拷贝一份放入串池(副本入池),1.8直接放入串池
// 返回串池中的对象
String x = "ab";
System.out.println( s2 == x);//1.6 t 1.8 t
System.out.println( s == x );//1.6 f 1.8 t
}
2
3
4
5
6
7
8
9
10
11
特性:
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是 StringBuilder (1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
- 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
- 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
性能调优:
- 调整 -XX:StringTableSize=桶个数
- 考虑将字符串对象是否入池
# 直接内存
示例代码:
static final String FROM = "E:\\..";
static final String TO = "E:\\..";
static final int _1Mb = 1024 * 1024;
public static void main(String[] args) {
io();
directBuffer();
}
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);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
直接内存为Java堆内存和系统内存共享的一块区域,少了一次缓冲区复制操作
分配与回收:
- 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
- ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存
禁用显示回收对直接内存的释放:
-XX:+DisableExplicitGC 显式的
System.gc(); // 显式的垃圾回收,Full GC
2
# 创建一个对象步骤
判断对象对应的类是否加载、链接、初始化;
- 虚拟机遇到一条new指令,首先去检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化。(即判断类元信息是否存在)。
- 如果该类没有加载,那么在双亲委派模式下,使用当前类加载器以ClassLoader + 包名 + 类名为key进行查找对应的.class文件,如果没有找到文件,则抛出ClassNotFoundException异常,如果找到,则进行类加载,并生成对应的Class对象。
为对象分配内存;
首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象。如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小;
如果内存规整:采用指针碰撞分配内存;
如果垃圾收集器选择的是Serial ,ParNew这种基于压缩算法的,虚拟机采用这种分配方式。一般使用带Compact(整理)过程的收集器时,使用指针碰撞。
如果内存不规整:空闲列表来为对象分配内存;
意思是虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。这种分配方式成为了 “空闲列表(Free List)”
标记清除算法清理过后的堆内存,就会存在很多内存碎片。
初始化分配到的空间;
所有属性设置默认值;
设置对象的对象头;
将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中;
执行init方法进行初始化;
初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量;
# 对象访问定位?
句柄
句柄池中存储的是稳定的句柄地址,对象被移动时只会改变数据指针;
直接指针;
速度快,节省一次指针定位开销;