【JVM】- 内存结构
引言
JVM:Java Virtual Machine
- 定义:Java虚拟机,Java二进制字节码的运行环境
- 好处:
- 一次编写,到处运行
- 自动内存管理,垃圾回收的功能
- 数组下标越界检查(会抛异常,不会覆盖到其他代码)
- 多态
- 比较:
内存结构
程序计数器(Program Counter Register)
作用
:记住下一条JVM的机制
特点
:
- 线程私有的
- 不会存在内存溢出
虚拟机栈
定义
:
- 每个线程运行所需要的内存;
- 每个栈由多个栈帧组成,对应每次方法调用所占的内存;
- 每个线程只有一个活动栈帧,对应每次方法调用所占用的内存
垃圾回收只设计堆内存中的无用对象,不涉及栈内存。
栈内存的分配可以通过-Xss
来设置,并不是越大越好。
方法内的局部变量不一定是线程安全的:
- 如果这个局部变量没有逃离方法的作用范围,则他是线程安全的
- 如果局部变量引用了对象,并逃离了方法的作用范围,则需要考虑线程安全
栈内存溢出
:
- 栈帧过多导致栈内存溢出:递归调用
- 栈帧过大导致栈内存溢出
本地方法栈
本地方法使用的内存就是本地方法栈(由C、C++代码写的方法)
堆
通过new关键字创建的对象都会使用堆内存
特点:
- 是线程共享的,堆中的对象都要考虑线程安全问题
- 有垃圾回收机制
前面的
程序计数器
、虚拟机栈
、本地方法栈
都是每个线程独有的,但是堆是线程共享的
可以使用-Xmx
参数来设置堆内存大小
可以使用jconsole
工具来查看堆内存占用情况
方法区
- 共享性:方法区是线程共享的内存区域
- 逻辑部分:方法区是JVM规范中的逻辑概念,具体实现因JVM版本而异
- 存储:和类相关的信息(成员方法、构造器、成员变量)、运行时常量池、静态变量、方法字节码、字段和方法信息
内存溢出:
- 1.8以前导致永久代内存溢出(1.8以前,方法区由永久代实现,位于堆中)
PermGen space
- 1.8之后导致元空间内存溢出(1.8以后,方法区由元空间实现,使用本地内存)
Metaspace
常量池:一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
运行时常量池:常量池是.class
文件中的,当类被加载时,他的常量池信息就会被放入运行时常量池,并把里面的符号地址变为真实地址。
StringTable
常量池中的信息,都会被加载到运行时常量池中。
- 常量池中的字符串只是符号,第一次用到时才会变成对象(懒加载)
- 利用运行时常量池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是StringBuilder(1.8、在堆中)
- 字符串常量拼接的原理是编译器优化(在编译期如果确定字符串,就把它放入常量池中)
- 可以使用intern方法,主动将常量池中还没有的字符串对象放入串池中
- 1.8:将这个字符串对象尝试放入串池,如果有则不会放入;如果没有则放入串池,会把串池中的对象返回
- 1.6:会把这个字符串对象尝试放入串池,如果有则不会放入;如果没有会把此对象复制一份,放入串池,会把串池中的对象返回(调用intern()方法创建的对象和放入串池的对象是两个对象)
public class Main {public static void main(String[] args) {/*StringTable ["a", "b", "ab"](hashtable结构,不能扩容)s1、s2、s3:在常量池中s4:new String("ab")*/String s1 = "a", s2 = "b", s3 = "ab"; // 懒加载:用到了才会把对应的字符串对象放入常量池中String s4 = s1 + s2; // 在堆中System.out.println(s3 == s4); // false/*s5:在编译期间已经确定结果为"ab",所以直接从常量池中取即可*/String s5 = "a" + "b"; // 在常量池中System.out.println(s3 == s5); // true/*x1.intern():将字符串对象尝试放入串池中,如果有则不会放入;如果没有,则放入串池,并返回串池对象*/String x1 = new String("c") + new String("d"); // 变量动态拼接,此时x1在堆中;"c"、"d"会在串池中String x2 = x1.intern();System.out.println("cd" == x1); // trueSystem.out.println("cd" == x2); // true/*String str1 = "cd";【串池】:["cd"]本来str2是在堆中,"c", "d"放入串池:String str2 = new String("c") + new String("d");【串池】:["cd", "c", "d"]使用str2.intern()方法,会将str2尝试放入串池【串池】:["cd", "c", "d"]将"cd"放入串池时,发现串池中已经存在该对象,"cd"对象放入失败*/String str1 = "cd";String str2 = new String("c") + new String("d");str2.intern();System.out.println(str1 == str2); // false}
}
StringTable位置
- JDK1.6:
- 运行时常量池位于永久代中(方法区)
- 字符串常量池位于永久代中(方法区)
- JDK1.8:
- 运行时常量池位于元空间中(方法区)
- 字符串常量池位于堆中(不在方法区里了)
由于永久代的Full GC触发时机是:永久代的空间不足才会触发,就会导致StringTable的回收时机并不会很频繁,但是StringTable又是一个需要被频繁使用的,这样很容易就会导致永久代空间不足。所以JDK8才把String Table存放位置改堆中。
StringTable性能调优
本质:调整HashTable中的桶个数
通过-XX:StringTableSize=200000
调整HashTable中的桶个数
如果系统中字符串常量的个数较多,可以适当的调整HashTable中的桶大小,减小hash冲突。
【案例
】:某平台要存储用户大量的信息,需要存储大量的用户信息,但是用户的地址信息大部分都可能是重复的,如果不加以区分,直接把这么多重复的地址信息全部存入内存,那么会占用大量的堆内存。它的解决方法就是采用字符串的intern()方法,这样就可以去除重复的地址,相同的地址只会在串池中存储一份,这样也能减少字符串对于内存的占用。
【结论
】:如果应用里有大量的重复的字符串,可以考虑使用字符串的intern()方法,将这些字符串全部放入常量池中,这样可以避免这些字符串重复存储。
直接内存(操作系统的内存,不属于java虚拟机)
- 常见于NIO操作,用于数据缓冲区。
- 回收分配成本高,读写性能高。
java程序每次从磁盘文件中读取,都需要先读取到系统内存,再读取到java堆内存,这样会造成很多不必要的浪费。
【改进
】:划出一块内存区域(direct memory),在这块内存区域中,java代码和系统内存都可以直接访问。从磁盘文件中读取后把数据放入直接内存,java代码也可以访问到这个直接内存,这样就会少了一次缓冲区的赋值操作。
- 不受JVM内存回收管理,垃圾回收并不会导致直接内存释放,因为直接内存是操作系统的内存,不属于java虚拟机。
- 使用Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
- ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会有ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存。