一、类加载时机
创建对象
创建子类对象
访问静态属性
调用静态方法
Class.forName(类全路径);
二、类加载的过程
2.1 加载:
通过一个类的全限定名获取定义此类的二进制文件
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成这个类的java.lang.class对象,作为方法区这个类各种数据的访问入口
2.2 验证
为了确保class文件的字节流中包含的信息符合当前虚拟机的要求
防止恶意代码攻击虚拟机
2.3 准备
准备阶段是为类变量(static 变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都会在方法区中进行分配。
ps:类变量是指static变量,实例变量在new对象时进行初始化。。初始值这个时候为0,因为此时没有执行java代码
2.4 解析
2.5 初始化
类初始化是类加载过程最后一步,到了初始化阶段,才开始执行类中的Java代码(字节码文件)
存放类的加载信息和定义的类变量。
Student s = new Student(“小明”,18);
s 是指针,存放在栈中。
new Student(“小明”,18) 是对象,也是实例 ,存放在堆中。
Student 类的信息/元数据存放在方法区。
三、JVM结构
ClassLoader:将符合要求的文件,加载到内存中
Execution Engine执行引擎:对字节码进行解析
NativeInterface本地接口:融合不同的开发语言的原声库为java所用
RuntimeDataArea:jvm内存空间结构模型
四、JVM内存结构
4.1 程序计数器
存储当前线程所执行的字节码行号(指令的偏移地址)
java 文件被翻译为字节码的时候,字节码大概类似于下面的样子
public void method(){
0 xxxxx...
1 xxxxxxx....
2 xxxx.....
3 xxxxxxxxxxx....
}
为什么程序计数器要保存字节码行号?
假如当CPU执行权从 A 线程,转移到 B 线程的时候,JVM就要暂时挂起线程 A ,去执行线程 B ;当线程 A 再次得到CPU执行权的时候,又会挂起B线程,继续执行 A 线程 ,CPU是记不住之前A执行到哪里的。但是程序计数器可以做到保存当前线程字节码行号(也就是地址)
特点:
线程私有,一个线程一个程序计数器
生命周期,随着当前线程创建而创建,当前线程消亡而消亡
4.2 java虚拟机栈
java栈存储的是栈帧,栈帧是随着方法调用而创建的,方法调用完成而销毁。压栈和出栈其实说的就是栈帧。
局部变量表:存储的是基本数据类型和对象的引用
当我们在代码中定义了一个变量时,java栈就会为这个变量分配内存空间,当该变量退出作用域后,java就会自动释放掉为该变量分配的内存空间。每个方法被调用的完整过程就对应着一个栈在虚拟机栈中从入栈到出栈的过程,每次调用方法都会在栈中创建栈帧
4.3 本地方法栈
与虚拟机栈作用非常相似,它们之间的区别不过是虚拟机栈为虚拟机执行java方法服务,本地栈是为虚拟机使用到的Navtive本地方法服务
4.4 java堆:
是虚拟机所管理的内存最大一块,堆是所用线程共享的区域,唯一目的是存放对象实例,几乎所有的对象都会在这里分配内存。java堆是垃圾回收器管理的主要区域,也被称为GC堆。
4.5 方法区:
和java堆一样,是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息,常量,静态变量,方法信息(构造方法/接口定义)即时编译器编译后的代码等数据。常量池是方法区中的一部分。
可以理解成class文件在内存中的存放位置。只要是new关键字创建的对象都会保存在堆里,方法区是永久代
五、虚拟机中对象创建的过程
5.1 如何创建
前言:java程序运行中无时无刻都有对象创建出来,创建对象通常是一个new关键字,而在虚拟机中对象的创建又是怎样的过程?
(1)类加载检查:
当JVM检测到有一条new指令时,首先先检查该指令的参数是否在常量池中定位到一个类的符号引用,并检查这个符号引用所代表的类是否已被加载、解析和初始化过。如果存在的话,JVM将直接使用已有的信息对该类进行操作。 如果没有,则执行相应的类加载过程。
(2)为新生对象分配内容:
类加载检查后,虚拟机将为新对象分配内存,对象所需内存的大小在类加载后就可以完全确认。
5.2 分配内存的两种方式:
(1)指针碰撞
把堆内存看成是绝对规整的,所有用过的内存放在一边,空闲的内存放在另一边。中间放着一个指针作为分界点的指示器。分配内存就是把指针向空闲区域挪动一段与对象相等的距离
(2)空闲列表
假如堆内存不是规整的,已使用的内存和空闲内存是交错的,就没办法指针碰撞了。虚拟机要维护一个表,用来记录哪些内存块是可用的,在分配内存的时候,找到适合的内存空间划分给当前对象。
选择哪种分配方式是由java堆是否规整决定,堆是否规整是由垃圾收集器是否带有压缩整理功能决定的。
内存分配完成之后,虚拟机将分配的内存空间都初始化为零,这里不包括对象头(Object Header)
(3)对对象进行设置:
接下来,虚拟机要对对象进行有必要的设置,比如这个对象是哪个类的实例。如何找到类的元数据信息,对象哈希码,对象的GC分代年龄等信息。这些信息存在对象头(Object Header)之中。
完成以上操作后,从虚拟机的角度来看,一个新对象就已经创建了。。。
5.3 对象的内存布局:
运行时数据:哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等等。
类型指针:虚拟机通过这个指针来寻找当前对象属于哪个类的实例
对象实际数据:是对象真正存储的有效信息,是程序代码中定义的各种类型的字段内容,无论是从父类继承下来的,还是子类中定义的
4, 对齐填充:给对象分配内存空间时,要求8个字节,不够的话,就填充满
5.4 JVM中的对象访问:
对象访问会涉及到java栈、java堆、方法区
Object obj = new Object();
Object obj 将会存在于java栈中本地变量表里,作为一个引用类型数据出现
new Object() 存在于堆中,形成一块存储Object类型所有实例数据
堆中存放引用类型,用来访问该类存放在方法区中的对象类型、父类、实现接口、构造方法等信息
(1)句柄访问
Java 堆 中将 会 划分 出 一块 内存 来作 为 句柄 池, reference 中 存储 的 就是 对象 的 句柄 地址, 而 句柄 中 包含 了 对象 实例 数据 和 类型 数据 各自 的 具体 地址 信息
(2)指针访问
Java 堆 对象 的 布局 中就 必须考虑如何放置访问类型数据 的相关信息, reference 中直接存储的就是对象地址
目前java 虚拟机采用的是第二种对象访问方式
六、垃圾回收算法
对象被判定为垃圾的标准是:没有被其它对象引用
垃圾收集器在回收堆之前,要确定堆中的对象,哪些存活?哪些死去?
8.1 引用计数算法:
python、游戏脚本语言采用引用计数算法进行内存管理
通过判断对象引用的数量来决定对象是否被回收,每个对象实例都有一个引用计数器,对象被引用就会+1,该对象超过了生命周期,计数器就会-1,引用计数为0就i会被当做垃圾回收对象。
优点:执行效率高,程序受影响小
缺点:无法检测出循环引用的情况,导致内存泄漏
8.2 标记清除算法:
标记:从根节点标记引用对象
清除:未被标记引用的对象是垃圾对象,会被回收
被root根对象引用的对象会被标记为存活对象,接下来就会把没标记的对象销毁
标记和清除都需要遍历对象,第一次标记,第二次清除
缺点:效率低,需要编辑所有对象,在垃圾回收时,需要暂停应用,体验差。为什么要暂停应用,因为会加入对象,没法确认引用关系,容易照成误清除对象。
8.3 标记压缩算法:
在标记清除的基础上,也是有根引用,把存活对象压缩到一边,把另外一边的垃圾对象清理。
解决了标记清除算法的碎片化问题,同时也比前者多做一步,要移动对象同时会影响效率
8.4 复制算法
8.5 分代算法
每一种算法都有自己的优点和缺点,根据垃圾回收对象的特点进行选择。
分代算法根据回收对象的特点进行选择,在JVM中,年轻代使用复制算法,老年代使用标记清除和标记压缩算法
七、垃圾收集器
7.1 串行垃圾回收器
单线程进行垃圾回收,垃圾回收时,Java应用所有线程都要停止工作,等待垃圾回收工作完成,这种现象称为STW。一般java应用不会采用。
7.2 并行垃圾回收器
在串行垃圾回收器上改进,采用了多线程,也需要让java应用停止工作,只不过执行更快,停止的时间更少。
7.3 CMS垃圾回收器
垃圾回收的过程,不会让应用停止工作
7.4 G1垃圾回收器
jdk1.7之后使用的全新收集器
八、虚拟机优化
做虚拟机的优化就是让GC越少越好,最理想的状态就是没有任何GC
8.1 JVM代的划分
方法区:永久代
堆:新生代、老年代
-
新生代
分为 Eden(伊甸园)、S1(斯歪文儿)、S2,新创建的对象全部在eden存放中
每次执行垃圾回收之后,优先将GC之后的存活对象放入S1和S2中
当多次GC之后,仍然存活的对象或者S1和S2占满时,将放入老年代中 -
GC:
当新对象生成时,并且在Eden申请空间失败时,就会触发GC -
Full GC:
永久代占满、老年代占满,处理时间比较长、因为范围广
查看虚拟机当前状态,cmd黑窗口:jvisualvm
8.1 JVM调优
Linux中在/etc/profile中加入
- 在jdk1.8以前,生产环境一般有如下配置
-XX:PermSize=512M -XX:MaxPermSize=1024M
表示在JVM里存储Java类信息,常量池和静态变量的永久代区域初始大小为512M,最大为1024M。在项目启动后,这个值是固定的,如果项目class过多,很可能遇到OutOfMemoryError: PermGen异常。
- 升级JDK1.8之后,上面的perm配置已经变成
-XX:MetaspaceSize=512M XX:MaxMetaspaceSize=1024M
九、各种引用
9.1 强引用
一般指的是Object obj = new object()
9.2 软引用
一些有用但是非必需。用软引用关联的对象,系统将要发生OOM之前,这些对象将会被回收
9.3 弱引用
public class TestSoftRef {
public static class User{
public int id = 0;
public String name = "";
public User(int id, String name) {
super();
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "User [id=" + id + ", name=" + name + "]";
}
}
public static void main(String[] args) {
User u = new User(1,"caojiulu");
SoftReference<User> userSoft = new SoftReference<>(u);
u = null;//保证new User(1,"caojiulu")这个实例只有userSoft在软引用
System.out.println(userSoft.get());
System.gc();//展示gc的时候,SoftReference不一定会被回收
System.out.println("AfterGc");
System.out.println(userSoft.get());//new User(1,"caojiulu")没有被回收
List<byte[]> list = new LinkedList<>();
try {
for(int i=0;i<100;i++) {
//User(1,"caojiulu")实例一直存在
System.out.println("********************"+userSoft.get());
list.add(new byte[1024*1024*1]);
}
} catch (Throwable e) {
//抛出了OOM异常后打印的,User(1,"caojiulu")这个实例被回收了
System.out.println("Throwable********************"+userSoft.get());
}
}
}
User [id=1, name=caojiulu]
AfterGc
User [id=1, name=caojiulu]
********************User [id=1, name=caojiulu]
********************User [id=1, name=caojiulu]
********************User [id=1, name=caojiulu]
********************User [id=1, name=caojiulu]
********************User [id=1, name=caojiulu]
********************User [id=1, name=caojiulu]
********************User [id=1, name=caojiulu]
User [id=1, name=caojiulu]
Throwablenull
9.4 弱引用
一些有用(程度比软引用更低),用弱引用关联的对象,只能生存到下一次GC之前,不管内存够不够,都会被回收。
public class TestWeakRef {
public static class User{
public int id = 0;
public String name = "";
public User(int id, String name) {
super();
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "User [id=" + id + ", name=" + name + "]";
}
}
public static void main(String[] args) {
User u = new User(1,"Caojiulu");
WeakReference<User> userWeak = new WeakReference<>(u);
u = null;
System.out.println(userWeak.get());
System.gc();
System.out.println("AfterGc");
System.out.println(userWeak.get());
}
}
User [id=1, name=Caojiulu]
AfterGc
null
9.5 虚引用
又称幽灵引用,最弱,被垃圾回收收到一个通知而已。
注意:
软引用 SoftReference和弱引用 WeakReference,可以用在内存资源紧张的情况下以及创建不是很重要的数据缓存。当系统内存不足的时候,缓存中的内容是可以被释放的。
例如,一个程序用来处理用户提供的图片。如果将所有图片读入内存,这样虽然可以很快的打开图片,但内存空间使用巨大,一些使用较少的图片浪费内存空间,需要手动从内存中移除。如果每次打开图片都从磁盘文件中读取到内存再显示出来,虽然内存占用较少,但一些经常使用的图片每次打开都要访问磁盘,代价巨大。这个时候就可以用软引用构建缓存。