一 Java虚拟机运行时数据区
(注:该图参考自《深入理解Java虚拟机》P39)
(注:该图参考于网络图)
1.程序计数器Program Counter Register
(注:该图来源于网络)
它类似CPU寄存器中的IP/PC寄存器,用于存放指令地址。因为Java虚拟机是多线程的,所以每一个线程都有一个独立的程序计数器结构,它与线程共存亡。不过JVM中的程序计数器指向的是正在执行的字节码地址,而CPU的IP/PC寄存器指向的是下一条指令的地址。
延伸一些计算机组成原理的知识:
2.Java虚拟机栈(Java Virtual Machine Stacks)
虚拟机栈中的元素为栈帧(Stack Frame),线程在调用Java方法时,会为每个方法创建一个栈帧,来存储局部变量表、操作栈、动态链接、方法出口等信息。每个方法被调用和完成的过程,都对应着一个栈帧从虚拟机上出栈和入栈的过程。
3.本地方法栈(Native Method Stack)
虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
(以上来源于《深入理解Java虚拟机:JVM高级特性与最佳实践》P40)
As depicted in Figure 5-13, a thread first invoked two Java methods, the second of which invoked a native method. This act caused the virtual machine to use a native method stack. In this figure, the native method stack is shown as a finite amount of contiguous memory space. Assume it is a C stack. The stack area used by each C-linkage function is shown in gray and bounded by a dashed line. The first C-linkage function, which was invoked as a native method, invoked another C-linkage function. The second C-linkage function invoked a Java method through the native method interface. This Java method invoked another Java method, which is the current method shown in the figure.
上图所示,该线程首先调用了两个Java方法,而第二个Java方法又调用了一个本地方法,这样导致虚拟机使用了一个本地方法栈。图中的本地方法栈显示为 一个连续的内存空间。假设这是一个C语言栈,期间有两个C函数,他们都以包围在虚线中的灰色块表示。第一个C函数被第二个Java方法当做本地方法调用, 而这个C函数又调用了第二个C函数。之后第二个C函数被第二个Java方法当做本地方法调用,而这个C函数又调用了第二个C函数。之后第二个C函数又通过 本地方法接口回调了一个Java方法(第三个Java方法)。最终这个Java方法又调用了一个Java方法(他成为图中的当前方法)。
4.堆(Heap)
Java堆被所有线程共享。Java堆是垃圾收集器管理的主要区域。用来存放对象实例。还可细分为新生代和老年代。
(注:该图来源于网络)
5.方法区(Method Area)
方法区与java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。也有人将方法区称为“永久代”。
运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的常量池中存放。当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
延伸1:
去面试总会被问到一个问题:String s = new String("xyz"); 创建了几个String Object?
以下是在网上查到的关于上述问题流传较为广泛的说法:
- 两个或一个,”xyz”对应一个对象,这个对象放在字符串常量缓冲区,常量”xyz”不管出现多 少遍,都是缓冲区中的那一个。New String 每写一遍,就创建一个新的对象,它一句那个 常量”xyz”对象的内容来创建出一个新 String 对象。如果以前就用过’xyz’,这句代表就不会 创建”xyz”自己了,直接从缓冲区拿。
对上述问题的修正:String s = new String("xyz"); 在运行时涉及几个实例?
在对该问题的答案的寻找中,在RednaxelaFX大牛的博客中看到了对该问题的质疑,总结下来认为若按第一种问法是没有标准答案的。
而若按java虚拟机规范定义的来看,将问题改为String s = new String("xyz"); 在运行时涉及几个实例?
答案则应该为两个,一个是字符串字面量"xyz"所对应的、驻留(intern)在一个全局共享的字符串常量池中的实例,另一个是通过new String(String)创建并初始化的、内容与"xyz"相同的实例。
对于HotSpot虚拟机来说,方法区是用“永久代”来实现的。因为容易遇到内存溢出问题。在JDK1.7的HotSpot中,已经把原本存放在永久代的字符串常量池移除。
"String池"即字符串常量池在JDK1.7之前是存在于方法区的。自JDK1.7开始,永久代中的字符串常量池被移到堆中,而元信息则被移到本地内存中的“元空间(Metaspace)”中,并且会被垃圾回收机制回收。
延伸2:不健壮代码的特征及解决办法
1、尽早释放无用对象的引用。好的办法是使用临时变量的时候,让引用变量在退出活动域后,自动设置为null,暗示垃圾收集器来收集该对象,防止发生内存泄露。
对于仍然有指针指向的实例,jvm就不会回收该资源,因为垃圾回收会将值为null的对象作为垃圾,提高GC回收机制效率;
2、我们的程序里不可避免大量使用字符串处理,避免使用String,应大量使用StringBuffer,每一个String对象都得独立占用内存一块区域;
String str = "aaa";
String str2 = "bbb";
String str3 = str + str2;
//假设执行完第三行之后之后str,str2以后再不被调用,那它就会被放在内存中等待Java的gc去回收,程序内过多的出现这样的情况就会报上面的那个错误,建议在使用字符串时能使用StringBuffer就不要用String,这样可以省不少开销;
3、尽量少用静态变量,因为静态变量是全局的,GC不会回收的;
4、避免集中创建对象尤其是大对象,JVM会突然需要大量内存,这时必然会触发GC优化系统内存环境;
5、尽量运用对象池技术以提高系统性能;生命周期长的对象拥有生命周期短的对象时容易引发内存泄漏,例如大集合对象拥有大数据量的业务对象的时候,可以考虑分块进行处理,然后解决一块释放一块的策略。
6、不要在经常调用的方法中创建对象,尤其是忌讳在循环中创建对象。可以适当的使用hashtable,vector
创建一组对象容器,然后从容器中去取那些对象,而不用每次new之后又丢弃。
延伸3:操作系统的知识
(1)JVM虚拟机的本质是一个操作系统,而操作系统的本质是计算机系统中的一个系统软件,负责管理计算机的软硬件资源。
(2)运行一个Java程序时,JVM会为该程序创建一个进程,并为该进程分配一个唯一标识及进程控制块PCB,同时也会分配一块虚拟的内存地址空间。
(3)类加载器Class Loader加载java程序类文件到方法区。方法区存放加载过的类的基本信息、常量、静态变量等。方法区是线程共享的。
(4)类加载完成后,主线程运行static main()时在虚拟机栈中建栈帧,压栈。
(5)执行到new Object()时,在堆heap区为对象分配地址空间,然后执行类的构造函数初始化。
参考:
(1)https://www.jianshu.com/p/1b2ded9db25d
(2)https://zhidao.baidu.com/question/981875738723747499.html
(3)