一、什么是虚拟机
平时我们使用的电脑主机、服务器,都是传统意义上的物理机,通过具体的CPU,内存等硬件实现。
虚拟机的定义:“虚拟机指通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统。"
虚拟机分为系统虚拟机和程序虚拟机。
系统虚拟机是对物理机的仿真,它可以提供一个可运行完整操作系统的软件平台。例如常用的VMWare,我们在使用VM创建一个linux系统时,常常要我们为系统分配内存,CPU。由此可见系统虚拟机也和物理硬件挂钩。
程序虚拟机则是专门执行计算机程序的平台。
二、什么是JVM虚拟机
JVM虚拟机就是一台程序虚拟机。
在我们将写好的Java程序的源代码编译成字节码文件(.class后缀)后,JVM虚拟机会读取字节码文件,经过一系列的步骤后,最终将我们的程序运行起来。
JVM虚拟机不受限于操作平台,具备可移植性,在windows、linux平台都可以运行编译好的字节码文件。
JVM虚拟机还有一个特性,跨语言性。很多刚学Java的人以为JVM虚拟机只有Java语言能够使用,实际上JVM虚拟机可以运行多种语言,例如Kotlin、Groovy、Jython,JRuby、JavaScript等。
三、JVM虚拟机的整体结构
粗略图
细化图
四、类加载器
我们写好Java程序后,通过编译器将XXX.java文件编译成了JVM虚拟机能够识别的XXX.class文件,也就是字节码文件。而我们在程序中写的自定义类,使用的基本数据类型,对象引用,方法等就存在于字节码文件当中,此时字节码里存储的数据只是静态数据,他们并没有被使用。类加载器的作用就是读取字节码的信息,经过一系列的步骤,将字节码里的静态数据最终加载到运行时数据区。
类加载的工作步骤主要分为三步,装载------->链接------->初始化
(一)装载(Loading)
1.通过一个类的全限定名获取定义此类的二进制字节流。
2.将该字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3.在堆内存中生成一个代表该类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。
举例:例如有一个实体类Animals所在的包叫做com.xxx.entity,类加载器用com.xxx.entity.Animals这个全限定名找到class文件,将class文件中的二进制数据加载到内存中,然后在运行时数据区中的堆内存创建一个代表该字节流的java.lang.Class对象,用来封装类在方法区内的数据结构。java.lang.Class对象就是Animals这个类的元对象,这个对象由类的变量,方法等各种信息。
补充:加载class文件的常见途径
1.直接本地磁盘上加载
2.通过网络传输
3.jar、war包
4.动态代理技术
(二)链接(Linking)
1.验证(Verify)
验证的主要作用就是确保被加载的类的正确性。也是连接阶段的第一步。说白了也就是我们加载好的.class文件不能对我们的虚拟机有危害,所以先检测验证一下。
主要包括文件格式验证、元数据验证、字节码验证、符号引用验证。
2.准备 (prepare)
准备阶段主要为类变量分配内存并设置初始值。也就是我们写的静态变量。
3.解析(resolve)
解析阶段主要是虚拟机将常量池中的符号引用转化为直接引用的过程。解析和初始化不一定是顺序的。
举例:
代码如下
public class Dog {
//类变量
static int name = 1;
//常量
final int name2 = 2;
public static void main(String[] args) {
}
}
通过javap -v反编译Dog.class文件后,得到的信息如下
Classfile /D:/JAVA基础/test/target/classes/com/jimmy/Dog.class
Last modified 2022年5月19日; size 519 bytes
SHA-256 checksum 338f56de0ce4231aaaf16be5207555b7976ebbdbbf7651474768395da6640b35
Compiled from "Dog.java"
public class com.jimmy.Dog
minor version: 0
major version: 59
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #8 // com/jimmy/Dog
super_class: #2 // java/lang/Object
interfaces: 0, fields: 2, methods: 3, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // com/jimmy/Dog.name2:I
#8 = Class #10 // com/jimmy/Dog
#9 = NameAndType #11:#12 // name2:I
#10 = Utf8 com/jimmy/Dog
#11 = Utf8 name2
#12 = Utf8 I
#13 = Fieldref #8.#14 // com/jimmy/Dog.name:I
#14 = NameAndType #15:#12 // name:I
#15 = Utf8 name
#16 = Utf8 ConstantValue
#17 = Integer 2
#18 = Utf8 Code
#19 = Utf8 LineNumberTable
#20 = Utf8 LocalVariableTable
#21 = Utf8 this
#22 = Utf8 Lcom/jimmy/Dog;
#23 = Utf8 main
#24 = Utf8 ([Ljava/lang/String;)V
#25 = Utf8 args
#26 = Utf8 [Ljava/lang/String;
#27 = Utf8 <clinit>
#28 = Utf8 SourceFile
#29 = Utf8 Dog.java
{
static int name;
descriptor: I
flags: (0x0008) ACC_STATIC
final int name2;
descriptor: I
flags: (0x0010) ACC_FINAL
ConstantValue: int 2
public com.jimmy.Dog();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_2
6: putfield #7 // Field name2:I
9: return
LineNumberTable:
line 3: 0
line 5: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/jimmy/Dog;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 args [Ljava/lang/String;
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_1
1: putstatic #13 // Field name:I
4: return
LineNumberTable:
line 4: 0
}
SourceFile: "Dog.java"
挨个分析
//由final修饰的常量在编译阶段就已经赋值为2
final int name2;
descriptor: I
flags: (0x0010) ACC_FINAL
ConstantValue: int 2
//静态变量在类加载链接中的准备阶段会开辟内存空间,给int基本类型赋默认值为0
//如果是引用类型则是null
//注意 静态变量在jdk1.8中内存位置在堆中的class对象身上
static int name;
descriptor: I
flags: (0x0008) ACC_STATIC
//Class常量池
//当java文件被编译成class文件之后,也就是会生成我上面所说的class常量池
//常量池中主要存放两类数据,一是字面量、二是符号引用。
//字面量:比如String类型的字符串值或者定义为final类型的常量的值。
//可以通过#17,发现常量name2的值 2
//符号引用:
// # 就是符号引用
//类或接口的全限定名(包括他的父类和所实现的接口)
//变量或方法的名称
//变量或方法的描述信息
//方法的描述:参数个数、参数类型、方法返回类型等等
//变量的描述信息:变量的返回值
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // com/jimmy/Dog.name2:I
#8 = Class #10 // com/jimmy/Dog
#9 = NameAndType #11:#12 // name2:I
#10 = Utf8 com/jimmy/Dog
#11 = Utf8 name2
#12 = Utf8 I
#13 = Fieldref #8.#14 // com/jimmy/Dog.name:I
#14 = NameAndType #15:#12 // name:I
#15 = Utf8 name
#16 = Utf8 ConstantValue
#17 = Integer 2
#18 = Utf8 Code
#19 = Utf8 LineNumberTable
#20 = Utf8 LocalVariableTable
#21 = Utf8 this
#22 = Utf8 Lcom/jimmy/Dog;
#23 = Utf8 main
#24 = Utf8 ([Ljava/lang/String;)V
#25 = Utf8 args
#26 = Utf8 [Ljava/lang/String;
#27 = Utf8 <clinit>
#28 = Utf8 SourceFile
#29 = Utf8 Dog.java
//字符串常量池 JDK1.8 内存位置 堆
//全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例
//然后将该字符串对象实例的引用值存到string pool中
//string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的
// 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串
//运行时常量池 JDK1.8 内存位置 元空间(本地内存)
//而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中
//由此可知,运行时常量池也是每个类都有一个
//class常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值
//而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串池
//运行时常量池和java.lang.Class对象
//装载阶段在堆内存中实例化出代表这个类的java.lang.Class对象
//这个对象将作为程序访问方法区中的类型数据的外部接口。
(三)初始化(Initialization)
1、初始化阶段就是执行类构造器方法<clinit>()的过程。
2、此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来的。
3、构造器方法中指令按语句在源文件中出现的顺序执行。
4、<clinit>()不同于类的构造器。(关联:构造器是虚拟机视角下的<init>())
5、若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕。
6、虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁。
举例:
public class Dog {
static int name = 1;
static String str;
static {
name = 2;
str = "Jimmy";
}
public static void main(String[] args) {
System.out.println(Dog.name);
}
}
JVM把需要赋值的变量收集起来,整合一个clinit方法,对这些类变量集体赋值。如果没有静态变量或者方法,是不会有clinit方法的。
(四)不同的类加载器
程序中常见的三种类加载器:
主要分为引导类加载器(BootStrap ClassLoader)、扩展类加载器(Extension ClassLoader)、系统类加载器(System ClassLoader)。
引导类加载器:
引导类加载器由c/c++实现,扩展类加载器和系统类加载器由引导类加载器加载,不继承ClassLoader类。主要负责java.lang等核心类库。
获取引导类加载器:ClassLoader bootStrapClassLoader = extensionClassLoader.getParent();
扩展类加载器:
Java语言编写,派生ClassLoader,父加载器为启动类加载器。
从java.ext.dirs中系统属性所指定的目录中加载类库,或从jdk安装目录jre/lib/ext子目录(扩展目录)下加载类库。用户创建的jar放在此目录下,也会由扩展类加载器自动加载。
获取扩展类加载器:ClassLoader extensionClassLoader = systemClassLoader.getParent();
系统类加载器:
java语言编写,派生ClassLoader,父加载器为扩展类加载器。
负责加载环境变量ClassPath或者系统属性java.class.path路径下的类库,该类加载器是程序中默认的类加载器。
获取系统类加载器:ClassLoader systemClassLoader = ClassLoader.getSysteamClassLoader();
(五)双亲委派机制
我们自己定义了一个java.lang.String类,这个是JDK提供的工具包中的String是相同的包名,而String是最顶级的类加载器------------引导类加载器负责的。
此处不放图了,反正并没有输出哈哈哈哈。
原理:我们自己定义的这个String类,编译成class文件后,开始被类加载器加载,首先从系统加载器开始,系统类加载器会检查自己加载过这个类没有,如果没有就继续往上一级的扩展类加载器检查,如果没有加载就继续往上,到最终的引导类加载器,引导类发现自己负责加载的模块中有String内,它就会加载JDK提供的核心类库中的String类。引导类加载器加载完成后,就不需要下两个类加载器加载了,说到底加载的String还是JDK的String,而不是我们自定义的。
双亲委派机制的作用大致就是为了保证系统文件不会被串改,导致程序不可用。
五、程序计数器(PC寄存器)
程序计数器是每个线程私有的,通过自增的方式,程序计数器存储着指向下一条字节码指令的地址。
如果程序计数器是共享的,那么会出现的问题就是在多线程的情况下,多个线程来回切换,无法知道自己这个线程执行到了哪个指定位置。
六、虚拟机栈(Java Stack)
整体结构
1.局部变量表(Local Variables)
局部变量变,也叫局部变量表或本地变量表,由一个数组来定义的。
局部变量表主要存储方法参数和定义在方法体内的局部变量,例如对象引用,基本数据类型,returnAddress(返回给pc寄存器方法结束后的下一条指令地址)。
局部变量表的内存大小在编译器就确认好了。
举例:
public class Dog {
public static void main(String[] args) {
test(1,1L);
Dog dog = new Dog();
dog.test2(1,1L);
}
public static void test(int a,long b){
long c = (long)a + b;
}
public void test2(int a,long b){
long c = (long)a + b;
}
}
test静态方法
start:方法开始的字节码指令的地址。
length:整个方法的字节码指令长度。
slot:可以看到基本数据类型int占一个槽位,long类型占两个槽位。
局部变量表的大小,例子中的两个long占四个位置,int占一个,总共五。
test2方法,注意,test2是一个对象方法
可以看到对象方法在局部变量表中多了一个变量,就是指向对象的this,默认占槽位开始的位置。
2.操作数栈(Operate Stack)
与局部变量表一样,均以字长为单位的数组。不过局部变量表用的是索引,操作数栈是弹栈/压栈来访问。操作数栈可理解为java虚拟机栈中的一个用于计算的临时数据存储区。
存储的数据与局部变量表一致含int、long、float、double、reference、returnType,操作数栈中byte、short、char压栈前(bipush)会被转为int。
数据运算的地方,大多数指令都在操作数栈弹栈运算,然后结果压栈。
值得注意的一点,如果代码中出现 int a = 1 + 1;那么运算的结果值在编译阶段就已经确定为2,不会再被操作数栈进行运算。
3.动态链接 (Dynamic Linking)
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如:invokedynamic指令
在java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
例子:
public class Dog {
public static void main(String[] args) {
Dog dog = new Dog();
dog.test1();
}
public void test1(){
System.out.println("开始调用test2");
test2();
}
public void test2(){
System.out.println("哈哈哈哈");
}
}
public void test1();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #13 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #19 // String 开始调用test2
5: invokevirtual #21 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: aload_0
// Method test2:()V 此处调用了test2方法 #27代表符号引用
9: invokevirtual #27
12: return
LineNumberTable:
line 10: 0
line 11: 8
line 12: 12
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 this Lcom/jimmy/Dog;
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Class #8 // com/jimmy/Dog
#8 = Utf8 com/jimmy/Dog
#9 = Methodref #7.#3 // com/jimmy/Dog."<init>":()V
#10 = Methodref #7.#11 // com/jimmy/Dog.test1:()V
#11 = NameAndType #12:#6 // test1:()V
#12 = Utf8 test1
#13 = Fieldref #14.#15 // java/lang/System.out:Ljava/io/PrintStream;
#14 = Class #16 // java/lang/System
#15 = NameAndType #17:#18 // out:Ljava/io/PrintStream;
#16 = Utf8 java/lang/System
#17 = Utf8 out
#18 = Utf8 Ljava/io/PrintStream;
#19 = String #20 // 开始调用test2
#20 = Utf8 开始调用test2
#21 = Methodref #22.#23 // java/io/PrintStream.println:(Ljava/lang/String;)V
#22 = Class #24 // java/io/PrintStream
#23 = NameAndType #25:#26 // println:(Ljava/lang/String;)V
#24 = Utf8 java/io/PrintStream
#25 = Utf8 println
#26 = Utf8 (Ljava/lang/String;)V
#27 = Methodref #7.#28 // com/jimmy/Dog.test2:()V
#28 = NameAndType #29:#6 // test2:()V
#29 = Utf8 test2
#30 = String #31 // 哈哈哈哈
#31 = Utf8 哈哈哈哈
#32 = Utf8 Code
#33 = Utf8 LineNumberTable
#34 = Utf8 LocalVariableTable
#35 = Utf8 this
#36 = Utf8 Lcom/jimmy/Dog;
#37 = Utf8 main
#38 = Utf8 ([Ljava/lang/String;)V
#39 = Utf8 args
#40 = Utf8 [Ljava/lang/String;
#41 = Utf8 dog
#42 = Utf8 SourceFile
#43 = Utf8 Dog.java
补充:
静态链接:如果被调用的目标方法在编译器可知,并且在运行期保持不变,这时候JVM可以直接讲方法的符号引用转化为直接引用。
动态链接:被调用的方法在编译器无法被确定,只能动态的转化。
栈上分配:对象不仅仅可以在堆内存中分配,也可以在栈上进行分配。而栈上分配的要求进行对象逃逸分析,分析这个对象在是否会在其他地方被引用。例如,一个方法内创建了一个Dog对象,这个Dog对象只在方法内被使用,并没有进行引用传递,那么这个对象就可以进行栈上分配。
标量替换:标量指的是无法被继续分解的最小数据单位,java中的基本数类型就是标量。
public class Dog {
String name;
int num;
}
class Demo {
public static void main(String[] args) {
Demo demo = new Demo();
demo.test();
}
public void test(){
//dog对象只在test方法中被使用,没有逃逸出方法,那么它就可以进行栈上分配
//而栈上分配的原理就是将dog对象进行拆分,变成一个个标量,这就是标量替换
//此处dog对象就被拆分成了name和num两个标量
//而这两个标量就存储在局部变量表中
Dog dog = new Dog();
}
}
七、堆(Heap)
堆空间是全局共享的内存空间,大部分垃圾回收都是在堆空间中发生的。
而堆空间又细分为新生代、老年代两个区域。
1.新生代区
新生代区又细分为Eden、Survivor From、Survivor To三个区域。
Eden(伊甸园区):基本上大部分新创建的对象所在的位置都是在Eden区,Eden区里面的对象大部分都消亡得很快,只有少部分对象会存活下来。
Survivor From(S0区):当Eden区内存被对象占满或新创建的对象太大,剩余的Eden内存已经不够装下新对象时,新生代区域会进行一次Minor GC。垃圾回收器会判断对象是否还被引用,如果没有就进行对象回收。剩下的没有被回收的对象就回进入S0区,并给这些对象年龄加1。
Survivor To(S1区):第二次进行Minor GC的时候,生存下来的对象会被存放在S1区,S0生存下来的对象也会被放入S1区。S0和S1在每次GC后都会交换位置,轮流存放存活的对象。
值得注意的是:
Minor GC只有在Eden区是主动的,S0和S1区是没办法主动触发GC的,只是被动的跟着Eden区进行GC操作。
JVM默认存活对象的年龄上限是15。超过15对象就会进入老年代。
Eden:S0:S1默认内存对比为8:1:1。
2.老年代区
进入老年代区域的对象有以下情况。
第一:自然存活年龄超过15了,对象就会进入老年代。
第二:新创建的对象在Eden区放不下,Minor GC后依旧放不下的大对象,会直接被放入老年代区。
那么如果大对象到了老年代依旧放不下,那么就回触发一次Full GC。
GC的部分后续学习完了再补充
注意:
指针碰撞: 此时堆内存中是空的,我们创建A对象,那么指针就会移动一个A对象大小的位置,此后再创建新对象,就根据指针的位置进行定位。
那么,如果在多线程的情况下,多个线程都在创建对象,根据多线程并发的知识,堆是共享的区域,那么我们势必要保证指针的同步,也就是线程安全问题。而保证线程同步,那肯定会浪费一定的资源和时间。
此时,有一个技术叫做TLAB(Thread Local Allocation Buffer),JVM在堆内存中提供了一个每个线程独有的缓存区,在Eden区中为每个线程划分一个独占的区域,每个线程在各种的区域中创建对象,就不用担心指针碰撞的问题了。
八、方法区(Meta Space)
它用于存储已被虚拟机加载的类型信息、常量、即时编译器编译后的代码缓存等。(主要讨论的是JDK8)
方法区是JVM规范提出来的逻辑概念,在JDK1.8的具体实现是在本地内存中实现的 。
方法区((Method Area)与Java堆一样,是各个线程共享的内存区域。
方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误: java.lang.outOfMemoryError。
主要储存信息
1.类型信息
对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:
①这个类型的完整有效名称(全名=包名.类名)
②这个类型直接父类的完整有效名(对于interface或是java.lang.0bject,都没有父类)
③这个类型的修饰符(public, abstract,final的某个子集)
④这个类型直接接口的一个有序列表
2.域信息(Field)成员变量
JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
域的相关信息包括:域名称、域类型、域修饰符(public, private,protected,static,final, volatile, transient的某个子集)
3.方法(Method)信息
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
方法名称
方法的返回类型(或void)·方法参数的数量和类型(按顺序)
方法的修饰符(public, private,protected,static, final,synchronized,native,abstract的一个子集)
方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
异常表(abstract和native方法除外)
每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
九、垃圾标记算法
1.引用计数算法
public class Test {
public static void main(String[] args) {
Animal animal = new Animal("cat");
Test test = new Test();
test.test1(animal);
}
public void test1(Animal animal){
test2(animal);
}
public void test2(Animal animal){
test3(animal);
}
public void test3(Animal animal){
System.out.println(animal);
}
}
class Animal{
String name;
public Animal(String name){
this.name = name;
}
}
实现逻辑:
由于Java没有采用引用计数算法,所以使用了一个小例子来说明这种算法。
animal这个对象在main方法中被创建出来,如果没有test1方法引用,那么它就只被引用了一次,引用+1。
但是test1方法引用了animal,下面的test2,test3以此类推,总共被引用了四次。
从test3方法开始出栈,引用-1,直到main方法出栈,引用=0。说明animal没有被任何地方所引用,所有可以被回收。
缺陷:
引用计数算法有一个很致命的缺陷,就是没有办法解决循环依赖的问题,有可能会造成内存泄漏。
方法停止引用A B对象
方法a b断开了对a b对象的引用,但是a b对象二者还是相互引用,此时如果使用引用计数器,那么a b二者还是会被确认为正在被引用。实际上,并没有任何方法在使用它们。那么a b对象始终都不会被回收,游离在内存中,也不会被使用,造成内存泄漏。
2.可达性分析算法
为了避免造成内存泄漏的问题,于是出现了可达性分析算法。
从堆外内存开始计算被引用的路径,层层往下,如果从根路径Root出发到这个对象是可达的,那么就说明这个对象直接或者间接被引用。
一般能够作为root的对象都是在堆外空间,比如栈,方法区等。
十、垃圾标记回收算法
1.标记-清除算法(Mark-Sweep)
过程:
先将没有被引用的对象全部标记出来,随后一次性清理掉。
缺点:
清理掉垃圾后,内存空间并没有被整理,内存是不是连续的,如果此时有一个大对象被创建,伊甸园区放不下,那么又会触发一次GC。
2.复制算法(Copying)
过程:
将一块内容划分为两块,在清理前标记出还被引用的对象,将这些不需要清理的对象复制到内容的另一边,最后将这边的对象直接清除。
优点:
效率很高,但是很耗费空间内存,实际可用内容只有一半。因此适合在有大量对象的情况下使用,一般新生代的垃圾回收算法都是使用的复制算法。
在伊甸园区内存满了或者剩下的内存不够存放新创建的对象,此时触发一次young GC,复制算法先标记出还被引用的对象,这些对象被复制到S0区,伊甸园区直接被清理掉所有对象,S0和S1区交换位置,以待下一次的young GC(S区的垃圾回收不能主动触发,只能被动跟随伊甸园区的young GC进行垃圾回收)。第二次young GC被触发,伊甸园区存活的对象被复制到S1区,S0区被动触发GC,标记本区中存活的对象此时还有没有继续被引用,如果有就复制到S1区,然后统一清理,S1和S1再一次交换位置。
缺点:
需要耗费大量的内存空间。
复制对象就需要重新调正指针,维护引用关系。
3.标记-整理算法(Mark-Compact )
过程:
分为标记-整理-清除三个阶段。
和标记清除算法多了一个步骤。也就是标记完成后,让需要存活的对象移动到内存另一边,让需要被清理的对象待在一边,然后直接清理掉这边的对象。
优点:
弥补了标记清除算法在清理完对象后导致空间不连续的缺点,同样也不需要像复制算法一样耗费大量的内存空间。
十一、垃圾回收器
1.Serial GC - SerialOld GC
注意:SerialOld GC是JVM client模式下默认的老年代垃圾回收器,JVM server模式下的搭配是Serial GC - CMS -SerialOld GC。
如图所示,Serial垃圾回收器就是一个串行化的回收期。无论是新生代还是老年代,每次发生垃圾回收,都会暂停用户线程,避免用户线程在运行过程中改变对象的引用等造成不稳定的因素,业,然后垃圾回收线程单独运行,进行垃圾回收。
Serial垃圾回收器在早期JDK版本,硬件设备不够强大的情况下,效率还是很高的。
2.ParNew GC - CMS - SerialOld GC
ParNew垃圾回收器和Serial回收器的区别就在于一个是单线程,一个是多线程并行执行。效率上有所提升。
CMS(Concurrent Mark Sweep)垃圾处理器,采用标记清除算法,更加注重响应速度。
过程:
初始标记阶段暂停用户线程,标记出被引用对象,也就是root能直接引用到的。
并发标记阶段和用户线程并发执行,遍历那些较长的引用路径。
重新标记再次暂停用户线程,多个线程并发重新修正在并发标记阶段产生引用变动的对象。
最后并发清理。
缺点:
在并发标记阶段,垃圾对象并没有被回收,如果此刻内存不够用了,那么就会老年代回收器会使用SerialOld GC。
采用的标记清除算法产生的不连贯内存空间,需要用一个列表来维护,而不是指针碰撞。
3.Parllel Scavenge - Parllel Old
Parllel Scavenage和ParNew听上去都是并行的处理,区别在于Parrllel Scavenge更注重吞吐量,提供控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
4.G1
太复杂了.............
十二、JVM指令集
1.Constants
类型指令
在 JVM
的虚拟机栈中,byte
,short
,char
和 boolean
全部都当成 int
类型操作的。 Constants
类型指令都是将常量数据存入操作数栈中。
public class Test {
public static void main(String[] args) {
int i = 5;
}
}
//字节码 -1~5 使用iconst
0 iconst_5
1 istore_1
2 return
public class Test {
public static void main(String[] args) {
int i = 127;
}
}
//字节码 -128~127 bipush
0 bipush 127
2 istore_1
3 return
public class Test {
public static void main(String[] args) {
int i = 128;
}
}
//字节码 -32768~32767 sipush
0 sipush 128
3 istore_1
4 return
public class Test {
public static void main(String[] args) {
int i = 32768;
}
}
//字节码 -2147483648~2147483647 ldc
0 ldc #2 <32768>
2 istore_1
3 return
public class Test {
public static void main(String[] args) {
long a = 0l;
long b = 1l;
float c = 0.0f;
float d = 1.0f;
float e = 2.0f;
double f = 0.0;
double g = 1.0;
}
}
//字节码 double 0.0~1.0 dconst flaot 0.0~2.0 fconst long 0~1 lconst
0 lconst_0
1 lstore_1
2 lconst_1
3 lstore_3
4 fconst_0
5 fstore 5
7 fconst_1
8 fstore 6
10 fconst_2
11 fstore 7
13 dconst_0
14 dstore 8
16 dconst_1
17 dstore 10
19 return
public class Test {
public static void main(String[] args) {
String s = null;
}
}
//字节码 null aconst
0 aconst_null
1 astore_1
2 return