一. 按照线程共享和线程私对JVM各区域进行划分
首先JVM的运行时数据区被分为五个部分:方法区,堆,虚拟机栈,本地方法栈, 程序计数器。
线程共享的区域:方法区,堆
线程私有的区域:虚拟机栈,本地方法栈,程序计数器
线程私有和共享的区别:当JVM初始运行时都会在方法区和堆中分配好空间,线程可以共享使用;而每次遇到一个线程都会为新线程分配一个虚拟机栈,本地方法栈和程序计数器,这些会随着该线程的销毁而销毁。也就是说线程私有的生命周期和所属线程是一样的;而线程共享的部分和java程序运行的生命周期相同。
正是因为如此,jvm的GC只发生线程共享的区域(大部分虚拟机GC发生在堆上)。
二.JVM内存模型的构成(方法区和堆)
jvm的内存划分如上图所示,分为堆内存和非堆内存。
堆内存分为新生代和老年代。
非堆内存为永久代,jdk1.8之后被元空间所替代,都是方法区的实现,最关键的一点:元空间并不在JVM中,而是使用本地内存。
1.方法区:
JVM启动时创建,物理内存空间和堆一样都是不连续的,可以对方法区的大小进行设置可以是固定的也可以是扩展的,关闭JVM会释放该区域内存。
方法区主要的储存信息:类的信息,域的信息,方法的信息,常量,静态变量,定义为final类型的常量。
a.类信息:类名,父类名,修饰符。
b.域信息:域名称,类型,修饰符,声明顺序
c.方法信息:方法名称,方法返回值类型,方法参数的数量和类型(按顺序),方法的修饰符。
d.常量:有一个常量池的概念不太懂
方法区的演变过程:
jdk1.6之前:有永久代,静态变量存到永久代中
jdk1.7:有永久代,但逐渐开始摆脱永久代,字符串常量池,静态变量移除,保存在堆中
jdk18之后:取消了永久代,将类信息,方法信息,常量等保存到元空间,但是字符串常量池,静态变量仍保留在堆中
(使用元空间替代永久代的原因是:永久代的大小不太好确定,并且对永久代调优不是很方便,处理不好会再出OOM问题。)
方法区的垃圾回收:
主要回收的是 a.常量池里废弃的常量 b.对于不再使用的类要进行卸载。(判定该类不再使用的方法:1.该类的所有相关实例都被回收 2.该类的加载器已经被回收 3.该类的对象没有被任何地方引用。并且通过反射无法访问到该类的任何方法。)
2.堆:
JVM启动时创建,物理内存空间也是不连续的,大小可以进行设置。
堆中主要储存的信息:对象实例。
堆又被划分为了新生代和老年代,大小比例为1:2。
新生代又被划分为Eden,Survivor To,Survivor From三个区域,大小划分为8:1:1,永远保证一个Survivor永远为空,会在垃圾回收里详谈。
堆的工作原理:
a.Eden区为java对象分配内存,当Eden区没有足够的空间时,发生一次Minor GC,将Eden区里存活的对象放入Survivor From区中,并清空Eden区。
b.Eden区清空后,继续为新的java对象分配堆内存。
c.当Eden区再次没有空间时,Eden区和Survivor From区同时进行Minor GC,将这两个区存活的对象放入Survivor To区中。
d.然后把Survivor To和Survivor From两个区交换。
e.jvm给每个对象设置年龄,没经过一次Minor GC,对象年龄增加一岁,当到达年龄阈值(默认15,当然可以通过参数自定义阈值),将对象放到老年代,老年代满了的时候进行Major GC。
三.栈(虚拟机栈和本地方法栈)
1.虚拟机栈:
虚拟机栈是线程私有的,随着线程的销毁而销毁。栈负责的是程序的运行或者说如何处理数据。
当每个线程在创建时,都会创建一个虚拟机栈,每一个栈帧对应一个java方法的调用。虚拟机栈由:局部变量表,操作数栈,动态链接,方法返回地址(方法返回出口)。
a.局部变量表:
(1).主要储存方法的形参和方法内部的局部变量。
(2).基本储存单位是Slot(变量槽)32位占一个Slot,64位占两个Slot,通过索引获取变量值。
(3).线程私有,所以本身不存在数据安全问题,但是多个线程修改指定线程上的变量时,如果不考虑同步问题,也会导致线程不安全,当通过形参访问或者返回StringBuilder变量时,也会导致线程不安全。
(4).局部变量表的大小在编译的时候就已经确定了,在程序运行时不能改变。
b.操作数栈:
类似于一种临时储存变量的空间,主要用于保存计算过程的中间结果,结合下图理解:
c.动态链接:
java源文件被编译成字节码文件之后,将所有的变量和方法要引用都作为符号引用保存到字节码文件的常量池中。动态链接的作用就是直接使用这些符号引用可以完成对方法的直接调用,方便识别并且也省空间。
当程序运行时会将这些符号引用从字节码文件的常量池移动到方法区的运行时常量池。
每个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用就是为了可以实现动态链接。下图为动态链接实例main方法中调用add方法:
d.方法返回地址(方法出口):
作用是当该栈帧方法执行完毕后,回到线程的某个具体位置。
2.本地方法栈:
a.本地方法栈类似于虚拟机栈,但是虚拟机栈服务的是JVM执行的Java方法,而本地方法栈服务的是JVM执行的native方法。
b.为什么要使用本地方法:native method就是一个java调用非Java代码的接口,有用C写的。为了java应用可以与java外面的环境交互,同时也为了与操作系统交互。
四.程序计数器
1.程序计数器是线程私有的
2.jvm中唯一不涉及outofmemoryError情况的区域:因为用来记录当前的指令地址,超出没有意义。
作用:在每一个线程里都有一个程序计数器,相当于一个当前线程的行号指示器,保证在多线程的情况切换会来可以在上次继续的基础上进行。
五.常量池相关概念
1.常量池的有哪几种
2.常量池的作用
3.常量池中存放的是什么
4.常量池之间的联系
5.关于常量池容易踩的坑
1.class文件常量池
class文件常量池是当.java文件编译成class文件时,在方法区的class相关文件信息除了存储类的相关信息、方法、接口等,还存在一个class常量池,里面存着:各种字面量和符号引用。
*什么是字面量和符号引用?
2.运行时常量池
运行时常量池是JVM方法区中的一部分,当class文件被类加载器加载到内存中时,JVM将class文件常量池中的内容转移到运行时常量池(运行时常量池也是每个类都有一个!),它和class常量池的一个相比较具有动态性,这个很重要。
*为什么说是动态性:因为java不仅可以在编译期加载常量,还可以在运行时期动态的加载常量:比如使用intern()方法。
*class常量池和运行时常量池在方法区中的关系如下图所示:
3.字符串常量池
String类平时使用率很高,为了效率:提升JVM性能和减少内存开销 ,避免重复创建相同字符串。String类单独私有维护一块特殊的内存的空间,也就是字符常量池。
字符串常量池的位置随着JDK版本的跟新换代而发生改变:
JDK7之前,字符串常量池存在于永久区中。
JDK7时,字符串常量池存在于堆中。
JDK7之后,元空间代替方法区,字符串常量池仍存在于堆中。
*两种常见的创建字符串对象的方式
a.字面值赋值 String a = “aaa”;
创建字符时,JVM先看字符串常量中是否有aaa这个对象,如果不存在创建一个新的对象,引用指向当前地址,如果存在,不会创建任何对象,直接改变地址引用指向aaa,所以str1和str2指向一同一个对象。
b.采用new关键字创建对象String a = new String(“aaa”);
懂得都懂String类new一个对象就会在堆中创建一个新对象,两个是不同对象。关键是流程,在new一个对象时,先要看字符串常量池中是否存在,如果有不创建,直接在堆上创建一个对象,把字符串对象的地址返回给新建堆上的对象引用;如果没有,现在字符串常量池上创建aaa字符串对象,然后在堆上创建一个对象,把地址返回给对象引用。
*字符串常量池优缺点:避免重复创建相同的字符,节省空间和时间,缺点是JVM要遍历找到所需要的字符串对象,稍微会浪费时间,但是时间成本相比很低。
4.intern()方法
作用:该方法是一种手动将字符串对象添加到字符串常量池的方法。当字符串调用intern()方法时,如果常量池中存在于该字符串等值的字符串时,直接返回池中字符串对象的引用;如果常量池中不存在等值字符串时,先在常量池中复制一份该字符串,并返回其引用。
对于不同版本的JDK:JDK6的时候字符串常量池在方法区永久代中,大小受限,所以不建议经常使用String.intern()方法;JDK7之后将字符串常量池移到了堆中,大小可以自己调节,所以可以继续使用intern()方法。
用处:该方法的耗时不可以忽略,但是在需要保存有限个被重复使用的值时这种场景时,可以使用该方法,放入字符串常量池,减少内存消耗,减少耗时,达到优化目的。