文章目录
一、JVM的概念
1.JDK、JRE、JVM的关系是什么?
什么是JVM ?
英文名称(Java Virtual Machine),就是JAVA虚拟机,它只识别.class类型文件,它能够将class文件中的字节码指令进行识别并调用操作系统向上的API完成动作。
什么是JRE?
英文名称(Java Runtime Environment ) , Java运行时环境。它主要包含两个部分:JVM的标准实现和Java的一些基本类库。相对于JVM来说,JRE多出来一部分Java类库。
什么是JDK?
英文名称(Java Development Kit ) ,Java开发工具包。JDK是整个Java开发的核心,它集成了JRE和一些好用的小工具。例如: javac.exe、java.exe、jar.exe等。
这三者的关系:一层层的嵌套关系。JDK >JRE >JVM。
2.JVM的作用与好处
java可以跨平台运行最主要的原因就是因为有java虚拟机,同时jvm可以屏蔽系统差异发明一些新的语言就可以使用jvm来完成。这样就做到了《一次编译到处运行》的特点。
3.JVM的内存模型以及分区情况和作用
要想知道JVM中内存区域的划分,拿首先就要知道JVM中的内存区域从什么地方来,JVM中的内存是JVM向操作系统中申请一块内存,在针对这个内存划分一个区域。
如图所示:方法区,堆为线程共有,栈,本地方法栈,程序计数器为线程私有。
分区 | 作用 |
---|---|
方法区 | 用于存储虚拟机加载的类信息,常量,静态变量等数据。 |
堆 | 存放对象实例,所有的对象和数组都要在堆上分配。是JVM所管理的内存中最大的一块区域。 |
栈 | Java方法执行的内存模型:存储局部变量表,操作数栈,动态链接,方法出口等信息。生命周期与线程相同。 |
本地方法栈 | 作用与虚拟机栈类似,不同点本地方法栈为native方法执行服务,虚拟机栈为虚拟机执行的Java方法服务。 |
程序计数器 | 当前线程所执行的行号指示器。是JVM内存区域最小的一块区域。执行字节码工作时就是利用程序计数器来选取下一条需要执行的字节码指令。 |
成员变量在堆上
局部变量在栈上
静态变量在方法区中
4.JVM对象创建步骤流程是什么?
创建流程如下图所示:
第1步: 虚拟机遇到一个new指令,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用的类是否已经被加载&解析&初始化。
第2步: 如果类已经被加载那么进行第3步;如果没有进行加载,那么就就需要先进行类的加载。
第3步: 类加载检查通过之后,接下来进行新生对象的内存分配。
第4步: 对象生成需要的内存大小在类加载完成后便可完全确定,为对象分配空间等同于把一块确定大小的内存从Java堆中划分出来
第5步: 内存大小的划分分为两种情况:
第一种情况:JVM的内存是规整的,所有的使用的内存都放到一边,空闲的内存在另外一边,中间放一个指针作为分界点的指示器。那么这时候分配内存就比较简单,只要讲指针向空闲空间那边挪动一段与对象大小相同的距离。这种就是“指针碰撞”。
第二种情况:JVM的内存不是规整的,也就是说已使用的内存与未使用的内存相互交错。这时候就没办法利用指正碰撞了。这时候我们就需要维护一张表,用于记录那些内存可用,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新到记录表上。
第6步: 空间申请完成之后,JVM需要将内存的空间都初始化为0值。如果使用TLAB,就可以在TLAB分配的时候就可以进行该工作。
二、动态内存管理(GC)
1、GC的具体作用
GC是垃圾回收机制,java中申请的内存可以被垃圾回收装置进行回收,GC可以一定程度的避免内存泄漏,但是会引入一些额外的开销。
2、GC回收的特点
GC中主要回收的是堆和方法区中的内存,栈中内存的释放要等到线程结束或者是栈帧被销毁,而程序计数器中存储的是地址不需要进行释放。
回收对象的基本单位:对于GC中回收的基本单位不是字节而是对象
GC回收的一般是已经不使用的位置的内存
回收对象的基本思路:
1)标记:找到这个对象是否需要回收,并且标记出来
2)回收:将这个对象回收回去
3、标记
1)引用计数法
每个对象都会分配一个专门的计数器变量当有一个新的引用指向这个变量的时候计数器就会加一,当有一个引用不指向这个变量计数器就会减一,当引用计数器为0时就会让这个对象标记回收。
但是这就引用出循环引用问题不能解决:
public class Test01 {
public static void main(String[] args) {
Test a = new Test();
Test b = new Test();
Test c = new Test();
a.t = b;
b.t = c;
c.t = a;
}
}
class Test{
Test t = new Test();
}
当想要使用对象a,找到a的引用,这个引用在对象b中,想找对象b的引用在a中
2)可达性分析
在java中GC采用可达性分析来描述
代码中的对象之间是有一定的关联的,他们构成的一个“有向图”,然后我们去遍历这个“有向图”如果对象遍历的到就不是垃圾,反之就是垃圾。
一般从什么地方开始遍历?
1、每个栈中栈帧的局部变量表
2、常量池中引用的对象
3、方法区中静态变量引用的对象
遍历的时候不是像二叉树一样从一个地方开始遍历,而是从多个地方遍历,这样的我们统称为GCRoot。
3)方法区类对象的回收规则
1、该类的所有实例都已经被回收
2、加载该类的ClassLoader也已经被回收了
3、该类对象没有在代码中使用
4、引用的类型
引用类型 | 特点 | 垃圾回收 |
---|---|---|
强引用 | 可以找到对象也能决定对象的生死 | 只要有强引用存在,被引用的对象就不会被垃圾回收。 |
软引用 | 可以找到对象也能在一定程度上决定对象的生死 | 只有在内存不足的情况下,被引用的对象才会被回收。 |
弱引用 | 可以找到对象但是不能决定对象的生死 | 只要垃圾回收执行,就会被回收,而不管是否内存不足。 |
虚引用 | 不能找到对象也不能决定对象的生死 | 跟踪垃圾回收器收集对象的活动,在GC的过程中,如果发现有PhantomReference,GC则会将引用放到ReferenceQueue中,由程序员自己处理,当程序员调用ReferenceQueue.pull()方法,将引用出ReferenceQueue移除之后,Reference对象会变成Inactive状态,意味着被引用的对象可以被回收了。 |
5、回收
1)标记清除
优点:简单高效
缺点:容易造成内存的碎片问题
2)标记复制
将内存划分为两个区域直接拷贝不是垃圾的对象放到另一个区域
优点:可以很好的解决内存的碎片问题,不会存在碎片
缺点: 需要额外的内存空间(如果生存的对象比较多这时就比较低效)
3)标记整理
类似于数组删除数据
优点:不想复制一样需要额外的内存空间也没有内存碎片
缺点:搬运的效率较低不适合频繁使用
6、分代回收
我们可以通过分代回收的方法来更好的清理内存,目前jvm就使用此种方式,
我们把堆中的内存分为新生代(伊甸区和生存区)和老年代
对象诞生于伊甸区,新对象的内存就是新时代中的内存
当经历过第一轮GC扫描时就会把标记的对象干掉,没有被干掉的就会被拷贝到生存区01
进入生存区中的对象也会经过扫描然后拷贝到生存区02(生存区中的对象是从另外一个生存区和伊甸区过来的)
当对象在生存区经历了若干次拷贝之后没有被回收就说明这个对象存活的时间比较长就会被拷贝到老年代
老年代中的对象也要经过GC扫描,由于老年代中的对象存活的时间比较长,扫描老年代中的周期会比较长
7、垃圾回收器的介绍
收集器 | 收集对象和算法 | 收集器类型 |
---|---|---|
Serial | 新生代,标记复制算法 | 单线程 |
ParNew | 新生代,标记复制算法 | 并行的多线程收集器 |
Parallel Scavenge | 新生代,标记复制算法 | 并行的多线程收集器 |
Serial Old | 老年代,标记整理算法 | 单线程 |
Parallel Old | 老年代,标记整理算法 | 并行的多线程收集器 |
CMS | 老年代,标记清除算法 | 并行与并发收集器 |
G1 | 跨新生代和老年代;标记整理+化整为零 | 并行与并发收集器 |
垃圾回收器中都是做两件事情标记和回收,当进行回收的时候应用线程就会停止工作STW。
可以看出来新生代由于回收的对象比较多,所以最适合标记复制算法,老年代由于回收的对象不多,不适合使用标记复制算法,相对来说,比较适合标记整理算法,由于标记清除算法缺点比较突出,会产生内存碎片,不太适合JVM的垃圾收集器。
CMS和G1的区别:
a)初始标记(只去找GCRoot直接关联的对象)
b)并发标记(和应用线程并发执行,去遍历所有对象)
cms会一直执行下去、G1发现老年代没有存活的对象之后就会直接回收。
c)最终标记(为了修正b产生的误差)
d)筛选回收
三、类加载器
1、类加载的基本过程
我们根据类名找到文件,并且读取文件构造解析,将内容读取到内存中去,并且构造相应的类对象,这个过程叫做加载。
如果这个类还有其他相关联的类,就会将其他的依赖内容引入,这个过程叫做链接。
初始静态成员并且执行静态代码块,这个过程叫做初始化。
2、什么时候触发类加载
1)构造该类的实例
2)调用该类的静态属性和静态方法
3)使用子类时会触发父类的加载
class Test{
static {
System.out.println("Test.static{}");
}
{
System.out.println("Test{}");
}
public Test() {
System.out.println("Test构造方法");
}
}
class Test0 extends Test{
static {
System.out.println("Test0.static{}");
}
{
System.out.println("Test0{}");
}
public Test0() {
System.out.println("Test0构造方法");
}
}
public class Test01 extends Test0{
public static void main(String[] args) {
System.out.println("开始");
new Test0();
new Test0();
System.out.println("结束");
}
}
执行静态代码块——执行代码块——执行构造方法
3、常见的类加载器
内置的三个加载器 | 加载内容 | 位置 |
---|---|---|
BootstrapClassLoader(启动类加载器) | 加载标准库中的类(String ArrayList…) | jdk\jre\lib\rt.jar(标准库中的类的实现在这个rt.jar中) |
ExternsionClassLoader(拓展类加载器) | 加载一个特殊的类 | jdk\jre\lib\ext 的所有jar包中找 |
ApplicationClassLoader(应用程序类加载器) | 加载应用程序自己写的类 | CLASS_PATH环境变量 java -cp 当前目录 |
4、双亲委派模型
双亲委派模型的本质就是类加载的过程中通过类名查找到类的.class文件的查找过程,属于加载过程中的内容。
当一个类开始加载时,会先从AppClassLoader开始,但是它不会立刻查找会先交给自己的父类,ExtClassLoader也会交给自己的父类,然后BootstrapClassLoader拿到类名之后在rt.jar中开始查找,找到就返回,没找到就会在ExtClassLoader中的ext目录中开始查找,找到就返回,没有找到就会在AppClassLoader中的CLASS——PATH环境变量中、-cp指定目录中、当前目录中三个地方去查找,如果找到就加载,没有找到就抛出一个ClassNotFoundException异常。
参考文献
https://blog.csdn.net/ashfiqa/article/details/122658797