• java虚拟机总结


    JDK、JRE、JVM的关系

     从上面这个图可以知道,jdk提供将.java的源代码转换为.class的字节码,并提供一些jRE相关的监控工具,而jre则是java的运行环境,是运行class文件的,而JVM就在JRE里面。

    所谓一次编写,到处运行的含义就是:可以编写一次.java文件或者转换过得.class文件,到每个操作系统都可以自己实现自己的JRE环境,有自己的JVM去运行java程序。

    内存管理

    1.JVM运行时数据区(5个部分)

    数据流、指令流、控制流

     程序计数器:指向当前线程正在执行的字节码指令的地址、行号。(线程的私有内存)

    线程没有存储单元,只负责执行,但多个线程只能去申请一个cpu的时间片,线程A用完时间片以后要让给线程B,因此,需要程序计数器把线程执行的状态存下来。同时程序计数器也要控制一些控制的指令。

    虚拟机栈:存储当前线程运行方法所需的数据、指令、返回地址

     其中存储的是线程运行到的方法的变量。操作数以及一些链接

     注意的点:

    1.线程一旦进入一个方法,该方法在虚拟机栈中的局部变量的空间是固定的,为什么能固定呢,基本数据类型的大小占用大小是固定的,对于对象类型来说,存的只是一个引用,因此所有局部变量的空间大小都是确定的。

    2.对象类型存放的是引用,因此,引用指向的应该是堆内存。

    3.动态链接:常量池的概念,对于接口创建的实例对象,要想找到该对象,那么在栈里存的只是一个字面量,然后根据字面量来找到真正的实例。

    4.出口就是出栈时指向的地址。栈帧出栈以后,就会被GC回收。

    5.方法A调用方法B,那么栈里会有几个栈:两个,栈不一定在运行到该代码时候才入栈,因为有重排序。

    6。递归时候有几个栈被压栈:多个,而且虚拟机栈中栈的数量可以指定或者内存可以执行。超过后会报异常。

    本地方法栈

    方法区:类信息、常量、静态变量、JIT(编译信息)

    为什么会存在方法区,而不是把这些常量和静态变量放在堆里呢?主要是因为,堆里存的数据可能会很多要用到这些,而且是变化的,存在方法区只需要改变一个值,而存在堆里需要改变很多值。

    JIT编译的代码是动态代理的一些类的信息

    堆(Heap) 存放对象实例、线程共享

    JVM的内存模型(JMM)(新生代、老年代、永久代)

    新生代和老年代在堆里面,永久代在方法区里(一部分JVM,1.8以前)

    因此垃圾回收的重点就是堆。 

     JVM运行时的内存从堆触发,在GC的角度还可以分为新生代(包括Eden区、From Survivor区、To Survivor区)和老年代


    对象

    对象是如何创建的

    虚拟机遇到一个new指令的时候,会去检查这个指令的参数能否在常量池里面定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载过、解析和初始化过了。

    如果没有,就必须执行相应的类加载过程。

    那么如何类加载呢?

    首先我们知道了Class文件的存储的格式细节。那么虚拟机把Class文件加载到内存里面去,并对数据进行校验,转换解析和初始化。最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。

    类加载的过程包括:加载、验证、准备、解析、初始化。

    加载

    1.通过类的全限定名来获得类的二进制字节流。(从zip(Jar包)、applet、动态代理、jsp、数据库等)

    2.将字节流所代表的静态存储结构转化为方法区的运行时数据结构。

    3.在内存中生成一个代表此类的java.lang.Class对象,作为方法区的各种数据的访问入口。

    对于数组类 而言,本身不通过类加载器来创建,而是由虚拟机直接创建,但数组类的元素类型还是要靠类加载器去创建。

    加载阶段完成以后,二进制字节流就按照虚拟机所需的格式存储在方法区之中,其中方法区的数据格式由虚拟机自行定义。然后在内存中实例化了一个java.lang.Class对象,这个对象是放在方法区里面的。(这与对象放在堆里不同)

    验证

    验证是连接阶段的第一步,目的是为了确保Class文件的字节流中包含的信息符合当前的虚拟机要求,而不会危害虚拟机自身的安全。

    问什么要验证?因为虚拟机不只有java语言,还有其他的语言,需要判断二进制文件是否会危害虚拟机。

    包括四个阶段的验证动作:

    1.文件格式验证:验证字节流是否符合Class文件的格式规范,并能被当前版本的虚拟机处理。

    2.元数据验证:从这一阶段开始,都是基于方法区的验证。对字节码描述的信息进行语义分析,保证符合java语言规范。

    3.字节码验证:通过数据流和控制流分析,保证程序语义合法,和逻辑。是对方法体进行验证,保证不会对虚拟机产生危害。

    4.符号引用验证:虚拟机将符号转换为直接引用的时候,对类自身以外的信息(常量池中的各种符号引用)做匹配性校验。

    准备

    正式为类变量分配内存,并设置类变量的初始值。这些变量要使用的内存都将在方法区分配。(类变量只包括静态的变量,不包括实例变量)。

    类变量在准备阶段的初始值是0,而不是赋予的值。赋值操作将在初始化的时候在类构造器中执行。

    如何变量被final修饰了,那么该变量就是常量,它将在准备阶段就赋值。

    解析

    虚拟机将常量池内的符号引用替换为直接引用的过程。

     解析动作主要针对:类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符。

     通过上面描述可以知道,解析就是怎么确认该类属于哪个接口。(和动态链接很相似)

    初始化

    真正开始执行类中定义的java程序代码(字节码),是执行类构造器方法的过程。

    类构造器:<clinit>()方法和构造函数不同,不需要显示的调用类构造器。虚拟机会保证在子类的<clinit>()方法方法执行之前,父类的已经执行完毕了。因此,虚拟机中的第一个执行<clinit>()方法的是Object类。

    上面这句话的含义就是父类的静态语句块要优先于子类的变量赋值。

    需要指出的是,类构造器只是针对静态变量,如果没有对变量的赋值,那么是不会调用该方法的。

    同时,虚拟机会保证<clinit>()方法在多线程中会被正确的加锁,解锁。但这样很可能会造成阻塞,这种阻塞是很隐蔽的。

    但需要指出的是,如果一个线程执行了<clinit>()方法,那么其他线程是不会再进入,因为同一个类加载器,一个类型只会初始化一次。

    类加载的时机

    在加载阶段,虚拟机并没有强制约束,但对于初始化阶段,有五种情况必须进行初始化类。

    1.遇到new、getstatic、putstatic、involkestatic这四条字节码指令的时候,如果类没有初始化,则需要先触发初始化。

    2.使用反射包对类进行反射调用的时候。

    3.父类没有初始化,要先初始化父类。

    4.虚拟机启动要先初始化主类。

    5.如下

     上述行为被称为对一个类进行主动引入,这种引入方式会触发初始化,但其他的引入方式不会触发初始化。

    1.子类中引用父类中定义的静态字段,不会触发子类的初始化。

    2.创建一个由类组件的数组,不会引起类的初始化。

    3.调用某一类中的静态常量,不会引起该类的初始化。

    类加载器

    任何一个类,都需要由加载它的类加载器和类本身一同确定其在java虚拟机中的唯一性。如果要比较两个类是否相等,只有在这两个类是同一个类加载器时才有意义。

    public class ClassLoaderTest {
        public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
            ClassLoader myloader=new ClassLoader() {
                @Override
                public Class<?> loadClass(String name) throws ClassNotFoundException {
                    try {
                        String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                        InputStream is = getClass().getResourceAsStream(fileName);
                        if(is==null){
                            return super.loadClass(name);
                        }
    
                        byte [] b=new byte[is.available()];
                        is.read(b);
                        return defineClass(name,b,0,b.length);
                    } catch (IOException e) {
                        throw new ClassNotFoundException(name);
                    }
                }
            };
    
            Object obj = myloader.loadClass("com.liuxinghang.threadpool.ClassLoaderTest").newInstance();
            System.out.println(obj.getClass());
            System.out.println(obj instanceof com.liuxinghang.threadpool.ClassLoaderTest );
    
        }
    }
    ClassLoaderTest

    双亲委派模型:

    在java虚拟机的角度,只存在两种不同的类加载器,一种是启动类加载器,使用c++实现,另一种是其他的类加载器,由java实现,独立于虚拟机之外,并全部继承抽象类CLassLoader。

    启动类加载器(BootStrap ClassLoader):虚拟机自带的,负责将lib路径下虚拟机可以识别的类库加载到内存中。启动类加载器无法被java类直接引用。用户在编写自定义类加载器的时候,需要把加载请求委派给引导类加载器。

    扩展类加载器(Extension ClassLoader):由官方实现,负责加载lib/ext目录下的或者指定类库,开发者可以直接使用。

    应用程序类加载器(Application ClassLoader):负责加载用户类路径(ClassPath)上指定的类库。

     以上的这种层次就成为双亲委派模型,它要求除了顶层的启动类加载器之外,其他的类加载器都应该有自己的父类加载器。使用组合关系来复用父类加载器的代码。

    双亲委派模型的工作过程:如果一个类加载器收到了类的加载请求,首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,所有的加载请求最终都传送给启动加载器,如果父类加载器说自己无法完成加载请求,那么子类加载器才会尝试自己加载。

     破坏双亲委派模型:这里以tomcat为类

    原因

    1、部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离。这是最基本的要求,两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求一个类库在一个服务器中只有一份,服务器应当保证两个应用程序的类库可以互相使用

    2、部署在同一个服务器上的两个Web应用程序所使用的Java类库可以相互共享。这个需求也很常见,比如相同的Spring类库10个应用程序在用不可能分别存放在各个应用程序的隔离目录中

    3、支持热替换,我们知道JSP文件最终要编译成.class文件才能由虚拟机执行,但JSP文件由于其纯文本存储特性,运行时修改的概率远远大于第三方类库或自身.class文件,而且JSP这种网页应用也把修改后无须重启作为一个很大的优势看待tomcat的实现


     介绍完了类加载器,那么我们回顾一下介绍类加载器时干什么?

    我们是研究对象是怎么创建的。

    一个对象在类加载之后,接下来虚拟机将会为新生的对象分配内存。对象所需的内存在类加载完毕之后就已经固定。为对象分配空间的任务就是把一块确定大小的内存从堆里面划分出来。

    那么如何划分可用空间呢?主要有两种方式

    1.指针碰撞:java内存绝对规整,用过的内存存在一边,没用过的在另一边,一个指针作为分界线。那么分配内存的操作仅仅是让指针挪动对象所需内存一样大的空间即可。

    使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞。

    2.空闲列表:堆内存不规整,已使用的内存和空闲的内存交错,虚拟机必须维护一个列表,记录哪些内存块是可用的。分配时,找到合适大小的内存分配给对象。并更新列表。

    使用CMS基于Mark-Sweep算法的收集器,采用空闲列表。

    如何解决线程安全的问题(对象创建在虚拟机中是很频繁的过程)

    1.虚拟机采用CAS配合失败重试的方法保证更新操作的原子性。

    2.把内存分配的动作按照线程划分在不同的空间中。即每一个java线程在堆中预先分配一小块内存,成为本地线程分配缓冲(Thread Local Allocation Buffer TLAB),哪个线程需要分配内存,就先在哪个线程的TLAB上分配,直到用完了才需要同步锁定。

    以上内存就分配完毕

    接下来,虚拟机将分配到的内存都初始化为0值。

    然后虚拟机对对象进行必要的设置。

     

    对象的内存布局

    对象在内存中分为3块区域:对象头。实例对象、对其填充

    对象头:

    1.存储对象自身的运行时的数据,官方成为markwork

     2.类型指针,对象指向类元数据的指针,通过这个指针来确定对象是哪个类的实例。

    实例数据:

    对其填充:

    对对象的访问:

    java程序需要通过栈上的reference数据来操作堆上的具体对象。主流的访问方式有两种

    1.使用句柄:java堆中会分出一块内存作为句柄池,reference中存储的就是对象的句柄地址。句柄中包含了对象实例数据的具体地址信息。

     2.使用直接指针访问

    reference中存储的就是对象的地址。

    二者的区别:


    那么怎么判断一个对象是否存活呢?

    很多语言使用的是引用计数算法:

     但这种算法在两个对象相互引用的时候是没办法判断是否存活的,这时候两个对象的引用不可能为0.

    在java中使用的是可达性分析算法:

    通过一系列称为“GCRoot”的对象作为起始点。从这些节点开始往下搜索,走过的路径叫做引用链。当一个对象到Root没有任何引用链相连的时候(图论中的不可达),证明该对象是不可用的。可以被判定为可回收的对象。

     四种引用类型:

    1.强引用:使用类似new关键字建立起来的引用,这类引用不会被回收。

    2.软引用:一些还有用,但并非必须的对象,这类引用在系统要发生溢出之前才会被回收,使用 SoftReference 类来创建软引用。

    3.弱引用:非必须对象,强度比软引用弱,该类引用只能生存到下次垃圾回收之前。使用 WeakReference 类来创建弱引用。

    4.虚引用:最弱的引用关系,

    对象不可达就真的死了吗?

    回收方法区

    永久代的回收主要是两个部分

    1.废弃常量:在字面池常量中,如果一个字符串“abc”已经进入常量池中,但现在系统中没有任何一个String对象叫做“abc”的,即没有任何地方引用该常量,那么此时发生内存回收,就会把“abc”常量清理出常量池。

    2.“无用的类”:要满足三个条件,才能算是无用的类


    垃圾是如何被回收的?

    垃圾回收算法

    1.标记-清除算法

    两个阶段:标记和清除。首先标记所有的待回收对象,标记完成后,统一回收所有被标记的对象。

    不足有两点:1.标记和清除两个过程效率都很低。2.空间问题,回收之后会产生大量的不连续的内存碎片。

    2.复制算法:

    将可用内存按容量划分为大小相等的两份,每次只只使用其中的一份,当这块内存用完了之后,将活着的对象复制到另一块内存,然后将已使用的内存一次清理掉。这样内存分配的时候就不用考虑内存碎片了,只要移动堆顶指针,按顺序分配内存即可。

     现代商业的虚拟机就是采用这种复制的算法,只是并非按照1:1来划分空间。

    HotSpot中,将内存划分为一块较大的Eden空间和两块较小的Survivor空间。每次使用一块Eden和其中一块Survivor,当回收时,将Eden和Survivor中还存活的对象一次性的复制到另一块Survivor中。最后清理掉Eden和刚才用过的Surivor。因此每次有10%的空间会被浪费。当Survivor空间不够的时候,需要依赖其他的内存(老年代)进行分配担保。

    3.标记-整理算法

    在老年代中,进行额外的分配担保,因此不能使用复制算法,使用的是标记-整理算法。先对存活的对象进行标记,先不对可回收对象进行清理,而是将存活对象全部向一端移动,然后直接清理掉端边界以外的内存。

     4.分代收集算法

    HotSpot的算法实现:

    1.枚举根节点:GCRoots节点主要在全局性的引用(常量或静态属性)、执行上下文(栈帧中的本地变量表)。

    可达性分析对执行时间敏感。1.引用很多,逐个检查很耗时。2.GC停顿,为了保持一致性(在分析过程中引用关系不能不断变化),需要停顿所有的GC线程。

    解决问题1是使用了准确式GC,并不需要检查所有的引用,而是使用一个OopMap的数据结构,去得知哪些地方放着引用。第二个问题无法避免

    2.安全点:在OopMap的协助下,HotSpot很快速的完成GCRoots的枚举,但如果为每一天指令都生成OopMap,那么GC空间成本会很高。因此,HotSpot只是在只在一些特定位置记录了OopMap的信息。这些位置就是安全点。

    即程序只有执行到安全点才会执行GC。

     

     3.安全区域


    垃圾收集器

    垃圾收集器是内存回收的具体实现。

     连线表明可以搭配使用。

    1.Serial收集器

    单线程收集器,只会使用一个CPU或者一条收集线程去完成垃圾收集工作。而且,在垃圾回收时,需要暂停所有的其他的工作线程。直到它收集结束。

     2.ParNew收集器

    是Serial收集器的多线程版本,

     

    ParNew收集器在单线程的环境下绝对不会比Serial收集器的好,因为存在了线程间的交互开销,ParNew收集器在通过超线程技术实现的两个CPU的环境中都不能保证超越Serial,它默认开启CPU个收集线程。

    3.Parallel Scavenge收集器

    新生代收集器,它关注的目标是达到一个可控制的吞吐量(CPU用于运行用户代码的时间与CPU总消耗时间之比。)高吞吐量能够高效的利用CPU的时间,尽快完成程序的运算任务。主要适合在后台运算而不需要太多交互的任务。

    缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。

    可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。

    4.Serial Old收集器

    是Serial的老年代版本,也是单线程收集器。使用标记-整理算法。主要在Client模式下的虚拟机使用。

    5.Parallel Old收集器

    是Parallel Scavenge收集器的老年版本,使用多线程和标记-整理算法。(吞吐量有限组合)

     6.CMS收集器(Concurrent Mark Sweep)

    以获取最短回收停顿时间为目标的收集器,用于互联网站和服务器端。

    基于标记-清除算法。运作过程分四步:

    初始标记:标记一下GCRoots能直接关联到的对象,速度很快。需要停止所有线程。

    并发标记:进行GCRoots追踪的过程,可以和用户线程一起运行。

    重新标记:修正并发标记期间,因用户程序继续运作而导致的标记产生变化的那部分标记记录。停顿时间比初始标记长,比并发标记短。

    并发清除:与用户线程一起运行。

    7.G1收集器(Garbage-First)

    收集器最前沿成果

    面向服务端应用的垃圾收集器。有四个优点

     分代:将java堆划分为多个大小相等的独立区域,新生代和老年代不在物理隔离,而是区域的集合。

    可预测的停顿时间:有计划的避免在整个java堆里进行全区域的垃圾回收,G1跟踪各个Region里面的垃圾堆积的价值大小(回收获得的空间大小以及时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。

     G1运作的步骤

    1.初始标记;仅仅标记一下GCRoots能直接关联到的对象,并修改TAMS(next top at mark start)的值。让下阶段用户程序并发运行的时候,能在正确可用的Region中创建新对象,这一阶段需要停顿线程。

    2.并发标记:从Root开始可达性分析,找出存活对象。与用户程序并发执行。

    3.最终标记:修正并发标记期间,因用户程序继续运作而导致的标记产生变化的那部分标记记录,虚拟机将这些记录存入RememberedSet LOgs中,然后把这些数据合并到RememberedSet中,需要停顿线程,但可以并发执行。

    4.筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。


    内存分配与回收策略

    JVM解决了两件事:1.给对象分配内存。2.回收对象分配的内存。

    对象主要分配在新生代的Eden区,如果启动了本地线程分配缓存,将按线程优先在TLAB上分配。少数情况下直接分配在老年代。

    1.对象优先在Eden区分配。当Eden区的内存不足时,虚拟机将发起一次Minor GC。

    MinorGC:新生代GC,发生在新生代的垃圾收集动作。很频繁,回收速度比较快。

    2.大对象直接进入老年代。大对象就是需要大量连续内存的对象。

    3.长期存活的对象将进入老年代。虚拟机给每一个对象定义了一个对象年龄计数器,如果对象在Eden区出生并经过第一次MinorGC后仍然存活,则年龄加1,每次熬过一次MinorGC,年龄都会加1,那么等增加到一定程度,就会移动到老年代。这个数可以设置。

    4.动态对象年龄判定。

     5.空间分配担保。每次发生MinorGC的时候,都会先检查老年代最大连续可用的空间是否大于新生代所有对象总空间,如果大于,那么MInorGC可以确保安全。如果不成立,则虚拟机会查看是否允许担保失败,如果允许,会继续检查老年代最大连续可用空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试一次MinorGC,但这种尝试是有风险的。如果小于,那么这时要进行一次FullGC。

    FullGC:老年代GC,发生在老年代,出现了老年代GC,经常会伴随至少一次的MinorGC(并不绝对)。FullGC的速度会比MinorGC的慢10倍左右。

    那么具体什么时候会执行GC呢?

    1,对象没有引用

    2,作用域发生未捕获异常

    3,程序在作用域正常执行完毕

    4,程序执行了System.exit()

    5,程序发生意外终止(被杀进程等)

    6、system.gc

    7、当应用程序空闲时,即没有应用线程在运行时,GC会被调用。

    8、Java堆内存不足时,GC会被调用。

    MinorGC的触发条件很简单,当Eden空间满了,就会触发一次MinorGC

    FullGC是很重要的GC,它运行时间很慢,因此,需要重点考虑引发FullGC的条件

    1.调用System.gc();这种方式虚拟机不一定执行。

    2.老年代空间不足:

     3.空间分配担保失败:

    4.jdk1.7及之前的永久代空间不足

    5.Concurrent ModeFailure

    执行CMSGC的过程要将对象放入老年代,若此时老年代空间不足就会触发FullGC。


    以上就是jvm的基础知识。

    java调优的总结

    结合jvm的各项参数对应用程序调优。

    性能定义:

    1.吞吐量:不考虑垃圾收集引起的停顿时间和内存消耗,垃圾收集器能支撑应用达到的最高性能指标:是应用程序运行时间与总时间之比,反应cpu的利用率。

    2.延迟:缩短由于垃圾回收所引起的停顿。避免应用运行时发生抖动。

    3.内存占用:垃圾收集器流畅运行所需要的内存数量。

    性能调优的原则:

    1.MinorGC回收原则:每次MinorGC都要尽可能多的收集垃圾对象,减少应用程序发生FullGC的频率。

    2.GC内存最大化原则:处理吞吐量和延迟问题,垃圾处理器能使用的内存越大,垃圾收集效果越好。

    3.GC调优3选2原则:在吞吐量、延迟、内存占用中,只能选择两个进行调优。

    性能调优流程:

    满足程序的内存使用需求 ->时间延迟的要求 ->吞吐量的要求。(优先级从高到低)

    1.确定内存的占用:

      1、初始化阶段 : jvm加载应用程序,初始化应用程序的主要模块和数据。

      2、稳定阶段:应用在此时运行了大多数时间,经历过压力测试的之后,各项性能参数呈稳定状态。核心函数被执行,已经被jit编译预热过。

      3、总结阶段:最后的总结阶段,进行一些基准测试,生成响应的策报告。这个阶段我们可以不关注。

    确定内存占用以及活跃数据的大小,我们应该是在程序的稳定阶段来进行确定,而不是在项目起初阶段来进行确定,如何确定,我们先看以下jvm的内存分配。

      -Xms 初始堆大小,默认为物理内存的1/64(<1GB)

      -Xmx 最大堆大小,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制

      -XX:NewSize 新生代空间大小初始值

      -XX:MaxNewSize 新生代空间大小最大值

      -Xmn 新生代空间大小,此处的大小是(eden+2 survivor space)

      -XX:PermSize 永久代空间的初始值&最小值

      -XX:MaxPermSize 永久代空间的最大值

      老年代: 老年代的空间大小会根据新生代的大小隐式设定 初始值=-Xmx减去-XX:NewSize的值 最小值=-Xmx值减去-XX:MaxNewSize的值

      最小值=-Xmx值减去-XX:MaxNewSize的值

      在设置的时候,如果关注性能开销的话,应尽量把永久代的初始值与最大值设置为同一值,因为永久代的大小调整需要进行FullGC 才能实现。

    计算活跃数据大小。。。

    2.延迟调优:

    在确定了应用程序的活跃数据大小之后,我们需要再进行延迟性调优,因为对于此时堆内存大小,延迟性需求无法达到应用的需要,需要基于应用的情况来进行调试。

    在这一步进行期间,我们可能会再次优化堆大小的配置,评估GC的持续时间和频率、以及是否需要切换到不同的垃圾收集器上。

    3.吞吐量调优:

    对于垃圾收集器来说,提升吞吐量的性能调优的目标就是就是尽可能避免或者很少发生FullGC 或者Stop-The-World压缩式垃圾收集(CMS),因为这两种方式都会造成应用程序吞吐降低。尽量在MinorGC 阶段回收更多的对象,避免对象提升过快到老年代。

     

  • 相关阅读:
    CF 1114D(538,div2) Flood Fill
    UVA 1640 The Counting Problem
    UVA 11971 Polygon
    UVA 1639 Candy
    CCPC 2019 秦皇岛 Angle Beats
    UVA1153-Keep the Customer Satisfied(贪心)
    UVA1613-K-Graph Oddity(贪心)
    UVA11925-Generating Permutations(贪心)
    UVA11491-Erasing ans Winning(贪心)
    UVA12545-Bits Equalizer(思维)
  • 原文地址:https://www.cnblogs.com/lovejune/p/12520430.html
Copyright © 2020-2023  润新知