OpenJDK8-Hotspot虚拟机学习笔记

一、JVM介绍

Java虚拟机就是二进制字节码的运行环境,可以让Java代码一次编译到处运行,能够自动的内存管理和垃圾回收。Java虚拟机包括类加载子系统、运行时数据区、执行引擎以及本地接口和本地方法库。

虚拟机结构

二、类加载子系统

类加载子系统中使用类加载器来加载.class文件,其中类加载器可以分为下面这几类:

  • 引导类加载器(Bootstrap ClassLoader):它加载的类无法直接获取类加载器,它是用c/c++来编写的。
  • 扩展类加载器(Extension ClassLoader):它继承于ClassLoader,父加载器为引导类加载器。主要加载jre/lib/ext下的jar。
  • 应用程序类加载器(AppClassLoader):它继承于ClassLoader,父加载器为扩展类加载器,是程序中默认的类加载器。
  • 用户自定义类加载器:可以防止源码泄漏。

.class文件进入类加载子系统需要经过一下几个流程:

  • 加载(Loading):在加载过程主要是形成了一个Class对象,存放在元空间,作为元空间这个类的各种数据的访问入口。
  • 链接(Linking)
    • 验证(Verification):保证加载类的正确性
    • 准备(Preparation):会对静态变量赋初始值(不包含final修饰的,final在编译的时候就分配了,准备阶段会显式初始化),类变量会分配在元空间中。
    • 解析(Resolution):将常量池内的符号饮用转为直接引用。(后续补充)
  • 初始化(Initialization):主要是执行类构造器方法(功能是执行Javac编译阶段收集的类变量的赋值动作和静态代码块中的语句合并起来,顺序执行)。在多线程中类构造器的执行过程会加锁。

当class加载遵循双亲委派原理:

  • 优先让父类加载器去加载,直到顶层父类加载器
  • 如果父类加载器无法加载,自己再去加载
  • 优势:避免类重复加载,防止核心SPI被篡改

三、运行时数据区

一个JVM实例只有一个Runtime实例,堆和元空间是每个线程池共享的,每个线程包括程序计数器、虚拟机栈和本地方法栈。

1、程序计数器

它是用来存储指向下一条指令的地址,也就是即将要执行的指令代码。也是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

为什么程序技术器需要线程池私有?如果多线程共享,那么多线程会共同操作这个公共变量,就会出现指令地址被覆盖,无法准确找到对应的下一条应该执行的指令。

2、虚拟机栈

虚拟机栈时线程私有的,它的生命周期和线程是一样的,虚拟机栈中保存着一个个栈桢,每个栈桢对应着相应的代码块。每个栈桢包含着一下结构:

  • 局部变量表:是一个数组,存储方法的参数和方法体内的局部变量(包括各类基本数据类型、对象引用(reference),以及returnAddress类型)。局部变量表越大,栈帧也就越大,会占用更多虚拟机栈空间,嵌套调用方法次数就越少。最基本存储单位是slot(变量槽),对于long和double类型的数据占用2个slot。对于非静态方法在局部变量表中,都会默认生成this对象放在首位,所以非静态方法可以使用this变量。
  • 操作数栈(表达式栈):根据字节码指令,往栈中写入数据或提取数据(入栈/出栈)
  • 动态链接(指向运行时常量池的方法引用):动态链接是为了将这些符号引用转换为调用方法的直接引用。静态链接,编译期间可知的是静态链接,在编译期间不可知的是动态链接。虚方法和废墟方法,如果在编译器就可以确定下来的就是非虚方法,例如:静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法,其他方法称为虚方法。
  • 方法返回地址(方法正常或异常退出定义):

虚拟机栈是不存在垃圾回收问题的,但是也会内存溢出的异常:

  • 如果采用固定大小的虚拟机栈,如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常。
  • Java虚拟机栈式可以动态扩展,尝试扩展的时候无法申请到足够的内存,或者没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机将会抛出一个OutOfMemoryError异常。

Java虚拟机栈内存大小修改指令:Xss 指定Java程序执行的栈的大小,默认单位为b,如果指定为其他单位,KB->k、MB->m、GB->g,其中在Linux、macOS中默认为1024kb,win服务器中取决于虚拟内存。

例:-Xss1m -Xss1024k

3、本地方法栈

管理本地方法的调用。本地方法栈是线程私有的。在内存溢出方面和虚拟机栈是相同的。本地方法栈也是会存在异常的,当发上一下情况时会抛出异常:

  • 如果采用固定大小的虚拟机栈,如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常。
  • Java虚拟机栈式可以动态扩展,尝试扩展的时候无法申请到足够的内存,或者没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机将会抛出一个OutOfMemoryError异常。

4、堆

一个JVM虚拟机实例只存在一个堆实例,在物理内存上堆是不连续的。StringTable和静态变量现在存放在堆中。虽然堆是线程贡献的但是,也会给每个线程在Eden区分配一块很小的线程私有缓冲区(Thread Local Allocation Buffer,TLAB),主要为了快速给对象分配内存地址,提高吞吐量。当TLAB内存不够时则会去公共区域分配内存,那么会形成内存地址竞争,则内存地址分配时会采用加锁处理。堆的内存分代可分为:年轻代(Eden、S0、S1)和老年代,其中在年轻代中,内存区域只有Eden和S区中的一块会被使用。大多数对象都在年轻代创建和销毁。一个对象的内存布局可以分为:

  • 对象头
    • 运行时原数据:哈希值、GC分代年龄、锁状态标识、线程持有的锁、偏向县城ID、偏向时间戳
    • 类型指针:确定该对象所属的类型
  • 实力数据:对象真正存储的有效信息

新生代堆内存对象分配情况:

  1. new的对象会在Eden区生成,当Eden区满的时候会触发Minor GC,经过Minor GC之后会把生存下来的对象分配在S0或S1
  2. 经过Minor GC之后Eden区得到释放,之后生成的对象又把Eden塞满了,又会经历一次Minor GC,把Eden存活下来的对象放在上次没有使用的S区(上次使用S0,那么这次就会放在S1,假设上次存放在了S0,那么这次就放在了S1区),接下来会判断S0区中存在的对象时候还在使用,如果还在使用,就会被拷贝到S1区,对于多次拷贝的对象达到阈值(阈值默认15).就会把这些对象晋升,也就是拷贝的老年代,对于无法存放的S1区的过多的对象,不用达到阈值,会直接存放在老年代。
  3. new的新对象,当Eden区放不下对象了,那么会放在老年代,如果老年代放不下,那么会执行Major GC,之后可以存放了,那么放在老年代了,如果还是没法存放的化就OOM了。

内存分配策略:

  • 优先分配到Eden
  • 大对象直接分配到老年代
  • 长期存活的对象分配到老年代
  • 动态对象年龄判断,对于S0、S1其中相同年龄的对象大小大于S0或S1空间的一半,这些对象可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。

设置堆内存的指令:

  • Xms:堆区起始内存
  • Xmx:堆区最大内存(一旦超过最大内存大小,抛出OutOfMemoryError)
  • XX:+PrintGCDetails:可以打印GC过程详情
  • XX:NewRatio=2:配置新生代和老年代堆结构的占比,后面的数据标识老年代占比(表示新生代占1,老年代占2,新生代占整个堆的1/3),一般情况下不会区修改这个比例,当生命周期较长的对象较多的化,建议提高老年代比例
  • XX:SurvivorRatio=8:配置新生代中Eden和S0、S1的比例,后面的数据标识Eden的占比,(表示Eden的占用8份S0和S1各占用1份)
  • XX:-UseAdaptiveSizePolicy:关闭自适应的分配策略
  • XX:+UseAdaptiveSizePolicy:打开自适应的分配策略,默认打开
  • XX:MaxTenuringThreshold=15:设置年轻到到老年代晋升阈值,当拷贝次数超过阈值数就会被放在老年代。
  • XX:UseTLAB:开启TLAB策略,默认时开启的。
    默认情况下:起始内存大小为物理内存大小除以64,最大内存大小为物理内存大小除以4

例:-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15,代表初始堆空间和最大的堆空间都为10m,并打印GC基本信息,并设置老年代占比为2,设置晋升阈值为15

为什么要进行分代:
分代主要时为了优化GC,对不同生命周期的对象,采用不用的GC策略。

堆时分配对象存储的唯一选择吗?(了解)
逃逸分析策略:如果方法内的对象没有被方法外部调用,则认为没有逃逸,则可以在栈上分配对象。

探索StringTable

字符串常量池是不会存储相同的字符串的。String的StringPool是一个固定大小的HashTable,默认值为60013(jdk7以后,jdk8以后1009是最小值)。如果String特别多则会倒是Hash冲突严重,则会导致链表很长降低String.intern的性能。可以通过-XX:StringTableSize来设置String Table的长度。

指令:

  • -XX:StringTableSize 来设置String Table长度。

String的内存模型
String的拼接,常量与常量拼接结果会放在常量池中,而有变量参与拼接操作结果放在非常量池的堆中,如果拼接结果调用了intern方法,则将会把数据存放在常量池中。

//生命变量
String a = "a"; //"a"存放在常量池的
String b = "b"; 
//常量拼接
String ab1 = "a"+"b"; //"ab"存放在常量池中

//变量拼接
String ab2 = a + b; //"ab"存放在非常量池的堆中
//一:生成一个StringBuilder调用append的方法进行拼接,然后调用toString()方法new String(char[] var),所以最后生成了 new String()对象

String ab3 = ab2.intern(); //“ab”存放在常量池中

String的intern方法,是将字符串存放在常量池中,如果常量池中已有则直接返回地址,如果没有则将堆中字符串的地址存放在常量池中再返回地址(jdk7以后)。

在代码中使用new String(“ab”)生成字符串时候生成2个对象,首先一个new 在非常量池中开辟空间生成的一个对象,另一个则是在常量池表中的“ab”(之前没有生成过“ab”);

String str = new String(“a”) + new String(“b”) ;中生成了多少个对象
1 : +操作采用了拼接操作所以生成 new StringBuilder();对象
2:“a”在常量池中生成了一个对象
3:new String(“a”)在非常量池堆中生成了一个字符串对象
4:“b”在常量池中生成了一个对象
5:new String(“b”)在非常量池堆中生成了一个字符串对象
6 因为StringBuilder最后会调用toString()方法,其中调用new String(char[] ,int ,int )方法构造了对象
所以最终生成了6个对象。

G1垃圾回收器对String的去重。
G1垃圾回收器主要是对String中value数组的去重,让value数组重复的公用一个重复value数据。

5、元空间

元空间是线程之间共享的,物理内存不连续可以固定大小,元空间保存着类型信息,字段信息,方法信息,JIT缓冲代码和运行时常量池,如果定义太多的类会导致元空间内存溢出,同样会抛出错误,java.lang.OutOfMemoryError:Metaspace。

运行时常量池是.class文件生的常量池表(各种自变量和符号引用)在运行时表现形式,相对于.class的常量池它存在这动态性。其中.class文件中常量池表中的符号引用,在运行时常量池中也变为真实地址。

元空间指令:

  • XX:MetaspaceSize:指定元空间大小,在windows下默认是21M。这是高水位线,一旦到达这条线,就会触发Full GC。然后这个高水位值将会重置。新的高水位的值取决于GC之后释放多少空间,如果释放的空间不足,那么会在不超过MaxMetaspaceSize时,适当提高,如果释放空间够多,则适当降低。高水位值设置过低,将会多次触发Full GC。此时建议设置一个较高的MetaspaceSize
  • XX:MaxMetaspaceSize:最大元空间大小,值为-1,即没有限制。所以如果不指定大小,有可能会耗尽所有可用的系统内存。

元空间中的垃圾回收:

  • 常量池中废弃的常量
  • 不再使用的类型:所有实例都被回收了(包含子类对象),加载该类的类的加载器已经被回收了,对应类的Class对象没有被引用。

直接内存:
也会导致OOM OutOfMemoryError: DIrect buffer memory
直接内存的大小不直接受限于-Xmx指定的最大堆的大小
缺点:分配回收成本高、不受JVM内存管理
指令:MaxDirectMemorySize
如果不指定,默认与堆的最大值-Xmx参数值一样

四、本地方法库和本地方法接口

native修饰的方法是非Java语言实现的。

五、执行引擎

执行引擎主要是执行PC寄存器所存储的指令。
可以通过指令来指定JVM来决定使用:

  • -Xint 完全采用解释器模式。
  • -Xcomp 完全采用JIT编译器模式执行程序。如果JIT编译器出现问题,解释器会介入执行。
  • -Xmixed 采用解释器+JIT编译器的混合模式共同执行程序。

解释器

对字节码采用逐行解释的方式执行,将字节码文件中的内容“翻译”为对应平台本地机器的指令。

JIT编译器

虚拟机将源代码直接编译成和本地机器平台相关的机器语言。
具有热点代码检测功能,可以将热点代码缓存下来保存在元空间中,提高代码执行效率。

在JVM模式为server的情况下,代码执行超过10000次这个阈值,就认为是热点代码,接下来会采用JIT编译器的模式来执行。client模式阈值为1500次。

为什么说Java是半编译半解释型语言?
是因为JVM的执行引擎中既包含解释器又包含编译器。

为什么拥有了高效的JIT编译器依然还是保留了解释器?
1、当程序启动时,解释器可以立即执行,省去编译事件
2、编译器需要转换为本地代码,然后执行效率才变高

垃圾回收

介绍:
每一次GC就会停止用户线程(STW),JVM在进行GC时,并非每次都对(新生代、老年代、元空间)区域一起回收,大部分时候回收的都是新生代。GC分为部分收集(Partial GC)和整堆收集(Full GC)

GC的分类:
部分收集:
- 新生代收集(Minor GC):Eden、s1、s0区垃圾收集。当年轻代Eden空间满了之后会触发Minor GC。S0、S1满了之后不会触发Minor GC,但是Minor GC会清理年轻代的内存。Minor GC会引发STW,暂停其他用户线程,等待垃圾回收结束,用户线程才恢复运行。Minor GC执行比较频繁,并且执行速度比较快。
- 老年代收集(Major GC):只针对老年代收集,目前只有CMS GC会有单独收集老年代的行为。通常执行Major GC会先出发Minor GC。一般Major GC执行速度要比Minor GC满,STW也会边长,用户线程等待时间也会变 长。
- 混合收集(Mixed GC):收集整个新生代以及部分老年代垃圾收集,目前只有G1 GC会有这种行为。
整堆收集(Full GC):整个Java堆和元空间的垃圾收集,调用System.gc()时,系统建议执行Full GC但不必然执行。其中老年代空间不足、元空间空间不足、通过Minor GC后进入老年代的平均大大于老年代的可用内存,会触发Full GC。当Full GC之后空间仍不足会抛出OOM(后续继续补充)

垃圾回收算法

标记阶段

1、引用计数算法(Java没有使用该算法)
对每一个对象都保存一个整形的引用计数器属性。用户记录对象被引用的情况。也就是当一个对象被引用那么计数器就加1,当不再饮用计数器就减1,当计数器为0时,表示对象不再被使用,就进行标记。

优点:容易实现,判定效率高。
缺点:增加了计数器也就增加了额外的内存消耗,每次都要进行计算增加了额外的时间开销,最主要的是无法处理循环引用的状况。

当处于循环引用时计数器始终不能为0,也就永远无法标记。

2、可达性分析算法(Java目前使用的标记算法)

基本思路:

  • 可达性分析算法是以根对象为集合(GC Roots)为起始点,按照从上往下的方式搜索被根对象集合所连接的目标对象是否可达。
  • 使用可达性分析算法后,内存中存活的对象都会被根对象直接或者间接连接着,搜索所走过的路径称为引用链。
  • 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以被标记
  • 在可达性算法中,只有被跟对象集合直接或者间接连接的对象才是存活对象。‘

在这里插入图片描述
在可达性分析算法分析期间为保证系统的一致性,必然要STW(Stop The World)也就是系统停顿。

在java中,哪些元素可以作为GC Roots

  • 虚拟机栈中引用的对象
  • 本地方法栈中引用的对象
  • 静态属性引用的对象
  • 常量的引用对象
  • 被同步锁所持有的对象
  • Java虚拟机内部引用的对象

判断一个对象是否可被回收至少要经历两次标记过程:
1、如果对象没有GC Roots引用链,则进行第一次标记。
2、判断对象有没有必要执行finalize方法,如果方法没有必要执行(没有被重写),那么对象被定位不可触及状态,进行第二次标记。要么执行了finlize方法但是对象没有被复活也要进行标记,如果对象复活了,那么对象将会被移出“即将回收”集合。

finalize方法,在任何对象要被回收之前会调用finalize方法,永远不要主动调用finalize方法
原因:

  • finalize 可能会导致对象复活。
  • finalize 的方法执行没有时间保障。
  • 一个糟糕的finalize会严重影响GC性能。

由于finalize方法的存在,虚拟机中的对象一般处于三种可能的状态。

  • 可触及:从根节点开始可以找到这个对象
  • 可复活:对象可能在有可能在finalize方法中复活
  • 不可触及:对象在调用finalize方法之后并没有复活。
清除阶段

1、标记-清除算法

当堆内存中有效内存被耗尽的时候就会停止整个程序,进行标记和清除执行过程:

  1. 标记:从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
  2. 清除:对堆内存从头到尾进行线性遍历,如果发现每个对象在其Header中没有标记为可达对象,则将其回收。

缺陷:

  • 效率不高
  • GC时需要停止用户线程,体验较差
  • 清理产生的内存空间不是连续的,会产生内存碎片,需要维护一个空闲列表。

清除:把需要清除对象的地址保存在空闲列表里。

2、复制算法

从引用根节点开始遍历,将遍历到的对象复制到另一块内存内,这样另一块内村的地址就是连续的。

优点:

  • 只执行一次遍历,运行高效
  • 复制后的内存是连续的。

缺点:

  • 需要2倍的内存。
  • 需要为地址之间的关系,对空间和时间消耗大。

适用于存货对象比较少的环境比如新生代幸存者0区(S0)和幸存者1区(S1)

3、标记-压缩算法

标记和压缩执行过程:

  1. 标记:从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
  2. 压缩:将所有的存货对象压缩到内存的一段,按照顺序排放。之后清理边界外所有的空间。

优点:

  • 解决标记-清除算法内存空间不连续

缺点:

  • 压缩过程需要对象移动时间和空间损耗大
  • 同样需要STW
垃圾回收算法

1、分代收集算法
年轻代:因为对象生命周期短、存活率低、回收频繁。所以在新生代的survivor区采用了复制算法。
老年代:因为区域大、对象生命周期长、存活率高、回收不频繁,一般采用标记-清除和标记-整理的混合实现的。

2、增量收集算法
间断的执行用户线程和垃圾回收线程,核心还是采用标记-清除算法和复制算法。
缺点:
间断性的执行应用程序代码,减少系统的停顿时间,线程的切换和上下文的消耗,使得垃圾回收成本整体上升,造成吞吐量下降。

3、分区算法
将堆分为一个个小区域,对于每个区域独立使用,独立回收。根本目的是为了减少停顿时间。

垃圾回收概述

1、System.gc()会显示触发Full GC,同时对老年代和新生代回收,尝试释放被丢弃对象占用的内存。然而并不保证一定会执行实际的方法。
2、内存溢出和内存泄漏:
内存溢出(OOM):OutOfMemorryError,没有空闲内存,并且垃圾回收器没法提供更多的内存。
内存泄漏(Memory Leak):内存泄漏是有可能导致OOM。

(严格上)对象不被程序用到了,但是GC又回收不了。
(宽泛上)因为设置的对象生命周期很长,导致出现了OOM。

3、安全点:可以停下来GC的地方成为安全点。
安全区域:是指在一段代码中,对象的引用关系不会发生变化,在这个区域中的任何位置开始gc都是安全的。

4、引用:

  • 强引用(StrongReference):普通的对象,无论在任何情况下,只要强引用关系还在,垃圾回收器就不会回收。
  • 软引用(SoftReference):在系统将要发生内存溢出之前,把这些对象列为回收范围之中进行第二次回收,如果内存还是不够,才会内存溢出。一般会用在高速缓存中。
  • 弱引用(WeakReference):被弱引用关联的对象只能存活到下一次GC之前,GC时无论空间是否充足,都会回收被弱引用关联的对象。一般会用在三级缓存中。
  • 虚引用(PhantomReference):无法通过虚引用获得一个实例,唯一的目的就是在对象回收时收到一个通知。

当虚引用对象被回收时会把对象加入引用队列里面,来通知应用程序回收情况。由于虚引用可以跟踪对象的回收时间,也可以将一些资源释放操作放置在虚引用中执行和记录。

Obejct obj = new Object();
ReferenceQueue phantomQueue = new ReferenceQueue();
PhantomReference<Object> pf = new PhantomReference<>(obj,phantomQueue);
obj = null ;

垃圾回收器

评价垃圾回收器的标准,在最大吞吐量优先的情况下,降低停顿时间。
jdk8中默认的垃圾回收器Parallel Scavenge和Parallel Old的组合

1、Parallel Scavenge垃圾回收器(吞吐量有先)
新生代垃圾回收器,采用了复制算法、并行回收和STW机制

2、Parallel Old收集器
老年代垃圾收集器,采用了标记-压缩算法,基于并行回收和STW机制

指令:

  • -XX:+UseParallelGC 手动指定使用Parallel
  • -XX:+ParallelGCThreads 设置年轻代GC线程数,CPU数量小于8个,采用等于CPU的数量的线程数,当CPU数量大于8个则采用3+(5*CPU个数)/8
  • -XX:+UseParalleloldGC 手动指定使用Parallel Old
  • -XX:MaxGCPauseMillis 设置垃圾回收器最大停顿时间
  • -XX:GCTimeRatio 垃圾回收时间占总时间比例,默认值为99 ,计算公式为(1/(N+1))
  • -XX:UseAdaptiveSizePolicy 设置Parrallel Scavenge收集器具有自适应调整策略,主要是完成Eden和Survivor的比例,晋升老年代的对象的年龄等参数会被自动调整,达到堆大小、吞吐量和停顿时间之间的平衡(默认时开启状态)

通过命令行查看参数
jps指令获取Java进程ID,然后通过jinfo -flag [参数名称] [进程号] 例:jinfo -flag GCTimeRatio 19237

3、G1回收期-区域化分代式(jdk9以后默认的垃圾回收器)

六、代码优化

1、栈上分配(在hotspot虚拟机上并没有使用)
介绍:
生成的对象只在方法内部使用的话尽量在方法内部生命,则生成对象不会逃逸,则可以在栈上分配对象。

指令:

  • XX:-DoEscapeAnalysis:不开启逃逸分析策略
  • XX:+DoEscapeAnalysis:开启逃逸分析策略,默认时开启的。

对比代码:

/**
 * -Xms256m -Xmm256m  -XX:+PrintGCDetails 开启逃逸分析策略
 * -XX:-DoEscapeAnalysis -Xms256m -Xmm256m  -XX:+PrintGCDetails 关闭逃逸分析策略
 * 
 * 在这两种策略下面执行的效果具有明显的差异
 */
public class DoEscapeAnalysisDemoTest {

    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 1000000000; i++) {
            User user = new User();
        }
        long end = System.currentTimeMillis();
        System.out.println("执行结束:" + (end - start) + "ms");
        TimeUnit.SECONDS.sleep(1000000);

    }
}

class User {

}

2、同步省略策略
介绍:
JIT编译器可以借助逃逸分析策略,判断同步块所使用的锁对象只能被一个线程访问而没有发布到其他线程。JIT编译器则会取消对这部分代码的同步。同步省略也叫锁消除。

3、分离对象或标量替换(hotspot已经在使用)
介绍:
在逃逸分析策略下,发现一个聚合量对象没有逃逸,则可以将这个聚合量变成几个小的聚合量和标量。这样就可以把一个大的聚合量分解成若干个小的标量来代替,不需要创建对象了,就不需要分配内存了。标量替换为栈上分配提供了很好的基础。

指令:
XX:-EliminateAllocation:关闭标量策略
XX:+EliminateAllocation:开启标量策略,默认时开启的。

对比代码:

/**
 * -Xms100m -Xmx100m -XX:+PrintGCDetails -XX:-EliminateAllocations 关闭标量替换
 * -Xms100m -Xmx100m -XX:+PrintGCDetails 打开标量替换
 */

public class EliminateAllocationDemoTest {

    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 1000000000; i++) {
            User user = new User();
            user.age = 10;
            user.name = "shjam";
        }
        long end = System.currentTimeMillis();
        System.out.println("执行结束:" + (end - start) + "ms");
        TimeUnit.SECONDS.sleep(1000000);

    }

    public static class User {
        String name;
        int age;
    }
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: java-7-openjdk-amd64虚拟机不存在,可能是由于以下原因所致: 1. 您使用的操作系统不兼容java-7-openjdk-amd64虚拟机,需要使用其他版本的虚拟机; 2. 您的系统中未安装java-7-openjdk-amd64虚拟机,您可以尝试安装该虚拟机或者使用其他可用的虚拟机; 3. 如果您使用的是云服务器,可能需要联系云服务提供商进行设置或安装java-7-openjdk-amd64虚拟机。 建议您可以尝试通过检查操作系统兼容性、安装虚拟机或者联系云服务提供商等方式来解决该问题,以确保您能够正常使用所需的虚拟机。 ### 回答2: Java-7-OpenJDK-AMD64虚拟机不是一种存在的虚拟机OpenJDK是一个开源的Java开发工具包,它包含了Java虚拟机Java类库和其他工具。但是,OpenJDK并没有一个特定的版本叫做Java-7-OpenJDK-AMD64虚拟机。也许您在尝试安装某个应用程序或软件时看到了这个错误消息,这意味着该程序需要Java虚拟机来运行,但是您可能尝试安装了错误的Java版本或操作系统。正确安装Java虚拟机后,您应该能够运行需要它的应用程序或软件。建议您检查所需的Java版本和操作系统,以确保您下载了正确的Java虚拟机。 ### 回答3: Java-7-openjdk-amd64虚拟机是指OpenJDK 7版本的Java虚拟机,支持64位的AMD等处理器架构。如果您在使用该虚拟机时发现它不存在,可能是以下原因导致: 1. 您尚未安装该虚拟机。您可以通过sudo apt-get install openjdk-7-jdk命令在Ubuntu上安装该虚拟机。 2. 该虚拟机已被您卸载或删除。您可以通过sudo apt-get remove openjdk-7-jdk命令在Ubuntu上卸载该虚拟机。 3. 您所使用的操作系统版本不支持该虚拟机或者该虚拟机已过时,建议使用更稳定、更新的版本,比如OpenJDK 8、OpenJDK 11等版本。 在进行Java开发时,选择适合自己的Java虚拟机非常重要,建议根据需要选择合适的开发环境和Java版本。需要注意的是,OpenJDK不同版本的Java虚拟机可能存在不同的特性和性能,您可以根据自己的需要进行选择。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值