JVM
1. Java虚拟机
所谓虚拟机(Virtual Machine),就是一台虚拟的计算机。它是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机和程序虚拟机。
- 系统虚拟机:大名鼎鼎的Visual Box,VMware就属于系统虚拟机,它们完全是对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台。
- 程序虚拟机:典型代表就是Java虚拟机,它专门为执行单个计算机程序而设计,在Java虚拟机中执行的指令我们称为Java字节码指令。
java虚拟机作用
Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释编译为对应平台上的机器指令执行。每一条Java指令,Java虚拟机规范中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里。
特点
- 一次编译,到处运行
- 自动内存管理
- 自动垃圾回收功能
位置
JVM是运行在操作系统之上的,它与硬件没有直接的交互。

整体结构

java代码执行流程

jvm架构模型
由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
2. 类加载子系统

2.1 类加载子系统的作用

- 类加载器子系统负责从文件系统或者网络中加载class文件,class文件在文件开头有特定的文件标识。
- ClassLoader只负责class文件的加载,生成一个大的class实例对象。至于它是否可以运行,则由ExecutionEngine决定。
- 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
2.2 类的加载过程

2.2.1 加载阶段
- 1.通过一个类的全限定名获取定义此类的二进制字节流。
- 2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 3.在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口。
2.2.2 链接阶段
链接阶段又分为3个子阶段:验证、准备、解析。
2.2.2.1 验证阶段
- 目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
- 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
2.2.2.2 准备阶段
- 为类变量分配内存并且设置该类变量的默认初始值,即零值。
private static int a =1; - 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
- 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
2.2.2.3 解析阶段
- 将常量池内的符号引用转换为直接引用的过程。
- 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
- 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
- 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等。
2.2.3 初始化阶段
初始化阶段就是执行类构造器方法
()的过程。
此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
- 构造器方法中指令按语句在源文件中出现的顺序执行。
<clinit>()不同于类的构造器。(关联:构造器是虚拟机视角下的<init> ())若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit> ()已经执行完毕。- 虚拟机必须保证一个类的
<clinit>()方法在多线程下被同步加锁。

2.3 类加载器分类
- JVM支持两种类型的类加载器,分别为引导类加载器(BootstrapClassLoader)和自定义类加载器(User-Defined ClassLoader)。
- 从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于(继承)抽象类ClassLoader的类加载器都划分为自定义类加载器。
无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个,如下所示:

public static void main(String[] args) {
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader); //获取系统类加载器 sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader); //获取扩展类加载器 sun.misc.Launcher$ExtClassLoader@677327b6
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader); //获取引导类加载器 null c语言编写的,无法获取
ClassLoader classLoader = MyTeST.class.getClassLoader();
System.out.println(classLoader); //自定义类的类加载器 默认使用的就是系统类加载器 sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoader stringClassLoader = String.class.getClassLoader(); //java的核心类库都使用引导类加载器加载
System.out.println(stringClassLoader); //获取String类的类加载器 使用引导类加载器 null c语言编写的,无法获取
}
2.3.1 Bootstrap ClassLoader (启动类加载器、引导类加载器)
- 启动类加载器(引导类加载器,Bootstrap classloader)这个类加载使用c/C++语言实现的,嵌套在JVM内部。
- 它用来加载Java的核心库(JAVA_HOME/jre/ lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
- 并不继承自java.lang.classLoader,没有父加载器。
- 加载扩展类加载器和应用程序类加载器,并指定为他们的父类加载器。
- 出于安全考虑,Bootstrap启动类加载器只加载包名为java、 javax、sun等开头的类

public static void main(String[] args) {
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL urL : urLs) {
//获取引导类加载器能加载的所有目录
System.out.println(urL);
}
}
-----------------------------------------------------------------
file:/F:/workenvironment/java1.8/jre/lib/resources.jar
file:/F:/workenvironment/java1.8/jre/lib/rt.jar
file:/F:/workenvironment/java1.8/jre/lib/sunrsasign.jar
file:/F:/workenvironment/java1.8/jre/lib/jsse.jar
file:/F:/workenvironment/java1.8/jre/lib/jce.jar
file:/F:/workenvironment/java1.8/jre/lib/charsets.jar
file:/F:/workenvironment/java1.8/jre/lib/jfr.jar
file:/F:/workenvironment/java1.8/jre/classes
2.3.2 Extension classLoader (扩展类加载器)
- Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。派生(继承)于classLoader类
- 父类加载器为
启动类加载器(Bootstrap ClassLoader) - 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的
jre/lib/ext子目录(扩展目录)下加载类库。 - 如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。

public static void main(String[] args) {
//获取扩展类加载器加载类所在目录
String extDirs = System.getProperty("java.ext.dirs");
System.out.println(extDirs);
}
-----------------------------------------------------------------
F:\workenvironment\java1.8\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
2.3.3 AppclassLoader (系统类加载器,应用程序类加载器)
-java语言编写,由sun.misc.Launcher$AppclassLoader实现,派生于classLoader类.
- 父类加载器为扩展类加载器(Extension classLoader)
- 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
- 该类加载是
程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载 - 通过classLoader.getSystemclassLoader()方法可以获取到该类加载器
2.4 关于classLoader
获取classLoader的方法
public static void main(String[] args) throws ClassNotFoundException {
MyTesT myTesT = new MyTesT();
ClassLoader classLoader = myTesT.getClass().getClassLoader();
System.out.println(classLoader); //sun.misc.Launcher$AppClassLoader@18b4aac2
Class aClass = Class.forName("test.MyTesT");
ClassLoader classLoader1 = aClass.getClassLoader();
System.out.println(classLoader1); //sun.misc.Launcher$AppClassLoader@18b4aac2
//获取当前线程上下文的classLoader
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
System.out.println(contextClassLoader); //sun.misc.Launcher$AppClassLoader@18b4aac2
}
2.5 双亲委派机制
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
工作原理
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
3.如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

优点
- 避免类的重复加载。
- 包含程序安全,防止核心API被篡改
- 自定义java.lang.String不会被加载,加载的是核心类库中的String类。
2.6 沙箱安全机制
自定义string类,但是在加载自定义string类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\string.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的string类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
2.7 其他
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
- 类的完整类名必须一致,包括包名。
- 加载这个类的classLoader(指classLoader实例对象)必须相同。
换句话说,在JVM中,即使这两个类对象(class对象)来源同一个class文件,被同一个虚拟机所加载,但只要加载它们的classLoader实例对象不同,那么这两个类对象也是不相等的。
JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
什么时候使用类加载器
Java程序对类的使用方式分为:主动使用和被动使用。
主动使用,又分为七种情况:
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量赋值。
- 调用类的静态方法
- 反射(比如: class.forName ( "com.atguigu . Test") )
- 初始化一个类的子类
- Java虚拟机启动时被标明为启动类的类
JDK 7 开始提供的动态语言支持: - java . lang. invoke.MethodHandle实例的解析结果,REF getstatic、REF putstatic、REF_invokestatic句柄对应的类没有初始化,则初始化
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。
3. 运行时数据区及线程
红色区域一个进程共享一份(所有线程共享),灰色区域一个线程共享一份。
Java虚拟机定义了若千种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。
- 每个线程:独立包括程序计数器、栈、本地栈。
- 线程间共享:堆、堆外内存(永久代或元空间(可以理解为方法区)、代码缓存)

3.1 线程
- 线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行的执行。
- 在Hotspot JVM里,每个线程都与操作系统的本地线程
直接映射。 - 当一个Java线程准备好执行以后,此时一个操作系统的本地线程也同时创建。Java线程执行终止后,本地线程也会回收。
- 操作系统负责所有线程的安排调度到任何一个可用的cPU上。一旦本地线程初始化成功,它就会调用Java线程中的run ()方法。
3.2 程序计数器(PC 寄存器)
3.2.1 作用
PC寄存器用来存储指向下一条指令的地址,即将要执行的指令代码。由执行引擎读取下一条指令。

3.2.2 特点
-
它是一块很小的内存空间,几乎可以忽略不证。也是运行速度最快的存储区域。
-
在JVM规范中,每个线程都有它自己的程序计数器,是
线程私有的,程序计数器生命周期与线程的生命周期保持一致。 -
任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;或
-
如果是在执行native方法(本地方法,对应本地方法栈),则是未指定值(undefned) 。native方法:调用c语言编写的程序时需要使用。
-
它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。|
-
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
-
它是唯一一个在Java 虚拟机规范中没有规定任何OutOtMemoryError情况的区域。
GC OOM
- PC寄存器没有垃圾回收机制。
- PC寄存器没有OOM(内存溢出溢出)。
3.2.3 示例
public static void main(String[] args) {
int i=10;
int j=20;
int k=i+j;
String s="hello";
System.out.println(k);
System.out.println(s);
}

3.2.4 执行步骤
- pc寄存器存放指令地址
- 执行引擎根据pc寄存器的指令地址判断接下来该执行哪条指令。
- 执行引擎操作局部变量表,操作数栈执行指令。
- 将指令翻译为机器指令,交给CPU运行。
3.2.5 PC寄存器的两个问题
使用PC寄存器存储字节码指令地址有什么用呢?
为什么使用PC寄存器记录当前线程的执行地址呢?
因为在并发情况下,CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。JVM的字节码解释器就需要通过改变Pc寄存器的值来明确下一条应该执行什么样的字节码指令。
PC寄存器为什么会被设定为线程私有?
- 我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?
- 为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个Pc寄存器和栈帧,这样一来各个线程之间便可以进行独立计算,从而不会出现相互千扰的情况。
4. 虚拟机栈
- 由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。
- 优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
4.1 虚拟机栈概述
栈是运行时的单位,而堆是存储的单位。
- 即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据
- 堆解决的是数据存储的问题,即数据怎么放、放在哪儿。

Java虚拟机栈是什么?
Java虚拟机栈(Java virtual Machine stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(stack Frame),对应着一次的Java方法调用,是线程私有的。
生命周期
生命周期和线程一致。
作用
- 主管Java程序的运行,它保存方法的局部变量(8中基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。
栈的特点(优点)
- 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
JVM直接对Java栈的操作只有两个:
- 每个方法执行,伴随着进栈(入栈、压栈)执行结束后的出栈工作
GC OOM
- 对于栈来说不存在垃圾回收问题。
- 存在OOM,会存在栈溢出异常。
栈中可能存在的异常
- Java虚拟机规范允许java栈的大小是动态的或者是固定不变的。
- 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个stackoverflowError异常。
- 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法中请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个outofMemoryError异常。
public static void main(String[] args) {
main(args);
}
-----------------------------------------
Exception in thread "main" java.lang.StackOverflowError
设置栈内存大小
我们可以使用参数-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。
默认栈大小

通过-Xss调整栈空间大小



4.2 栈的存储单位
虚拟机栈中存储什么
- 每个线程都有自己的栈,栈中的数据都是以栈帧(stack Frame)为基本单位存在。在这个线程上正在执行的每个方法都各自对应一个栈帧(stack Frame)。栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
- JVM直接对Java栈的操作只有两个,就是对栈帧的
压栈和出栈,遵循“先进后出”/“后进先出”原则。
在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(CurrentMethod),定义这个方法的类就是当前类(current class)
- 执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
- 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。
特点
-
不同线程中所包含的栈帧是不允许存在相互引用的(栈空间是线程私有的),即不可能在一个栈帧之中引用另外一个线程的栈帧。
-
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
-
方法的结束方式分为两种,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
4.2.1 栈帧的内部结构
每个栈帧中存储着:
- 局部变量表(Local variables)
- 操作数栈(operand stack)(或表达式栈)
- 动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
- 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
- 一些附加信息
4.3 局部变量表
- 局部变量表也被称之为局部变量数组或本地变量表
- 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference) ,以及returnAddress类型。
- 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
- 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。



public static void main(String[] args) {
Test3 test3 = new Test3();
int i=10;
test3.method1();
}


关于slot的理解
参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。
-
局部变量表,最基本的存储单元是
slot(变量槽) -
局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。
-
在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot。
- byte 、 short 、 char在存储前被转换为int,boolean也被转换为int,0表示false,非0 表示true。
- long和double 则占据两个slot。
JVM会为局部变量表中的每一个slot(可以看做每一行)都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。
- 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会
按照顺序(main方法第一个参数是形参args)被复制到局部变量表中的每一个slot上。

如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问long或double类型变量)
public static void main(String[] args) {
Test3 test3 = new Test3();
long l=10L; //占两个索引
int i=10;
}

如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。
//实例方法,不加static
public void method1() {
long l=10L;
int i=10;
}


slot的重复利用
栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
public void method1() {
long l=10L;
int i=10;
{
int m=0;
m=i+1;
}
// k会占用m的slot位置
int k=50;
}

补充
- 在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
- 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
4.4 操作数栈
定义
每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出(Last-In-First-out)的操作数栈,也可以称之为表达式栈(Expression stack) 。
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop)。
某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈。
比如:执行复制、交换、求和等操作。

特点
-
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中
变量临时的存储空间。 -
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
-
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的code属性中,为max_stack的值。

-
栈中的任何一个元素都是可以任意的Java数据类型。
- 32bit的类型占用一个栈单位深度
- 64bit(long double)的类型占用两个栈单位深度
-
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据访问。
-
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新pc寄存器中下一条需要执行的字节码指令。
-
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。
-
另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
代码示例
public void method3() {
//byte、short、char、boolean、int在数组中都以int类型来保存
byte i=15;
int j=8;
int k =i+j;
}

面试
i++ 和 ++i 的区别
public static void method4() {
int i=10;
int i1=i++; //10
int i2=++i; //12
int i3=10;
i3=++i3; //11
int i4=10;
i4=i4++; //10
int i5=10; //12
int i6 =i5++ + ++i5; //22
}

4.5 方法返回地址

- 存放调用该方法的pc寄存器的值。一个方法的结束,有两种方式:
- 正常执行完成
- 出现未处理的异常,非正常退出
- 无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
4.10 虚拟机栈的面试题
举例栈溢出的情况?
- 方法自己调用自己,递归的时候可能会出现。
public static void main(String[] args) {
main(args);
}
调整栈大小,就能保证不出现溢出吗?
- 不能保证,如果是递归方法出现问题,一直递归,调整栈大小还是会溢出,只是把报错的时间后延而言。
分配的栈内存越大越好吗?
- 不是,jvm虚拟机内存是固定的,栈内存分配的空间大的话,堆内存和其他区域空间就会相对减少。
垃圾回收是否会涉及到虚拟机栈?
- 不会,虚拟机栈满了的话,再进入方法就会报StackOverFlowError,不会用到垃圾回收机制。
方法中定义的局部变量是否线程安全?
- 不一定
- 如果该局部变量在该方法内部消耗,即没有将该变量返回,是线程安全的。
- 如果该局部变量经过方法处理后返回出去,就不能保证之后该变量还是线程安全的。
5. 本地方法
什么是本地方法
- 简单地讲,一个Native Method就是一个Java调用非Java代码的接口。一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如c。
- 这个特征并非Java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern "c"告知C++编译器去调用一个c的函数工"Anative method is a Java method whose implementation isprovided by non-java code . "
- 在定义一个native method时,并不提供实现体(有些像定义一个Javainterface) ,因为其实现体是由非java语言在外面实现的。
- 本地接口的作用是融合不同的编程语言为Java所用,它的初衷是
融合C/C++程序。
5.1 本地方法栈
- Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
- 本地方法栈,也是线程私有的。
- 允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)
- 如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个stackoverflowError异常。
- 如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个 outofMemoryError异常。
- 本地方法是使用
c语言实现的。 - 它的具体做法是Native Method stack中登记native方法,在Execution Engine执行时加载本地方法库。
6. 堆空间(重要)
6.1 堆的核心概述
- 一个进程对应一个JVM实例,对应一个运行时数据区Runtime,对应一个堆和一个方法区。由多个线程共享。
- 一个进程可以包含多个线程。
- 每个线程都拥有自己的虚拟机栈、pc寄存器、本地方法栈。
特点
- 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
- Java 堆区在
JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。堆内存的大小(启动前)是可以调节的。 - 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
- 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区( ThreadLocal Allocation Buffer,TLAB)。
- 数组和对象
可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。 - 在方法结束后,堆中的对象
不会马上被移除,仅仅在垃圾回收的时候才会被移除。(gc线程启动时,main线程需要停止。频繁gc会影响用户线程性能。) - 堆,是Gc ( Garbage collection,垃圾收集器)执行垃圾回收的重点区域。

6.2 设置堆内存大小及OOM
新建两个进程
public class test1 {
public static void main(String[] args) {
System.out.println("test1启动....");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test1结束....");
}
}
-------------------------------------------------
public class test2 {
public static void main(String[] args) {
System.out.println("test2启动....");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test2结束....");
}
}
6.2.1 设置堆内存大小
设置初始堆空间大小和最大堆空间大小
- test1设置为10M
- test2设置为20M

启动main方法
- 启动jdk自带工具



-Xms10m -Xmx10m -XX:+PrintGCDetails
- 设置堆空间内存初始值和最大值,打印gc垃圾回收详情
public class HeapDemo {
private String name;
public HeapDemo(String name) {
this.name = name;
}
public static void main(String[] args) {
HeapDemo demo1 = new HeapDemo("张三");
HeapDemo demo2 = new HeapDemo("李四");
int[] ints = new int[10];
Object[] objects = new Object[20];
}
}

堆空间大小的设置
- Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,大家可以通过选项"-Xmx"和"-Xms"来进行设置。(设置的只是新生代和老年代的大小,不包括元空间)
- “-Xms"用于表示堆区的起始内存,等价于一XX: InitialHeapsize
- “-Xmx"则用于表示堆区的最大内存,等价于-XX:MaxHeapsize
- 一旦堆区中的内存大小超过“-Xmx"所指定的最大内存时,将会抛出outOfMemoryError异常。
通常会将-Xms和一Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。- 默认情况下,初始内存大小:物理电脑内存大小/ 64
- 最大内存大小:物理电脑内存大小/ 4
public static void main(String[] args) {
long totalMemory = Runtime.getRuntime().totalMemory()/1024/1024;
long maxMemory = Runtime.getRuntime().maxMemory()/1024/1024;
System.out.println("默认堆空间总大小"+totalMemory+"MB");
System.out.println("默认堆空间最大堆内存"+maxMemory+"MB");
System.out.println("系统内存大小为:"+maxMemory*4.0/1024+"GB");
}
--------------------------------------------------------------------
默认堆空间总大小121MB
默认堆空间最大堆内存1787MB
系统内存大小为:6.98046875GB
6.2.2 OOM
程序模拟OOM
public class HeapSpaceDetail {
public static void main(String[] args) {
ArrayList<Object> list = new ArrayList<>();
while (true){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(new OomTest(new Random().nextInt(1024*1024)));
}
}
}
------------------------------------------------------------------
class OomTest {
private byte[] oom;
public OomTest(int oom) {
this.oom = new byte[oom];
}
}


通过抽样器查看内存占用情况
- 根据什么类型实例占用内存最多,分析出现问题的情况。

6.3 年轻代和老年代
存储在JVM中的Java对象可以被划分为两类:
- 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
- 另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致。
Java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(oldGen)其中年轻代又可以划分为Eden(伊甸园区)空间、Survivor0(幸存者0区)空间和survivor1(幸存者1区)空间(有时也叫做from区、to区) 。

配置新生代与老年代在堆结构的占比。
- 默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
- 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
开发中一般情况不会调这些参数。如果明确知道生命周期较长的对象占比比较大,可以将老年区空间调大。

配置新生代中伊甸园区和幸存者区内存占比
- 在HotSpot中,Eden空间和另外两个survivor空间缺省所占的比例是8:1:1
- 开发人员可以通过选项“一XX:survivorRatio”调整这个空间比例。
- 比如-XX :SurvivorRatio=8
- 几乎所有的Java对象都是在Eden区被new出来的。绝大部分的Java对象的销毁都在新生代进行了。
- IBM公司的专门研究表明,新生代中 80%的对象都是“朝生夕死”的。可以使用选项"-Xmn"设置新生代最大内存大小。
这个参数一般使用默认值就可以了。
6.4 图解对象的一般分配过程
概述
为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑Gc执行完内存回收后是否会在内存空间中产生内存碎片。
过程
-
- new的对象先放伊甸园区。此区有大小限制。
- 2.当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor Gc),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区。
- 3.然后将伊甸园中的剩余对象移动到幸存者0区。
- 幸存者0区满的话不会触发YGC,伊甸园区满的时候触发YGC,将伊甸园区和幸存者0区进行回收。将还需使用对象存放到幸存者1区(幸存者0到幸存者1,年龄计数加1。)

- 4.如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。

- 5.如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
- 6.啥时候能去养老区呢?可以设置次数。默认是15次。可以设置参数:-XX:MaxTenuingThreshold=
进行设置。

- 7.在养老区,相对悠闲。当养老区内存不足时,再次触发GC: Major GC,进行养老区的内存清理。
- 8.若养老区执行了Major Gc之后发现依然无法进行对象的保存,就会产生ooM异常
总结
- 针对幸存者S0,s1区的总结:复制之后有交换,谁空谁是to(幸存者0或幸存者1)。
- 关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集。

6.5 GC

7. 方法区(重要)

7.1 栈、堆、方法区的交互关系
新建一个对象

7.2 方法区的理解
《Java虛拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”
- 但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap (非堆),目的就是要和堆分开。
- 所以,方法区看作是一块独立于Java堆的内存空间。
特点
-
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。
-
方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
-
方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
-
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虛拟机同样会抛出内存溢出错误: java. lang . outofMemoryError:PermGen space 或者java. lang . OutOfMemoryError: Metaspace
- 加载大量的第三方的jar包; Tomcat部署的工程过多(30-50个) ;大量动态的生成反射类
-
关闭JVM就会释放这个区域的内存。

浙公网安备 33010602011771号