jvm笔记1

[TOC]

第一章 类加载

大体过程

加载->链接->初始化

jvm笔记1_第1张图片

具体过程

jvm笔记1_第2张图片

加载:

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

链接

验证(Verify) :

目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身 安全。
主要包括四种验证,文件格式验证,语义验证,字节码验证,符号引用验证。

准备(Prepare):

为类变量分配内存并且设置该类变量的默认初始值,即零值。
这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。

解析(Resolve) :

将常量池内的符号引用转换为直接引用的过程。
事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟材规范》的class文件 格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的
CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等

初始化:

初始化阶段就是执行类构造器方法()的过程。
此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
构造器方法中指令按语句在源文件中出现的顺序执行。
()不同于类的构造器。(关联:构造器是虚拟机视角下的() )·若该类具有父类,JVM会保证子类的()掉行前,父类的()已经执行完毕。
虚拟机必须保证一个类的 ()方法在多线程下被同步加锁。

一般自定义类的加载器

//对于用户自定义类来说:|
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader);//sun.misc.Launcher$AppCLassLoader@18b4aac2

为什么要自定义类加载器?

隔离加载类
修改类加载的方式
扩展加载源
防止源码泄漏

双亲委派机制

工作原理

1)如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
2)如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
3)如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委怎么破坏双亲委派机制派模式。

代码实现

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;
    }
}

怎么破坏双亲委派机制

遗留问题 在双亲委派模型出现之前。由于双亲委派模型是在JDK1.2之后才被引入的,而类加载器和抽象类java.lang.ClassLoader则是JDK1.0时候就已经存在,面对已经存在 的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。为了向前兼容,JDK1.2之后的java.lang.ClassLoader添加了一个新的proceted方法findClass(),在此之前,用户去继承java.lang.ClassLoader的唯一目的就是重写loadClass()方法,因为虚拟在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()。JDK1.2之后已不再提倡用户再去覆盖loadClass()方法,应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里,如果父类加载器加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派模型的。

第二次 上层需要下层 这个模型自身的缺陷所导致的,双亲委派模型很好地解决了各个类加载器的基础类统一问题(越基础的类由越上层的加载器进行加载),基础类之所以被称为“基础”,是因为它们总是作为被调用代码调用的API。但是,如果基础类又要调用用户的代码,那该怎么办呢。

一个典型的例子便是JNDI服务,它的代码由启动类加载器去加载,但JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现部署在应用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识”这些代码,该怎么办?为了解决这个困境,Java设计团队只好引入了一个不太优雅的设计:线程上下文件类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。有了线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如我们最常用的JDBC

第三次 为了满足热部署 自定义

优势

避免类的重复加载
保护程序安全,防止核心API被随意篡改上

劣势

上层无法调用下层

对类加载器的引用

JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。

第二章 内存区域与内存区域溢出

内存区域整体图

jvm笔记1_第3张图片

jvm笔记1_第4张图片

灰色区域为线程独有

红色区域为线程共享

程序计数器

每一个线程都对应有一个程序计数器,由执行引擎读取 可以控制跳转 异常处理等

例子

public class PCRegister {
    public static void main(String[] args) {
        int i = 20;
        int j = 30;
        int k = i + j;
        String str = "hello";
        System.out.println(str);
    }
}
 0 bipush 20     操作数栈push 20
 2 istore_1     操作数栈压slot[1] 20
 3 bipush 30   
 5 istore_2
 6 iload_1      操作数栈加载 slot[1] 20
 7 iload_2     
 8 iadd         操作数栈相加同时结果入栈  The int result is value1 + value2. The result is pushed onto the operand stack.
 9 istore_3     
10 ldc #2   加载常量池#2
12 astore 4  
14 getstatic #3  The value of the class or interface field is fetched and pushed onto the operand stack.
17 aload 4
19 invokevirtual #4  调用虚方法
22 return

左边是行号 右边是指令 因为指令大小不同 所以行号不连续

如果是java方法,当前指示器是一个地址 如果是native方法 指示器当前值为空(undifine)

问题:使用PC寄存器存储字节码指令地址有什么用?为什么使用PC寄存器存储?为什么是线程私有?

因为CPU需要不停的切换各个线程,这个时候切换回来后,需要知道从哪里接着继续执行。方便各个线程之间可以独立计算

JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。

虚拟机栈

栈是线程私有

栈帧是栈的元素。每个方法在执行时都会创建一个栈帧。

栈帧中存储了局部变量表、操作数栈、动态连接和方法出口等信息。

每个方法从调用到运行结束的过程,就对应着一个栈帧在栈中压栈到出栈的过程。

栈帧的内部结构

分为五大类:

  • 局部变量表(Local Variables)
  • 操作数栈(Operand Stack)
  • 动态链接(Dynamic Linking)
  • 方法返回地址(Return Address)
  • 一些附加信息

局部变量表(Local Variables)

  1. 局部变量表也被称之为局部变量数组或本地变量表。
  2. 定义为一个数字数组,主要用于存储方法参数和定义在方体内的局部变量,这些数据包含基本数据类型,对象引用,以及returnAddress类型。
  3. 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据的安全问题
  4. 局部变量表所需的容量大小是在编译期间确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表大小的。
  5. 局部变量表中最基本的存储单元是Slot(变量槽)

注意参数也在局部变量表 静态实例0起 非静态方法1起 多放了个this

Slot

  1. 在局部变量表中,包括 引用类型(reference),returnAddress类型的变量。32位以内的类型占一个Slot,64位的类型占用两个Slot。(long double)
  2. 如果当前帧是由构造左去或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。

栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。

public void test4(){
    int a = 0;
    {
        int b = 0;
        b =a + 1;
    }
    //变量c使用之前已经销毁的变量的位置
    int c = a + 1;
}

类变量和实例变量的区别。

我们知道类变量和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。

public void test1(){
    int i ;
    System.out.println(i);
}
//错误的

在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。

操作数栈

每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出(Last-In-First-out)的操作数栈,也可以称之为表达式栈(Expression stack) 。
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop).

某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈。
比如:执行复制、交换、求和等操作

操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也佘随之被创建出来,这个方法的操作数栈是空的。
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的code属性中,为max_stack的值。(数组,一旦创建,其长度就是确定的!)
栈中的任何一个元素都是可以任意的Java数据类型。
32bit的类型占用一个栈单位深度
64bit的类型占用两个栈单位深度
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据访问。

另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。

栈顶缓存

前面提过,基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数。
由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存(Tos,Top-of-stack Cashing)技术,将

动态链接

每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。

包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接( Dynamic Linking)。比如: invokedynamic指令
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

jvm笔记1_第5张图片

jvm笔记1_第6张图片

方法的调用:虚方法与非虚方法

如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称之为非虚方法。

静态变量、私有方法、final方法、实例构造器、父类方法都是非虚方法。

其它方法称之为虚方法、

普通调用指令:
  1. invokestatic : 静态方法,解析阶段确定唯一方法版本
  2. invokespecial : 调用(实例方法)方法、私有方法以及父类方法,解析阶段确定唯一方法版本
  3. invokevirtual : 调用所有虚方法
  4. invokeinterface : 调用接口方法
动态调用指令:
  1. invokedynamic : 动态解析所需要调用的方法,然后执行
前四条指令固化在虚拟机的内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定版本。其中invokespecial和invokestatic指令调用的方法称为非虚方法,其余的(final修饰除外)称为虚方法。

方法重写的本质

Java语言中方法重写的本质:
1.找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C。
2.如果在过程结束;如果不同类型c中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过,则返回java. lang. IllegalAccessError异常。
3.否则,按照继承关系从下往上依次对c的各个父类进行第⒉步的搜索和验证过程。
4.如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
IllegalAccessError介绍:
程序试图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。一般的,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。

方法返回地址(return address)

存放调用该方法的pc寄存器的值。

一个方法的结束,有两种方式:

正常执行完成
出现未处理的异常,非正常退出

无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。

本地方法栈

JVM 中的栈包括 Java 虚拟机栈和本地方法栈,两者的区别就是,Java 虚拟机栈为 JVM 执行 Java 方法服务,本地方法栈则为 JVM 使用到的 Native 方法服务。

JVM中的线程说明

线程是一个程序中的运行单元,JVM允许一个应用有多个线程并行的执行任务。

在Hotspot JVM中,每个线程都与操作系统的本地线程之间映射,当一个Java线程准备好执行后,此时一个操作系统的本地线程也会同时创建,Java线程执行终止后,本地线程也会回收。

操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,它就会调用Java线程的run()方法。

JVM线程的主要几类:

  • 虚拟机线程: 这种线程的操作是需要JVM到达安全点才会出现,这些操作必须在不同的线程中发生的原因是它们都要到达安全点,这样堆才不会发生变化。这种线程的执行类型包括"stop-the-world"的垃圾收集,线程栈收集,线程挂起以及偏向锁的撤销。
  • 周期任务线程: 这种线程是时间周期事件的体现(比如中断),它们一般用于周期性操作的调度执行。
  • GC线程: 这种线程对在JVM中不同类的垃圾收集行为提供了支持。
  • 编译线程: 这种线程在运行时会将字节码编译成本地代码。
  • 信号调度线程: 这种线程接收信号并发送给JVM,在它内部通过调用适当的方法进行处理。

jvm笔记1_第7张图片

堆是Java虚拟机所管理的内存中最大的一块存储区域。堆内存被所有线程共享。主要存放使用new关键字创建的对象。几乎所有对象实例以及数组都要在堆上分配。垃圾收集器就是根据GC算法,收集堆上对象所占用的内存空间。

Java堆分为年轻代(Young Generation)和老年代(Old Generation);

年轻代又分为伊甸园(Eden)和幸存区(Survivor区);幸存区又分为From Survivor空间和 To Survivor空间。

堆空间溢出案例 软件为Jvisual

public class OOMTest {
    public static void main(String[] args) {
        ArrayList list = new ArrayList<>();
        while (true){
            try {
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            list.add(new Picture(new Random().nextInt( 1024*1024)));

        }

    }
}
class Picture{
    byte[] pic;

    public Picture(int len) {
        this.pic = new byte[len];
    }
}

jvm笔记1_第8张图片

jvm笔记1_第9张图片

设置比例

配置新生代与老年代在堆结构的占比。
默认-XX :NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5

新生代Eden和Survivor默认比例是8:1,但是实际是6:1

注意在堆区的eden园中大约有1%的TLAB TLAB相关后续再展开

创建对象过程

  1. 检查类是否被加载 若未加载 执行类加载 if(new指令的参数不在常量池 || 此符号引用的类没有被加载、解析和初始化)--------->{执行相应类加载}
  2. 堆区分配空间大小 采用指针碰撞(标记整理)或者维持一个空闲列表(标记清除) 比如Serial收集器和ParNew收集器 /cms (然后实际上cms仍然维持这一个高效缓冲区)
  3. 处理线程安全:先TLAB再 CAS+失败重试

    • CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而且假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
    • TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
  4. 初始化零值 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
  5. 设置对象头 虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
  6. 执行init方法 (在构造器之前有显示初始化)经过1-5步骤,从JVM的角度看一个新的对象已经产生,但从Java程序上看,对象创建才刚开始。执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。(构造方法 代码块 显示赋值)

对象分配过程

为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑Gc执行完内存回收后是否会在内存空间中产生内存碎片。

  1. new的对象先放伊甸园区。此区有大小限制。
  2. 当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行:垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区
  3. 然后将伊甸园中的剩余对象移动到幸存者0区。
  4. 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
  5. 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
  6. 啥时候能去养老区呢?可以设置次数。默认是15次。或者大对象直接进入垃圾回收
    ·可以设置参数:-XX:MaxTenuringThreshold=进行设置。

内存分配策略

如果对象在Eden出生并经过第一次MinorGC后仍然存活,并且能被survivor容纳的话,将被移动到survivor 空间中,并将对象年龄设为1。对象在survivor区中每熬过一次Minorcc ,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,其实每个JVM、每个Gc都有所不同)时,就会被晋升到老年代中。

针对不同年龄段的对象分配原则如下所示:·优先分配到Eden
大对象直接分配到老年代

尽量避免程序中出现过多的大对象·长期存活的对象分配到老年代。

动态对象年龄判断

如果survivor 区中相同年龄的所有对象大小的总和大于survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold 中要求的年龄。

空间分配担保

一XX: HandlePromotionFailure

jvm笔记1_第10张图片

对象在内存中的布局

对象头(Header)

第一部分用于存储对象自身运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中却无法确定数组的大小。

实例数据(Instance Data)

实例数据部分是对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录下来。这部分的存储顺序会受到虚拟机分配策略参数(FieldAllocationStyle)和字段在Java源码中定义顺序的影响。

对齐填充(Padding)。

对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数,因此,当实例数据部分没有对齐时,就需要对齐填充来补全。

类指针(classPoint)

对象指向它的类元素的指针。在不开启对象指针压缩的情况下是8字节。压缩后变为4字节,默认压缩。

数组长度(仅仅是数组对象)

某面试题 new Object是多大 答案 16字节 markword+classPoint+padding

User (int id,String name) User u = new User(1,‘张三’);占用多少字节 markword+classPoint+padding 8+4+4(一个int)+4(汉字4个char) +padding 4

Full GC触发机制:(后面细讲)触发Full GC执行的情况有如下五种:

(1)调用system.gc()时,系统建议执行Full Gc,但是不必然执行
(2)老年代空间不足
(3)方法区空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、survivor space0(From Space)区向survivor space1 (ToSpace)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
说明: full gc是开发或调优中尽量要避免的。这样暂时时间会短一些。

为什么要分代?

优化GC性能 不同代之间存放的对象不同 采用的收集方法不同 达到一个较好的效果

三大假说

1)弱分代假说 (Weak Generational Hypothesis):

绝大多数对象都是朝生夕灭的。

2)强分代假说 (Strong Generational Hypothesis):

熬过越多次垃圾收集过程的对象就越难以消亡。

3)跨代引用假说(Intergenerational Reference Hypothesis):

跨代引用相对于同代引用来说仅占极少数。

这其实是可根据前两条假说逻辑推理得出的隐含推论

存在互相引用关系的两个对象,是应该倾向于同时生存或者同时消亡的

依据这条假说,我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录 每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立记忆集

测试堆空间常用的jvm参数:

-XX:+PrintFLagsInitial :查看所有的参数的默认初始值
-XX:+PrintFlagsFinal:查看所有的参数的最终值(可能会存在修改,不再是初始值)
-Xms :初始堆空间内存(默认为物理内存的1/64)
-Xmx:最大堆空间内存(默认为物理内存的1/4)
-Xmn:设置新生代的大小。(初始值及最大值)
-XX: NewRatio:配置新生代与老年代在堆结构的占比
-XX: SurvivorRatio:设置新生代中Eden和Se/s1空间的比例
-XX:MaXTenuringThreshold:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails:输出详细的GC处理日志
打印gc简要信息:-XX:+PrintGc -verbose:gc -XX:HandlePromotionFailure:是否设置空间分配担保

总结几大初始化

final 变量 编译初始化

statc 编译 类加载的初始化

nonStatic 变量对象创建过程

局部变量表变量 需要手动赋值

总结几个优化方案

优化方案

1,逃逸分析和栈上分配

逃逸分析的基本行为就是分析对象动态作用域:

当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。分配在栈上
当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。

如何快速的判断是否发生了洮逸分析,就看new的对象实体是否有可能在方法外被调用。

常见的栈上分配的场景

在逃逸分析中,已经说明了。分别是给成员变量赋值、方法返回值、实例引用传递。

2,同步省略

如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。

3,分离对象或标量替换。

有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。(java虚拟机的栈)默认开启

方法区 :永久代/元空间

方法区同 Java 堆一样是被所有线程共享的区间,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。

更具体的说,静态变量+常量+类信息(版本、方法、字段等)+运行时常量池存在方法区中。

运行时常量池是方法区的一部分。

永久代曾经是方法区的一种实现 j8以后改成了元空间

JDK 8 使用元空间 MetaSpace 代替方法区,元空间并不在JVM中,而是在本地内存中

类型信息

对每个加载的类型(类class、接口interface、枚举enum、注解annotation), JVM必须在方法区中存储以下类型信息:
①这个类型的完整有效名称(全名=包名.类名)
②这个类型直接父类的完整有效名(对于interface或是java.lang.object,都没有父类)
③ 这个类型的修饰符(public,abstract,final的某个子集)
④这个类型直接接口的一个有序列表

域(Field)信息:成员属性

JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
域的相关信息包括:

  • 域名称、
  • 域类型、
  • 域修饰(public,private,protected,static,final,volatile,transient的某个子集)

方法(Method)信息

JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:

  • 方法名称
  • 方法的返回类型(或void)
  • 方法参数的数量和类型(按顺序)I
  • 方法的修饰符(public, private, protected,static, final,synchronized, native,abstract的一个子集)
  • 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
  • 异常表(abstract和native方法除外)
每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、
被捕获的异常类的常量池索引

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分
常量池表(Constant Pool Table)是class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。在加载类和接口到虚拟机后,就会创建对应的运行时常量池。
JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。
运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。
运行时常量池,相对于class文件常量池的另一重要特征是:具备动态性。
运行时常量池类似于传统编程语言中的符号表(symbol table),但是它所包含的数据却比符号表要更加丰富一些。
当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛outofMemoryError异常。

如何解决OOM

1、要解决OOM异常或heap space的异常,一般的手段是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(MemoryLeak)还是内存溢出(Memory Overflow)。
2、如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots引用链的信息,就可以比较准确地定位出泄漏代码的位置。
3、如果不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

方法区演进细节

版本 特点
jdk1.6及之前 有永久代(permanent generation),静态变量存放在永久代上
jdk1.7 有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中
jdk1.8及之后 无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆

jvm笔记1_第11张图片

jvm笔记1_第12张图片

jvm笔记1_第13张图片

ps:静态变量实体一直在堆空间,静态变量引用才放在方法区中static Object a的a

永久代为什么要被元空间替换?

1,为永久代设置空间大小是很难确定的。

2,对永久代进行调优是很困难的。

3,特殊情况的intern的不同情况执行效果不同

StringTable为什么要调整?

jdk7中将stringTable放到了堆空间中。因为永久代的回收效率很低,在full gc的时候才会触发。而full gc是老年代的空间不足、永久代不足时才会触发。
这就导致stringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。

栈堆方法区(元空间的关系)

jvm笔记1_第14张图片

java的编译解释

解释器:但Java虚拟机启动时会根据预定义的规范,对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行,响应快

JIT编译器:就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言

Java是半编译半解析器型语言:

Java1.0时,是“解释执行的”,但是后期java可以直接生成本地代码的编译器

String

  • String实现了Serializable接口:表示字符串是支持序列化的,

    实现了Comparable接口:表示String可以比较大小

  • String在jdk8以前内部定义了final char[] value用于存储字符串数据。jdk9改为byte[]

字符串常量池中是不会存储相同内容的字符串的

String的String Pool是一个固定大小的Hashtable,默认值大小长度是1009(jdk6),如果放进StringPool的String非常多,就会造成Hash冲突严重,从而导致链表会分长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降

String的内存分配

  • 在Java语言中有8种基本数据类型和一种比较特殊的类型String,这些类型为了使他们在运行过程种速度更快、更节省内存,都提供了一种常量池概念
  • 常量池就类似于一个Java系统级别提供的缓存。8种基本数据类型的常量池就是系统协调的,

    String类型的常量池比较特殊。它的主要使用方法有两种

    • 使用双引号
    • intern()方法

JDK6以前,字符串常量池存在永久代,1.7以后调整到Java堆中

字符串拼接操作

  • 常量与常量的拼接结果在常量池,原理是编译器优化
  • 常量池中不会存在相同内容的常量
  • 只要其中有一个是变量,结果就在堆中。(如果拼接符号的前后出现了变量,则相当于在堆空间中new string(),具体的内容为拼接的结果,拼接的原理是StringBuilder
  • 如果拼接的结果调用intern()方法,则主动将常 中还没有的字符串对象放入池中,并返回此对象地址

通过StringBuilder的append()的方式添加字符串的效率要远高于使用String的字符串拼接方式!

  • 使用StringBuilder的append()方式:自始至终只创建过一个StringBuilder的对象
  • 使用String的字符串拼接方式:创建过多个StringBuilder和String的对象

jvm笔记1_第15张图片

intern()方法的使用:

  • jdk1.6

    若串池中有,则并不会放入,返回已有的串池中的对象的地址

    若没有,会把此对象复制一份,放入串池,并返回串池中对象地址

  • jdk1.7

    若串池中有,则并不会放入。返回已有的串池中的对象的地址

    若没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址

public class Test4 {
    public static void main(String[] args) {
        String s = new String("1");
        s.intern();         //调用此方法之前,字符串常量池中已经存在“1”
        String s2 = "1";
        System.out.println(s == s2);    //jdk6:false   jdk7/8:false

        String s3 = new String("1") + new String("1");
        //s3变量记录的地址为:new String("11")
        //字符串常量池中,不存在11
        s3.intern();    //使在字符串常量池中生成11

        String s4 = "11";   //使用的是上一行代码执行时,常量池生成的“11”地址jdk7以后这里是对s3的引用
        System.out.println(s3 == s4);   //jdk6:false   jdk7/8:false
    }
public class Test4 {
    public static void main(String[] args) {
        String s = new String("a") + new String("b");
        //上一行代码执行完之后,字符串常量池中并没有“ab”
        String s2 = s.intern();
        //jdk1.6,在串池中创建一个字符串“ab”
        //jdk1.8,串池中没有创建字符串“ab”,而是创建一个引用,指向new String
        System.out.println(s2 =="ab");//jdk6:true jdk8:true
        System.out.println(s == "ab");//jdk6:false jdk8:true

    }
}

你可能感兴趣的:(javajvm)