JVM
一、什么是JVM?
JVM:Java Virtual Machine(Java虚拟机)— Java程序的运行环境
JVM的好处是:
- 一次编写,到处运行
- 自动内存管理,垃圾回收功能
- 数组下标越界检查
- 多态
二、运行时数据区域
线程私有:虚拟机栈、本地方法栈、程序计数器
线程共有:堆、方法区
2.1 程序计数器
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器
。字节码解释器工作就是通过这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环等基础功能都需要依赖这个计数器来完成。
特点:
- 内存区域唯一一个没有OutOfMemory(OOM)的情况;
- 线程私有:随着线程的创建而创建,随着线程的结束而死亡。
2.2 虚拟机栈
栈是有一个个栈帧组成,栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。
栈的特点:先进后出、后进先出。
问题:方法内的局部变量是否线程安全?
- 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的。
- 如果是局部变量引用了对象,并逃离了方法的作用方法,需要考虑线程安全。
public static void m1() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
public static void m2(StringBuilder sb) {
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
public static StringBuilder m3() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
栈内存溢出例子:递归调用时,没有设置好递归中终止条件。StackOverflowError
public class Test01 {
static int count = 0;
public static void main(String[] args) {
try {
m1();
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(count); // 50724。设置VM Options:-Xss256k后变为5176
}
}
public static void m1() {
count++;
m1();
}
}
设置栈的大小:-Xss
垃圾回收器(GC)不会涉及栈内存。因为栈帧会在方法执行完毕后自动回收掉。
2.3 本地方法栈
本地方法栈与虚拟机栈发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本机方法栈则是为虚拟机使用到的本机(Native)方法服务。
本地方法是有C或C++编写的,方法中有native关键字修饰。
2.4 堆
Java堆是虚拟机管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。通过 new 关键字,创建对象都会使用堆内存。
特点:
- 它是线程安全的,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
堆内存溢出例子:死循环。OutOfMemoryError
堆内存诊断工具:
- jps工具:查看当前系统中有哪些java进程
- jmap工具:查看堆内存占用情况
- jconsole工具:图形界面化,多功能的监测工具,可以连续监测。jdk/bin目录下jconsole.exe工具
public static void main(String[] args) throws InterruptedException {
System.out.println("1...");
Thread.sleep(30000);
byte[] bytes = new byte[1024 * 1024 * 10]; //10MB
System.out.println("2...");
Thread.sleep(30000);
bytes = null;
System.out.println("3...");
System.gc();
Thread.sleep(1000000);
}
1、进入编译文件(包目录),执行jps,查看所有进程
2、查看堆内存空间情况 map -heap 进程号
2.5 方法区
jdk1.7之前:方法区的实现是永久代;
jdk1.7及以后:方法区的实现是元空间;
常量池:可以看做是一张表,虚拟机指令根据这张表找到要执行的类名、方法名、参数类型等信息。
运行时常量池:是方法区的一部分。当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
StringTable:
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
// 反编译文件(4-27行):new StringBuilder().append("a").append("b").toString --- new String("ab");
// s4 = s1 + "b"; 也会new String("ab")。导致s4 == s3 为false
String s4 = s1 + s2;
// javac在编译期间的优化,结果已经在编译期间确定为ab
String s5 = "ab";
// intern():将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
String s6 = s5.intern();
System.out.println(s3 == s4); // false
System.out.println(s3 == s5); // true
System.out.println(s3 == s6); // true
String x2 = new String("c") + new String("s");
String x1 = "cs";
x2.intern(); // 若把这一行代码向上移动一行,下面的结果则为true
System.out.println(x1 == x2); // false
}
String s = new String(“a”) + new String(“b”);会创建几个对象?6个
字符串常量池中:“a”, “b”
对象:new StringBuilder(), new String(“a”), new String(“b”), new String(“ab”)
字符串常量池中,没有生成"ab"
三、垃圾回收
垃圾回收主要在堆空间和方法区中。程序计数器、虚拟机栈、本地方法栈3个区域随着线程而生,随着线程而灭,栈中的栈帧随着方法的进入和退出有条不紊地进行着出栈和入栈操作。
3.1 堆空间的基本结构
- 新生代
- Eden(伊甸园)区
- Survivor0(s0)
- Survivor1(s1)
- 老年代
其中:新生代:老年代=1:2, Eden:S0:S1=8:1:1
3.2 分代回收
- 大多数对象优先进去Eden区
- 当Eden区满了以后,会触发Minor GC,会转移到幸存区(s0,s1)
- 幸存区会有个年龄计数器,每经历一次Minor GC,年龄计数器就加1,
- 当年龄计数器 > 15后,会转移到老年代
- 若老年代也满了,会触发Full GC
大对象直接放入老年代,例如:数组、字符串。
3.3 死亡对象判断方法
- 引用计数算法(未使用)
- 原理:在对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。
- 优点:实现简单、效率高;但是目前主流的虚拟机中没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。
- 可达性分析算法(使用)
- 原理:通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
- 哪些对象可以作为GC Roots呢?
- 栈中使用到的参数、局部变量表、临时变量等
- 静态变量
- 常量引用
- …
3.4 强、软、弱、虚引用
- 强引用:在程序代码之外普遍存在的引用赋值,即类似于"Object obj = new Object()"这种引用关系。无论任何情况下,只要强引用关系还在,垃圾收集器就永远不会回收掉被引用的对象。(永不回收)
Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收
obj = null; //手动置null,这样JVM就可以适时的回收对象了
- 软引用:只要软引用关联着的对象,在系统将要发生内存溢出前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。(内存不足,才回收)
SoftReference
- 弱引用:也是描述那些非必要对象,但是它的强度比软引用更若一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。(发现即回收)
WeakReference
- 虚引用:它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成引影响,也无法通过虚引用来取得一个对象实例。当一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
PhantomReference
四、类加载过程
4.1 类加载的时机
一个类型从被加载到虚拟机中开始,到卸载出内存为止,它的整个生命周期将会经历加载
、验证
、准备
、解析
、初始化
、使用
和卸载
七个阶段,其中验证、准备、解析三个部分统称为连接。
4.2 加载
在加载阶段,JVM需要完成以下三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
4.3 验证
目的:确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当做代码运行后不会危害虚拟机自身的安全。
-
文件格式验证
- 是否以魔数0xCAFEBABE开头。
- 主、次版本号是否在当前JVM接收范围之内。
- 常量池的常量中是否有不支持的常量类型。
- …
-
元数据验证
- 这个类是否有父类。
- 这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
- 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
- …
-
字节码验证
最复杂的一个阶段。通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。比如保证任意时刻操作数栈和指令代码序列都能配合工作。
4.4 准备
准备阶段是正式为类变量分配并设置类变量初始化的阶段。
- 被
static
修改的变量。 - 类变量随着Class对象一起放在
堆
中(JDK1.7及以后)。 - 设置
初始值,而非赋值
。例如 public static final a = 1; (此时 a 只初始化为 0; 赋值为1还得等到初始化阶段) - 如果static变量是final的基本类型,以及字符串变量,那么编译阶段就确定了,赋值在准备阶段完成。
- 如果static变量是final的引用类型,那么赋值也会在初始化阶段完成。
4.5 解析
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。
4.6 初始化
初始化阶段是执行初始化方法()方法的过程。
以下几种情况下,会对类进行初始化(主动):
-
main方法所在的类,总会被首先初始化
-
new会导致初始化
-
Class.forName
-
子类初始化,如果父类还没初始化,会引发父类初始化
-
首次访问这个类的静态变量或静态方法
-
访问父类的静态变量,只会触发父类的初始化
以下几种情况下, 不会导致类的初始化:
- 访问类的static final 静态变量(基本类型或字符串变量)
- 类对象.class
- 创建类的数组
- 类加载器的loadClass方法
- Class.forName的参数2为false时
五、类加载器
5.1 类加载器
JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader
:
- 启动类加载器(Bootstrap Class Loader):最顶层的加载类,由C++编写,负责加载
%JAVA_HOME%\lib
目录中的jar包和类或者被-Xbootclasspath
参数所指定的路径中存放的类。 - 扩展类加载器(Extension Class Loader):负责加载
%JAVA_HOME%\lib\ext
目录中或者java.ext.dirs系统变量中所指定的路径中的所有类库。 - 应用程序类加载器(Application Class Loader):负责加载claspath中的所有类库。
- 自定义类加载器(User Class Loader)
5.2 双亲委派模型
- 工作流程
- 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载都是如此,因此所以的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试去完成加载。
- 双亲委派模型的好处?
- 保证Java程序的稳定运行
- 避免类的重复加载
- 保证程序安全,防止API被随意篡改
待补充:类文件结构、垃圾收集器、字节码指令。