摘自 :https://www.cnblogs.com/boycelee/p/14095080.html
原创文章,转载请标明出处!https://www.cnblogs.com/boycelee/p/14095080.html
一、背景
相对于C/C++C程序员,Java程序员会相对轻松一些,因为Java虚拟机的内存管理机制会管理内存,不需要开发人员手动进行内存管理,也不容易出现内存泄露和内存溢出的。但如果不了解虚拟机如何管理内存,在内存出现问题时就会束手无策,所以学习虚拟机如何管理内存也是一件必要的事情。
二、运行时内存区域概述
1、官方描述
The Java Virtual Machine defines various run-time data areas that are used during execution of a program.
2、中文翻译
Java虚拟机定义了在程序运行期间的各种运行时数据区域。
3、内存区域简述
在《Java虚拟机规范》中运行时数据区域会包括PC寄存器、Java虚拟机栈、堆、方法区、运行常量池、本地方法栈。因为运行时常量池是方法区的一部分,所以本篇文章将常量池放在方法区章节中的子节来讲解。
4、运行时数据区简图
5、运行时数据区详图
三、JVM线程
JVM数据区域与线程关系
1、官方描述
Some of these data areas are created on Java Virtual Machine start-up and are destroyed only when the Java Virtual Machine exits. Other data areas are per thread. Per-thread data areas are created when a thread is created and destroyed when the thread exits.
2、中文解释
一部分数据区域与虚拟机进程同生共死,另一部分数据区域与线程同生共死。
3、关系图
四、PC寄存器
1、官方解释
The Java Virtual Machine can support many threads of execution at once (JLS §17). Each Java Virtual Machine thread has its own pc (program counter) register. At any point, each Java Virtual Machine thread is executing the code of a single method, namely the current method ([§2.6](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.6)) for that thread. If that method is not native , the pc register contains the address of the Java Virtual Machine instruction currently being executed. If the method currently being executed by the thread is native, the value of the Java Virtual Machine's pc register is undefined. The Java Virtual Machine's pc register is wide enough to hold a returnAddress or a native pointer on the specific platform.
2、中文翻译
Java虚拟机支持同时执行多个线程。每一个Java虚拟机线程都有自己的PC寄存器。在任何时刻,Java虚拟机一个线程都只在执行某一个单一函数代码。如果函数不是native函数,PC寄存器中就包含当前正在被执行的Java虚拟机指令的地址。反之当前函数是native函数,pc寄存器中的值是undefined。pc寄存器的大小足够存储返回地址或native指针。
3、概述
(1)PC寄存器并非真正意义上的物理寄存器,pc寄存器是对物理寄存器的一种模拟;
(2)PC寄存器是一块较小的内存空间;
(3)可以将其看做当前线程执行的字节码指令的“行号指示器”;
(4)字节码解释器的工作就是改变pc寄存器的值来选取下一条需要执行的字节码指令;
(5)PC寄存器的作用是存储下一条指令地址,也就是即将要执行的指令代码,然后由执行引擎读取下一条指令;
(6)在Java虚拟机规范中,PC寄存器是线程私有的,其生命周期与线程生命周期保持一致;
(7)PC寄存器是Java虚拟机规范中没有规定任何OutOtMemoryError的区域。
3、什么是上下文切换?
当单核处理器执行多线程代码时,会为每个线程分配CPU时间片,CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是在切换前会保存上一个任务的状态,以便于下次切换回任务时,可以再次加载这个任务之前的状态。所以任务从保存到再一次加载的过程就是一次“上下文切换”。CPU通过不停进行上下文切换,让我们觉得多个线程是同时执行的。
4、什么是CPU时间片?
CPU时间片就是CPU分配给每个线程的时间段。由于CPU只有一个核数有限,只能同时处理程序的一部分任务,不能同时满足所有要求,为了公平处理多线程问题,就引入的时间片的概念,为每个线程分配时间片,轮流执行。
5、为什么PC寄存器是“线程私有”的?
由于Java虚拟机多线程是通过线程上下文切换的方式来实现的。在任何时刻,一个处理器只会执行一条程序中的指令,因此在上下文切换后为了能够恢复到正确的执行位置,每个线程都需要有一个独立的PC寄存器,线程之间独立存储,互不影响。
五、虚拟机栈
1、官方解释
Each Java Virtual Machine thread has a private Java Virtual Machine stack, created at the same time as the thread. A Java Virtual Machine stack stores frames ([§2.6](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.6)). A Java Virtual Machine stack is analogous to the stack of a conventional language such as C: it holds local variables and partial results, and plays a part in method invocation and return. Because the Java Virtual Machine stack is never manipulated directly except to push and pop frames, frames may be heap allocated. The memory for a Java Virtual Machine stack does not need to be contiguous.
2、中文解释
Java虚拟机栈是线程私有的,与线程同生共死。Java虚拟机中有一个个存储栈帧。Java的栈能够存储局部变量与部分返回结果,并参与函数的调用与返回。因为Java虚拟机栈只push或pop栈帧,不直接操作Java虚拟机栈,栈帧可能被堆分配。Java虚拟机栈的内存不需要连续。
3、概述
Java虚拟机栈描述的是Java函数执行的线程内存模型。每个函数被执行时,Java虚拟机会同步创建一个栈帧用于存储局部变量标配、操作数栈、动态链接和函数返回地址等信息。函数被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈和出栈的过程。
4、栈的结构
5、栈的存储
(1)栈是线程私有,栈中的数据都已栈帧形式存在。栈帧是栈的基本单位;
(2)线程中正在执行的函数都有其对应的栈帧;
(3)栈帧是一个内存区块,是一个数据集合,其中存储着函数执行过程中的数据信息;
6、栈的运行原理
(1)JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈。
(2)在同一线程同一时间下,只会有一个活动的栈帧,且该栈帧是当前正在执行的函数对应的栈帧,即栈顶栈帧也称为“当前栈帧”。
(3)执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
(4)若方法中调用了其他方法,对应的新的栈帧就会被创建,放在栈顶,成为新的当前栈帧。
7、局部变量表
(1)概述
1)局部变量表定义为一个数组,主要用于存储方法参数和定义在方法体内的局部变量。包括基本数据类型和对象引用以及returnAddress类型(指向特定指令地址的指针,例如pc寄存器中的值就是returnAddress类型)。
2)局部变量表所需容量在编译期间已经确定下下来,并保存在方法的Code属性的maximun local variables数据项中。在函数运行期局部变量表大小不会改变。
(2)举例代码
public class Car {
public static void main(String[] args) {
Car car = new Car();
String name = "Boyce Car";
}
}
(3)字节码文件中的局部变量表
1)start pc表示字节码指令的行号;
2) length是pc指针起始位置到结束位置的距离;
3) start pc与length共同描述变量的作用域范围。
(4)Slot
1)局部变量表的最基本存储单元Slot(变量槽)
2)在局部变量表中,32位以内的类型只占一个Slot(包括returnAddress类型),64位类型(long和duble) 占两个Slot。
3)局部变量表中,每一个Slot都会分配到一个访问索引,通过这个索引可以访问到局部变量表中对应的局部变 量值。
4)当一个实例方法被调用时,它的方法参数和方法体内定义的局部变量将会按照顺序被复制到局部变量表中 的Slot上。
5)当需要访问局部变量表中的64位的局部变量值时,只需要使用2个Slot索引中的前一个索引即可。
6)如果当前帧由构造函数或实例方法创建的,那么该对象引用this将存放在index为0的Slot处。
8、操作数栈
(1)每个栈帧都包含一个先进后出的操作数栈。
(2)操作数栈在方法执行过程中,根据字节码指令,往栈中写入或读取数据,即入栈或出栈。
(3)操作数栈的作用主要是用于保存计算过程的中间结果,同时作为计算过程中变量的临时存储空间。
(4)操作数栈根据push/pop进行操作,无法使用索引方式访问。
(5)如果一个函数带有返回值,其返回值会被压入当前栈帧的操作数栈中,并更新pc寄存器中的下一条字节码指令。
(6)Java虚拟机的指令架构是基于栈式架构,其中的栈指的就是操作数栈。
(7)基于栈式结构计算过程的字节码指令:
9、栈顶缓存
(1)操作数栈存储于内存,频繁操作进行IO操作影响执行效率。HotSpot虚拟机的设计者提出了栈顶缓存技术,将栈顶元素缓存在寄存器中,以此减少IO操作,提升运行效率。(处理器访问任何寄存器和 Cache 等封装以外的数据资源都可以当成 I/O 操作,包括内存,磁盘,显卡等外部设备。)
10、动态链接
(1)每个栈帧内部都包含一个纸箱运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前代码能够实现动态链接(Dynamic Linking)。例如:invokedynamic指令。
(2)Java源文件被编译成字节码文件时,字面量与符号引用都被保存至字节码文件的常量池中。例如:当一个函数调用另一个函数时,就通过常量池中指向的函数的符号引用来表示,动态链接的作用就是将这些符号引用转换为调用函数的直接引用(函数在实际运行时内存中的入口地址)。
11、方法返回地址
(1)存放调用该函数的主函数的pc寄存器的值;
(2)一个函数的结束有两种方式:1)正常执行完成;2)异常,非正常退出;
(3)无论通过哪种方式退出,在函数退出后都返回到该函数被调用的位置,程序才能继续执行。正常退出时,调用方的pc寄存器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而如果通过异常退出,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
六、本地方法栈
1、官方解释
An implementation of the Java Virtual Machine may use conventional stacks, colloquially called "C stacks," to support native methods (methods written in a language other than the Java programming language). Native method stacks may also be used by the implementation of an interpreter for the Java Virtual Machine's instruction set in a language such as C. Java Virtual Machine implementations that cannot load native methods and that do not themselves rely on conventional stacks need not supply native method stacks. If supplied, native method stacks are typically allocated per thread when each thread is created.
2、中文翻译
Java虚拟机的实现可以使用传统的堆栈,以支持本地方法,本地方法栈也可以用于实现Java虚拟机指令集的解释器。Java虚拟机不能加载本地方法且不提供本地方法栈。如果提供本地方法栈,则线程创建时为每个线程分配一个本地方法栈。
3、概述
(1)本地方法栈与Java虚拟机栈相似,Java虚拟机栈用于管理Java函数的执行问题,而本地方法栈则是用于管理本地函数(Native)的执行问题。
(2)《Java虚拟机规范》中对本地方法栈没有强制规定,因此虚拟机可以根据需求自由实现。如Hot-Spot虚拟机就直接将本地方法栈和虚拟机栈合二为一。
(3)栈深度溢出或栈扩展失败时会分配抛出StackOverflowError和OutOfMemoryError异常。
(4)当线程调用本地方式时,它和虚拟机就有相同的权限,不再受虚拟机的限制。
4、代码示例
在安卓开发时,我们需要调用C/C++代码,我们就需要用到JNI(Java Native Interface)。
package com.example.nativetest1;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = findViewById(R.id.sample_text);
tv.setText(stringFromJNI());
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
}
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_nativetest1_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
七、堆空间
1、官方描述
The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated.
The heap is created on virtual machine start-up. Heap storage for objects is reclaimed by an automatic storage management system (known as a garbage collector); objects are never explicitly deallocated. The Java Virtual Machine assumes no particular type of automatic storage management system, and the storage management technique may be chosen according to the implementor's system requirements. The heap may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger heap becomes unnecessary. The memory for the heap does not need to be contiguous.
2、中文翻译
(1)堆空间是线程共享的,类的对象实例与数组分配的内存都在堆空间中。
(2)堆空间在虚拟机启动时创建,堆空间的内存由垃圾回收器进行回收,对象不显示释放。Java虚拟机不限定垃圾回收器,垃圾回收器技术可以根据实现者的需求自行选择。堆空间大小可以是固定,也可以是根据需求进行扩展的。堆的内存空间可以物理上不连续。
3、概述
(1)Java虚拟机实例中只有一个堆内存,堆是虚拟机管理的最大一块内存,其被所有线程共享。
(2)Java堆存在为唯一目的就是存储对象实例。
(3)《Java虚拟机规范》中规定:“所有对象实例和数组都应该在堆上分配”。随着Java语言的发展未来可能被打破,但是目前仍然没有。
(4)《Java虚拟机规范》中规定:“堆可以处于物理上不连续的内存空间中,但逻辑上它应该被视为连续的”。
(5)《Java虚拟机规范》中并没有对堆的划分有任何要求,“新生代、老年代、Eden、Survivor”等名词只是一部分垃圾回收器的设计,而非某一虚拟机固有的内存布局。
(6)所有线程都共享堆空间,但还是可以划分线程私有的缓冲区(Thread Local Allocation Buffer, TLAB)。
(7)数组和对象永远不会存储在栈上,因为栈帧中只保存引用,引用指向对象和数组在堆中存放的位置。
4、堆、栈和方法区的关系
栈负责解决执行问题,堆负责解决数据存储问题。
5、TLAB
为什么会出现TLAB?
(1)堆空间是线程共享的。
(2)JVM中会频繁创建对象实例,因此在并发环境下操作堆空间的内存区域是线程不安全的。
(3)当多线程同时操作同一地址时,就需要加上同步机制,这就会影响对象实例创建速度。
(4)如何解决解决这一问题呢?这就出现了TLAB。
什么是TLAB?
(1) JVM为每个线程分配了一个私有的缓存区域。
(2)多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种分配方式称为“快速分配策略”。
(3)基于OpenJDK衍生出来的JVM都提供了TLAB的设计。
(4)TLAB默认栈堆空间(Eden区)的1%。
对象分配(开启TLAB时)
(1)JVM将TLAB作为内存分配的首选。
(2)默认详情下,TLAB仅占堆空间(Eden区)的1%。
(3)当对象在TLAB空间分配失败时,JVM就会尝试通过使用加锁机制确保数据操作的原子性,从而直接在堆空间(Eden区)分配内存。
6、逃逸分析
(1)逃逸分析的本质是分析对象动态作用域。
(2)当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
(3)当一个对象在方法中被定义后,它被外部方法所引用时,则认为发生逃逸。
(4)发生逃逸
public static StringBuffer createStringBuffer(String s1, String s2) {
StringBuffer stringBuffer = new StringBuffer();
return stringBuffer;
}
(5)未发生逃逸
public static String createStringBuffer(String s1, String s2) {
StringBuffer stringBuffer = new StringBuffer();
return stringBuffer.toString();
}
判断方式是new的对象实例是否能在方法外被调用。
7、逃逸分析-代码优化
使用逃逸分析后,编译器可以对代码做如下优化
栈上分配
(1)将堆上分配转化为栈上分配。对象在程序中被分配,如果要使对象不会发生逃逸,对象可以在栈上分配,而不是堆上分配。
(2)JIT编译器在编译期间会借助逃逸分析来判断对象是否逃逸出方法,如果没有逃逸,就可能会被优化为栈上分配,最后线程结束后栈空间被回收,局部变量对象也会被回收。
同步省略
(1)如果对象只能被一个线程访问到,那么这个对象可以不考虑同步。
(2)场景:在动态编译同步块代码时,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能被一个线程访问。如果确定只有一个线程能访问到,JIT编译器在编译这个代码块时就会取消对这部分代码的同步。这个过程叫“同步省略”也叫“锁消除”。
标量替换
(1)有的对象可能不需要一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
(2)变量(Scalar)是指一个无法再分解成为更小的数据的数据。例如:Java中原始数据类型就是标量。
(3)聚合量(Aggregate)是指一个能够被分解成为更小数据的数据。例如:Java对象。
(4)应用场景是在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问,经过JIT优化,就会把这个对象拆解为若干个成员变量来替代(变量)。这个过程就叫标量替换。
小结
(1)目前逃逸分析技术并不成熟。
(2)逃逸分析本身也耗费性能,无法保证逃逸分析消耗的性能小于函数本身。
(3)目前只有变量替换被应用。
(4)目前堆是存储对象实例的唯一选择。
八、方法区
1、官方解释
The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the "text" segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods ([§2.9](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.9)) used in class and instance initialization and interface initialization.
The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.
2、中文解释
(1)方法区是线程共享的,类似传统语言用于存储编译代码的存储区。
(2)方法区用于存储类结构信息,例如运行时常量池、域信息、函数数据、函数与构造函数代码、类元信息和实例初始化、接口初始化中使用的特殊函数。
(3)Java虚拟机启动时就会创建方法区,逻辑上方法区是堆空间的一部分(实际上不是)。方法区可以不实现垃圾收集策略(Hotspot有实现)。方法区大小可以固定,也可以是根据计算需求扩展的。方法区内存物理上可以不连续。
3、概述
(1)在《Java虚拟机规范中》中方法区逻辑上是堆空间的一部分,但实际Hot-Spot虚拟机实现时,却将堆空间与方法区做了区分,方法区还有一个别名叫做Non-Heap(非堆)。所以可以将方法区看做独立于堆的内存空间。
(2)方法区是线程共享的。
(3)方法区的大小和对空间一样可以通过参数设置,是可以扩展的。
(4)方法区的大小决定能够保存多少类,如果类太多就会造成方法区内存溢出。
(5)关闭JVM方法区内存就会释放。
4、方法区结构
(1)存储已经被虚拟机加载的类型信息、常量、静态变量、域信息、方法信息、即时编译器编译后的代码缓存等。(JDK8之前)
(2)类型信息。对每个加载的类型(类class、接口interface、枚举enum、注解annotation)。JVM必须在方法区中存储以下类型信息:
a)该类型的完整名称(包名.类型)。
b)该类型直接父类的完整名称(接口或Object类都没有父类)。
c)该类型的修饰符(public、abstract、final)。
d)该类型直接接口的有序列表。
(3)域信息。
a)JVM必须在方法区中保存类型的所有域的相关信息和域的声明顺序。
b)域相关信息有域名称、域类型、域修饰符(public、private、protected、static、final、volatile、transient)
(4)方法信息。JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
a)方法名称
b)方法的返回类型(或void)
c)方法参数的数量和类型(按顺序)
d)方法的修饰符(public、private、protected、static、final、syschronized、native、abstract)
e)方法的字节码(bytecodes)、操作数栈、局部变量表以及大小
f)异常表
(5)源代码
/**
* @author jianw.li
* @date 2020/12/2 10:51 下午
* @Description: 方法区测试
*/
public class MethodAreaTestDemo extends MethodAreaTest implements Serializable {
public int num = 1;
private static String str = "测试测试";
public void sub() {
int i = 10;
int j = 1;
int k = i - j;
System.out.println(k);
}
private static int add(int a, int b){
int c = a + b;
return c;
}
public static void main(String[] args) {
add(1, 2);
}
}
(6)字节码文件
通过javap -v MethodAreaTestDemo.class查看
Classfile /Users/lijianwei/IdeaProjects/LeeBoyceJVMTest/out/production/LeeBoyceJVMTest/com/ljw/MethodAreaTestDemo.class
Last modified 2020-12-2; size 985 bytes
MD5 checksum f3555565267ef4cbb0c07bebb42263a6
Compiled from "MethodAreaTestDemo.java"
//注释:存放至方法区的类信息
public class com.ljw.MethodAreaTestDemo extends com.ljw.MethodAreaTest implements java.io.Serializable
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #9.#38 // com/ljw/MethodAreaTest."<init>":()V
#2 = Fieldref #8.#39 // com/ljw/MethodAreaTestDemo.num:I
#3 = Fieldref #40.#41 // java/lang/System.out:Ljava/io/PrintStream;
#4 = Methodref #42.#43 // java/io/PrintStream.println:(I)V
#5 = Methodref #8.#44 // com/ljw/MethodAreaTestDemo.add:(II)I
#6 = String #45 // 测试测试
#7 = Fieldref #8.#46 // com/ljw/MethodAreaTestDemo.str:Ljava/lang/String;
#8 = Class #47 // com/ljw/MethodAreaTestDemo
#9 = Class #48 // com/ljw/MethodAreaTest
#10 = Class #49 // java/io/Serializable
#11 = Utf8 num
#12 = Utf8 I
#13 = Utf8 str
#14 = Utf8 Ljava/lang/String;
#15 = Utf8 <init>
#16 = Utf8 ()V
#17 = Utf8 Code
#18 = Utf8 LineNumberTable
#19 = Utf8 LocalVariableTable
#20 = Utf8 this
#21 = Utf8 Lcom/ljw/MethodAreaTestDemo;
#22 = Utf8 sub
#23 = Utf8 i
#24 = Utf8 j
#25 = Utf8 k
#26 = Utf8 add
#27 = Utf8 (II)I
#28 = Utf8 a
#29 = Utf8 b
#30 = Utf8 c
#31 = Utf8 main
#32 = Utf8 ([Ljava/lang/String;)V
#33 = Utf8 args
#34 = Utf8 [Ljava/lang/String;
#35 = Utf8 <clinit>
#36 = Utf8 SourceFile
#37 = Utf8 MethodAreaTestDemo.java
#38 = NameAndType #15:#16 // "<init>":()V
#39 = NameAndType #11:#12 // num:I
#40 = Class #50 // java/lang/System
#41 = NameAndType #51:#52 // out:Ljava/io/PrintStream;
#42 = Class #53 // java/io/PrintStream
#43 = NameAndType #54:#55 // println:(I)V
#44 = NameAndType #26:#27 // add:(II)I
#45 = Utf8 测试测试
#46 = NameAndType #13:#14 // str:Ljava/lang/String;
#47 = Utf8 com/ljw/MethodAreaTestDemo
#48 = Utf8 com/ljw/MethodAreaTest
#49 = Utf8 java/io/Serializable
#50 = Utf8 java/lang/System
#51 = Utf8 out
#52 = Utf8 Ljava/io/PrintStream;
#53 = Utf8 java/io/PrintStream
#54 = Utf8 println
#55 = Utf8 (I)V
{
//注释:存放至方法区的域信息
public int num;
descriptor: I
flags: ACC_PUBLIC
public com.ljw.MethodAreaTestDemo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method com/ljw/MethodAreaTest."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field num:I
9: return
LineNumberTable:
line 10: 0
line 12: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/ljw/MethodAreaTestDemo;
//注释:存放至方法区的函数信息
public void sub();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: iconst_1
4: istore_2
5: iload_1
6: iload_2
7: isub
8: istore_3
9: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
12: iload_3
13: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
16: return
LineNumberTable:
line 16: 0
line 17: 3
line 18: 5
line 19: 9
line 20: 16
LocalVariableTable:
Start Length Slot Name Signature
0 17 0 this Lcom/ljw/MethodAreaTestDemo;
3 14 1 i I
5 12 2 j I
9 8 3 k I
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: iconst_1
1: iconst_2
2: invokestatic #5 // Method add:(II)I
5: pop
6: return
LineNumberTable:
line 28: 0
line 29: 6
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 args [Ljava/lang/String;
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #6 // String 测试测试
2: putstatic #7 // Field str:Ljava/lang/String;
5: return
LineNumberTable:
line 13: 0
}
SourceFile: "MethodAreaTestDemo.java"
5、方法区变化
方法区、永久代以及元空间的关系
(1)方法区不等于永久代不等于元空间。
(2)永久代、元空间只是方法区的实现方式。
(3)永久代的使用,容易导致Java程序OOM(超过-XX:MaxPermSize上限)。
(4)JDK8将将方法区的实现方式由永久代改为元空间。
(5)元空间的本质与永久代类似,都是对Java虚拟机规范中方法区的实现。不过元空间与永久代最大的区别在于元空间不在虚拟机设置的内存中,而是使用本地内存。
方法区变化细节
版本 | 说明 |
---|---|
JDK6 | 永久代实现方法区。静态变量、字符串常量池存放在永久代。 |
JDK7 | 永久代实现方法区。已经逐渐去除“永久代”,将字符串常量池、静态变量移至堆中存储。 |
JDK8 | 元空间实现方法区。类信息、域信息、函数信息、运行时常量池存储至本地内存的元空间中。但字符串常量池和静态变量仍然存放在堆中。 |
6、常量池与运行时常量池
(1)运行时常量池是方法区的一部分。
(2)常量池是Class文件的一部分。常量池用于存放编译期间的字面量和符号引用(字面量和符号引用后续文章讲),这部分内容将在类加载后存放到方法区运行时常量池中。
(3)JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组一样是通过索引访问的。
(4)运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能获取到的方法或字段引用。
(5)什么是字面量?a)文本字符串;b)八种基本类型的值;c)被声明为final的常量等。
(6)什么是符号引用?a)类和方法的全限定名;b)字段名称和描述符;c)方法名称和描述符。
(7)为什么需要运行时常量池?Java的字节码需要使用数据支持,这些数据不能够直接存储在字节码文件中。为了字节码文件中可以使用到数据,可以将数据存放在常量池中,再由字节码文件中存放的“指向常量池的引用”指向常量池中的数据。
7、为什么使用元空间替换永久代?
(1)官方解释
由于JRockit虚拟机与Hotspot虚拟机融合,JRockit虚拟机虚拟机的用户不需要也不习惯去设置永久代。所以融合之后索性就去掉了永久代。
一部分类元数据存放在本地内存中,而字符串常量池与静态变量则存放置堆空间中。类元数据仅受限于可使用的本地内存大小,而不是-XX:MaxPermSize
。
(2)永久代的空间大小难以设置
如果动态加载的类过多,容易造成内存溢出(OOM),元空间相对于永久代的好处是使用本地内存而非虚拟机内存,默认情况下元空间大小仅受本地内存限制。
(3)永久代调优困难
对方法区的垃圾回收困难。对于类信息的回收需要同时满足3个条件:1)该类的所有势力都已经被回收,堆中不在存在任何该类和其派生子类的实例;2)该类的类加载器已经被回收;3)该类的对象不再被引用,且无法通过反射访问该类的函数。需要同时满足以上条件类才能够“允许”被回收。
8、为什么将字符串常量池移至堆空间?
JDK7中将StringTable放在堆空间中。因为永久代的回收效率很低,只有在full gc时才会触发。而full gc只有在老年代、永久代空间不足时才会触发。实际开发过程中,会创建大量字符串,放在堆空间相对于方法区回收效率更高。
9、方法区垃圾回收
(1)《Java虚拟机规范》中提到可以不要求虚拟机在方法区中实现垃圾收集。
(2)方法区垃圾收集主要2部分内容:1)常量池中废弃的常量;2)不在使用的类。
(3)方法区中的常量池中主要存放两大类常量:1)字面量;2)符号引用。
(4)常量池中的常量没有任何地方引用就可以被回收。
(5)类的回收条件非常苛刻,必须同时满足3个条件。(可以看上文方法区调优困难中对方法区垃圾回收困难的描述)。
九、直接内存
1、概述
(1)直接内存并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
(2)直接内存是在Java堆外内存,是直接想系统申请的堆外内存空间。
(3)本机直接内存不受Java堆大小限制。
(4)访问直接内存的速度要高于Java堆的速度。读写性能要求高时可以考虑直接内存。
(5)Java的NIO库允许Java程序使用直接内存,用于数据缓冲区。
(6)直接内存的缺点是回收成本高,不受JVM回收机制管理。
(7)直接内存可以通过参数设置大小,默认与堆的最大参数值一致。
十、对象实例化过程
1、对象实例化
(1)对象创建方式
1)new
2)反射
3)clone()
4)反序列化
(2)创建对象步骤
1)判断对象对应类是否加载、连接、初始化。
当虚拟机遇到new指令时,首先会去检查这个指令的参数能够在Metaspace的常量池中定位到类的符号引用,并检查这个符号引用代表的类是否已经被加载、解析和初始化(类元信息是否存在)。
2)对象内存分配
a.计算对象占用空间大小,然后在堆中划分一块内存给新对象。
b.指针碰撞。如果能内存规整,只需要使用一个指针作为分界点的指示器,分配的内存就是将指针移动一段与对象大小相等的距离。如果垃圾收集器基于压缩算法,具备整理过程的收集器,虚拟机就会使用这种方式分配内存
c.空闲列表。如果内存不规整,虚拟机需要维护一个列表, 记录哪些内存块可以使用,哪些内存块已经被使用,在分配时在列表中找到一块足够大小的空间分配给对象实例,并更新列表上的内容。
d.采用哪种方式分配内存,取决于垃圾收集器是否具备整理(compact)功能。
3)并发问题
a.采用cas配上失败重试保证操作的原子性。
b.每个线程预先分配TLAB。
4)初始化
为所有属性设置默认值。例如int类型变量设置默认值为0,String变量设置默认值为null等。
5)设置对象头
a.运行时元数据(MarkWord)。哈希值、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳
b.类型指针。指向类元信息,确定该对象所属的类型。
6)执行init方法进行初始化
a.显示初始化或代码块中初始化
b.构造器中初始化
c.初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。
7)小结
整理流程:加载类元信息->对象内存分配->处理并发问题->属性默认值初始化->(零值初始化)->设置对象头信息->属性显性初始化、代码块初始化、构造器初始化->实例化完成。
2、内存布局
(1)示例代码
public class Car {
private int price = 300000;
private String brand = "BMW";
private Plant plant;
public Car() {
this.plant = new Plant();
}
public static void main(String[] args) {
Car car = new Car();
}
}
public class Plant {
}
(2)内存布局图
3、对象访问
(1)句柄访问
(2)直接指针访问
十一、参考
[1]《The Java Virtual Machine Specification》Java SE 8 Edition
[2]《深入理解Java虚拟机》第二版、第三版
[3]《宋红康JVM教程》
[4]《Java并发编程艺术》
[5] 《JEP 122: Remove the Permanent Generation》
十二、最后
懂得不多,做得很少。如果文章有不足之处,欢迎留言讨论。
原创文章,转载请标明出处!https://www.cnblogs.com/boycelee/p/14095080.html