JVM基础详解

Posted Serendipity sn

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM基础详解相关的知识,希望对你有一定的参考价值。

目录

JVM

1.类加载

<1>.父子类执行的顺序

1.父类的静态变量和静态代码块(书写顺序)

2.子类的静态变量和静态代码块(书写顺序)

3.父类的实例代码块(书写顺序)

4.父类的成员变量和构造方法

5.子类的实例代码块

6.子类的成员变量和构造方法

<2>类加载的时机

如果类没有进行初始化,则需要先进行初始化,虚拟机规范则是严格规定有且只有5种情况必须先对类进行初始化(而加载,验证,准备要在这个之前开始)

1.创建类的实例(new的方式),访问某个类的静态变量,或者对该静态变量赋值,调用类的静态方法

2.反射的方式

3.初始化某个类的子类,则其父类也会被初始化

4.java虚拟机启动时被标记为启动类的类,直接使用java.exe来运行的某个主类(如main类)

5.使用jdk1.7的动态语言支持时

<3>类的生命周期

七个阶段:加载,验证,准备,解析,初始化,使用和卸载。其中验证,准备和解析三个部分被称为连接

解析阶段在某些情况下可以在初始化阶段之后再进行,这是为了支持java语言的运行时绑定(动态绑定)

<4>类加载的过程

接下来我们详细讲解一下Java虚拟机中类加载的全过程,也就是加载、验证、准备、解析和初始化这5个阶段所执行的具体动作。

1.加载

<1>通过一个类的全限定名来获取定义此类的二进制字节流。

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

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

2.验证

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

3.准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。

假设一个类变量的定义为:

public static int value=123;

那变量value在准备阶段过后的初始值为0而不是123,因为这时候尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。

4.解析

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

符号引用:符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。

直接引用:直接引用是和虚拟机实现的内存布局相关的。如果有了直接引用,那引用的目标必定已经在内存中存在。

5.初始化

在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器< clinit>()方法的过程。

了解:

< clint>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问:

public class Test
    static
        i=0; //给变量赋值可以正常编译通过
        System.out.print(i); //这句编译器会提示"非法向前引用"
    
    static int i=1

1.< clinit>()方法(Class类的构造方法)与类的构造函数(或者说实例构造器< init>()方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的< init>()方法执行之前,父类的()方法已经执行完毕。因此在虚拟机中第一个被执行的< clinit>()方法的类肯定是java.lang.Object。

2.< clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成()方法。

3.接口中定义的变量使用时,接口才会初始化:接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生< clinit>()方法。但接口与类不同的是,执行接口的< clinit>()方法不需要先执行父接口的< clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的< clinit>()方法。

4.虚拟机会保证一个类的< clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的< clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行< clinit>()方法完毕。如果在一个类的< clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

<5>类加载器

类加载器可以分为:启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器。他们的关系一般如下:

1.启动类加载器(BootstrapClassLoader)
这个类由C++语言实现,是虚拟机自身的一部分,并不继承ClassLoader,不能操作它。用来加载Java的核心类。

2.扩展类加载器(ExtClassLoader)
这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的。它负责加载<JAVA_HOME>\\lib\\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。

3.应用程序类加载器(AppClassLoader)
它负责在 JVM 启动时加载来自 Java 命令的 -classpath 或者 -cp 选项、java.class.path 系统属性指定的 jar 包和类路径。在应用程序代码里可以通过 ClassLoader 的静态方法 getSystemClassLoader() 来获取应用类加载器。如果没有特别指定,则在没有使用自定义类加载器情况下,用户自定义的类都由此加载器加载。

4.2 自定义加载器
用户自定义了类加载器,则自定义类加载器都以应用类加载器作为父加载器。应用类加载器的父类加载器为扩展类加载器。这些类加载器是有层次关系的,启动加载器又叫根加载器,是扩展加载器的父加载器

<6>类加载机制——双亲委派模型

双亲委派模型的过程:如果一个类加载器收到了类加载的请求,它首先不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层次的类加载器都是如此,因此所有的加载请求信息最终都会传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(即它的搜索范围没有找到所需要的类)时,子加载器才会尝试自己去完成加载

先查找,再进行加载

(1)从下往上找

(2)从上往下加载

双亲委派模型的好处:双亲委派模型对于java程序的稳定运行极为重要

劣势:无法满足灵活的类加载方式。(解决方案:自己重写loadClass破坏双亲委派模型 例如SPI机制)

2.Java内存模型(JMM)

<1>线程私有的内存区域

程序计数器: 一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。

Java虚拟机栈: 每个方法执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈和出栈的过程。

此区域一共会产生以下两种异常:

<1>如果线程请求的栈深度大于虚拟机所允许的深度(-Xss设置栈容量),将会抛出StackOverFlowError异常。

<2>虚拟机在动态扩展时无法申请到足够的内存,会抛出OOM(OutOfMemoryError)异常

本地方法栈: 本地方法栈与虚拟机栈的作用完全一样,他俩的区别无非是本地方法栈为虚拟机使用的Native方法服务,而虚拟机栈为JVM执行的Java方法服务。

<2>线程共享的内存区域

Java堆: 在JVM启动时创建,所有的对象实例以及数组都要在堆上分配。如果在堆中没有足够的内存完成实例分配并且堆也无法再拓展时,将会抛出OOM。

方法区/元数据区:

用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
此区域的内存回收主要是针对常量池的回收以及对类型的卸载。当方法区无法满足内存分配需求时,将抛出OOM异常。

运行时常量池: 编译期及运行期间产生的常量被放在运行时常量池中。
这里所说的常量包括:基本类型、包装类(包装类不管理浮点型,整形只会管理-128到127)和String。
类加载时,会查询字符串常量池,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。

直接内存: 直接内存并不是虚拟机运行时数据区的一部分, 也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。

会发生OOM的区域方法区,堆,Java虚拟机栈,本地方法栈

<3>内存模型

JDK1.7:

JDK1.8:

<4>对常量池的说明

<1>类常量池: Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

<2>运行时常量池:运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。

<3>字符串常量池: 存储字符串对象,或是字符串对象的引用。

3.垃圾回收前置知识

<1>对创建对象,内存,GC的笼统理解

<2>垃圾回收策略(如何判断对象已死)

(1)引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值

就减1;任何时刻计数器为0的对象就是不可能再被使用的。但是它很难解决对象之间相互循环引用的问题

(2)可达性分析算法

通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为

GC Roots引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来

说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。

如图:Object5和Object6即不可达

注意:当一个引用t=null时,代表t不指向任何对象,而不是指向一个null的对象

<3> 四种引用类型

强引用就是我们最常见的普通对象的引用,只要还有强引用指向一个对象,就能表明还有对象还"活着",垃圾回收器就不会回收这种对象

软引用用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之

前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内

存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。

弱引用垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存.

虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对

其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用

(PhantomReference)关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

<4>内存泄漏和内存溢出

内存溢出:内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误

内存泄漏:内存泄漏是指程序中已经动态分配的堆内存由于某种原因程序未释放或无法释放(new了不delete),造成了系统内存的浪费,导致程序运行速度减慢甚至崩溃等严重后果.

内存泄漏的堆积终将会导致内存溢出.但内存溢出不一定时内存泄漏导致的.

<5>需要垃圾回收的内存

(1).方法区(jdk1.7)/元空间(jdk1.8)

JDK1.7的方法区在GC中一般称为永久代(Permanent Generation)

JDK1.8的元空间存在于本地内存,GC也是即对元空间垃圾回收。

永久代或元空间的垃圾收集主要回收两部分内容:废弃常量和无用的类。此区域进行垃圾收集

的“性价比”一般比较低。

1.新生代(Young Generation):又可以分为Eden空间、From Survivor空间、To Survivor空间。

新生代的垃圾回收又称为Young GCYGC)、Minor GC。指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。

朝生夕灭:

    public static void main(String[] args) 
        createObject();
    
    public static void createObject()
        Object[] temp=new Object[10];
        for (int i = 0; i <10 ; i++) 
            temp[i]=new Object();
        
    

例如上述代码:当调用createObject方法时,会创建10个Object对象,而当方法结束,局部变量temp销毁,则创建的十个对象不可达,即方法调用结束之后,对象无用.

即:方法调用,方法返回之后,方法栈帧出栈,局部变量消失,则局部变量引用的对象不可达

2.老年代(Old Generation、Tenured Generation)

老年代垃圾回收又称为Major GC

指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程。

Major GC的速度一般会比Minor GC慢10倍以上。

3.Full GC:在不同的语义条件下,对Full GC的定义也不同,有时候指老年代的垃圾回收,有时候指全堆(新生代+老年代)的垃圾回收,还可能指有用户线程暂停(Stop-The-World)的垃圾回收(如GC日志中)。

4.GC

<1>垃圾回收算法

(1)老年代回收算法

1.标记清除算法(Mark-Sweep):先将无用对象标记出来,再进行删除

缺陷:

  1. 效率问题,标记和清除两个过程的效率都不高。

  2. 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

2.标记整理算法(Mark-Compact) :先对无用对象进行标记,然后将有用对象向一端移动,最后清除最后一个有用对象后面的无用对象即可

(2)新生代回收算法

1.复制算法(Copying算法)

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

缺点:内存利用率只有50%。

2.分代收集算法(不固定,不同的内存划分,使用不同的收集算法)

新生代中98%的对象都是"朝生夕死"的,所以并不需要按照复制算法所要求1 : 1的比例来划分内存空间,而是将内存(新生代内存)分为一块较大的Eden(伊甸园)空间和两块较小的Survivor(幸存者)空间,每次使用Eden和其中一块Survivor(两个Survivor区域一个称为From区,另一个称为To区域)。HotSpot默认Eden与Survivor的大小比例是8 : 1,也就是说Eden :Survivor From : Survivor To = 8 : 1 : 1。所以每次新生代可用内存空间为整个新生代容量的90%,只有10%的内存会被”浪费“。

<2>垃圾回收的过程

1.Eden空间不足,触发Minor GC:**用户线程创建的对象优先分配在Eden区,当Eden区空间不够时,会触发Minor GC:将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。(分代收集算法)

2.垃圾回收结束后,用户线程又开始新创建对象并分配在Eden区,当Eden区空间不足时,重复上次

的步骤进行Minor GC

3.年老对象晋升到老年代

4.Survivor空间不足,存活对象通过分配担保机制进入老年代

5.老年代空间不足,触发Major GC

<3>内存分配与回收策略

1.对象优先在Eden分配:大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发生一次Minor GC。

2.大对象直接进入老年代:

所谓的大对象是指,需要大量连续空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。

大对象对虚拟机的内存分配是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前触发GC以获取足够的连续空间来放置大对象。

3.长期存在的对象进入老年代:,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且把对象年龄设为1。对象在Survivor空间中每"熬过"一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将晋 升到老年代中。

4.动态对象年轻判定 :在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄

5.空间分配担保策略:

Survivor区只占新生代10%空间,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(HandlePromotion)

过程

<4>垃圾收集的影响

STW:

执行垃圾回收其中某一步骤时,需要暂停用户线程,也就是Stop-The-World(STW).程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。

中断方式:

1.抢先式中断:不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上(现在基本不用)

2.主动式中断:是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起

垃圾收集器中的两个概念:

1.并行(Parallel) : 指多条垃圾收集线程并行工作,用户线程仍处于等待状态。

2.并发(Concurrent) : 指用户线程与垃圾收集线程同时执行(不一定并行,可能会交替执行),用户程

序继续运 行,而垃圾收集程序在另外一个CPU上。

吞吐量和用户体验(停顿时间):

1.吞吐量:CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

用户体验优先:用户线程单次停顿时间短,即使总的停顿时间长一点也可以接受。

吞吐量优先:用户线程总的停顿时间短,即使单次停顿时间长一点也可以接受。

5.垃圾收集器

<1>概览

重点:

1.用户体验优先的收集器组合

2.吞吐量优先的收集器组合

<2>垃圾收集器

1.Serial收集器(新生代收集器,串行GC)

特性:单线程,复制算法,Stop The World(STW) 单核CPU适用,现在基本不用

2.ParNew收集器(新生代收集器,并行GC)

特性:单线程,复制算法,Stop The World(STW)

搭配CMS收集器,在用户体验优先的程序中使用

3.Parallel Scavenge收集器(新生代收集器,并行GC)

特性:多线程,复制算法,可控制的吞吐量。

吞吐量优先收集器,适用吞吐量需求高的任务型程序

4.Serial Old收集器(老年代收集器,串行GC)

单线程,标记-整理算法

5.Parallel Old收集器(老年代收集器,并行GC)

多线程,标记整理算法

6.CMS收集器(老年代收集器,并发GC)

并发收集,低停顿。标记-清除算法

整个过程分为4个步骤:

  1. 初始标记(CMS initial mark) 初始标记仅仅只是标记一下GC Roots能直接关联到的对象,

速度很快, 需要“Stop The World”。

  1. 并发标记(CMS concurrent mark) 并发标记阶段就是进行GC Roots Tracing的过程。(找引用链)

  2. 重新标记(CMS remark) 重新标记阶段是为了修正并发标记期间因用户程序继续运作而导

致标记产生 变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍

长一些,但远比并发标 记的时间短,仍然需要“Stop The World”。

  1. 并发清除(CMS concurrent sweep) 并发清除阶段会清除对象。

缺陷:

  1. CMS会抢占CPU资源。并发阶段虽然不会导致用户线程暂停,但却需要CPU分出精力去执行多条

垃圾收集线程,从而使得用户线程的执行速度下降。

  1. CMS的“标记-清除”算法,会导致大量空间碎片的产生
  2. CMS无法处理浮动垃圾,可能会出现“Concurrent Mode Failure”而导致另一次Full GC

浮动垃圾问题:并发清理的过程中,由于用户线程还在执行,因此就会继续产生对象和垃圾,这些新的垃圾没有被标记,CMS只能在下一次收集中处理它们。这也导致了CMS不能在老年代几乎完全被填满了再去进行收集,必须预留一部分空间提供给并发收集时程序运作使用

7.G1垃圾收集器(用户体验优先)

特性:

1.用在heap memory很大的情况下,把heap划分为很多很多的region块,然后并行的对其进行垃圾回收。

2.G1垃圾回收器回收region的时候基本不会STW,而是基于 most garbage优先回收(整体来看是基于"标记-整理"算法,从局部即两个region之间基于"复制"算法)的策略来对region进行垃圾回收的。

3.用户体验优先

4.无论如何,G1收集器采用的算法都意味着 一个region有可能属于Eden,Survivo或者Tenured内存区域。

G1的年轻代垃圾收集: H区域表示大对象

G1老年代垃圾收集:

对于老年代上的垃圾收集,G1垃圾收集器也分为4个阶段,基本跟CMS垃圾收集器一样,但略有不同:

  1. 初始标记(Initial Mark)阶段 - 同CMS垃圾收集器的Initial Mark阶段一样,G1也需要暂停应用程序的执行,它会标记从根对象出发,在根对象的第一层孩子节点中标记所有可达的对象。但是G1的垃圾收集器的Initial Mark阶段是跟minor gc一同发生**的。也就是说,在G1中,你不用像在CMS那样,单独暂停应用程序的执行来运行Initial Mark阶段,而是在G1触发minor gc的时候一并将年老代上的Initial Mark给做了。

  2. 并发标记(Concurrent Mark)阶段 - 在这个阶段G1做的事情跟CMS一样。但G1同时还多做了一件事情,就是如果在Concurrent Mark阶段中,发现哪些Tenured region中对象的存活率很小或者基本没有对象存活,那G1就会在这个阶段将其回收掉,而不用等到后面的clean up阶段。这也是Garbage First名字的由来。同时,在该阶段,G1会计算每个 region的对象存活率,方便后面的clean up阶段使用 。

  3. 最终标记(CMS中的Remark阶段) - 在这个阶段G1做的事情跟CMS一样, 但是采用的算法不同,G1采用一种叫做SATB(snapshot-at-the-begining)的算法能够在Remark阶段更快的标记可达对象。

  4. 筛选回收(Clean up/Copy)阶段 - 在G1中,没有CMS中对应的Sweep阶段。相反 它有一个Clean up/Copy阶段,在这个阶段中,G1会挑选出那些对象存活率低的region进行回收,这个阶段也是和minor gc一同发生的,如下图所示:

6.垃圾回收的时机

<1>System.gc()

显示的调用System.gc():此方法的调用是建议JVM进行 FGC(Full GC),虽然只是建议而非一定,但

很多情况下它会触发 FGC,从而增加FGC的频率。一般不使用此方法,让虚拟机自己去管理它的内存。

<2>JVM垃圾回收机制决定

创建对象时需要分配内存空间,如果空间不足,触发GC

java.lang.Object中有一个fifinalize() 方法,当JVM 确定不再有指向该对象的引用时,垃圾收集器在对象上调用该方法。fifinalize() 方法有点类似对象生命周期的临终方法,JVM 调用该方法,表示该对象即将“死亡”,之后就可以回收该对象了。注意回收还是在JVM 中处理的,所以手动调用某个对象的fifinalize() 方法,不会造成对象的“死亡”。

<3>GC触发的时机

Minor GC触发条件:创建对象在Eden区,且Eden区空间不足

Majar GC触发的条件:对象需要存放在老年代,而老年代空间不足,都会触发

1.新生代年老对象晋升

2.大对象直接进入

3.Minor GC的分配担保机制

4.CMS无法处理浮动垃圾,可能会出现“Concurrent Mode Failure”而导致另一次Full GC

7.常用的JVM监控工具

以上是关于JVM基础详解的主要内容,如果未能解决你的问题,请参考以下文章

jvm调优

JVM参数设置分析

[转]JVM系列三:JVM参数设置分析

JVM系列三:JVM参数设置分析

耗时几个月,终于找到了JVM停顿十几秒的原因

JVM 的垃圾回收器详解