3 java JVM

JVM

什么是反射

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性,这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

2、共享性不同

栈内存是线程私有的。
堆内存是所有线程共有的

4、空间大小
栈的空间大小远远小于堆的

1.栈内存存储的是局部变量而堆内存存储的是实体

2.栈内存的更新速度要快于堆内存,因为局部变量的生命周期很短;

3.栈内存存放的变量生命周期一旦结束就会被释放,而堆内存存放的实体会被垃圾回收机制不定时的回收。

Java内存结构 (区别于java内存模型)

方法区和堆是所有线程共享的内存区域;而java栈、本地方法栈和程序员计数器是运行是线程私有的内存区域。

  • Java堆(Heap),是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。 有垃圾回收 有OOMError

  • 方法区(Method Area),方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 有垃圾回收 有OOMError

  • 程序计数器(Program Counter Register),程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是存储下一条指令的地址。取出就是执行,执行完再取下一条。任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法程序计数器会存储当前线程正在执行的java方法的JVM指令地址;或者,如果实在执行native方法,则是未指定值(undefined)没有垃圾回收 它是唯一一个在java虚拟机规范中没有规定任何OOM情况的区域。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

    唯一一个既没有GC 也没有OOM OutOfMemoryError:

  • JVM(虚拟机栈)栈(JVM Stacks),与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。 每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

  • 本地方法栈(Native Method Stacks),本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用 本地方法是由C语言编写

程序计数器为什么是私有

程序计数器主要有下面两个作用:

字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。

所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。

虚拟机栈为什么是私有

虚拟机栈: 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

字符串常量池 引用常量池 Stingtable

类加载器和类加载过程

类加载过程

加载->链接-》初始化

加载 -》验证-》准备—>解析-》初始化

类加载子系统作用

  • 类加载子系统负责从文件系统或者 络中加载Class文件,class文件在文件开头有特定的文件标识;
  • ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定
  • 加载的类信息存放于一块成为方法区的内存空间。除了类信息之外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
类加载器ClassLoader角色

初始化
  • 初始化阶段就是执行类构造器方法clinit()的过程。
  • 此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。

类加载器分类

  • JVM支持两种类型的加载器,分别为引导类加载器(BootStrap ClassLoader)自定义类加载器(User-Defined ClassLoader)
  • 从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器
  • 什么是双亲委派机制/mark>(这个图很重要)

    当某个类加载器需要加载某个文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

    1. 如果内存规整一指针碰撞

    2. 如果内存不规整:

      1. 虚拟机需要维护一个列表
      2. 空闲列表分配
    3. 处理并发安全问题:分配空间在堆里面分配的,而堆是共享的,多个线程就会出现安全问题。通常来讲,虚拟机采用两种方式来保证线程安全:

      1. 采用 CAS+失败重试 保证更新的原子性 CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
      2. 每个线程预先分配一块TLAB 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
    4. 初始化分配到的空间(默认初始化):所有属性设置默认值,这样就可以保证对象实例字段在不赋值时可以直接使用 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

    5. 设置对象的对象头 初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

    6. 执行init方法进行初始化( 显示初始化): 一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

    虚拟机栈的5道面试题

    1.举例栈溢出的情况StackOverflowError)

    • 递归调用等,通过-Xss设置栈的大小;

    2.调整栈的大小,就能保证不出现溢出么/p>

    • 不能 如递归无限次数肯定会溢出,调整栈大小只能保证溢出的时间晚一些

    3.分配的栈内存越大越好么/p>

    • 不是 会挤占其他线程的空间

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

    • 不会
    内存区块 Error GC
    程序计数器 ? ?
    本地方法栈 ? ?
    jvm虚拟机栈 ? ?
    ? ?
    方法区 ? ?

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

    • 要具体情况具体分析

    本地方法栈

    • Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用

    https://juejin.cn/post/6844904173671202829#heading-3

JDK 8以后: 新生区+养老区+元空间

  • Young Generation Space:又被分为Eden区和Survior区 Young/New
  • Tenure generation Space: Old/Tenure
  • 配置新生代与老年代在堆结构的占比

    • 默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
    • 图解对象分配过程

      1. new的对象先放伊甸园区。此区有大小限制。
      2. 当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区
      3. 然后将伊甸园中的剩余对象移动到幸存者0区。
      4. 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
      5. 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
      6. 啥时候能去养老区呢以设置次数。默认是15次。·可以设置参数:-XX:MaxTenuringThreshold=进行设置。
      7. 在养老区,相对悠闲。当老年区内存不足时,再次触发GC:Major GC,进行养老区的内存清理。
      8. 若养老区执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常。

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

      常用调优工具

      • JDK命令行
      • Eclipse:Memory Analyzer Tool
      • Jconsole
      • VisualVM
      • Jprofiler
      • Java Flight Recorder
      • GCViewer
      • GC Easy

      如何判断对象可以被回收/h3>

      判断对象是否存活一般有两种方式:

      • 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
      • 可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对 0

      Minor GC、Major GC、Full GC(看不懂)

      JVM在进行GC时,并非每次都针对上面三个内存区域(新生代、老年代、方法区)一起回收的,大部分时候回收都是指新生代。

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

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

      触发机制

      • 年轻代GC(Minor GC)触发机制

        • 当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC.(每次Minor GC会清理年轻代的内存,Survivor是被动GC,不会主动GC)
        • 因为Java对象大多是朝生夕死,所以Minor GC 非常频繁,一般回收速度也比较快,这一定义既清晰又利于理解。
        • Minor GC 会引发STW(Stop the World),暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行。
      • 老年代GC(Major GC/Full GC)触发机制

        • 指发生在老年代的GC,对象从老年代消失时,Major GC 或者 Full GC 发生了
        • 出现了Major GC,经常会伴随至少一次的Minor GC(不是绝对的,在Parallel Scavenge 收集器的收集策略里就有直接进行Major GC的策略选择过程)
          • 也就是老年代空间不足时,会先尝试触发Minor GC。如果之后空间还不足,则触发Major GC
        • Major GC速度一般会比Minor GC慢10倍以上,STW时间更长
        • 如果Major GC后,内存还不足,就 OOM了
      • Full GC触发机制

        • 触发Full GC执行的情况有以下五种
          • ①调用System.gc()时,系统建议执行Full GC,但是不必然执行
          • ②老年代空间不足
          • ③方法区空间不足
          • 通过Minor GC后进入老年代的平均大小 大于老年代的可用内存
          • ⑤由Eden区,Survivor S0(from)区向S1(to)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
        • 说明:Full GC 是开发或调优中尽量要避免的,这样暂停时间会短一些

      为什么有TLAB(Thread Local Allocation Buffer)

      • 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据

      • 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的

      • 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度(因为Eden区分配内存是需要加锁的 )TLAB就应运而生

        至于为什么内存分配需要加锁,因为线程可能竞争一块内存空间,而那一块内存空间只能分配给一个对象

      什么是TLAB

      • 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内
      • 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略**
      • 所有OpenJDK衍生出来的JVM都提供了TLAB的设计

      说明

      • 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选
      • 在程序中,开发人员可以通过选项“-XX:UseTLAB“ 设置是够开启TLAB空间
      • 默认情况下,TLAB空间的内存非常小,仅占有整个EDen空间的1%,当然我们可以通过选项 ”-XX:TLABWasteTargetPercent“ 设置TLAB空间所占用Eden空间的百分比大小
      • 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配了内存

      堆是分配对象的唯一选择么(不是)

      在《深入理解Java虚拟机》中关于Java堆内存有这样一段描述:随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。
      在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。
      ?此外,前面提到的基于OpenJDK深度定制的TaoBaoVM,其中创新的GCIH(GCinvisible heap)技术实现off-heap,将生命周期较长的Java对象从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。

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

      堆区本章小节

      堆、栈、方法区的交互关系

      • 在jdk8中,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Metaspace)来代替

      《深入理解Java虚拟机》书中对方法区存储内容描述如下:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。(也不要死记,后面随着JDK的更新,也有些变化,静态变量,运行时常量池中的StringTable,和字符串常量池都放在了了堆中

      几种在常量池内存储的数据类型包括:

      • 数量值
      • 字符串值
      • 类引用
      • 字段引用
      • 方法引用

      小结

      常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型、字面量等信息。

      方法区的演进细节这里说的静态变量指的是变量名

      声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2021年6月13日
下一篇 2021年6月13日

相关推荐