小白肝JVM(篇一)

JVM

一、JVM内存结构

1.JVM内存区域

1.1 JVM运行时数据区

在这里插入图片描述

1.2 程序计数器

线程私有

​ 线程私有就是线程与线程之间不共享,各线程之间不影响,独立存储。

程序计数器

​ 官方解释:程序计数器是一块比较小的内存空间,是线程私有的生命周期与线程一样,它可以看作是当前线程所执行的字节码的行号指示器,记住下一条jvm指令的执行地址。

​ 通俗解释:一个.java文件被编译成.class文件,然后通过类加载被JVM执行,.class文件也是一个程序,那么里面就有代码,程序计数器就是指向.class文件中的每行代码的东西,程序计数器指向哪一行就执行哪一行。

【注意】:程序计数器是唯一一个没有内存溢出的区

字节码解释器

​ 说到程序计数器,就不能不说字节码解释器,字节码解释器就是将二进制字节码解释成机器码交给CPU,字节码解释器会改变程序计数器的值来选取下一条需要执行的字节码指令。

1.3 虚拟机栈
1.3.1 定义

​ 与程序计数器一样,虚拟机栈也是线程私有的它的生命周期和线程的生命周期是一样的。虚拟机栈是描述Java方法执行的内存模型,每个方法在执行的同时会创建一个栈帧(每个方法执行所需要的内存),每一个方法从执行到结束对应着入栈和出栈的过程。

1.3.2 栈溢出

​ 两种导致栈溢出的情况:

  • 栈帧过多导致栈内存溢出:当栈内存的栈帧过多时,比如:递归方法终止条件没设置好可能会导致栈内存溢出(StackOverFlowError)。
  • 栈帧过大导致栈内存溢出:当栈内存种的一个栈帧太大使得整个栈都装不下,那就会导致栈内存溢出。

​ 两种异常状况:

  • StackOverFlowError:如果线程请求的栈的深度大于JVM所允许的栈深度,那么就会抛出StackOverFlowError
  • OutOfMemeryError:如果JVM栈可以动态扩展(目前的虚拟器大多都可以),如果扩展时无法申请到足够的内存,则会抛出OutOfMemoryError
1.4 本地方法栈

本地方法栈

​ 本地方法栈和虚拟机栈差不多,也是线程私有的,生命周期与线程一样,发挥的作用时非常相似的,不过虚拟机栈是用来执行Java方法,而本地方法栈是来执行被Native所修饰的方法。

Native方法

​ JVM是用C语言写的,所以底层会有一些C语言或者C++的类库,那么Java如果想要调用其他语言的类库中的方法时,就会在本地方法栈中执行。

1.5 Java堆

​ Java堆是JVM内存中最大的一块,是所有线程共享的,在虚拟机启动时创建(虚拟机栈与线程的生命周期一样),几乎所有的对象实例和数组都在这块。所以Java堆也是GC垃圾回收机制的主要管理区域。

​ Java堆可以不是内存物理连续的,只要逻辑连续即可,当堆的内存不够时会抛出OutOfMemoryError

1.6 方法区

方法区

​ 方法区和Java堆都是所有线程共享的,物理内存不一定连续,方法区中存储的是JVM加载的类信息、常量、静态变量等数据

方法区中,还可以选择不进行垃圾收集,但这并不是”永久代“,在对方法区数据回收的主要是常量池的回收和对类型的卸载。

​ 当方法区无法满足内存分配需求时会抛出OutOfMemoryError

运行时常量池(常量池)

​ 常量池是方法区中的一部分,用来存放一些”字面量“和符号引用

​ 常量池也是方法区中的一部分,那么当常量池无法申请到内存空间时就会抛出OutOfMemoryError

2.对象的创建

2.1 对象创建的前提

​ 当JVM遇到new指令时(即创建对象时)先会检查在常量池中能否定位到这个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过,如果没有,那么必须先得执行类加载,待类加载结束之后就可以确定这个对象需要分配多少内存空间。

2.2 分配对象空间

​ 给对象分配内存空间是在堆上分配,前面讲过,堆中的内存可以是物理连续的,也可以是逻辑连续的。根据这个可以分为以下不同分配空间的方式:

指针碰撞

​ 假设JVM堆中的内存空间是物理连续的,且绝对规整,所有用过的内存放一边,没有用过的内存放在另外一边,那么可以设置一个指针指向中间。当分配内存时,只需将指针向空闲区域移动相应类型大小即可,这就是”指针碰撞“。

空闲列表

​ 假设JVM堆中的内存空间是逻辑连续的,空闲区域和使用区域相互交错,那么就不能使用指针碰撞的方式。而是通过一张表记录堆中空闲的地址,当这个空闲地址被占用时,表会及时更新数据,这就是”空闲列表“。

2.3 对象的访问定位

​ 在创建完对象之后又该如何访问对象,虚拟机中有两种方式:

使用句柄

​ 如果使用句柄访问的话,堆中会分出一块内存用来做”句柄池“,句柄中存放对象实例数据和类型数据的地址。

​ 当对象被创建之后,栈中会有reference(引用),引用中存储的就是句柄的地址,句柄中存储的是实例数据和类型数据的地址。

【优点】:引用中存放的是稳定的句柄地址,在对象被移除之后只会改变句柄中的地址,而引用本身不需要改变。
在这里插入图片描述

直接指针

​ 使用”直接指针“会让引用直接指向对象,即引用中存储的是对象的地址。

【优点】:直接指针的优点就是速度快,它节省了一次指针定位的时间开销,对于Sun HotSpot而言,是使用这种方式访问对象的。
在这里插入图片描述

二、垃圾回收机制

1.垃圾收集器

1.1 对象存活判定算法
1.1.1 引用计数算法

​ 给对象中添加一个引用计数器,当对象被调用时计数器加1;当引用失效时计数器减1,;任何时刻一个对象的计数器为0时就是不可能被引用了。

​ 引用计数器的实现简单,判定效率也比较高,也有一些公司使用这种算法,但是主流的Java虚拟机是没有采用这种的。

1.1.2 可达性分析算法

​ 在主流的商用程序语言(Java、C#,甚至包括古老的Lisp)的主流实现中,都是通过可达性分析算法判断对象是否存活的。

​ 这个算法是通过一个”GC ROOTS“的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到达GC ROOTS没有任何路径时,则证明此对象是不可用的。

​ 虽然Object4、Object5和Object6互相有关联,但是它们到达不了GC ROOTS,所以它们会被判定为垃圾回收的对象。
在这里插入图片描述

​ 在Java语言中,可作为GC ROOTS的对象有以下几种:

  • 虚拟机栈中(栈帧中的本地变量表)中引用的对象
  • 方法区内类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(一般说的Native方法)引用的对象
1.2 引用

​ 在JDK1.2之后,Java对引用的概念进行了补充,将引用分为强引用、软引用、弱引用和虚引用,这4中引用强度逐次降低。

  • 强引用:强引用就是在程序代码中普遍存在的,比如"Object o=new Object()"这类的引用,只要强引用还在,对象就不可能被垃圾回收。
  • 软引用:软引用来描述一些还有用但非必需的对象,在系统将要发生内存溢出异常时,将会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,则会抛出内存溢出异常。
  • 弱引用:弱引用也是用来描述一些非必需的对象,但它的强度比软引用还要弱。被弱引用关联的对象只能生存到下一个垃圾收集发生之前,当垃圾收集器工作时,无论内存空间是否够用,都会回收被弱引用关联的对象。
  • 虚引用:虚引用是最弱的一种引用关系。一个对象是否具有虚引用和其生命周期没有任何关系,也无法通过虚引用来取得一个对象实例,设置虚引用的目的就是当这个对象被回收之后会收到一个系统通知。
1.3 垃圾收集算法
1.3.1 标记-清除算法

​ 最基础的收集算法就是”标记-清除算法“,和它的名字一样,分为”标记“和”清除“两个部分:首先标记要清除的对象,在标记完之后统一进行回收。它之所以是基础算法,是因为后面的回收算法都是基于标记-回收算法之上对其缺点进行改进的。

【缺点】

  • 效率问题:标记和清除两个过程的效率都不高。
  • 空间问题:标记清除之后会产生大量的不连续空间,当需要分配一个比较大的空间时,无法找到足够大的连续空间就不得不提前触发另一次垃圾收集动作。
1.3.2 复制算法

​ 为了解决标记-清除算法的效率问题,复制算法出现了。复制算法将可使用内存分为两个半块,每次只使用其中的一块,当这一块的内存用完了之后,会将这块内存中还存活的对象复制到另外一块上区,然后将这块的内存回收,内存分配时也不用考虑内存碎片等问题,只不过这种算法的代价太大,只能使用内存的一半。

1.3.3 标记-整理算法

​ 当存活对象较多时,使用复制算法效率就会变低,因此在老年代中使用”标记-整理算法“。标记的过程仍与”标记-清除算法“一样,但是后续步骤不是对可回收对象进行回收,而是让所有存活的对象都向一端移动,然后直接清除掉边界以外的内存。

1.3.4 分代收集算法

​ 目前的商业虚拟机的垃圾回收都是使用的”分代收集算法“,分代收集是根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集都会发现有大批对象死去,那么就可以使用复制算法,只需要付出少量复制成本就可以解决。在老生代中,由于对象存活率高,就要使用”标记-清除“或者”标记-整理“算法进行回收。

1.4 垃圾收集器

​ JVM中有Serial收集器、ParNew收集器、Parallel Scavenge收集器、Serial Old收集器、CMS收集器、G1收集器。以下主要结束CMS和G1收集器。

1.4.1 CMS收集器

​ CMS收集器是基于”标记-清除算法“的收集老年代的收集器,CMS收集器的内存回收是和用户线程一块并发执行的,整个过程分为4个部分:

  • 初始标记:仅仅只是标记以下GC Roots能够关联到的对象,速度很快
  • 并发标记:进行GC Roots Tracing的过程,用来找到那些不可达GC Roots的对象
  • 重新标记:修正并发标记期间用户程序继续运作而导致标记产生变动的那一部分对象标记记录,耗时较长
  • 并发清除:

CMS处理浮动垃圾

​ 由于CMS收集器在并发清理阶段用户线程都还在运行着,那么伴随着程序的运行就会有垃圾产生,但是这一部分垃圾产生于GC标记之后,只好留到下一次垃圾收集时处理,这就叫做浮动垃圾。也是由于垃圾回收阶段用户线程还在继续执行,就不可能使内存占满之后再启动垃圾回收,因此CMS收集器当老年代使用了68%的空间后就会被激活,如果在应用中老年代的增长不是很快,那么可以适当调高参数提高出发百分比,以达到降低内存回收次数提高效率。

​ 在JDK1.6中,CMS收集器的启动阈值已经达到92%,如果在CMS预留的内存空间不够时就会出现"Concurrent Mode Failure"失败,就会临时启动Serial Old收集器重新收集老年代。

1.4.2 G1收集器

G1收集器的特点

  • 并发于并行:在进行垃圾收集期间,用户线程仍可以正常进行。
  • 分代收集
  • 空间整合:从整体来看,G1是使用”标记-整理算法“,从局部来看,G1使用”复制算法“。无论是哪种,这两种算法都意味着G1运行期间都不会产生内存碎片。
  • 可预测的停顿

G1收集器运作步骤

  • 初始标记:仅仅只是标记以下与GC Roots关联的对象
  • 并发标记:使用可达性分析算法在堆中找到存活对象,耗时较长。
  • 最终标记
  • 筛选回收
2.内存分配和回收策略

2.1 内存分配

对象优先在Eden区(新生代)分配

​ 大多数情况下,对象在新生代Eden区分配,当Eden区没有足够空间时会发起一次Minor GC(Minor GC是新生代GC,指发生在新生代的GC),如果本次GC之后还是没有足够空间,那么会启用分配担保机制分配到老年代中

大对象直接进入老年代

​ 大对象是指需要大量连续内存空间Java对象。

长期存活的对象进入老年代

​ 既然有分代收集算法,那么内存回收时就必须要能识别那些对象应放在新生代,哪些对象放在老年代。为了做到这点,虚拟机会给每个对象定义了一个对象年龄计数器,如果对象在Eden区出生并经历过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设置为1,对象在Survivor每经历过一次Minor GC,年龄就会增长一岁,当增长到15岁时,就会被晋升到老年代中。

【Survivor区】:新生代中分为Eden区和Survivor区,其空间比例为8:1

三、类加载机制

​ 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

1.加载方式
1.1 静态加载

通过new关键字来创建User的实例对象

public static void main(String[] args){
    User user=new User();
}
1.2 动态加载

通过Class.forName()来加载类,然后调用newInstance()方法实例对象

public static void main(String[] args){
    Class c=new Class.forName("java.com.drl.entity.User");
    Object o=c.newInstance();
}

通过类加载器的方式加载类,然后调用newInstance方法实例对象

public static void main(String[] args){
    Class c=classLoader.loadClass("java.com.drl.entity.User");
    Object o=c.newInstance();
}
2. 类加载器
2.1 定义

​ 通过一个类的全限定名称获取到描述这个类的二进制字节流的代码块叫做类加载器。

2.2 类加载器分类
  • 启动类加载器:负责将存放在Java_HOME\lib目录的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。
  • 扩展类加载器:负责将JAVA_HOME\lib\ext目录中的或被java.ext.dirs系统变量所指定的路径中的所有类库
  • 应用类加载器:负责加载用户类路径(ClassPath)上所指定的类库,如果开发者没有定义过自己的类库,一般情况下这个就是程序中默认的类加载器
  • 自定义类加载器:通过继承java.lang.ClassLoader类的方式实现
2.3 类加载的步骤

​ 类加载种有加载、验证、准备、解析和初始化这5个步骤。

  • 加载:通过一个类的权限定性名称找到相应的class文件然后导入
  • 验证:检查加载class文件的正确性
  • 准备:为类变量(只给被static的变量,即静态变量)分配内存并设置类变量初始值的阶段
/*
	这块的设置变量初始值是让a=0,而不是123
	在初始化阶段时才会执行Java代码,使a=123
*/
public static int a="123";
  • 解析:虚拟机将常量池内的符号引用替换成直接引用的过程
  • 初始化:执行类中定义的静态变量和静态代码块
2.4 双亲委派模型

​ 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是这样,因此所有的类加载请求最终都会传到启动类加载器,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类),子加载器才会去尝试自己加载。

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
    // First, check if the class has already been loaded
    Class<?> c = findLoadedClass(name);
    if (c == null) {
        long t0 = System.nanoTime();
        try {
            if (parent != null) {
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
            // ClassNotFoundException thrown if class not found
            // from the non-null parent class loader
        }

        if (c == null) {
            // If still not found, then invoke findClass in order
            // to find the class.
            long t1 = System.nanoTime();
            c = findClass(name);

            // this is the defining class loader; record the stats
            PerfCounter.getParentDelegationTime().addTime(t1 - t0);
            PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
            PerfCounter.getFindClasses().increment();
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}
}

四、问题辨析:

1.垃圾回收是否涉及栈内存?

​ 答:不涉及,栈内存是用来存放线程执行方法所需要的内存,当方法开始执行和执行完毕时对应着栈帧入栈和弹栈的过程,会被自动回收掉,不需要垃圾回收管理栈内存。

2.栈内存的分配越大越好吗?

答:不是,虚拟机的内存是一定的,当将栈内存分配大的时候会导致运行的线程变少(一个线程一个栈),并且栈越大也只是能够调用更多的方法,和调用的效率无关。

3.方法内的局部变量是否线程安全?

​ 答:是的,每个线程都会有自己独立的栈(或着说栈是线程私有的),方法内的局部变量会保存在栈内,不会被多个线程共享,故不会有线程安全问题。

【篇一,后面有精力会继续深入JVM,看这点先去试水】

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值