你应该知道的 JVM 基础知识

Java 程序运行机制步骤

  1. 首先编写 Java 源代码,源文件的后缀为 .java
  2. 利用 JVM 编译器将源代码编译成字节码文件,字节码文件的后缀名为 .class
  3. 由 JVM 解释器运行字节码文件,将字节码文件解释成当前机器能够看懂的二进制数据
  4. 进行类的转载(加载、链接、初始化)
  5. 将数据读入到内存中,将不同的数据(变量)放入运行时数据区中的不同区域内
    在这里插入图片描述

JVM 运行时数据区

在这里插入图片描述

  • 程序计数器(Program Counter Register):
  1. 每一个线程都有一个程序计数器,是线程私有的
  2. 是一个指针,指向方法区的方法字节码,每当执行引擎读取下一条指令时,就计一次数,所占内存非常小
  3. 在多线程环境下时,某一个时刻一个cpu之会执行一个线程的指令,当要执行另一个线程时,可通过程序计数器来得知下一条需要执行的指令
  4. JVM 解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能
  • Java 虚拟机栈(Java Virtual Machine Stacks):
  1. 一个线程有一个栈内存,栈内存包含八大基本数据、对象的引用、方法(栈帧)
  2. 栈中储存的是一个个栈帧
  3. 栈帧中保存局部变量和部分计算结果,参与方法的调用与返回,当一个方法被调用时,就创建一个栈帧,方法结束,该栈帧弹出栈,线程一旦结束,栈也就关闭
  4. 一个栈帧内包含 自己的本地变量数组、方法索引、输入输出参数、本地变量、对象引用、父帧、子帧
  5. 栈内存不会产生垃圾
  • 本地方法栈(Native Method Stack):
  1. 本地方法栈是为 JVM 调用 Native 方法服务的,Native method(本地方法)特点是与某些硬件交互
  2. 用native关键字修饰的方法,说明调用该方法超过Java作用范围,Java要去调用底层C语言的方法,凡是Java调用的native方法,会进入JVM的本地方法栈,进而使用JNI调用本地方法接口
  • 方法区(Methed Area):
  1. Java 中所有类的方法信息包含在内,静态变量、常量、类信息(构造方法、接口)、运行时常量池也包含在内
  2. Java8 之后,方法区在元空间中,元空间在本地内存中
  • Java 堆(Java Heap):

*堆内存当中,存在新生区(新生代 PSYoungGen)、养老区(老生代 ParOldGen)
* 一个JVM只有一个堆内存
* 堆内存存储对象的实例、类、方法、常量、变量
*
* 新生区又包含三个区(伊甸区 Eden、幸存者to区、幸存者from区)
* 所有的对象第一次new出来的时候都在伊甸区,当经过GC时,死亡的死亡,存活下来的进入幸存者区
* 当伊甸区的容量满了,新生代会GC,也称为轻量级GC(轻GC)
*
* 最终未死亡的对象会进入养老区
* 当养老区空间满了,也会发生GC,被称为重量级GC(重GC)(Full GC)
* 当新生区与养老区空间都满了,还有新的对象创建出来,则发生OOM内存溢出错误
*
* 元空间(永久区)
* 永久区也被称为非堆
* 在Java8后,此区域常驻内存,JVM方法区也在此区域中,运行时常量池在方法区中
* 此区域是存储JDK自带的class对象,interface元数据、存储类信息与Java运行时环境
* 此区域不会被垃圾回收
* 在JDK1.6之前 永久代,常量池在方法区
* JDK1.7 永久代,但被慢慢淘汰,永久代和方法区都在堆中,常量池在方法区中
* JDK1.8 无永久代,元空间在本地内存中,方法区在元空间中,常量池在方法区中
*
* ps:
* 元空间逻辑上存在,物理上不存在
* 将JVM总内存和最大内存调制1024M时,将新生区与养老区内存总数相加,结果等于最大内存

TLAB(Thread Local Allocation Buffer)

我们说堆是线程共享区域,但会不会有以下问题:

在并发环境下,A线程与B线程同时创建Person对象,但A、B线程的person引用指向同一个实例对象

为解决该问题,对象的内存分配过程就必须进行同步控制。一个HotSpot虚拟机的解决方案:

每个线程在Java堆中预先被分配一小块内存,然后再给对象分配内存的时候,直接在自己这块”私有”内存中分配,当这部分区域用完之后,再分配新的”私有”内存。

这种方案被称之为 TLAB(Thread Local Allocation Buffer)分配。这部分Buffer是从堆中划分出来的,但是是本地线程独享的。

该方案是给每个线程在Eden区分配一块空间,一个线程创建的对象实例就放在自己所属的内存中,当然一个线程所属内存中的对象对于其他线程是可以访问的,在TLAB分配之后,并不影响对象移动到幸存者区或养老区,也不影响其被GC

那如果某个线程的TLAB内存满了怎么办:

1、如果一个对象需要的空间大小超过TLAB中剩余的空间大小,则直接在堆内存中对该对象进行内存分配
2、如果一个对象需要的空间大小超过TLAB中剩余的空间大小,则废弃当前TLAB,重新申请TLAB空间再次进行内存分配

上面两种方案如何选择:

JVM定义了refill_waste参数,可理解为“最大浪费空间”
当请求分配的内存大于refill_waste的时候,选择方案1
若小于refill_waste值,则会废弃当前TLAB,选择方案2

对TLAB进行调优的参数:

  1. 选择开启或者关闭TLAB,通过设置-XX:+/-UseTLAB参数来指定是否开启TLAB分配。
  2. TLAB默认是eden区的1%,可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。
  3. 默认情况下,TLAB的空间会在运行时不断调整,使系统达到最佳的运行状态。如果需要禁用自动调整TLAB的大小,可以使用-XX:-ResizeTLAB来禁用,并且使用-XX:TLABSize来手工指定TLAB的大小。
  4. TLAB的refill_waste也是可以调整的,默认值为64,即表示使用约为1/64空间大小作为refill_waste,使用参数:-XX:TLABRefillWasteFraction来调整。
  5. 如果想要观察TLAB的使用情况,可以使用参数-XX+PringTLAB 进行跟踪。

Java 中都有哪些引用类型

  • 强引用:发生 gc 的时候不会被回收,例如Object obj = new Object(),只要 obj 指向堆中的 Object 实例,即使内存不足,该对象也不会被 gc
  • 软引用:有用但不是必须的对象,在发生内存溢出之前会被回收
  • 弱引用:有用但不是必须的对象,在下一次GC时会被回收
  • 虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知

Java 垃圾回收

垃圾回收的区域只发生在堆中
1.当新生代的伊甸区容量满了,引发轻GC,将活下来的对象复制到 幸存者to区(也就是一个空的区域)
2.轻GC之后,伊甸区就为空
3.幸存者区分为to区与from区,如何区别?哪个区为空,哪个就是to区
4.两个区不断移动对象,总会保证有一个区是空的
5.当一个对象经历了15次GC还能存活下来(默认),该对象进入养老区(-XX:MaxTenuringThreshold)此参数可改变对象经历X次GC进入养老区

当养老区满了,会触发重GC,养老区与新生区都会进行清除
但会导致 Stop The World(STW),造成很大的性能开销
STW 就是对堆进行彻底清理,在该过程中,工作线程是无法继续执行的
如果Full GC被触发,这时有大量请求进来,则会被拒绝服务,所以要尽量减少Full GC次数
新生区与养老区的区域、容量设计,也是尽量让对象晚一点进入养老区

Safe Point

Safe Point
发起Full GC的时间点被称为 Safe Point
这个时间点的选定不能太少而使GC时间太长,也不能过于频繁而过分增大运行时开销

Safe Point 主要指的是以下特定位置:
1.循环的末尾
2.方法返回前
3.调用方法的call之后
4.抛出异常的位置

JVM 如何判断当前对象是否要被 GC

  • 引用计数器法:

例如 Person p = new Person(“张三”); 这时张三对象的引用数为1
将p置为null,张三对象的引用数为0,就可被清除了
但也会出现循环引用的问题,例如 Person p1 = new Person(“张三”); Person p2 = new Person(“李四”);
将 p1.ref = p2; p2.ref = p1; 使张三李四两实例中的属性ref都指向对方,将p1、p2置为null
这样就形成了循环引用,张三李四两实例的引用数都为1,但没有引用指向他们,JVM无法GC

  • 可达性分析算法:

可达性算法的原理是以一系列叫做 GC Root 的对象为起点出发,引出它们指向的下一个节点,再以下个节点为起点,引出此节点指向的下一个结点
通过 GC Root 串成的一条线就叫引用链,直到所有的结点都遍历完毕
如果相关对象不在任意一个以 GC Root 为起点的引用链中,则这些对象会被判断为垃圾,会被 GC 回收。
|
当发生GC时,JVM会给要回收的垃圾对象一次存活的机会,那就是调用对象的finalize()方法,该方法只会调用一次
如果执行finalize()方法后,此对象变成了可达,则不会回收,之后如果该对象再次被GC,则忽略finalize()直接清除
|
GC Root 有以下几类:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中 JNI(即一般说的 Native 方法)引用的对象

JVM 的垃圾回收算法

  • 标记-清除算法
    该算法是标记可回收的对象,再清除这些对象,该算法实现简单,不需要对象进行移动,但是效率不高,会产生大量不连续的内存碎片。

  • 标记-整理算法
    该算法标记可回收的对象后将所有存活的对象压缩到内存的一端,使他们紧凑的排列在一起,然后对端边界以外的内存进行回收。回收后,已用和未用的内存都各自一边。该算法解决了内存碎片的问题,但仍需要进行局部对象移动,一定程度上降低了效率。

  • 复制算法

该算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾收集时,遍历当前使用的区域,把存活对象复制到另外一个区域中,最后将当前使用的区域的可回收的对象进行回收。该算法实现简单、运行高效,不用考虑内存碎片,但使得内存的利用率不高。

1.轻GC当中使用的算法叫复制算法
2.复制算法所需的两块区域正好符合幸存者to区与from区
3.每次GC,都会将伊甸区与from区存活的对象放入to区,再清除其他区域
4.之后被清除的区域就是to区,存放对象的区就是from区

  • 分代回收
    堆中的新生代一般用复制算法,因为新生代中会有大量对象死亡,少量对象存活。
    老生代中的对象存活时间较长,使用标记清除算法或标记整理算法。

类加载器有哪些

  • 根类加载器(Bootstrap ClassLoader):用来加载java核心类库,无法被java程序直接引用(加载 jre 中 jre/lib 下的 jar 文件,例如会加载 rt.jar 下所有的 class 文件,由C++实现,不是 ClassLoader 子类)。
  • 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
  • 系统类加载器(system class loader):它根据 Java 应用的类路径来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它。用户自定义类加载器,通过继承java.lang.ClassLoader类的方式实现。

类装载的执行过程

  1. 加载:将类的字节码文件读入到内存中,并为之创建一个 java.lang.class 对象的过程
  2. 链接:
  • 校验:确保当前 class 文件中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全(文件格式验证、元数据验证、字节码验证、符号引用验证)
  • 准备:给类中的静态变量分配内存空间
  • 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址
  1. 初始化:对静态变量和静态代码块执行初始化工作,为静态变量赋值
  2. 使用:执行代码中的操作
  3. 卸载:JVM 通过 GC 将类信息与相关实例数据从 JVM 内存区域中移除

类加载时机

new一个对象
访问某个类或接口的静态变量,或者对该静态变量赋值
调用类的静态方法
反射(Class.forName(“xxx”))
初始化一个类的子类(会首先初始化子类的父类)
JVM启动时标明的启动类,即文件名和类名相同的那个类

双亲委派加载机制

如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。使用该机制可保证类被成功加载,还可保障一个特定的类被加载一次,避免重复加载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值