1.JVM学习结构导图
2.内存结构
2.1 程序计数器
- 线程私有
- 不会有内存溢出的情况
-
2.2 虚拟机栈
每个线程需要运行的内存空间,即虚拟机栈
- 虚拟机栈中存入的是栈帧,可以理解为运行的方法,一个虚拟机栈中可以有多个栈帧
- 每个线程(每个时刻)只能有一个活动栈帧 ,对应着当前正在执行的方法,方法执行时入栈,执行完毕时弹出栈
- 垃圾回收不涉及虚拟机栈,因为虚拟机栈是由一个个栈帧组成的,方法执行完毕栈帧出栈,不需要通过垃圾回收去回收内存
栈内存不是分配越大越好,设置的命令为 -Xss size ;物理内存是一定的,栈内存越大,即每个线程需要占用的内存空间就会越大,导致可分配的线程数就会越少。
linux 1024KB
macOS 1024KB
Solaris 1024KB
Windows 取决于虚拟内存大小
方法内的局部变量不会存在线程安全问题,若该变量属于静态变量,则会出现线程安全问题
- 栈内存溢出 Java.lang.stackOverflowError
- 无限递归(栈帧过多)
- 每个栈帧所占用的内存过大直接超过虚拟机栈的最大内存
线程运行诊断:
一些带有native关键字的方法就是需要JAVA去 调用本地的C或者C++方法,因JAVA有时候没法直接去和操作系统底层交互,所以需要用到本地方法
- Object类中的clone wait notify hashCode 等 Unsafe类都是native方法
2.4 堆
2.4.1 定义
- 通过new关键字创建的对象都会被放在堆内存,jvm运行时数据区,占用内存最大的区域
- 被所有的线程共享
- 堆可以分为新生代和老年代,新生代由 To 、From 、Eden 组成
设置的命令 -Xmx -Xms :JVM初始分配的堆内存大小是由-Xms设置的,默认是物理内存的1/64
2.4.2 堆内存检测工具
堆内存溢出:java.lang.OutofMemoryError :java heap space.
jps工具 :查看当前系统中有哪些Java进程
- jmap :查看堆内存的占用情况
- jconsole :图形界面工具,可以连续检测
-
2.5 方法区
2.5.1 定义
方法区在JVM启动时创建,实际的物理内存空间可以不连续,关闭JVM就会释放这个区域
- 方法区存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等
- jdk1.6 版本是由PermGen永久代实现的,且由JVM管理,【java.lang.OutOfMemoryError:PermGen space】而1.8版本之后是由本地内存进行管理,脱离了JVM,由元空间实现(元空间不再使用堆内存,使用的是操作系统内存)【java.lang.OutOfMemoryError:Metaspace】
- 内存中存放类信息、静态变量、常量等数据,属于各个线程共享的区域
2.5.2 方法区的演进
- Jdk 1.6 及之前:有永久代(静态变量存放在永久代上)、字符串常量池(1.6在方法区)
- Jdk 1.7 :有永久代,但已经逐步 “ 去永久代 “,字符串常量池、静态变量移除,保存在堆中
- dk 1.8 及之后: 无永久代,常量池1.8在元空间。但静态变量、字符串常量池仍在堆中
- 为什么元空间要取代永久代:
- 设置空间大小很难确定
- 永久代参数设置过小,在某些场景下,如果动态加载的类过多,容易产生Perm区的OOM,比如某个实际Web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误
- 永久代参数设置过大,导致空间浪费
- 默认情况下,元空间的大小受本地内存限制
- 永久代进行调优很困难:(方法区的垃圾收集主要回收两部分:常量池中废弃的常量和不再使用的类型,而不再使用的类或类的加载器回收比较复杂,full gc 的时间长
- 设置空间大小很难确定
2.5.3 方法区的设置
- 1.8之前:使用 -XX:MaxPermSize=8m 指定永久代内存大小
- 1.8及之后:使用 -XX:MaxMetaspaceSize=8m 指定元空间大小
2.5.4 常量池、运行时常量池、StringTable(串池)
2.5.4.1 常量池
- 常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型、字面量等信息
对于class 文件用javap -v *.class 后得到的信息中,Constant pool即常量池
2.5.4.2 运行时常量池
常量池是*.class文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实的内存地址
- 方法区内常量池之中主要存放的两大类常量:字面量和符号引用,字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
2.5.4.3 StringTable(串池)
- 常量池是.class文件,存放堆中数据的引用地址,而不是真实的对象,运行时常量池是jvm运行时将常量池中数据放入池中,此时引用地址真正的指向对象而不是.class文件;Stringtable是哈希表(不能扩容),它也叫做串池,用来存储字符串,这3个不是同一个东西,我们需要进行区分
- StringTable中存储的并不是String类型的对象,存储的而是指向String对象的索引,真实对象还是存储在堆中
- jdk1.6中,StringTable是放在永久代(方法区)中,jvm进行FullGC才会对常量池进行垃圾回收,影响效率,因此在jdk1.8中将StringTable放在堆中,jvm内存紧张时就会对StringTable进行垃圾回收
StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间: -XX:StringTableSize=桶个数(最少设置为1009以上)
2.5.4.4 其他
StringTable为什么要调整?
- jdk7中将StringTable放到了堆空间中。因为永久代的回收效率很低,在full gc的时候才能触发。而full gc是老年代的空间不足、永久代不足才会触发。
- 这就导致StringTable回收效率不高,而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足,放到堆里,能及时回收内存。
- 常量池中的字符串仅是符号,只有在被用到时才会转化为对象
- 字符串变量拼接的原理是StringBuilder
- 字符串常量拼接的原理是编译器优化
- 可以使用intern方法,主动将串池中还没有的字符串对象放入串池中
- 无论是串池还是堆里面的字符串,都是对象
- jdk1.8 调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中,如果串池中没有该字符串对象,则放入成功.如果有该字符串对象,则放入失败,无论放入是否成功,都会返回串池中的字符串对象,StringTable是放在堆中的
- jdk1.6 调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中,如果串池中没有该字符串对象,会将该字符串对象复制一份,再放入到串池中,如果有该字符串对象,则放入失败,无论放入是否成功,都会返回串池中的字符串对象,此时无论调用intern方法成功与否,串池中的字符串对象和堆内存中的字符串对象都不是同一个对象,StringTable是属于常量池的一部分
2.6 直接内存
文件读写的流程:先将文件从磁盘文件读到系统内存的缓冲区,然后再从系统内存缓冲区拷贝到Java缓冲区,该区域才是JVM可以回收的。
直接内存,是在系统内存和java内存直接有个类似通道的区域,都是可以进行访问的地方,不需要从系统内存复制到java堆内存,提高了效率
直接内存的回收不是通过JVM的垃圾回收来释放的,而是通过unsafe.freeMemory来手动释放
ByteBuffer的实现内部使用了Cleaner(虚引用)来检测ByteBuffer。一旦ByteBuffer被垃圾回收,那么会由ReferenceHandler来调用Cleaner的clean方法调用freeMemory来释放内存