目录
一、JRE/JDK/JVM是什么关系?
JRE(JavaRuntimeEnvironment,Java运行环境),也就是Java平台。所有的Java 程序都要在JRE下才能运行。程序开发者不需要关心底层JRE的实现,不同的操作系统会有对应的不同的JRE,不同厂商提供的JRE也会有很大差异,对此Sun公司只提供了开发规范而未做实现细节要求。
JDK(Java Development Kit)是程序开发者用来开发、调试java程序用的java工具类库,提供了日常开发中通用的基础设施的实现。在JDK的安装过程中会自带JRE,所以在JDK的安装目录下有一个名为jre的目录,用于存放JRE文件。
JVM(JavaVirtualMachine,Java虚拟机)是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。 JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。
二、运行时数据区的组成
根据 JVM 规范,JVM在执行JAVA程序的过程中会把它所管理的内存分为虚拟机栈、本地方法栈、程序计数器(PC寄存器)、堆、方法区五个部分,这五个部分称为JAVA运行时的数据区,如下图所示:
各区域跟线程的关系如下:
1.程序计数器
又称作为PC寄存器,在汇编语言中,是指CPU中的寄存器,它保存的是程序当前执行的指令的地址,当CPU需要执行指令时,根据程序计数器中保存的地址获取到指令,在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令。JVM中的程序计数器并不是物理概念上的CPU寄存器,但是功能相同,即保存当前需要执行的指令的地址,其存储的数据所占空间的大小不会随程序的执行而发生改变,注意如果线程执行的是native方法,则程序计数器中的值是undefined。
因为一个CPU的内核同一时刻只能执行一条线程中的指令,所以多线程并发执行时CPU是通过线程调度在多个线程间来回切换执行。为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。所以,程序计数器是每个线程所私有的。
2.Java栈(虚拟机栈)
Java栈是Java方法执行的内存模型,随线程创建和销毁,线程的栈内存大小通过参数-Xss 指定,jdk5以后默认为1M。因为每个线程都对应一个独立的虚拟机栈,所以栈大小直接影响所能创建的线程数量,应该根据应用的实际运行情况调整。Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、动态链接、方法返回地址(Return Address)等。详情参考:
https://blog.youkuaiyun.com/ychenfeng/article/details/77247807
重点关注StackOverflowError堆栈溢出错误产生的过程,以下面的代码为例说明:
public class TestDemo {
private int index = 1;
public void method() {
index++;
//如果不注释掉,则不会抛出异常
/*
if(index>10000){
return;
}
*/
method();
}
@Test
public void testStackOverflowError() {
try {
method();
} catch (StackOverflowError e) {
System.out.println("程序所需要的栈大小 > 允许最大的栈大小,执行深度: " + index);
e.printStackTrace();
}
}
}
抛出异常过程详解:
执行testStackOverflowError方法会创建一个线程,同时创建一个与该线程关联的虚拟机栈(栈内存)
第一次调用method()时,会创建一个栈帧并压栈,执行index++会在该栈帧的操作数栈中写入数据,接着递归调用methed()方法,又会创建一个栈帧并压栈,因为method()方法没有返回逻辑会一直不断的递归调用method()方法,就不断的创建新的栈帧并压栈,从而导致实际栈内存不断扩大。当栈内存超过系统配置的栈内存,就会出现java.lang.StackOverflowError异常。
如果不注掉if代码,当条件被触发时,该次method()方法调用完成,该次调用对应的栈帧出栈,如果有返回值则写入下一个栈帧的操作数帧,然后调用结果往上不断返回,对应的栈帧不断出栈,直到方法执行完成。
3.本地方法栈
本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。在JVM规范中,并没有对本地方法栈的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。
4、方法区
方法区在JVM中也是一个非常重要的区域,它与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。在JVM规范中,没有强制要求方法区必须实现垃圾回收。很多人习惯将方法区称为“永久代”,是因为HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将永久代移除了。
5.Java 堆
在虚拟机启动时创建,是Java 虚拟机所管理的内存中最大的一块,主要用于存放对象实例,几乎所有的对象实例都在这里分配内存,因此Java 堆是垃圾收集器管理的主要区域,又叫做GC 堆,如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。Java对象的内存布局参考:https://www.jianshu.com/p/91e398d5d17c
Java8中堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。从JDK8开始永久代(PermGen)被元空间(Metaspace)代替用于实现方法区,两者最大的区别是元空间并不在虚拟机中,而是使用本地内存,因此,默认情况下,元空间的大小仅受本地内存限制。所有新创建的对象都将在新生代Eden区域中分配内存,如果年轻代的数据在一次或多次GC后存活下来,那么将被转移到老年代,这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。
默认情况下,老年代占三分之二的堆空间,年轻代占三分之一的堆空间, eden区占8/10 的年轻代空间,survivor from区占1/10 的年轻代空间,survivor to占1/10 的年轻代空间。通过命令java -XX:+PrintFlagsFinal -version查看所有默认的jvm参数。
堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大。Sun Hotspot为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的。但如果对象过大的话则仍然是直接使用堆空间分配,因此通常多个小的对象比大的对象分配起来更加高效。
三、运行时常量池与String对象
运行时常量池是方法区的一部分,class文件中用于存放编译期生成的各种字面量和符号引用的常量池会在类加载后放到运行时常量池中保存。字面量包含文本字符串和final常量值,如String s="test"中的test,final int num=124中的124;符号引用包含类和接口的全限定名,字段的名称和描述符和方法的名称和描述符。应用程序也可在程序运行期间将新的常量放入常量池中,如调用String对象的intern()方法,如果常量池没有跟该String equal()的String对象,则会将该String放入运行时常量池中。参考如下用例:
@Test
public void test() throws E