JVM知识点

254 阅读9分钟

1.JVM内存区域

JVM内存分为五个区,分别是:堆、jvm栈、本地方法栈、方法区、程序计数器

其中,堆和方法区是线程共享的,而栈和程序计数器每个线程都会单独为其分配空间。

1.1方法区:

存放已被类加载的类信息、静态变量、常量、编译后的代码等数据。

1.2堆:

存放对象实例,几乎所有对象实例和数组都在这里分配内存。

1.3虚拟机栈:

由一个一个栈帧组成,每个栈帧主要有局部变量表、操作数栈、动态链接、方法出口信息等。
生命周期同线程。

1.4本地方法栈:

和虚拟机栈基本一样,不过这个栈存的是native方法的信息。

1.5程序计数器:

让字节码解释器通过改变程序计数器来依次读取指令,在多线程情况下记录当前线程执行的位置。

2.Java对象创建的过程

2.1.类加载检查:

虚拟机在接收到new指令后会先去检查 这个指令的参数是否能在常量池中定位到这个类的符号引用·,
并且检查这个符号引用代表的类是否被加载、解析、初始化过。如果没有就先进行类加载过程。  

*:常量池中包含两部分:字面量、符号引用。
字面量就是字符串、final值、基本数据类型值等;符号引用指类和结构的完全限定名、字段名称和描述
符、方法名和描述符。所以·的意思就是看看先前是否有要new的类。

2.2.分配内存:

在类加载完成后即可确定对象的大小,然后从堆中取出一块内存空间进行分配,
分配方式有两种:指针碰撞、空闲列表。  

2.2.1内存分配的两种方式

选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。
而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩")。

    指针碰撞:
        使用场合:java堆规整,没有内存碎片。
        原理:用过的内存全都放到一边,没用过的放另一边,中间有个分界值指针,只需向着没用过的内存方向将该指针移动对象内存大小位置即可。
        GC收集器:Serial、ParNew
    空闲列表:
        使用场合:有内存碎片。
        原理:虚拟机会维护一个列表,记录着哪些内存块是可以用的,在分配的时候找一块足够大的内存块划分
        给对象实例,最后更新列表。
        GC收集器:CMS

2.2.2存分配的并发问题(线程安全后)

2.3.初始化默认值:

将除对象头以外的内存空间设为0值,保证对象的实例字段在java代码中可以不赋初值就使用。

2.4.设置对象头:

对象头信息是与对象自身定义无关的额外存储成本,包含Mark Word、类型指针。需要对其进行必要的设置。

关于对象头(也和锁有关系):【Java对象解析】不得不了解的对象头

2.5.执行init方法:

完成了上述操作,从虚拟机角度已经完成对象的创建了,但从java角度,还需完成init方法初始化对象。

3.对象分配规则

java堆中分为:新生代和老年代,而新生代又分为Eden区、From survivor区、To Survivor区。

流程:一般来说,对象优先分配在Eden区,经过一次minorGC后,对象还存活的话就放到Survivor
区中,同时该对象的年龄也会+1,当对象年龄到达阈值
后转移到老年代。

规则:  
  1.如果放入对象时Eden没有足够的空间,就执行一次minor GC。  
      2.大对象(需要大量连续空间的对象,如字符串)直接放入老年代,避免在Eden和两个
      Survivor区之间进行大量内存拷贝。  
      3.如果Survivor区中相同年龄的对象大小总和超过Survivor区空间的一半,就将大于等于
      该年龄的对象都放入老年代中。  

full GC和major GC?

4.GC垃圾回收

4.1对象存活判断

4.1.1判断对象存活的两种方式

引用计数法:为对象添加引用计数器,当这个对象被引用了计数器+1,引用失效,计数器-1;
为0时表示这个对象不可再被使用。  
    缺点:无法解决对象之间循环引用的问题。
可达性分析算法:从GC Roots为起点出发向下搜索,节点走过的路称引用链,如果某一对象和
GC Roots间没有引用链就证明该对象是不可达的(但这时还不能说这个对象死亡)。

可作为 GC Roots 的对象包括:1.方法区中常量引用的对象 2.方法区中静态属性引用的对象
3.虚拟机栈中引用的对象 4.本地方法中引用的对象

4.1.2强引用、软引用、弱引用、虚引用

强引用:垃圾器宁愿抛出OutOfMemoryError 错误,使程序异常终止,也不会回收它来解决内存
不足的问题。平时new一个对象就是强引用。

软引用:如果内存空间够,就不回收它;内存不够就回收它们。软引用可以实现内存敏感的高速缓存,
如:浏览器的后退按钮。
    后退时,要显示的网页内容是重新进行请求呢还是从缓存中取出呢?这就要看内存是否充足了:
        如果该对象还没有被回收掉,就直接从内存中取出;
        如果值没有了,表示此时内存吃紧已经被回收掉了,那么就重新构建一个。

弱引用:具有更短暂的生命周期。如果回收器发现到只具有弱引用的对象,不论此时内存是否充足,
都会将它回收。

虚引用:并不会决定对象的生命周期,对象任何时候都有可能被回收。它的作用主要是用来跟踪对象
被垃圾回收的活动。

4.1.3不可达对象真正死亡的时候

要真正宣告一个对象死亡至少要经过两次标记。
不可达对象被第一次标记并进行一次筛选,条件是此对象是否有必要执行finalize方法,有必要时再
将该对象放在一个队列中进行第二次标记,如果这个对象还是没有与引用链有关系,那就真的被回收了。
  没必要执行的条件:没有覆盖finalize方法/finalize方法已经被虚拟机调用过。

4.2.GC算法

标记-清除算法、标记-整理算法、复制算法、分代收集算法

4.2.1标记-清除算法

分为两个阶段:标记、清除。
先标记出需要回收的对象,在标记完成后统一回收清除所有被标记的对象。
缺点:1.效率问题;2.空间问题:在清除后有大量不连续的碎片。

4.2.2标记-整理算法

标记过程同上,但清除不是直接清除,而是先让所有存活对象向一端移动,最后清除掉端边界以外的内存。

4.2.3复制算法

分出两块大小相同的内存,每次使用一块,当其中一个内存使用完毕后,就把还存活的对象移动到另一块
中,然后再把这一块使用空间一次清理掉。

4.2.4分代收集算法

其实就是根据新生代。老年代的特点分别使用上述算法。

4.3垃圾回收器(只看了最基本的)

4.3.1 Serial收集器、Serial old

单线程收集器,新生代采用复制算法,老年代采用标记-整理算法,特点是执行时必须stop the world
停止其他所有工作线程。

4.3.2 ParNew收集器

Serial收集器的多线程版本

4.3.3 Parallel Scavenge 收集器、Parallel old

跟ParNew没多大区别,但它更关注吞吐量

4.3.4 CMS收集器

老年代里面的唯一一个基于标记-清除算法,它是老年代并行收集器,以获取最短回收停顿时间为目标的
收集器,具有高并发,低停顿的特点,追求最短GC回收停顿时间。

4.3.5 G1收集器

唯一一个可以同时用于年轻代和老年代的垃圾收集器。G1收集器采用标记-整理的算法,避免碎片。

5.类加载过程

5.1 加载

将代码转化为字节流,让这些字节流的静态存储结构转换为方法区内的运行时数据结构,在堆中创建一个
class对象作为方法区那些数据的访问入口。

5.2 验证

JVM规范验证和代码逻辑验证

5.3 准备

内存分配和初始化。内存在方法区中分配,只对static修饰的变量进行内存分配和初始化为零值。
其他实例对象会在初始化阶段再初始化。

5.4 解析

就是将常量池中的符号引用替换为直接引用的过程,也就是得到类、字段、方法在内存中的指针或偏移量
的过程。

5.5 初始化

遇到:new、反射、继承初始父类、main等才会初始化。

5.6 使用

JVM从入口方法执行代码。

5.7 卸载

程序执行完毕,JVM销毁创建的Class对象,负责运行的JVM也退出内存。
卸载类需要满足3个要求:
    该类的所有的实例对象都已被GC,也就是说堆不存在该类的实例对象;
    该类没有在其他任何地方被引用;
    该类的类加载器的实例已被GC。

6.类加载器

BootstrapClassLoader(启动类加载器);
ExtensionClassLoader(扩展类加载器);
AppClassLoader(应用程序类加载器)。

7.双亲委派机制

7.1原理

当加载一个类时,系统先判断当前类是否被加载过,是就直接返回,不是在去加载。
而加载的时候该类自己先不尝试加载而是交给父类加载器加载,因此所有的请求最终都应该
传到顶层的启动加载器BootstrapClassLoader。而当父类加载器无法处理时才会一级一级
往下尝试加载。

7.2好处

避免类的重复加载,也保证了 Java 的核心 API 不被篡改。