什么是JVM
JVM是Java虚拟机的缩写,是用来解析和运行Java程序的一个抽象虚拟的计算机。Java虚拟机主要包括类加载器、运行时数据区、执行引擎、本地方法接口和垃圾收集五大模块。
什么是类加载器
类加载器就是将class字节码文件加载到内存中,并且对其进行验证、准备和解析、初始化的工作,最终形成在内存中可以直接使用的数据类型。
类加载过程
- 加载:把.class的二进制字节码文件加载近jvm中
- 链接:
- 验证:确保被加载类的正确性
- 准备:为类变量(静态变量)分配内存,设置默认值(初始化之前不赋予初始值)
- 解析:把类中的符号引用转换成直接引用
- 初始化:为类变量赋予初始值
- 实例化:为新对象分配内存空间,为实例变量赋值
类加载器的分类
APP应用程序类加载器 --> Ext扩展类加载器 --> Boot启动类加载器
双亲委派机制
步骤:类加载器收到类加载的请求,会把这个请求委托给父类加载器去加载,并一直向上委托直到启动类加载器,若其能加载成功,则成功返回;否则,抛出异常,通知子加载器去加载。
作用:
- 防止重复加载同一个.class,保证数据安全。
- 保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象。不同加载器加载同一个.class也不是同一个class对象。这样保证了Class执行安全。
package java.lang;
public class String {
//双亲委派机制:安全
//app-->exc-->boot(最终执行)
public String toString(){
return "hello";
}
public static void main(String[] args) {
//s 为lang包下的String类而不是自定义的String类,将报错
String s = new String();
System.out.println(s.getClass().getClassLoader());
s.toString();
}
}
沙箱安全机制
沙箱的基本组件
字节校验器:确保Java类文件遵循JAVA语言规范。这样可以帮助Java程序实内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类
类加载器:其中类加载器在三个方面对java沙箱起作用
它防止恶意软件代码去干涉善意代码;
守护被信任的类库边界
将代码归入保护域,确定了代码可以进行哪些操作
存取控制器:存取控制器可以控制核心API对操作系统的存取权限。而这个控制的策略设定,可以由用户指定。
安全管理器:是API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。
安全软件包:java.security下的类和扩招包下的类,允许用户为自己的应用增加新的安全特性,包含:安全提供者 消息摘要 数字签名 加密 鉴别
运行时数据区
PC寄存器
PC寄存器是一个指针,每个线程都有一个程序计数器,数据为线程私有,用于记录当前线程要执行的字节指令所处的地址,通过程序计数器来选取下一条需要执行的字节码指令,如该方法为native的,则PC寄存器中不存储任何信息。
本地方法栈
JVM采用本地方法栈来支持native方法的执行,此区域用于存储每个native方法调用的状态
方法区域
方法区域逻辑上存在物理上不存在,可存放静态变量、常量、类信息(构造方法、接口定义),常量池数据,其数据为所有线程共享。JDK1.7以前是在永久代中,JDK1.8是在元空间中(元空间不在虚拟机中,使用本地内存)
栈
栈内存,主管程序的运行,生命周期和线程同步;线程结束,栈内存也就是释放,对于栈来说,不存在垃圾回收问题,一旦线程结束,栈就over。栈内存中存在:8大基本类型+对象引用+实例的方法;数据为线程私有。
堆
堆是jvm调优的主要区域,一个JVM只有一个堆内存,大小可以调节;主要存放类(具体实例), (类中)方法、(类中)常量,(类中)变量,保存我们所有引用类型的真实对象;
堆内存分为三部分:新生代、老年代、永久代(JDK1.8之后为元空间),新生代与老年代内存比例为1:2
新生代分为:伊甸区、幸存者0区,幸存者一区,比例为8:1:1,为GC的主要场所。
对象在堆内存中的历程
- 所有对象都在伊甸区创建,并且绝大部分在伊甸区完成整个生命周期
- 对象持续创建的情况下,当伊甸区空间用完时,JVM会对伊甸区进行GC(轻GC:只针对新生代),将存活下来的对象移动到幸存者0区
- 若0区也满了,再次进行GC,在将存活对象移动到幸存者1区,此时0区和1区是互相交替的一个过程(from->to的关系,空的一方为to),若新生代的三区内存都满了,就将GC15次后仍然存活的对象移动到老年代
- 若老年代对象也满了,JVM进行重GC(fullGC:针对老年代的GC,偶尔伴随对新生代的GC以及对永久代的GC),若老年代执行了重GC内存还是满的没办法创建新对象,就会产生OOM异常(OutOfMemoryError)
GC在不同区域实行分代算法;新生代使用复制算法,老年代使用标记清除与标记整理算法。
方法区和永久代的关系
永久带里面存的东西基本上就是方法区规定的那些东西,所以永久带可以看成是方法区的一种实现(奥迪和汽车的关系),当然,在hotspot jdk8中metaspace元空间可以看成是方法区的一种实现。
- jdk1.6之前:永久代,常量池是在方法区;
- jdk1.7:永久代,但是慢慢的退化了,去永久代,常量池在堆中(字符常量池在堆中,静态常量池和运行常量池在方法区)
- jdk1.8之后:无永久代,字符串常量池在堆中,运行时常量池在元空间
对象在内存中的实例化过程
创建一个Animal的对象,那么Animal类的一些信息(类信息、静态信息都存在于方法区中)而Animal类被实例化出来之后,被存储到java堆中,当我们去使用的时候,都是使用Animal对象的引用,比如Animal animal= new Animal();这里的animal就是存放在java栈中的,即animal真实对象的一个引用。
public class Animal {
String name;
int age;
Cat cat;
void myName(){
System.out.println("我是"+name);
}
public static void main(String[] args) {
Animal animal = new Animal();
animal.name = "猫科动物";
animal.age = 3;
Cat cat = new Cat();
cat.name = "哆啦A梦";
animal.cat = cat;
}
}
class Cat {
String name;
}
堆内存调优
使用内存快照分析工具MAT或Jprofiler分析Dump内存文件,快速定位内存泄露。
Jprofile使用:
1.在idea中下载jprofile插件
2.联网下载jprofile客户端
3.在idea中VM参数中写参数 -Xms1m -Xmx8m -XX: +HeapDumpOnOutOfMemoryError
4.运行程序后在jprofile客户端中打开找到错误 告诉哪个位置报错
命令参数详解
// -Xms设置初始化内存分配大小/164
// -Xmx设置最大分配内存,默以1/4
// -XX: +PrintGCDetails // 打印GC垃圾回收信息
// -XX: +HeapDumpOnOutOfMemoryError //oom DUMP
GC垃圾回收
JVM将没有被使用的对象回收的过程称为GC,GC大部分发生在新生代,主要分为两种,分别是轻GC(新生代),重GC(老年代)
GC算法
-
引用计数法:每个对象都有一个计数器,对象被引用一次加1,引用失效减1,计数器变为0被GC,计数器本身有一定消耗,并且较难处理循环引用,JVM一般不采用这种算法。
-
复制算法:将内存分为两块,只是用其中一块,当内存满了进行GC,将存活的对象复制到另一块内存上。优点是效率高,没有内存碎片,缺点是需要双倍内存空间,有点浪费;适合对象存活率低的情况使用,新生代采用这种算法。
-
标记清除算法:第一阶段扫描整个内存区域,将存活的对象进行标记,第二阶段再次扫描,清除没有被标记的对象。优点是不需要额外内存空间,缺点是存在内存碎片以及两次扫描耗时严重效率低并且应用程序会暂时停止。
-
标记整理算法:第一阶段与标记清除一致,第二阶段将存活对象都向一端移动,然后再清理掉边界以外的内存。优点是节约内存空间还没有内存碎片,缺点是效率最低。
-
总结:
内存效率:复制算法 > 标记清除算法 > 标记压缩算法
内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法
内存利用率:标记压缩算法 = 标记清除算法 > 复制算法
这几种算法各有优劣,所以没有最好的算法,只有最合适的算法(分代收集算法)