JVM学习

3 篇文章 0 订阅

JVM

类加载子系统

在加载阶段,Java虚拟机主要完成以下三件事情:

  1. 通过一个类的全限定名来获取此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的进行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

类的生命周期

加载->链接->初始化

链接:验证->准备->解析

  1. 验证:进行字节码文件的各种验证,比如:文件格式验证(是否以魔数0xCAFEBABE开头)等,元数据验证(这个类是否有父类,是否继承了不允许被继承的类)等,字节码验证(对类的方法体进行验证,保证类的方法在运行时不会做出危害虚拟机安全的行为),符号引用验证(该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源)
  2. 准备:为类变量定义初始值(final修饰的常量会直接赋程序员定义的值,而不是默认值
  3. 解析:将符号引用替换为直接引用

初始化:就是执行类构造器<clinit>方法的过程。clinit方法是编译器自动收集类中所有的类变量的赋值动作和静态代码块中的语句合并而成的。方法会被自动的加锁同步

类加载器

类加载器分类

JVM中有三个重要的ClassLoader,除了BoottsrapClassLoader外,其它类加载器均由Java实现,并且全部继承自java.lang.ClassLoader:

  1. BootstrapClassLoader(启动类加载器):最顶层的加载类,由C++实现,负责加载<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数指定的路径中的所有类
  2. ExtensionClassLoader(扩展类加载器):该类是在类sun.misc.Launcher&ExtClassLoader中以Java代码形式实现的。负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。
  3. AppClassLoader(应用程序类加载器):也叫系统类加载器。面向我们的用户的加载器,负责加载用户类路径上所有的类库。

双亲委派模型

在类加载的时候,系统会先判断当前类是否被加载过。已经被加载过的类会直接返回,否则才会尝试加载。加载的时候,首先会将该请求委派给父类加载器的loadClass()处理,因此所有的请求最终都会传送到顶层的启动类加载器BootstrapClassLoader中。当父类加载器无法处理时,都由自己来处理。当父类加载器为null时,会使用启动类加载器BootstrapClassLoader作为父类加载器。

类的双亲委派模型是用 组合 实现的,而不是继承实现的。双亲并不意味着有一个父亲,一个母亲。

双亲委派模型的好处:

  1. 避免类的重复加载,父类加载器已经加载一次之后,子类加载器没有必要再加载一次
  2. 避免核心API被修改
    1. 自定义类:java.lang.String
    2. 自定义类:java.lang.A

loadClass()的源码:

 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
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }

获取类加载器的几种方式

//1、通过Class对象的getClassLoader()方法
        ClassLoader classLoader = A.class.getClassLoader();
        System.out.println(classLoader);
        //2、获取当前线程上下文的ClassLoader
        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
        System.out.println(contextClassLoader);
        //3、获取系统的ClassLoadeer
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);

JVM运行时数据区

在这里插入图片描述

线程私有的:程序计数器、本地方法栈、虚拟机栈

线程共享的:堆、方法区

程序计数器

没有GC。 没有OOM

虚拟机栈(Java栈)

栈管运行,堆管存储

没有GC

以栈帧存储。

栈帧
  1. 局部变量表(Local Variables)
  2. 操作数栈
  3. 动态链接
  4. 方法返回地址
  5. 一些额外的附加信息
局部变量表(Local Variables
  1. 存放方法参数和方法内部定义的局部变量
  2. 局部变量表所需的容量大小是在编译期间确定下来的,就在方法的code属性的max_locals数据项中
  3. 存储单元是Slot(变量槽)
操作数栈(Operand Stack)

也称为表达式栈。

操作数栈的深度在编译期间就确定了,保存在方法的code属性的max_stacks中。

栈顶缓存技术

动态链接(Dynamic Linking)

或叫 指向运行时常量池的方法引用

方法调用

静态链接 和 动态链接

非虚方法(解析):方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。“编译期确定,运行期不可变”,有:私有方法,静态方法,父类方法,实例构造器,final方法

调用方法的字节码指令:

  1. invokestatic:用于调用静态方法
  2. invokespecial:用于调用实例构造器<init>()方法、私有方法和父类方法
  3. invokevirtual:用于调用所有的虚方法
  4. invokeinterface:用于调用接口方法,会在运行时再确定一个实现该接口的对象
  5. invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法

invokestatic,invokespecial和被invokevirtual调用的被final修饰的方法都是非虚方法。

修改invokestatic调用静态方法,invokespecial调用

动态分派是很频繁的动作,需要搜索类型元数据。为了优化,在方法区中建立了虚方法表(Virtual Method Table)。虚方法表一般在类加载的连接阶段进行初始化。

分派(Dispatch)
  1. 静态分派

    1. 编译器(编译期间)进行的是静态分派。
    2. 虚拟机(编译器)在重载时是通过参数的静态类型来确定方法的重载版本的。
    3. 所有依赖静态类型来决定方法执行版本的分派动作,都叫静态分派
    4. 最典型的表现就是方法重载
    5. 变长参数的重载优先级是最低的
  2. 动态分派

    1. 在运行期根据实际类型确定方法执行版本的分派过程称为动态分派(运行期间进行的是动态分派)。
    2. invokevirtual指令时解析过程大致分为以下几步:
      1. 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C
      2. 如果在类型C中找到与常量中的描述符和简单名称都对应的方法,则进行访问权限验证,如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回IllegalAccessError异常
      3. 否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程
      4. 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常
    3. 正是因为invokevirtual第一步就是在运行期确定接收者的实际类型,所以调用过程中不是把常量池中的符号引用解析到直接引用上就结束了。还会根据接收者的实际类型来选择方法版本,这个过程就是方法重写的本质。
  3. 多分派与单分派

    1. 首先来看编译器

      1. 编译器进行的是静态分派

      2. 在这里插入图片描述

      3. 编译时选择目标方法的依据有两点:一是静态类型是Father还是Son;二是方法参数是QQ还是360。这次方法的选择结果是产生了两条invokevirtual指令,两条指令的参数分别为常量池中指向Father::hardChoice(360)Father::hardChoice(QQ)方法的符号引用。因为是根据两个宗量进行选择,所以**java语言的静态分派属于多分派类型**

    2. 再看看运行阶段虚拟机的选择,也就是动态分派的过程。

      1. 这时参数的静态类型、实际类型都不会对方法的选择造成任何影响。唯一可以影响选择的因素只有该方法的接受者的实际类型是Father还是Son。因为只有一个宗量可以作为选择依据,所以**java语言的动态分派属于单分派类型**
    3. 总结:如今(直到Java 12和预览的Java 13)的Java语言是一门 静态多分派、动态单分派的语言。

      方法返回地址(Return Address

虚拟机栈中,栈帧作为存储单位,而栈帧中比较重要的结构是 局部变量表操作数栈;其余三个部分:方法返回地址附加信息动态链接可以并称为栈帧信息

方法有两种返回类型:

  1. 正常退出。此时 :主调方法的PC寄存器的值就可以作为返回地址。
  2. 异常退出。此时 :通过异常处理器表来确定。
附加信息

与调试、性能收集相关的信息。


虚拟机栈的五道面试题
  1. 举例栈溢出的情况?

    答:通过-Xss来设置栈的大小。StackOverflowError,OOM

  2. 调整栈大小,就能保证不出现溢出吗?

    答:不一定。本来就是一个死循环的话……

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

    答: 不是。

  4. 垃圾回收是否会涉及到虚拟机栈?

    答:不会。

  5. 方法中定义的局部变量是否线程安全?

    答:~~是。虚拟机栈是线程安全的。~~具体情况具体分析。如果局部变量被返回值返回了出去……

本地方法接口

native

该部分不属于运行时数据区

本地方法栈(Native Method Stacks)

Java 虚拟机栈类似,Java 虚拟机栈管理Java方法的调用,本地方法栈管理非Java方法的调用。

堆(Java Heap)

在这里插入图片描述

堆空间大小的设置:

-Xms-Xmx

-XX:InitialHeapSize-XX:MaxHeapSize

默认情况下,初始内存大小:物理电脑内存大小 / 64

最大内存大小:物理电脑内存大小 / 4

-XX:NewRatio=2:默认是2 。代表老年代与新生代的比例。

HotSpot中,Eden空间和另外两个Survivor空间缺少所占的比例是8:1:1。 开发人员可以通过-XX:SurvivorRatio调整这个空间比例。比如:-XX:SurvivorRatio=8.

-XX:-UseAdaptiveSizePolicy:关闭自适应的内存分配策略。

-Xmn:设置新生代的空间大小。一般不设置。

各种GC的对比

在这里插入图片描述

总结内存分配策略
  • 优先分配在Eden
  • 大对象直接进入老年代
    • HotSpot虚拟机提供了-XX:PretenureSizeThreshold参数,指定大于该设置值的对象直接分配在老年代。该参数只对SerialParNew两款新生代收集器有效。
    • 大对象容易导致内存明明还有不少的空间就提前触发垃圾收集。
  • 长期存活的对象将进入老年代
    • 默认晋升的阈值是15,可以通过-XX:MaxTenuringThreshold=参数进行设置。
  • 动态对象年龄判定
    • 如果在Survivor区中低于或等于某年龄的对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,而无须等到-xx:MaxTenuringThreshold中的数值。
  • 空间分配担保
    • 在发生一次Minor GC之前,虚拟机必须检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果这个条件成立,则这一次Minor GC可以确保是安全的。
    • 如果不成立,则先检查-XX:HandlePromotionFailure是否允许担保失败。

TLAB

可以通过-XX:+/-UseTLAB来确认是否启用

代码优化(逃逸分析)

在这里插入图片描述

逃逸可以分为三种类型:

  • 不逃逸
  • 方法逃逸
    • 当对象在一个方法里面被定义以后,可能被外部方法所引用,例如作为调用参数传递进其它方法中,这种称为方法逃逸。
  • 线程逃逸
    • 可能被外部方法访问到,例如赋值给可以在其它线程中访问的实例变量,这种称为线程逃逸。
栈上分配:

如果对象没有发生线程逃逸,可以采用栈上分配的策略。

支持方法逃逸

标题替换

如果一个对象不会逃逸出方法,并且这个对象可以被拆分,那么程序真正执行的时候,可能不去创建这个对象,而改为直接创建它的若干个被 这个方法使用的成员变量来代替。

同步省略

如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其它线程访问,那么这个变量的读写就肯定不会有竞争,对这个变量实施的同步措施就可以安全地消除掉。


方法区

HotSpot中方法区的演进

jdk 7及以前,习惯上把方法区,称为永久代。jdk 8开始,使用元空间取代了永久代。元空间使用的是直接内存。

设置方法区大小及OOM
  1. jdk 7及以前的时候,通过设置永久代的大小来设置方法区的大小:
    1. -XX:PermSize=N:方法区(永久代)的初始大小
    2. -XX:MaxPermSize=N:方法区(永久代)的最大大小,超过这个值将会抛出OOM:PermGen
  2. jdk 8及以后,通过设置元空间的大小来设置方法区的大小:
    1. -XX:MetaspaceSize=N:设置Metaspace的初始大小
    2. -XX:MaxMetaspaceSize=N:设置Metaspace的最大大小
方法区的内部结构
常量池和运行时常量池

class文件中有常量池,内存中有运行时常量池。

方法区的演进细节

HotSpot中方法区的变化:

jdk版本说明
jdk1.6 及以前在永久代(permanent generation),静态变量存放在永久代上。
jdk1.7在永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中。
jdk1.8无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆。
StringTable为什么要调整位置?

jdk7中将StringTable放到了堆中。因为永久代的回收效率很低,在full gc的时候都会触发。而full gc是老年代空间不足、永久代不足时才会触发。

这就导致StringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。

如何证明静态变量放在哪里?

对象的实例化内存布局与访问定位

对象的实例化

在这里插入图片描述

  1. 加载类元信息
  2. 划分对象的内存空间(为对象分配空间)
  3. 处理并发安全问题
  4. 初始化分配到的空间
  5. 设置对象的对象头
  6. 执行init初始化

对象的内存布局

对象的访问定位

直接内存(Direct Memory)

缺点:

  • 分配回收成本较高
  • 不受JVM内存回收管理

大小可以通过MaxDirectMemorySize设置。


虚拟机执行引擎

解释器


StringTable

  • 字符串常量池中是不会存储相同的字符串的。

拼接操作与append操作的效率对比

  • StringBuilderappend()的方式:自始至终只创建过一个StringBuilder的对象
  • 使用字符串拼接方式:创建过多个StringBuilderString的对象
    • 内存中由于创建了较多的StringStringBuilder的对象,内存占用更大;如果进行GC, 需要花费更多的时间。

改进的空间:如果基本确定添加的长度不高于某个数值,建议使用StringBuilder(int capacity)的构造器。

intern()方法的使用

  • jdk 1.6及之前:
    • 如果字符串常量池中已经有了该字符串,则直接返回该串的地址。
    • 如果没有该字符串,则将该串复制一份,放入常量池,然后返回池中对串的引用
  • jdk 1.7及之后:
    • 如果字符串常量池中已经有了该字符串,则直接返回该串的地址。
    • 如果没有该字符,则在池中引用一下当前堆中串的地址,然后再返回这个地址。

new String()到底创建了几个对象

StringTable的垃圾回收问题


垃圾回收相关章节的说明

什么是垃圾?为什么要GC?

垃圾回收相关算法

标记阶段相关算法

引用计数算法(Reference Counting)

当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。

对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。

优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性

缺点:

  • 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销
  • 每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销
  • 引用计数器有一个更严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
可达性分析算法(根搜索算法、追踪性垃圾收集(Tracing Garbage Collection))

所谓GC Roots根集合就是一组必须活跃的引用。

基本思路:

  • 可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上到下的方式搜索被根对象集合所连接的目标对象是否可达
  • 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所超过的踒称为引用链(Reference Chain)
  • 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以被标记为垃圾对象
  • 在可达性分析算法中,只有根对象集合直接或间接连接的对象才是存活对象。
GC Roots的选取
  1. 在虚拟机栈(栈帧中的本地变量表)中引用的对象
    1. 比如:各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量
  2. 本地方法栈JNI中引用的对象
  3. 方法区中类静态属性引用的对象
    1. 如:Java类的引用类型静态变量
  4. 方法区中常量引用的对象
    1. 如:字符串常量池中的引用的对象
  5. 所有被同步锁(synchronized关键字)持有的对象
  6. Java虚拟机内部的引用
    1. 基本数据类型对应的Class对象
    2. 常驻的异常对象
    3. 系统类加载器
  7. 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
对象的finalization机制

Java虚拟机中的对象可以分为有三种状态:

  1. 可触及的:从根结点开始,可以到达这个对象
  2. 可复活的:对象的所有引用都被释放,但是对象有可能在finilize()方法中被复活。
  3. 不可触及的:对象的finalize()方法被调用,并且没有复活,那么就会进入不可触及状态。 不可触及状态的对象不可能被复活,因为**finalize()方法只会被调用一次**。

以上三种状态中,只有在对象不可被触及时才可被回收。

判定一个对象objA是否可被回收,至少要经过再次标记过程:

  1. 如果对象objAGC Roots没有引用链,则进行第一次标记。
  2. 进行筛选,判断此对象是否有必要执行finalize()方法。
    1. 如果对象objA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA被判定为不可触及的
    2. 如果对象objA重写了finalize()方法,且还未执行过,那么objA会被放置在一个名为F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。
    3. finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记。如果objAfinalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA会被移出**“即将回收”**集合。之后,对象会再次出现没有引用的情况。在这个情况下,finalize方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize()方法只会被调用一次。

JVM类型

  1. SUN Classic JVM
    1. 解释器和JIT不能同时使用
  2. Exact VM
    1. 准确式内存管理
    2. 具备现代高性能虚拟机的雏形
      1. 热点探测
      2. 编译器和解释器混合工作模式
  3. Hotspot虚拟机
    1. 它的名称中的HotSpot指的就是它的热点代码探测技术
  4. BEA公司的JRockit
    1. 专注于服务器端应用
  5. IBM 的J9
    1. 有影响力的三大商用虚拟机之一
  6. KVM和CDC/CLDC HotSpot
    1. JavaME产品
  7. Azul VM
    1. 高性能Java虚拟机中的战斗机
    2. 与特定硬件平台绑定、软硬件配合的专有虚拟机
  8. Liquid VM
  9. Apache Harmony
  10. Microsoft JVM
  11. TaobaoJVM
  12. Dalvik VM
  13. Graal VM

类加载器

获取类加载器的方法:

  1. 利用ClassLoader.getSystemClassLoader()方法
  2. 利用某个类的getClassLoader()方法
  3. 获取线程的上下文加载器:Thread.currentThread.getContextClassLoader()
  4. 本地方法:DriverManager.getCallerClassLoader()

JVM参数设置

堆空间相关的参数

  1. -XX:+PrintFlagsInitial:打印参数的初始值

  2. -XX:+PrintFlagsFinal:打印参数的最终值

  3. -Xms:堆空间的初始大小(默认为机器内存的1/64)

  4. -Xmx:堆空间的最大大小(默认为机器内存的1/4)

  5. -Xmn:新生代的大小

  6. -XX:SurvivorRatio:

  7. -XX:NewRatio

  8. -XX:MaxTenuringThreshold:设置对象晋升的阈值

  9. -XX:+PrintGCDetails:输出详细的GC处理日志

    1. -verbose:gc:打印GC简要信息
    2. -XX:+PrintGC:打印GC简要信息
  10. -XX:HandlePromotionFailure:是否启用空间分配担保。jdk6 update24 之后,(JDK7之后),该参数不会再造成影响,始终为true

  11. -Xss:Java虚拟机栈的大小

  12. -Xms:堆的初始大小(年轻代+老年代) 等价于:-XX:InitialHeapSize

  13. -Xmx:堆的最大大小 等价于:-XX:MaxHeapSize

  14. 默认-XX:NewRatio=2:表示新生代占1,老年代占2,新生代占整个堆的1/3.

    可以修改-XX:NewRatio=4:表示新生代占1,老年代占4,新生代占老年代的1/5.

  15. -Xmn:设置新生代的最大空间的大小

  16. -XX:MaxTenuringThreshold=<N>:设置什么时候去养老区

  17. -XX:+PrintFlagsInitial
    
    -XX:+PrintFlagsFinal
    
    -Xmn:设置新生代的大小
    
    -MaxTenuringThreshold:设置新生代的最大年龄
        
    //打印GC简要信息
    -XX:+PrintGC
    
    -verbose:gc
       //JDK7及以后,该参数不会再有实际影响,默认为true
    -XX:HandlePromotionFailure:是否设置空间分配担保
    -XX:UseTLAB:设置是否开启TLAB
    -XX:TLABWasteTargetPercent:设置TLAB空间所占用Edan空间的百分比
    -XX:+EliminateAllocation:开启了标量替换(默认打开),允许将对象打散分配在栈上。
    
  18. 代码优化(逃逸分析):栈上分配、同步省略、标量替换。

    逃逸分析只有在服务器端才能开启,参数:-server

关于对象分配过程的总结

  1. 针对幸存者S0,S1区的总结:复制之后有交换,谁空谁是to
  2. 关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不再永久区/元空间收集。

为对象分配内存:TLAB

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值