一、生命周期与内存
1.1 jvm生命周期
-
虚拟机的启动:
- Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的。
-
虚拟机的执行:
- 一个运行中的Java虛拟机有着一个清晰的任务:执行Java程序。
- 程序开始执行时他才运行,程序结束时他就停止。
- 执行一个所谓的Java程序的时候,真真正正在执行的是一个叫做Java虚拟机的进程。
-
虚拟机的退出
- 有如下的几种情况:
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止
- 某线程调用Runtime类或System类的exit方法,或Runtime类的halt方法,并且Java安全管理器也允许这次exi t或halt操作。
- 除此之外,JNI ( Java Native Interface )规范描述了用JNI Invocation API来加载或卸载Java虚拟机时,Java虚拟机的退出情况。
- 有如下的几种情况:
1.2 内存结构概述
1.2.1 概述类的加载器和类加载过程
-
类加载子系统作业
- 类加载器子系统负责从文件系统或看网络中加载class文件,class文件在文件开头有特定的文件标识。
- ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
- 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字而量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
-
类加载器ClassLoader角色:
-
-
class file 存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中来根据这个文件实例化出n个一模一样的实例。
-
class file 加载到JVM中,被称为DNA元数据模板,放在方法区。
-
在class文件->JVM->最终成为元数据模板,此过程就要一个运输工具(类装载器Class Loader) ,扮演一个快递员的角色。
-
-
类的加载过程:
-
Loading(加载)
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象, 作为方法区这个类的各种数据的访问入口
-
Linking(链接)
- 验证(Verify)
- 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
- 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
- 准备(Prepare)
- 为类变量分配内存开且设置该类变量的默认初始值,即零值。
- 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
- 这里不会为实例变量分配初始化,类变量会@在方法区中,而实例变量是会随着对象一起分配到Java堆中。
- 解析(Resolve)
- 将常量池内的符号引用转换为直接引用的过程。
- 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
- 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的Class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
- 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、 CONSTANT Fieldref info、 CONSTANT Methodref info等。
- 验证(Verify)
-
初始化(Initialization)
-
初始化阶段就是执行类构造器方法()的过程。
-
此方法不需定义,罪javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
-
以下是一个示例:
-
public class InitializationTest { public static int num = 1; static { num = 2; } public static void main(String[] args) { System.out.println(num); } }
-
使用jclasslib查看过程:
-
-
构造器方法中指令按语句在源文件中出现的顺序执行。
-
有如下代码:
-
public class InitializationTest { public static int num = 1; static { num = 2; number = 20; System.out.println(num); //注意这里报错,非法向前引用 //System.out.println(number); } //linking的prepare阶段初始化为0值,在initialization阶段执行为结果为10 public static int number = 10; public static void main(String[] args) { System.out.println(num); System.out.println(number); } }
-
num和number执行结果为2和10
-
jclasslib查看如下
-
-
需要注意,
没有静态代码块或者静态变量,就没有<clinit>()过程
. -
()不同于类的构造器。(关联:构造器是虚拟机视角下的() )
-
若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕。
-
虚拟机必须保证一个类的()方法在多线程下被同步加锁。保证这个类只被加载一次
-
-
1.2.2 类加载器分类
- JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader) 。
- 从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。
- 无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个,如下所示:
- Bootstrap ClassLoader是一类,其他是一类
这里的四者之间的关系是包含关系。不是上层下层,也不是子父类的继承关系
。
有如下代码:
public class ClassLoaderTest {
public static void main(String[] args) {
//系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@dad5dc
//扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@ed5528
//引导类加载器,获取不到
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader);//null
//对于用户自定义类
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@dad5dc
//可以看到用户自定义类默认使用系统类加载器加载
//看看String,结果为null,间接证明String使用引导类加载器加载--->java核心类库由引导类加载器加载
ClassLoader stringClassLoader = String.class.getClassLoader();
System.out.println(stringClassLoader);//null
}
}
1.2.3 类加载器介绍
-
虚拟机自带的加载器:
-
启动类加载器(引导类加载器,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等开头的类
- 这个类加载使用
-
扩展类加载器(Extension ClassLoader)
Java语言编写
,由sun.misc.Launcher$ExtClassLoader实现。派生于ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。
如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载
。
-
应用程序类加载器(系统类加载器,AppClassLoader)
- java语言编写,由sun.misc.Launcher$AppClassLoader实现
- 派生于ClassLoader类
- 父类加载器为扩展类加载器
- 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库.
- 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
- 通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器
-
-
以下是一个示例:
-
public class ClassLoaderTest1 { public static void main(String[] args) { //获取BootstrapClassLoader能够加载的api的路径 URL[] urLs = Launcher.getBootstrapClassPath().getURLs(); for (URL urL : urLs) { System.out.println(urL); } System.out.println("====================="); //获取扩展类加载器能够加载的api的路径 String extDirs = System.getProperty("java.ext.dirs"); for (String path : extDirs.split(";")) { System.out.println(path); } } } //运行结果如下: /* file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_73/jre/lib/resources.jar file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_73/jre/lib/rt.jar file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_73/jre/lib/sunrsasign.jar file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_73/jre/lib/jsse.jar file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_73/jre/lib/jce.jar file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_73/jre/lib/charsets.jar file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_73/jre/lib/jfr.jar file:/C:/Program%20Files%20(x86)/Java/jdk1.8.0_73/jre/classes ===================== C:\Program Files (x86)\Java\jdk1.8.0_73\jre\lib\ext C:\WINDOWS\Sun\Java\lib\ext */
-
-
用户自定义类加载器
-
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。
-
为什么要使用自定义类加载器
- 隔离加载类
- 修改类加载的方式
- 扩展加载源
- 防止源码泄漏
-
实现步骤:
- 开发人员可以通过继承抽象类java. lang. ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
- 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass()方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中
- 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承.URLClassLoader类,这样就可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。
-
示例:
-
public class CustomClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] result = getClassFromCustomPath(name); if (result == null) { throw new FileNotFoundException(); } else { return defineClass(name, result, 0, result.length); } } catch (FileNotFoundException e) { e.printStackTrace(); } return super.findClass(name); } private byte[] getClassFromCustomPath(String name) { //从自定义路径加载指定类,细节略 //如果指定路径的字码文件进行了加密,则需要在此方法中进行解密操作。 return null; } }
-
-
1.2.4 关于ClassLoader
- ClassLoader类,它是-一个抽象类,其后所有的类加载器都继承自ClassLoader (不包括启动类加载器)
方法名 | 描述 |
---|---|
getParent() | 返回该类加载器的超类加载器 |
loadClass(String name) | 加载名称为name的类,返回结果为java.lang.Class类的实例 |
findClass(String name) | 查找名称为name的类,返回结果为java.lang .Class类的实例 |
findLoadedClass(String name) | 查找名称为name的已经被加载过的类,返间结果为java.lang .Class类的实例 |
defineClass(String name, byte[] b,int off,intlen) | 把字节数组b中的内容转换为一个Java类,返回结果为java.lang.Class类的实例 |
resolveClass(Class<?> c) | 连接指定的一个Java类 |
- 获取ClassLoader途径
方式一:获取当前类的ClassLoader |
---|
clazz.getClassLoader() |
方式二:获取当前线程上下文的ClassLoader |
Thread.currentThread().getContextClassLoader() |
方式三:获取系统的ClassLoader |
ClassLoader.getSystemClassLoader() |
方式四:获取调用者的ClassLoader |
DriverManager.getCallerClassLoader() |
1.2.5 双亲委派机制
Java虚拟机对class宋件采用的是
按需加载
的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java 虚拟机采用的是双亲委派模式
,即把请求交由父类处理,它是一种任务委派模式。
-
在说原理之前,先看一个案例:
-
//我们新建一个java.lang包,故意和jdk里面的String冲突 package java.lang public class String { static { System.out.println("我是自定义String类的静态代码块"); } }
-
然后测试
-
public class StringTest { public static void main(String[] args) { java.lang.String str = new java.lang.String(); } }
-
发现String类中的静态代码块没有执行,说明还是使用的jdk的Sting
-
-
所以,为了保护项目不会因为这个原因挂掉,我们就引入了双亲委派机制
-
工作原理:
- 如果一个类加载器收到了类加载请求,它不会自己先去加载,而是把这个请求委托给父类的加载器去执行
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终到达项层的启动类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会
尝试自己去加载,这就是双亲委派模式。
-
可以看出我们自定义的String并没有被加载,怎么证明呢,我们看下面代码:
-
package java.lang public class String { static { System.out.println("我是自定义String类的静态代码块"); } public static void main(String[] args) { System.out.println("hello string"); } }
-
我们在自定义String方法(包名要为java.lang)里面,执行main后,会报错
-
-
特别提醒:java.lang包下,尽管定义不和jdk冲突的类,会抛出
安全异常
,阻止我们定义在这个包下
-
-
这就很好地证明了双亲委派机制
- 优势:
- 避免类的重复加载
- 保护程序安全,防止核心API被随意篡改
- 优势:
-
-
沙箱安全机制
- 自定义string类,但是在加载自定义string类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是
沙箱安全机制
。
- 自定义string类,但是在加载自定义string类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是
1.2.6 其他
- 在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类的方式都被看作是对类的被动使用,都不会导致类的初始化。
- 主动使用,又分为七种情况
二、运行时数据区
内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行。不同的JVM对于内存的划分方式和管理机制存在着部分差异。结合JVM虚拟机规范,来探讨一下经典的JVM内存布局。
- Java虚拟机定义了若千种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对 应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。
- 每个线程:独立包括程序计数器、栈、本地栈。
- 线程间共享:堆、堆外内存(永久代或元空间、代码缓存)(垃圾回收95%在堆区,5%在栈区)
2.2 程序计数器(PC寄存器)
2.2.1 PC寄存器介绍
JVM中的程序计数寄存器(Program Counter Register) 中,Register 的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有 把数据装载到寄存器才能够运行。这里,并非是广文上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子), 并且也不容易引起一些不必要的误会。
JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟
。
- 作用:PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
- 它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域。
- 在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
- 任何时间一个线程都只有一个方法在执行,也就是所谓的
当前方法
。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;或者,如果是在执行native方法,则是未指定值(undefned) - 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
- 它是唯一一个在Java虚拟机规范中没有规定任何OutofMemoryError
情况的区域。
2.2.2 举例说明
- 代码示例:
public class PCTest {
public static void main(String[] args) {
int i = 1;
int j = 2;
int k = i + j;
String s = "abc";
System.out.println(s);
System.out.println(k);
}
}
- javap -v PCTest.java反编译后结果如下
- 其中,前面的序号为指令地址(偏移地址),后面的为操作指令
2.2.3 两个常见问题
-
使用PC寄存器存储字节码指令地址有什么用呢?
- 因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
- JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令
-
PC寄存器为什么会被设定为线程私有?。
- 我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
- 由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。
2.3 虚拟机栈
2.3.1 虚拟机栈概述
-
由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器
-
优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令
。 -
栈是运行时的单位,而堆是存储的单位。
- 即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。
- 堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
-
JAVA虚拟机栈是什么?
- Java虚拟机栈(Java Virtual Machine Stack) ,早期也叫Java栈。每个线程在创建时都会创建一个虛拟机栈,其内部保存一个个的栈帧(Stack Frame) ,对应着一次次的Java方法调用。
- 是线程私有的
- 生命周期和线程一致
- 主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回
-
栈中可能出现的异常:
- Java 虚拟机规范允许Java栈的大小是动态的或者是固定不变的。
- 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将 会抛出一个
StackOverflowError异常。 - 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈那Java虚拟机将会抛出一个OutOfMemoryError异常。
- 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将 会抛出一个
- Java 虚拟机规范允许Java栈的大小是动态的或者是固定不变的。
-
设置栈内存的大小
- 我们可以使用参数
-Xss
选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。 - 如
-Xss256k
- 我们可以使用参数
2.3.2 栈的存储单位
-
每个线程都有自己的栈,栈中的数据都是以
栈帧(Stack Frame)的格式存在
。 -
在这个线程上正在执行的
每个方法都各自对应一一个栈帧(Stack Frame)
。 -
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
-
JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。
-
在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame) ,与当前栈帧相对应的方法就是当前方法(Current
Method) ,定义这个方法的类就是当前类(Current Class) -
执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
-
如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。
2.3.4 栈的内部结构
- 每个栈中存储着
局部变量表
(Local variables)操作数栈
(operand Stack) (或表达式栈)动态链接
(Dynamic Linking) ( 或指向运行时常量池的方法引用) .方法返回地址
(Return Address) (或方法正常退 出或者异常退出的定义)一些附加信息
2.3.4.1 局部变量表
-
概述
- 局部变量表也被称之为局部变量数组或本地变量表
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量
,这些数据类型包括各类基本数据类型、对象引用(reference) ,以及returnAddress类型。- 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此
不存在数据安全问题
局部变量表所需的容量大小是在编译期确定下来的
,并保存在方法的Code属性的maximum local variables数据项中。 在方法运行期间是不会改变局部变量表的大小的。- 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。
- 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
-
关于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都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
-
当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会
按照顺序
被复制到局部变量表中的每一个Slot上 -
如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可
。(比如:访问1ong或double类型变量) -
如果当前帧是由
构造方法或者实例方法创建的
,那么该对象引用this将会存放在index为0的slot处
,其余的参数按照参数表顺序继续排列。- 所以静态方法是不能调用this的
-
栈帧中的局部变量表中的槽位是可以
重用的
,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。-
例如,有以下代码:
-
public void test4(){ int a = 0; { int b = 0; b = a+1; } int c = a+1; }
-
-
我们可以看到以下信息
-
我们发现只有三个变量(即槽数)我们看看本地变量表,应该是四个变量呀(this,a,b,c)为什么呢
-
我们发现,b的起始PC加长度,不等于13,而是8,说明b在大括号之外就挂了,但是此时数组已经开辟,所以此时声明c变量时,c就重复利用b开辟的slot空间,所以上面的最大槽数才会是3
-
-
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
-
2.3.4.2 操作数栈
-
每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的
操作数栈
,也可以称之为表达式栈
(Expression Stack) -
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop)。
- 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈。
- 比如:执行复制、交换、求和等操作
-
操作数栈,
主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
-
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,
这个方法的操作数栈是空的
。 -
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_stack的值。
-
栈中的任何一个元素都是可以任意的Java数据类型。
- 32bit的类型占用一个栈单位深度
- 64bit的类型占用两个栈单位深度操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据访问。
-
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一"条需要执行的字节码指令。
-
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。
-
另外,我们说Java虚拟机的
解释引擎是基于栈的执行引擎
,其中的栈指的就是操作数栈。 -
代码示例:
-
public class OperateStackTest { public void testAddOperation() { byte i = 15; int j = 8; int k = i + j; } }
-
字节码指令如下
-
解析
- bipush 15(
范围在-128~127为bipush,-32768~32767为sipush,-2147483648~2147483647为ldc
):将15压入操作数栈 - istore_1:出栈操作,将15取出,放入局部变量表中1的位置(非静态方法0的位置放的是this)
- bipush 8:将8压入操作数栈
- istore_2:出栈操作,将8取出,放入局部变量表中2的位置
- iload_1,iload_2:将局部变量表里面索引为1,2位置的15和8取出
放入操作数栈
- iadd:将15和8相加(执行引擎翻译成机器指令交给CPU执行)
- istore_3:将结果23放入局部变量表中1的位置
- bipush 15(
-
我们使用了操作数栈的2个空间,使用了局部变量表的四个空间(算上this),所以,如下图,操作数栈的最大深度为2,局部变量表最大槽数为4
-
2.3.4.3 动态链接
-
每一个栈帧内部都包含一个指向
运行时常量池
中该栈帧所属方法的引用
包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)
。比如: invokedynamic指令 -
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用( Symbolic Reference) 保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么
动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
。 -
静态链接:
- 当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。
-
动态链接:
- 如果被调用的
方法在编译期无法被确定下来
,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。
- 如果被调用的
-
对应的方法的绑定机制为:早期绑定(Early Binding)和晚期绑定(Late Binding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
- 早期绑定:早期绑定就是指被调用的目标方法如果在
编译期可知
,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目。标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。 - 如果被调用的方法在
编译期无法被确定下来
,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。
- 早期绑定:早期绑定就是指被调用的目标方法如果在
-
虚方法与非虚方法
- 如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的这样的方法称为非虚方法。
- 静态方法、私有方法、final方法、实例构造器、父类方注都是非虚方法。
- 其他方法称为虚方法。
- 子类对象的多态性的使用前提:①类的继承关系②方法的重写
-
虚拟机提供了以下几条方法调用指令:
-
普通调用指令:
- invokestatic: 调用静态方法,解析阶段确定唯一方法版本(非虚方法)
- invokespecial: 调用方法、 私有及父类方法,解析阶段确定唯一方法版本(非虚方法)
- invokevirtual: 调用所有虚方法
- invokeinterface: 调用接口方法
-
动态调用指令
- invokedynamic:动态解析出需要调用的方法,然后执行
- JVM字节码指令集一直比较稳定,一直到Java7中才增加了一个invokedynlmic指令,这是
Java为了实现「动态类型语言」支持而做的一种改进
。- 动态类型语言和静态类型语言两者的区别就在于对
类型的检查
是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。 - 说的再直白一点就是,静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。
- 动态类型语言和静态类型语言两者的区别就在于对
- 但是在Java7中并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令。
直到Java8的Lambda表达式的出现,invokedynamic指令的生成,在Java中才有 了直接的生成方式
。 - Java7中增加的动态语言类型支持的本质是对Java虚拟机规范的修改,而不.是对Java语言规则的修改,这一块相对来讲比较复杂, 增加了虚拟机中的方法调用,最直接的受益者就是运行在Java平台的动态语言的编译器。
- JVM字节码指令集一直比较稳定,一直到Java7中才增加了一个invokedynlmic指令,这是
- invokedynamic:动态解析出需要调用的方法,然后执行
-
前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指 令则支持由用户确定方法版本。其中
invokestatic指令和invokespecial指令调用的方法称为非虛方法,其余的(final修饰的除外)称为虚方法
。 -
虚方法表:
-
在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,为了提高性能,JVM采用在类的方法区建立一个
虚方法表(virtual method table) (非虚方法不会出现在表中)来实现。使用索引表来代替查找
。 -
每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
-
那么虚方法表什么时候被创建?
- 虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。
-
-
2.3.4.4 方法返回地址
- 存放调用该方法的pc寄存器的值。
- 一个方法的结束,有两种方式:
- 正常执行完成
- 出现未处理的异常,非正常退出
- 无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,
即调用该方法的指令的下一条指令的地址
。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
2.4 本地方法接口
//内容较少,就放这里讲述
-
什么是本地方法
- 简单地讲,一个Native Method就是一个Java调用非Java代码的接口。一个Native Method是这样 一个Java方法:该方法的实现由非Java语言实现,比如C。这个特征并非Java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern '"C"告知C++编译器去调用一个c的函数。
- “A native method is a Java method whose implementation isprovided by non-java code.”
- 在定义一一个native method时,并不提供实现体(有些像定义一个Java interface),因为其实现体是由非java语言在外面实现的。
- 本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序。
-
为什么要使用Native Method
-
Java使用起来非常方便,然而有些层次的任务用Java实现起来不容易,或者我们对程序的效率很在意时,问题就来了。
-
与Java环境外交互:
有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因
。你可以想想Java需要与一些底层系统,如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解Java应用之外的繁琐的细节。
-
与操作系统交互
- JVM支持着Java语言本身和运行时库,它是Java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎样,它毕竟不是一一个完整的系统,它经常依赖于一些底层系统的支持。这些底层系统常常是强大的操作系统。
通过使用本地方法,我们得以用Java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用c写的
。还有,如果我们要使用一些Java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。
- JVM支持着Java语言本身和运行时库,它是Java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎样,它毕竟不是一一个完整的系统,它经常依赖于一些底层系统的支持。这些底层系统常常是强大的操作系统。
-
Sun’ s Java
Sun的解释器是用C实现的,这使得它能像一些普通的C一 样与外部交互
。jre大部分是用Java实现的,它也通过一些本地方法与外界交互。 例如:类java. lang. Thread的setPriority() 方法是用Java实现的,但是它实现调用的是该类里的本地方法setPriority0()。这个本地方法是用C实现的,并被植入JVM内部,在windows 95 的平台上,这个本地方法最终将调用win32 SetPriority() API。 这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库
(external dynamic link library) 提供,然后被JVM调用。
-
2.5 本地方法栈
-
Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
-
本地方法栈,也是线程私有的。
-
允许被实现成固定或者是可动态扩展的内存大小。( 在内存溢出方面是相同的)
- 如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java 虚拟机将会抛出一一个stackoverflowError异常。
- 如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虛拟机将会抛出一个OutofMemoryError 异常。
-
本地方法是使用C语言实现的。
-
它的具体做法是Native Method Stack中 登记native方法,在Execution Engine 执行时加载本地方法库。
-
当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虛拟机拥有同样的权限
。- 本地方法可以通过本地方法接口来
访问虛拟机内部的运行时数据区
。 - 它甚至可以直接使用本地处理器中的寄存器
- 直接从本地内存的堆中分配任意数量的内存。
- 本地方法可以通过本地方法接口来
-
并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等
。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。 -
在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一。
2.6 堆
2.6.1 堆的核心概述
- 一个JVM实例只存在一一个堆内存,堆也是Java内存管理的核心区域。
- Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
- 堆内存的大小是可以调节的。
- 《Java虚拟机规范》规定,堆可以处于
物理上不连续
的内存空间中,但在逻辑上它应该被视为连续
的。 - 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(ThreadLocal Allocation Buffer,TLAB)
- 《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。(The heap is the run-time data area from which memory for all class instances and arrays is allocated )
- 我要说的是:“几乎”所有的对象实例都在这里分配内存。一从实际使用角度看的。
- 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
- 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
- 堆,是GC (Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。
2.6.2 内存细分
-
现代垃圾收集器大部分都基3 F分代收集理论设计,堆空间细分为:
- Java 7及之前堆内存逻辑上分为三部分:新生区+养老区+永久区
- Young Generation Space 新生区 Young/New
- 又被划分为Eden区和Survivor区
- Tenure generation space 养老区 old/ Tenure
- Permanent Space 永久区 Perm
- Young Generation Space 新生区 Young/New
- Java 8及之后堆内存逻辑上分为三部分:新生区+养老区+元空间
- Young Generation Space 新生区 Young/New
- 又被划分为Eden区和Survivor区
- Tenure generation space 养老区 old/ Tenure
- Meta Space 元空间 Meta
- Young Generation Space 新生区 Young/New
- Java 7及之前堆内存逻辑上分为三部分:新生区+养老区+永久区
-
设置堆空间大小10m:
-Xms 10m
- -X: 是JVM的运行参数
- -Xms: 用来设置堆空间(年轻代+老年代)的初识内存大小
- ms: 是memory start
- 默认是物理电脑内存大小的1/64
- -Xmx: 用来设置堆空间(年轻代+老年代)的最大空间大小
- 默认是物理电脑内存大小的1/4
- 手动设置:
-Xms600m
-Xmx600m
- 开发中建议将初始堆内存大小和最大堆内存大小设置为相同的值
2.6.3 年轻代与老年代
-
存储在JVM中的Java对象可以被划分为两类:
- 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
- 另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致。
-
Java堆区进一步细分的话, 可以划分为年轻代( YoungGen)和老年代(OldGen)
-
其中年轻代又可以划分为Eden空间、Survivor0空间和Survivor1空间(有时也叫做from区、to区)
-
默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3(默认就是这个)
-
可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
-
在HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1当然开发人员可以通过选项"-XX:SurvivorRatio"调整这个空间比例。比如
-
几乎所有的Java对象都是在Eden区被new出来的。
-
绝大部分的Java对象的销毁都在新生代进行了。
- IBM公司的专门研究表明,新生代中80号的对象都是“朝生夕死”的。
-
可以使用选项"-Xmn"设置新生代最大内存大小
- 这个参数- -般使用默认值就可以了。
-
2.6.4 Minor GC Major GC Full GC
-
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。
-
针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)
- 部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:
- 新生代收集(Minor GC / Young GC) :只是新生代(Eden\S0,S1)的垃圾收集
- 老年代收集(MajorGC/oldGC):只是老年代的垃圾收集。
- 目前,只有CMSGC会有单独收集老年代的行为。
注意,很多时候Major GC会和Ful1 GC混淆使用,需要具体分辨是老年代回收还是整堆回收
。
- 混合收集(MixedGC):收集整个新生代以及部分老年代的垃圾收集。
- 目前,只有G1 GC会有这种行为
- 部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:
-
年轻代GC(Minor GC)触发机制:
- 当年轻代空间不足时,就会触发Minor GC, 这里的年轻代满指的是Eden代满,Survivor满不会引 发GC。(每次 Minor GC会清理年轻代的内存。)
- 因为Java 对象
大多都具备朝生夕灭
的特性,所以Minor GC非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。 - Minor GC会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
-
老年代GC (Major GC/Full GC)触发机制:
- 指发生在老年代的GC,对象从老年代消失时,我们说“Major GC"或“Ful1T GC”发生了。
- 出现了Major GC, 经常会伴随至少一次的Minor GC (但非绝对的,在Paral1el Scavenge收集器的收集策略里就有直接进行MajorGC的策略选择过程)。
- 也就是在老年代空间不足时,会先尝试触发Minor GC。 如果之后空间还不足,则触发Major GC
- Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长。
- 如果Major GC后,内存还不足,就报OOM了。
-
Fu11 GC触发机制:
- 触发full GC 执行的情况有如下5种
- 调用System.gc()时,系统建议执行Fu11 GC,但是不必然执行
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
- 由Eden区、survivor space0 (From Space) 区向survivor space1 (To Space) 区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小.
说明: full gc是开发或调优中尽量要避免的。这样暂时时间会短一些
。
- 触发full GC 执行的情况有如下5种
2.6.5 内存分配策略
- 如果对象在Eden出生并经过第一次MinorGC 后仍然存活, 并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor区中每熬过一次MinorGC ,年龄就增加1 岁,当它的年龄增加到一定程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代中。
- 对象晋升老年代的年龄阈值,可以通过选项-XX:MaxTenuringThreshold来 设置
- 针对不同年龄段的对象分配原则如下所示:
- 优先分配到Eden
- 大对象直接分配到老年代
- 尽量避免程序中出现过多的大对象
- 长期存活的对象分配到老年代
- 动态对象年龄判断
- 如果Survivor 区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
- 空间分配担保
- -XX : HandlePromotionFailure
2.6.6 堆是分配对象存储的唯一选择吗?
-
在《深入理解Java虚拟机》中关于Java堆内存有这样一段描述:
- 随着JIT编译期的发展与
逃逸分析技术
逐渐成熟,栈上分配、标量替换优化技术
将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。
- 随着JIT编译期的发展与
-
在Java虛拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是
如果经过逃逸分析(EscapeAnalysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配
。这样就无需在堆,上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。 -
此外,前面提到的基于openJDK深度定制的TaoBaoVM,其中创新的GCIH (GC invisible heap) 技术实现off-heap,将生命周期较长的Java对象从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。
-
如何将堆上的对象分配到栈,需要使用逃逸分析手段。
-
这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
-
通过逃逸分析,Java Hotspot编译 器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
-
逃逸分析的基本行为就是分析对象动态作用域:
-
当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
-
//没有发生逃逸 public void test(){ Object o = new Object(); //user o o = null; }
-
-
当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。
-
//获取一个StringBuffer对象 //可能被外界使用,不使用栈上分配 public StringBuffer getStringBuffer() { StringBuffer sb = new StringBuffer(); sb.append("a"); return sb; }
-
上述代码如果想要StringBuffer sb不逃出方法,可以这样写:
-
public StringBuffer getStringBuffer() { StringBuffer sb = new StringBuffer(); sb.append("a"); return sb.toString(); }
-
-
-
逃逸分析:代码优化
- 使用逃逸分析,编译器可以对代码做如下优化:
栈上分配
。将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。同步省略
。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。分离对象或标量替换
。有的对象可能不需要作为-一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。标量(Scalar)
是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。- 相对的,那些还可以分解的数据叫做
聚合量(Aggregate)
,Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。 - 在JIT阶段,如果经过逃逸分析,发现-一个对象不会被外界访问的话,那么经过JIT优化,就
会把这个对象拆解成若千个其中包含的若干个成员变量来代替。这个过程就是标量替换
。
- 使用逃逸分析,编译器可以对代码做如下优化:
2.7 方法区
2.7.1 基本理解
- 方法区(Method Area) 与Java堆一样,是各个线程共享的内存区域。
- 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
- 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误: java.lang.OutOfMemoryError:PermGen space或者java.lang.OutOfMemoryError: Metaspace
- 关闭JVM就会释放这个区域的内存。
2.7.2 方法区的演进
- 在jdk7及以前,习惯上把方法区,称为永久代。jdk8开始, 使用元空间取代了永久代。
- 本质上,方法区和永久代并不等价。仅是对hotspot而言的。 《Java虛拟机规范》对如何实现方法区,不做统一要求。例如: BEA JRockit/ IBM J9中不存在永久代的概念。
- 现在来看,当年使用永久代,不是好的idea。导致Java程序更容易00M (超过-XX : MaxPermSize上限)
- 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:
元空间不在虛拟机设置的内存中,而是使用本地内存
。 - 永久代、元空间二者并不只是名字变了,内部结构也调整了。
- 根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OOM异常。
2.7.3 设置方法区大小
-
方法区的大小不必是固定的,jvm可以根据应用的需要动态调整。
-
jdk7及以前:
- 通过-XX: PermSize来设置永久代初始分配空间。默认值是20.75M
- -XX:MaxPermSize来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M
- 当JVM加载的类信息容量超过了这个值,会报异常OutofMemoryError : PermGen space。
-
jdk8及以后:
- 元数据区大小可以使用参数-XX:MetaspaceSize和-XX :MaxMetaspaceSize指定,替代上述原有的两个参数。
- 默认值依赖于平台。windows下,-XX:MetaspaceSize是21M,-XX : MaxMetaspaceSize的值是-1,即没有限制。
- 与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会拋出异常OutOfMemoryError: Metaspace
- -XX : MetaspaceSize:设置初始的元空间大小。对于- -个64位的服务器端JVM来说,其默认的-XX:MetaspaceSize值为21MB。这就是初始的高水位线,一旦触及这个水位线,Full GC将 会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。
- 如果初始化的高水位线设置过低,上述高水位线调整情况会 发生很多次。通过垃圾回收器的日志可以观察到Full GC多次调用。为了避免频繁地GC,建议将-XX :MetaspaceSize设置为一个相对较高的值。
2.7.3 方法区的内部结构
- 《深入理解Java虚拟机》书中对方法区(Method Area) 存储内容描述如下:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
- 对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:
- 这个类型的完整有效名称(全名=包名.类名)
- 这个类型直接父类的完整有效名(对于interface或是java.lang.object,都没有父类)
- 这个类型的修饰符(public, abstract, final的某个子集)
- 这个类型直接接口的一个有序列表
- 域(Field)信息
- JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
- 域的相关信息包括:域名称、域类型、域修饰符(public, private,protected, static, final, volatile, transient的某个子集)
- 方法信息:JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
- 方法名称
- 方法的返回类型(或void)
- 方法参数的数量和类型(按顺序)
- 方法的修饰符(public, private, protected, static, final,synchronized, native, abstract的一个子集)
- 方法的字节码(bytecodes)、操作数栈、局部变量表及大小( abstract和native方法除外)
- 异常表( abstract和native方法除外)
- 每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
- 运行时常量池vs常量池
- 方法区,内部包含了运行时常量池。
- 字节码文件,内部包含了常量池。
- 要弄清楚方法区,需要理解清楚ClassFile,因为加载类的信息都在方法区。
- 要弄清楚方法区的运行时常量池,需要理解清楚ClassFile中的常量池。
- 运行时常量池( Runtime Constant Pool) 是方法区的一部分。
- 常量池表( Constant Pool Table) 是Class文件的一部分,用于存放
编译期生成的各种字面量与符号引用
,这部分内容将在类加载后存放到方法区的运行时常量池中。运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池。 - JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的
- 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。
- 运行时常量池,相对于Class文件常量池的另一重要特征是:具备动态性。
- String. intern ()
- 运行时常量池,相对于Class文件常量池的另一重要特征是:具备动态性。
- 运行时常量池类似于传统编程语言中的符号表(symbol table) ,但是它所包含的数据却比符号表要更加丰富一些。
- 当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会拋OutOfMemoryError异常。
2.8 对象实例化内存布局与访问定位
2.8.1 对象的实例化
- 创建对象的方式
- new
- 直接new
- Xxx的静态方法
- XxxBuilder/XxxFactory的静态方法
- Class的newInstance()
- 反射的方式,只能调用空参的构造器,权限必须是public
- Constructor的newInstance(Xxx)
- 反射的方式,可以调用空参、带参的构造器,权限没有要求
- 使用clone()
- 不调用任何构造器 ,当前类需要实现Cloneable接口 ,实现clone()
- 使用反序列化
- 从文件或者网络中获取二进制流
- 第三方库Objenesis
- new
- 对象创建的步骤
- 判断对象对应的类是否加载、链接、初始化
- 为对象分配内存
- 如果内存是规整的,那么虚拟机将采用的是指针碰撞法( Bump The Pointer )来为对象分配内存。意思是所有用过的内存在一边,空闲的内存在另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离罢了。如果垃圾收集器选择的是Serial、ParNew这种基 于压缩算法的,虚拟机采用这种分配方式。一般使用带有compact (整理)过程的收集器时,使用指针碰撞。
- 如果内存不是规整的,已使用的内存和未使用的内存相互交错,那么虚拟机将采用的是空闲列表法
来为对象分配内存。意思是虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。这种分配方式成为“空闲列表( Free List ) "。
- 处理并发安全问题
- 采用CAS失败重试、区域加锁保证更新的原子性
- 每个线程预先分配一块TLAB——通过 -XX:+/-UseTLAB参数来设定
- 初始化分配到的空间
- 所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用
- 设置对象的对象头
- 执行init方法进行初始化
2.8.2 内存布局
- 对象头
- 运行时元数据
- 哈希值
- GC分代年龄
- 锁状态标志
- 线程持有的锁
- 偏向线程的id
- 偏向时间戳
- 类型指针
- 指向类元数据InstanceClass,确定该对象所属的类型
- 如果是数组,还需要记录数组的长度
- 运行时元数据
- 实例数据
- 说明
2.8.3 对象的访问定位
-
句柄访问
-
直接指针
2.9 执行引擎
- 执行引擎是Java虚拟机核心的组成部分之一。
- “虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。
- hotspot虚拟机默认使用解释器和JIT(即时编译器)混合
- 缺省情况下HotSpot VM是采用解释器与即时编译器并存的架构,当然开发人员可以根据具体的应用场景,通过命令显式地为Java虚拟机指定在运行时到底是完全采用解释器执行,还是完全采用即时编译器执行。如下所示:
- -Xint:完全采用解释器模式执行程序;
- -Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行。
- -Xmixed:采用解释器+即时编译器的混合模式共同执行程序。
- 命令行下使用: java -Xint -version
- 在HotSpot VM中内嵌有两个JIT编译器,分别为Client Compiler和Server Compiler,但大多数情况下我们简称为C1编译器和C2编译器。开发人员可以通过如下命令显式指定Java虚拟机在运行时到底使用哪一种即时编译器,如下所示:
- -client:指定Java 虚拟机运行在Client模式下,并使用C1编译器;
- C1编译器会对字节码进行简单和可靠的优化,耗时短。以达到更快的编译速度。
- -server:指定Java虚拟机运行在Server模式下,并使用C2编译器。
- C2进行耗时较长的优化,以及激进优化。但优化的代码执行效率更高。
- 在不同的编译器上有不同的优化策略,C1编译器上主要有方法内联,去虚拟化、冗余消除。
- 方法内联:将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减少参数传递和跳转过程
- 去虚拟化:对唯一 的实现类进行内联
- 冗余消除:在运行期间把一 些不会执行的代码折叠掉
- C2的优化主要是在全局层面,逃逸分析是优化的基础。基于逃逸分析在C2.上有如下几种优化:
- 标量替换:用标量值代替聚合对象的属性值
- 栈上分配:对于未逃逸的对象分配对象在栈而不是堆
- 同步消除:清除同步操作,通常指synchronized
的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。
- -client:指定Java 虚拟机运行在Client模式下,并使用C1编译器;
- hotspot虚拟机默认使用解释器和JIT(即时编译器)混合
- 缺省情况下HotSpot VM是采用解释器与即时编译器并存的架构,当然开发人员可以根据具体的应用场景,通过命令显式地为Java虚拟机指定在运行时到底是完全采用解释器执行,还是完全采用即时编译器执行。如下所示:
- -Xint:完全采用解释器模式执行程序;
- -Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行。
- -Xmixed:采用解释器+即时编译器的混合模式共同执行程序。
- 命令行下使用: java -Xint -version
- 在HotSpot VM中内嵌有两个JIT编译器,分别为Client Compiler和Server Compiler,但大多数情况下我们简称为C1编译器和C2编译器。开发人员可以通过如下命令显式指定Java虚拟机在运行时到底使用哪一种即时编译器,如下所示:
- -client:指定Java 虚拟机运行在Client模式下,并使用C1编译器;
- C1编译器会对字节码进行简单和可靠的优化,耗时短。以达到更快的编译速度。
- -server:指定Java虚拟机运行在Server模式下,并使用C2编译器。
- C2进行耗时较长的优化,以及激进优化。但优化的代码执行效率更高。
- 在不同的编译器上有不同的优化策略,C1编译器上主要有方法内联,去虚拟化、冗余消除。
- 方法内联:将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减少参数传递和跳转过程
- 去虚拟化:对唯一 的实现类进行内联
- 冗余消除:在运行期间把一 些不会执行的代码折叠掉
- C2的优化主要是在全局层面,逃逸分析是优化的基础。基于逃逸分析在C2.上有如下几种优化:
- 标量替换:用标量值代替聚合对象的属性值
- 栈上分配:对于未逃逸的对象分配对象在栈而不是堆
- 同步消除:清除同步操作,通常指synchronized
- -client:指定Java 虚拟机运行在Client模式下,并使用C1编译器;