学习视频链接:B站 遇见狂神说
认识JVM
JVM,全称Java Virtual Machine,英文为Java虚拟机,“虚拟机(Virtual Machine)是指通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统”,但是 虚拟机本质还是该计算机系统的一个进程,为了方便描述,我们把整个计算机当成一幢大楼,而虚拟机则是某一个楼层。大楼划分了一个区域给一个楼层,让这个楼层自己管理自己,也就对应着,计算机划分了一个内存给JVM,让JVM自己管理自己。JVM的内存区域划分为两大块,线程隔离的区域和线程共享的区域。
JVM体系结构
三种JVM
- Sun公司 HotSpot
Java HotSpot(TM) 64-Bit Server VM (build 25.271-b09, mixed mode)
- BEA
JRockit
- IBM
J9VM
类加载器
启动类加载器
(父)启动类加载器(BootstrapClassLoader):rt.jar
扩展类加载器
(母)扩展类加载器(ExtClassLoader): ext/*.jar
应用类加载器
App/SystemClassLoader: 应用/系统类加载器,加载当前classpath的所有类。
自定义类加载器
XxxClassLoader:用户自定义的类加载器,默认使用双亲委派,委托上级来加载。
工作流程
如果一个类加载器接收到了类加载的请求,它首先把这个请求委托给他的父类加载器去完成,每个层次的类加载器都是如此,因此所有的加载请求都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它在搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
生命周期
源码分析
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检查这个classsh是否已经加载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// c==null表示没有加载,如果有父类的加载器则让父类加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果父类的加载器为空 则说明递归到bootStrapClassloader了
//bootStrapClassloader比较特殊无法通过get获取
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {}
if (c == null) {
//如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
双亲委派机制
Java中为了保证类加载的安全,使用了双亲委派机制。优先从启动类加载器中加载,这个称为“父”。“父”无法加载到,再从扩展类加载器中加载,这个称为“母”。
双亲委派。如果都加载不到,才会考虑从应用类加载器中加载。直到加载到为止。
当某个类加载器需要加载某个
.class
文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。
- 类加载器收到类加载的请求
- 将这个请求向上委托为父类加载器去完成,一直向上委托,直到启动类加载器
- 启动类加载器检查是否能够加载当前的这个类,能加载就结束,使用当前的加载器,否则抛出异常,通知子加载器进行加载
- 重复步骤 3
面试问题
为什么需要双亲委派机制?(也就是双亲委派的优点)
①双亲委派机制使得类加载出现层级,父类加载器加载过的类,子类加载器不会重复加载,可以防止类重复加载;
②使得类的加载出现优先级,防止了核心API被篡改,提升了安全,所以越基础的类就会越上层进行加载,反而一般自己的写的类,就会在应用程序加载器(Application)直接加载。
如何打破双亲委派?
①自定义类加载器,重写loadClass方法
②使用线程上下文类加载器
沙箱安全机制
什么是沙箱
沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——==CPU、内存、文件系统、网络==。不同级别的沙箱对这些资源访问的限制也可以不一样。
所有的Java程序运行都可以指定沙箱,可以定制安全策略。
沙箱的基本组件
字节码校验器(bytecode verifier):确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。
类装载器(class loader):其中类装载器在3个方面对Java沙箱起作用
- 它防止恶意代码去干涉善意的代码;
- 它守护了被信任的类库边界;
- 它将代码归入保护域,确定了代码可以进行哪些操作。
虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。
类装载器采用的机制是双亲委派模式。
- 从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
- 由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。
存取控制器(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。
安全管理器(security manager):是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。
安全软件包(security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:
- 安全提供者
- 消息摘要
- 数字签名
- 加密
- 鉴别
Native
Java在内存区域中专门开辟了一块标记区域——本地方法栈,用来登记native方法,凡是带了native关键字的,会进入到本地方法栈中,调用本地方法接口(JNI),在最终执行时,通过JNI(Java Native Interface)加载本地方法库中的方法。
JNI作用:扩展Java的使用,融合不同的编程语言为Java所用。最初是想融合C,C++的,因为Java诞生的时候,C,C++横行,想要立足的话就要有能调用C的程序
本地方法栈(Native Method Stack):具体做法是Native Method Stack中登记native方法,在执行引擎(Execution Engine)执行时加载本地库(Native Libraies)。
PC寄存器
程序计数器:Program Counter Register
每个线程都有一个程序计数器, 是线程私有的,就是一个指针, 指向方法区中的方法字节码(用来存储指向像一条指令的地址, 也即将要执行的指令代码),在执行引擎读取下一条指令, 是一个非常小的内存空间,几乎可以忽略不计。
方法区
方法区:Method Area
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关。
栈
栈:先进后出,后进先出
队列:先进先出(FIFO:first input first output
线程结束,栈内存也就释放了,对于栈而言,不存在垃圾回收问题。
堆
堆,Heap。一个JVM只有一个堆内存,堆内存的大小是可以调节的。
类加载器读取类文件后,一般会把类、方法、常量、变量放到堆中,保存所有引用类型的真实对象。
堆内存中细分为以下三个区域:
- 新生区
- 养老区
- 永久区
在JDK8后,永久存储区更换为元空间。
方法区是一种定义、概念。而所谓永久代或元空间是其一种实现机制。(元空间逻辑上存在,物理上不存在)
GC垃圾回收,主要是在伊甸园区和养老区。
若内存满了,OOM,堆内存不够。java.lang.OutOfMenoryError:Java heap space
新生区
- 一个类诞生和成长(甚至死亡)的地方。
- 所有的对象都是在伊甸园区new出来的。
- 幸存者区(存活区)分为0,1区(from space ,to space谁空谁是to)。
养老区
主要存放JVM认为生命周期比较长的对象(经过几次的新生区(Young Gen)的垃圾回收后仍然存在,换个说法就是新生区没干掉,没杀死的来到了养老区),内存大小相对会比较大,垃圾回收也相对没有那么频繁(譬如可能几个小时一次)。
大多对象都是临时对象。
永久区
这个区域常驻内存中,用来存放jdk本身携带的Class对象和Interface元数据,存储的是Java运行时的一些环境或类信息,该区域不存在垃圾回收,关闭VM虚拟机就会释放这个区域的内存。
- jdk1.6之前:永久代,常量池是在方法区
- jdk1.7:永久代,但是慢慢的退化了,
去永久代
,常量池在堆中 - jdk1.8之后:无永久代,常量池在元空间
一个启动类,加载了大量的第三方jar包;Tomcat部署过多的应用,大量动态地生成的反射类,不断地被加载,直到内存满,就会出现OOM。
GC垃圾回收
垃圾回收区域在堆和方法区,99%(大多)在堆中。
GC分为两类:轻GC(普通的GC)、重GC(全局GC)
GC常用算法
标记清除法,标记压缩,复制算法,引用计数法。
引用计数法
给对象添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1;任何时刻计数器为0的对象就是不可能再被使用的。但是此算法有个致命缺陷无法解决循环引用的问题。
复制算法
新生区使用这种算法进行gc 。将可用的内存按容量划分为大小相等的两块(from,to),每次只是用其中一块(总有一块是空的【to区域】)。当这一苦熬的内存用完了,就将还存活着的对象复制到另外一块上面,然后把已使用过的内存空间一次清理完。
- 每次GC都会讲Eden区中的对象清到幸存区中,一旦Eden区被GC,那么Eden区就会是空的
- 两个幸存区保持谁空谁是to的原则,即每次GC回将上一轮的from区中的对象复制,然后加上Eden区中新的被GC进来的对象,合在一起放入空的to区中,然后to区变成下一轮的from区,原来的from区被清空,变成新一轮的to区
- 当一个对象经历了15次(默认值)GC还没有死,那么就会进入养老区中
注:可通过-XX:MaxTenuringThreshold=value
来调节进入养老区的时间,这也是JVM性能调优的一种方式
优点:没有内存碎片
缺点:浪费内存空间,多了一半空间为空,加入存活率100%就会发生OOM,所以复制算法常用与存活率较低的新生区中
标记清除法
养老区通常使用这种算法。先标记出所有需要回收的对象,在标记完成后统一回收(清除)所有被标记的对象。
优点:不需要额外空间
缺点:两次扫描,浪费时间,同时会产生内存碎片
标记压缩算法
标记清除压缩算法,对于标记清除的进行压缩。