}

2.运行时数据区

image-20200928153607955

image-20200928154456602

image-20200928154757784

1.jvm线程说明

每个线程:程序计数器,栈,本地栈

线程间:堆,堆外内存(永久代或源空间,代码缓存)

image-20200928155706327

image-20200928160443899

2.pc寄存器(程序计数器)

用于存储下一跳指令的地址

  • 它是一块很小的内存空间,几乎可以忽略不记,也是运行速度最快的存储区域
  • 在JVM规范,每个线程都有它自己的程序计数器,是线程私有的,声明舟曲和线程一致
  • 任何时间一个线程都只有一个方法在执行,也就所谓的当前方法。程序计数器会存储当前线程正在执行的java方法的jvm指令地址;
  • 如果是native方法,则是未指定值(undefned)

image-20200928161349960

image-20200928162907232

3.栈(虚拟机栈)

有不少开发人员一提到java内存结构,就会非常粗粒度的将JVM中的内存区理解为仅有Java堆(heap)和java栈(stack)?为什么?

是运行时的单位,而是存储的单位。

栈是解决程序运行问题,即程序如何执行,或者说如何处理数据。

堆解决的是数据存储的问题,即数据怎么放,放在哪。

image-20200929105413148

字节码指令

a.概念

每个线程在创建的时候都会创建一个虚拟机栈,其内部保存一个个栈帧(stack frame),对应这一次次方法的调用

b.作用

主管java程序的运行,保存方法的局部变量(8种数据类型,对象的引用地址)、部分结果、参与方法的调用和返回

  • 局部变量 vs 成员变量或属性
  • 基本数据类型变量 vs 引用类型变量(类,数组,接口)
public class JVMStackDemo {
    public static void main(String[] args) {
        JVMStackDemo js = new JVMStackDemo();
        js.a();
    }
    public void  a (){
        int a = 10;
        System.out.println("a方法被调用");
        b();
    }
    public void  b (){
        int b = 20;
        System.out.println("b方法被调用");
    }
}

image-20200929112423286

c.特点

  • 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
  • JVM对栈的操作只有2个:
    • 每个方法的执行:进栈
    • 方法执行后的:出栈
  • 栈不存在垃圾回收
  • 存在OOM

image-20200929112932175

d.开发中的常见的异常

  • Java虚拟机允许Java栈的大小是动态的或者是固定不变的

    • 如果采用固定大小的Java虚拟栈, 那么每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈运行的最大容量,那么java虚拟机会抛出StackOverflowError
    • 如果Java虚拟机栈可以动态扩展,但是在动态扩展的时候无法申请到足够的内存,那么就会抛出OutOfMemoryError

f.栈中存储什么

  • 每个线程都有自己的栈,栈中的数据都是以栈帧(stack frame)的格式存在
  • 每个方法都是有一个对应的栈帧
    • 栈帧是一个内存区域,是一个数据集,有方法执行过程中的各种数据信息

g.栈运行原理

  • jvm直接对java栈的操作只有两个,就是对栈帧的压栈出栈,遵循先进后出的原则
  • 在一个活动的线程中,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧),和其对应的是当前方法,定义这个方法的类就是当前类
  • 执行引擎所需的字节码指令只针对当前栈帧操作
  • 如果这个方法调用了其他方法,对应的新的栈帧就会被创建,成为新的栈顶栈帧

4.栈帧的结构

  • 局部变量表(local variables)
  • 操作数栈(operand stack)或表达式栈
  • 动态链接(dynamic linking)或指向运行时常量池的方法引用
  • 方法返回地址(return address)或方法正常退出或异常退出的定义
  • 一些附加信息

image-20200929122226743

a.局部变量表

局部变量表也叫局部变量数组或本地变量表

  • 定义为一个数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型,对象引用(reference),以及返回地址类型(returnAddress)类型
  • 局部变量表是线程私有的,所以不存在数据安全问题
  • 局部变量表所需的容量大小是在编译期确定的,并保存在方法的Code属性的maximum local variables数据项中,在方法运行期间是不会改变局部变量表的大小的
  • 局部变量表的最基本存储单元是sold(变量槽)
    • image-20200930100953225
    • image-20200930101137003
public class LocalVariablesDemo {
    private  int  count = 0;

    public static void main(String[] args) {
        LocalVariablesDemo lvd = new LocalVariablesDemo();
        int num = 10;
        lvd.test1();
    }

    private void test1() {
        String name = "like";
    }
}

image-20200930095643718

b.操作数栈

image-20200930110811688

image-20200930114057774

c.动态链接(指向运行时常量池的方法引用)

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

方法的调用

绑定的是一个字段,方法或者类在符号引用被替换为直接引用的过程

  • 早期绑定:

    被调用的目标方法如果在编译期可知,且运行期间保持不变时,就可以将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用

  • 晚期绑定:

    被调用的目标方法在编译期无法被确定下来,只能够在程序运行期间根据实际的类型绑定相关的方法

非虚方法:

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

  • 静态方法,私有方法,final方法,实例的构造器,父类方法都是非虚方法
  • 其他都是虚方法
调用指令
  • 普通调用指令

    1. invokestatic:调用静态方法,解析阶段确定唯一方法版本
    2. invokepecial:调用< init >方法,私有方法,父类方法,解析阶段确定唯一版本
    3. invokevirtual:调用所有虚方法
    4. invokeinterface:调用接口方法
  • 动态调用指令:

    1. invokedynamic:动态解析出需要调用的方法,然后执行

      image-20201002093502690

    前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持用户确定方法版本。其中invocestatic指令和invocespecial指令调用的方法为非虚方法,其他的是虚方法(除了final修饰的)

虚方法表

为了提高性能,JVM采用在类的方法区建立一个虚方法表(非虚方法不会存在)来实现,使用索引表来代替查找。

  • 每个类中都有一个虚方法表,保存着各个方法的实际入口
  • 在类的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕

d.方法返回地址

  • 存放调用该方法的pc寄存器的值
  • 一个方法的结束,有两种方式:
    • 正常执行完毕
    • 出现异常,非正常退出
  • 无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的吓一跳指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分的信息。

正常退出和异常退出的区别:通过异常完成出口退出的不会给他的上层调用者参数任何的返回值

两种退出方法:

  1. 执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者——正常退出
    • 一个方法在正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定。
    • 在字节码指令中,返回指令包含iretrun(返回值是boolean,byte,chart,short,int类型时使用),lretrun,freturn,dreturn和areturn。还有一个retrun指令共声明为void的方法,实例化方法,类和接口的初始化方法使用

e.一些附加信息

f.栈的相关面试题

1.举例栈溢出的情况

stackoverflowError

  • 通过-Xss设置栈的大小
  • 一个方法内调用本身,无限递归,一直创建栈帧,撑爆了栈空间

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

不能,可以使StackOverflowError出现的晚一点

3.垃圾回收会涉及到栈空间吗?

不会,所以不能用垃圾回收来避免StackOverflowError

4、分配的栈内存越大越好吗?

不是

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

安全

5.本地方法栈

image-20201002145721733

image-20201002145921246

6.堆空间

  • 一个JVM只有一个堆内存,堆页数Java内存管理的核心区域

  • Java堆在JVM启动的时候就创建了,空间大小也就确定了。是JVM管理的最大一块的空间。

    • 堆是由bootStrap 类加载器创建的
    • 堆内存大小是可以调节的
  • 所有的对象实例以及数组都应当运行时分配在堆

  • 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置

  • 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。

  • 堆是GC(gGarbage collection)垃圾回收器执行垃圾回收的重点区域

    image-20201003104108975

1、heapDemo

//-Xms10m -Xmx10m
public class HeapDemo {
    public static void main(String[] args) {
        System.out.println("start");
        try {TimeUnit.SECONDS.sleep(1000); } catch (InterruptedException e) {e.printStackTrace();}
        System.out.println("end");
    }
}

2、使用jvisualvm查看heapDemo运行时的数据

image-20201003102226275

3、堆内存细分

  • Java 7以前将堆内存分类三部分:新生区,养老区,永久区

    image-20201003105451680

  • Java 8以后将堆内存分为:新生区,养老区,元空间

    image-20201003105558558

  • image-20201003105605479

  • 元空间 == 方法区

image-20201003110407996

新生区:

image-20201003110442474

老年区:

image-20201003110453455

元空间:

image-20201003110505164

4、查看堆空间大小设置

  • -Xms:堆起始内存,-XX:InitialHeapSize
    • memory start
  • -Xmx:堆最大内存,-XX:MaxHeapSize
  • 当堆内存超过-Xmx所规定的内存时,就会抛出outofmemoryError
  • 获取运行时的内存大小
    • Runtime.getRuntime().maxMemory();
      Runtime.getRuntime().totalMemory();
  • 查看设置的参数
    • jps 查看进程id-> jstat -gc 进程id
    • -XX:PrintGCDetails

s0和s1同时只使用一个

D:\Java\project\work\src\main\java\Java虚拟机_JavaVirtualMachine\runttimedata>jstat -gc 29672
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT    CGC    CGCT     GCT
512.0  512.0   0.0   504.0   2048.0   1502.8    7168.0     371.1    4864.0 2988.4 512.0  327.3       1    0.001   0      0.000   -          -    0.001

s0和s1同时只使用一个

image-20201003131213857

5、OOM

outofmemoryError

OOM-Java heap space(堆空间溢出)

//-Xms50m -Xmx50m
private static void oomJavaHeapSpace() {
    ArrayList<int[]> ints = new ArrayList<>();
    while (true) {
        try {TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) {e.printStackTrace();}
        ints.add(new int[1024 * 1024]);
    }
}

image-20201003165606810

6.新生代和老年代

存储在JVM中的对象可以分为两类

  • 一类是存储时间较短的,创建和消亡都非常迅速‘’

  • 另一类生命周期非常长,甚至可能和JVM保持一致

    image-20201004102534518

设置新生代和老年代在对空间的比例

  • 默认:-XX:NewRatio= 2,新生代:老年代 = 1:2

    image-20201004103402403

设置新生代中的伊甸园区和幸存者区的比例

-XX:-UserAdaotiveSizePolicy:关闭自己适应的内存分配策略

默认是8:1:1,但是实际上不是,需要设置:-XX:SurvivorRatio=8

6.25:1:1

image-20201004105213643

绝大部分的Java对象的销毁都在新生代进行了

  • 新生代中80%的对象都是“朝生夕死”的
  • 可以使用-Xmn设置新生代最大内存大小

7.对象分配过程

image-20201004115227977

新生代垃圾回收

  • 伊甸园区满了就会触发young-gc
  • 幸存者区不会主动发生gc
  • 在新生代区中代数超过15就会到老年区
  • 针对幸存者s0,s1区的总结:复制后有交换,谁空谁就是to(from…to)

关于垃圾回收:频繁在新生区手机,很少在老年区手机,几乎不在元空间手机(永久区)

image-20201005091251234

8、Minor GC、Major GC、Full GC对比

JVM在进行GC时,并不是每次都对(新生代,老年代,元空间)一起回收,大部分的回收都指的是新生代。

针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆手机(full gc)

  • 部分收集:不是完整的收集整个Java堆的垃圾收集。其中又分为:
    • 新生代收集(Minor GC / Young GC):只回收新生代
    • 老年代收集(Major GC / Old GC):只回收老年代
      • 目前只有CMS GC垃圾回收器会有单独收集老年代的行为。
      • 很多时候Major GC 会和 Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
    • 混合收集(Mixed GC):收集整个新生代以及部分老年代
      • 只有G1垃圾回收器会有这种行为。
  • 整堆收集(Full GC):收集整个java堆和方法区的垃圾回收

a.新生代GC触发机制(Minor GC \ Young GC)

  • 新生代空间不足时,这里的新生代指的是Eden区满,Surivor满不会触发GC。
  • 应为Java对象大多是朝生夕死的。所以Minor GC非常频繁
  • Minor GC会触发STW,暂停其他用户的线程,等垃圾回收结束才恢复

b.老年代GC(Major GC /Full GC)

  • 在老年代发生gc时,对象从老年代消失时,我们说Major gc 或者full gc发生了
  • 出现老年代gc,经常会伴随者年轻代gc
    • Parallel scavenge收集器的收集策略里会直接进行老年代gc
    • 在老年代空间不足时,会先触发年轻代gc,如果还不足就会触发老年代gc
  • 老年代gc速度比年轻代gc慢10倍以上,swt时间更久
  • 如果老年代gc后内存还不足就会报OOM

触发:

  1. 调用system.gc(),系统建议执行full gc,但是不是必然执行
  2. 老年代空间不足
  3. 方法区空间不足
  4. 通过新生代gc后进入老年代的平均大小大于老年代的可用内存
  5. 有eden区,幸存者区(from区)向to区复制时,对象大小大于to区可用内存,则把该对象转移到老年代,且老年代的可用内存小于该对象大小

full gc是开发或调优中尽量要避免的

9.为什么要把Java堆分代?

image-20201005184138710

image-20201005184228998

10.内存分配策略

如果对象在eden区出生并且经过一次young gc 后仍然能存活,并且被survivor容纳的话,将被移动到survivor空间中,设置年龄为1,每熬过一次young gc 就加一岁,当到达一定岁数时(默认为15)就会被晋升到老年代

-XX:MaxTenruingThreshold:设置晋升到老年代的年龄

image-20201005191955067

11.TLAB

Thread Local Allocation Buffer,为什么需要tlab

  • 堆是所有线程的共享区域,任何线程都可以方法到堆中的数据
  • 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆中划分内存空间是线程不安全的
  • 为了避免多个线程操作同一地址,需要使用加锁等机制,影响了分配速度

什么是tlab

  • 从内存模式而不是垃圾回收的角度,对eden区继续进行划分,JVM为每个线程分配了一个私有缓存区域,包含在eden区域内
  • 多线程同时分配内存时,使用Tlab可以避免一系列的非线程安全问题,同时还能够提升内存的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略

image-20201005194330154

12.堆中内存分配常用指令

  • -XX:+PrintFlagsInitial:查看所有的参数的默认初始值
  • -XX:+PrintFlagsFinal:查看所有的参数的最终值
  • -Xms:初始堆空间内存(默认为物理内存的1/64)
  • -Xmx:最大堆内存 (默认为物理内存的1/4)
  • -Xmn:设置新生代的大小
  • -XX:NewRatio:设置新生代和老年代在堆结构的占比 -XX:NewRatio=2
  • -XX:SurvivorRatio:设置新生代eden和s0/s1空间的比例 -XX:SurvivorRatio=8
  • -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
  • -XX:+PrintGCDetails:输出详细的GC处理日志
    • -XX:+PrintGC -verbose:ge 打印简单的gc信息
  • -XX:HandlerPromotionFailure:是否设置空间分配担保
    • image-20201005201457042

13.堆是分配对象的唯一选择吗

image-20201006100538152

8.逃逸分析

  • 如何向堆上的对象分配到栈,需要用到逃逸分析手段
  • 这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法
  • 通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要讲这个对象分配到堆上
  • 逃逸分析就是分析对象动态作用域:
    • 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸
    • 如果被外部方法所引用,则认为发生了逃逸。例如作为参数传递到其他地方。

image-20201006102438507

优化

  1. 栈上分配:将堆分配转为为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。

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

  3. 分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

    1. image-20201006114806259

public class EscapeAnalysisDemo {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 1; i <= 1000000; i++) {
            EscapeAnalysisDemo escapeAnalysisDemo = new EscapeAnalysisDemo();
        }
        long end = System.currentTimeMillis();

        System.out.println(end - start);


        try {TimeUnit.SECONDS.sleep(10000); } catch (InterruptedException e) {e.printStackTrace();}
    }
}

关闭逃逸分析:-XX:-DoEscapeAnalysis

image-20201006112745967

image-20201006112810995

开启逃逸分析:-XX:+DoEscapeAnalysis

image-20201006112919584

image-20201006112930724

总结

image-20201006114859027

image-20201006115005911

还是存储堆上,但是可以存储在栈上

image-20201006115234534

posted @ 2020-10-06 11:55  likelovecode  阅读(132)  评论(0)    收藏  举报