java虚拟机

JDK、JRE、JVM三者的关系

  • JDK(Java Development Kit)是针对Java开发的产品、是整个Java的核心,包括Java运行环境JRE、Java工具包和Java基础类库。
  • JRE(Java Runtime Environment)是运行Java程序所必须的环境的集合,包含JVM标准实现及Java核心类库。
  • JVM(Java Virtual Machine)是整个Java跨平台的最核心的部分,可以运行以Java语言写做的软件程序。全部的Java程序都会首先被编译为.class文件,这种类文件能够在虚拟机上运行,class文件并不直接与机器的操做系统相对应,而是通过虚拟机间接与操做系统交互,由虚拟机将程序解释给本地系统执行。

Java运行时区域

clipboard.png

程序计数器

内存中较小的内存空间,经过计数器的值能够选取下一条执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都须要依赖这个计数器来完成。html

线程私有,生命周期跟线程相同。java

若是正在执行一个Native方法,那么这个计数器值将为空。程序员

虚拟机栈

线程私有,生命周期跟线程相同。算法

每一个方法在执行同时都会建立一个栈帧,用于存储局部变量表、操做数栈、动态连接、方法出口等信息。apache

在Java虚拟机规范中,对这个区域规定了两种异常状况:若是线程请求的栈深度大于虚拟机所容许的深度,将抛出StackOverflowError异常;
若是虚拟机栈能够动态扩展,若是扩展时没法申请到足够的内存,就会抛出OutOfMemoryError异常。ubuntu

本地方法栈

跟虚拟机栈所发挥的做用类似,区别在于虚拟机栈为虚拟机执行Java(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。数组

Java堆

用于存放对象实例,是Java虚拟机所管理的内存中最大的一块,同时也是全部线程共享的一块内存区域。浏览器

由于Java堆是垃圾收集器管理的主要区域,所以不少时候也被称为“GC"堆。因为如今收集器基本都采用分代收集算法,因此Java堆还能够细分为缓存

  • 新生代
  • 老年代
  • 永久代(永久代是Hotspot虚拟机特有的概念,是方法区的一种实现,别的JVM都没有这个东西。在Java 8中,永久代被完全移除,取而代之的是另外一块与堆不相连的本地内存——元空间。)

当一个对象被建立时,它首先进入新生代,以后有可能被转移到老年代中。安全

新生代存放着大量的生命很短的对象,所以新生代在三个区域中垃圾回收的频率最高。为了更高效地进行垃圾回收,把新生代继续划分红如下三个空间:

  • Eden
  • From Survivor
  • To Survivor

方法区

与Java堆同样,各个线程共享的内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即便编译器编译后的代码等数据。

运行时常量池

方法区的一部分,用于存放编译器生成的各类字面量和符号引用。

运行时常量池相对于class文件常量池的另一个重要特征是具有动态性,Java语言并不要求常量必定只有编译期才能产生,也就是并不是预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的即是String类的intern()方法。

直接内存

在JDK1.4中新加入了NIO类,引入了一种基于通道与缓冲区的I/O方法,它可使用Native函数库直接分配堆外内存,而后经过一个存储在Java堆中的DirectByteBuffer对象做为这块内存的引用进行操做。
堆外内存之 DirectByteBuffer 详解

HotSpot虚拟机对象

对象的建立

在语言层上,建立对象一般仅仅是一个new关键字而已,而当虚拟机遇到一条new执行时,将由一下步骤:

  • 检查类是否加载、解析、初始化过,没有则先执行相应的类加载过程。
  • 在堆中分配内存

    • 划分可用空间:

      • 指针碰撞:堆内存规整
      • 空闲列表:堆内存不规整
    • 并发问题

      • 同步:采用CAS配上失败重试的方式保证更新操做的原子性
      • 把内存分配动做按照线程划分在不一样的空间之中进行
  • 将分配到的内存空间都初始化零值
  • 设置对象的类实例、元数据、哈希码、GC分代年龄等信息。
  • 执行<init>方法

对象的内存布局

对象在内存中储存的布局能够分为3块区域:

  • 对象头

    • 对象运行时数据、哈希码、GC分代年龄、锁状态标记、线程持有的锁、偏向线程ID等
    • 类型执行:即对象执向它的类元数据的指针,指明对象数据哪一个类的实例。
  • 实例数据

    • 对象真正存储的有效信息
  • 对齐填充

    • 占位符做用

对象的访问定位

  • 句柄定位
  • 直接指针

内存溢出

内存溢出out of memory,是指程序在申请空间时,没有足够的内存空间供其使用,出现了Out of memory error。

堆内存溢出

当new一个对象或者数组时,若是超出了Jvm的head内存最大限制就会爆出异常。

伪代码:

while(ture){
    new Object();
}

栈内存溢出

在Java虚拟机规范中,对这个栈规定了两种异常状况,若是线程请求的栈深度大于虚拟机所容许的深度,将抛出StackOutFlowError异常,若是虚拟机能够动态扩展(当前大部分Java虚拟机均可动态扩展,只不过Java虚拟机规范中也容许固定长度的虚拟机栈),当扩展时没法申请获得足够的内存时将会抛出OutOfMemory。

StackOutFlowError

线程中的stack是线程私有的,默认大小一般为1M,能够经过-Xss来设置,-Xss越大,则线程获取的内存越大。
常见问题在线程内过分的调用函数,函数调用会消耗栈空间。

伪代码:

public void SOFETest(){
    SOFETest();
}

OutOfMemoryError

Java的栈空间被全部线程分配成一块一块的,每一个线程只占一块。而Jvm的栈空间的最小分配单位有-Xss来决定。-Xss有两个语义,即定义每一个线程的栈大小,也定义了虚拟机的最小栈内存的分配单位。

若是申请的线程没有得到栈空间能够分配了就会抛出OutOfMemoryError。表示栈空间不足,溢出异常。

代码:该代码可能致使JVM没法申请获得太多的栈内存而致使操做系统由于栈空间不足假死。

public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        for(int i =0;i<1020000000;i++){
            new Thread(new Runnable(){
                @Override
                public void run() {
                    int a = 1000;
                    try {
                        countDownLatch.await();
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
                
            }).start();
        }
        countDownLatch.countDown();
    }
}

内存泄漏

内存泄漏memory leak,指程序在申请内存以后,没法释放已申请的内存空间,一次内存泄漏危害能够忽略,屡次memory leak将致使oom。

内存泄漏是指你向系统申请分配内存进行使用(new),但是使用完了之后却不归还(delete),结果你申请到的那块内存你本身也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给须要的程序。

jvm性能调优监控工具使用详解

该部份内容转自:JVM性能调优监控工具jps、jstack、jmap、jhat、jstat、hprof使用详解

jps(Java Virture Machine Process Status Tool)

jps主要用来输出JVM中运行的进程状态信息。语法格式以下:

jps [options] [hostid]

若是不指定hostid就默认为当前主机或服务器。

命令行参数选项说明以下:

-q 不输出类名、Jar名和传入main方法的参数
-m 输出传入main方法的参数
-l 输出main类或Jar的全限名
-v 输出传入JVM的参数

好比下面:

root@ubuntu:/# jps -m -l
2458 org.artifactory.standalone.main.Main /usr/local/artifactory-2.2.5/etc/jetty.xml
29920 com.sun.tools.hat.Main -port 9998 /tmp/dump.dat
3149 org.apache.catalina.startup.Bootstrap start
30972 sun.tools.jps.Jps -m -l
8247 org.apache.catalina.startup.Bootstrap start
25687 com.sun.tools.hat.Main -port 9999 dump.dat
21711 mrf-center.jar

jstack

jstack主要用来查看某个Java进程内的线程堆栈信息。语法格式以下:

jstack [option] pid
jstack [option] executable core
jstack [option] [server-id@]remote-hostname-or-ip

命令行参数选项说明以下:

-l long listings,会打印出额外的锁信息,在发生死锁时能够用jstack -l pid来观察锁持有状况
-m mixed mode,不只会输出Java堆栈信息,还会输出C/C++堆栈信息(好比Native方法)

jstack能够定位到线程堆栈,根据堆栈信息咱们能够定位到具体代码,因此它在JVM性能调优中使用得很是多。下面咱们来一个实例找出某个Java进程中最耗费CPU的Java线程并定位堆栈信息,用到的命令有ps、top、printf、jstack、grep。

第一步先找出Java进程ID,我部署在服务器上的Java应用名称为mrf-center:

root@ubuntu:/# ps -ef | grep mrf-center | grep -v grep
root     21711     1  1 14:47 pts/3    00:02:10 java -jar mrf-center.jar

获得进程ID为21711,第二步找出该进程内最耗费CPU的线程,可使用ps -Lfp pid或者ps -mp pid -o THREAD, tid, time或者top -Hp pid,我这里用第三个,输出以下:

clipboard.png

TIME列就是各个Java线程耗费的CPU时间,CPU时间最长的是线程ID为21742的线程,用

printf "%x\n" 21742

获得21742的十六进制值为54ee,下面会用到。

OK,下一步终于轮到jstack上场了,它用来输出进程21711的堆栈信息,而后根据线程ID的十六进制值grep,以下:

root@ubuntu:/# jstack 21711 | grep 54ee
"PollIntervalRetrySchedulerThread" prio=10 tid=0x00007f950043e000 nid=0x54ee in Object.wait() [0x00007f94c6eda000]

能够看到CPU消耗在PollIntervalRetrySchedulerThread这个类的Object.wait(),我找了下个人代码,定位到下面的代码:

// Idle wait
getLog().info("Thread [" + getName() + "] is idle waiting...");
schedulerThreadState = PollTaskSchedulerThreadState.IdleWaiting;
long now = System.currentTimeMillis();
long waitTime = now + getIdleWaitTime();
long timeUntilContinue = waitTime - now;
synchronized(sigLock) {
    try {
        if(!halted.get()) {
            sigLock.wait(timeUntilContinue);
        }
    } 
    catch (InterruptedException ignore) {
    }
}

它是轮询任务的空闲等待代码,上面的sigLock.wait(timeUntilContinue)就对应了前面的Object.wait()。

jmap(Memory Map)和jhat(Java Heap Analysis Tool)

jmap用来查看堆内存使用情况,通常结合jhat使用。

jmap语法格式以下:

jmap [option] pid
jmap [option] executable core
jmap [option] [server-id@]remote-hostname-or-ip

若是运行在64位JVM上,可能须要指定-J-d64命令选项参数。

jmap -permstat pid

打印进程的类加载器和类加载器加载的持久代对象信息,输出:类加载器名称、对象是否存活(不可靠)、对象地址、父类加载器、已加载的类大小等信息,以下图:

clipboard.png

使用jmap -heap pid查看进程堆内存使用状况,包括使用的GC算法、堆配置参数和各代中堆内存使用状况。好比下面的例子:

root@ubuntu:/# jmap -heap 21711
Attaching to process ID 21711, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 20.10-b01

using thread-local object allocation.
Parallel GC with 4 thread(s)

Heap Configuration:
   MinHeapFreeRatio = 40
   MaxHeapFreeRatio = 70
   MaxHeapSize      = 2067791872 (1972.0MB)
   NewSize          = 1310720 (1.25MB)
   MaxNewSize       = 17592186044415 MB
   OldSize          = 5439488 (5.1875MB)
   NewRatio         = 2
   SurvivorRatio    = 8
   PermSize         = 21757952 (20.75MB)
   MaxPermSize      = 85983232 (82.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 6422528 (6.125MB)
   used     = 5445552 (5.1932830810546875MB)
   free     = 976976 (0.9317169189453125MB)
   84.78829520089286% used
From Space:
   capacity = 131072 (0.125MB)
   used     = 98304 (0.09375MB)
   free     = 32768 (0.03125MB)
   75.0% used
To Space:
   capacity = 131072 (0.125MB)
   used     = 0 (0.0MB)
   free     = 131072 (0.125MB)
   0.0% used
PS Old Generation
   capacity = 35258368 (33.625MB)
   used     = 4119544 (3.9287033081054688MB)
   free     = 31138824 (29.69629669189453MB)
   11.683876009235595% used
PS Perm Generation
   capacity = 52428800 (50.0MB)
   used     = 26075168 (24.867218017578125MB)
   free     = 26353632 (25.132781982421875MB)
   49.73443603515625% used
   ....

使用jmap -histo[:live] pid查看堆内存中的对象数目、大小统计直方图,若是带上live则只统计活对象,以下:

root@ubuntu:/# jmap -histo:live 21711 | more

 num     #instances         #bytes  class name
----------------------------------------------
   1:         38445        5597736  <constMethodKlass>
   2:         38445        5237288  <methodKlass>
   3:          3500        3749504  <constantPoolKlass>
   4:         60858        3242600  <symbolKlass>
   5:          3500        2715264  <instanceKlassKlass>
   6:          2796        2131424  <constantPoolCacheKlass>
   7:          5543        1317400  [I
   8:         13714        1010768  [C
   9:          4752        1003344  [B
  10:          1225         639656  <methodDataKlass>
  11:         14194         454208  java.lang.String
  12:          3809         396136  java.lang.Class
  13:          4979         311952  [S
  14:          5598         287064  [[I
  15:          3028         266464  java.lang.reflect.Method
  16:           280         163520  <objArrayKlassKlass>
  17:          4355         139360  java.util.HashMap$Entry
  18:          1869         138568  [Ljava.util.HashMap$Entry;
  19:          2443          97720  java.util.LinkedHashMap$Entry
  20:          2072          82880  java.lang.ref.SoftReference
  21:          1807          71528  [Ljava.lang.Object;
  22:          2206          70592  java.lang.ref.WeakReference
  23:           934          52304  java.util.LinkedHashMap
  24:           871          48776  java.beans.MethodDescriptor
  25:          1442          46144  java.util.concurrent.ConcurrentHashMap$HashEntry
  26:           804          38592  java.util.HashMap
  27:           948          37920  java.util.concurrent.ConcurrentHashMap$Segment
  28:          1621          35696  [Ljava.lang.Class;
  29:          1313          34880  [Ljava.lang.String;
  30:          1396          33504  java.util.LinkedList$Entry
  31:           462          33264  java.lang.reflect.Field
  32:          1024          32768  java.util.Hashtable$Entry
  33:           948          31440  [Ljava.util.concurrent.ConcurrentHashMap$HashEntry;

class name是对象类型,说明以下:

B  byte
C  char
D  double
F  float
I  int
J  long
Z  boolean
[  数组,如[I表示int[]
[L+类名 其余对象

还有一个很经常使用的状况是:用jmap把进程内存使用状况dump到文件中,再用jhat分析查看。jmap进行dump命令格式以下:

jmap -dump:format=b,file=dumpFileName pid

我同样地对上面进程ID为21711进行Dump:

root@ubuntu:/# jmap -dump:format=b,file=/tmp/dump.dat 21711     
Dumping heap to /tmp/dump.dat ...
Heap dump file created

dump出来的文件能够用MAT、VisualVM等工具查看,这里用jhat查看:

root@ubuntu:/# jhat -port 9998 /tmp/dump.dat
Reading from /tmp/dump.dat...
Dump file created Tue Jan 28 17:46:14 CST 2014
Snapshot read, resolving...
Resolving 132207 objects...
Chasing references, expect 26 dots..........................
Eliminating duplicate references..........................
Snapshot resolved.
Started HTTP server on port 9998
Server is ready.

注意若是Dump文件太大,可能须要加上-J-Xmx512m这种参数指定最大堆内存,即jhat -J-Xmx512m -port 9998 /tmp/dump.dat。而后就能够在浏览器中输入主机地址:9998查看了:

clipboard.png

上面红线框出来的部分你们能够本身去摸索下,最后一项支持OQL(对象查询语言)。

jstat(JVM统计监测工具)

语法格式以下:

jstat [ generalOption | outputOptions vmid [interval[s|ms] [count]] ]

vmid是Java虚拟机ID,在Linux/Unix系统上通常就是进程ID。interval是采样时间间隔。count是采样数目。好比下面输出的是GC信息,采样时间间隔为250ms,采样数为4:

root@ubuntu:/# jstat -gc 21711 250 4
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       PC     PU    YGC     YGCT    FGC    FGCT     GCT   
192.0  192.0   64.0   0.0    6144.0   1854.9   32000.0     4111.6   55296.0 25472.7    702    0.431   3      0.218    0.649
192.0  192.0   64.0   0.0    6144.0   1972.2   32000.0     4111.6   55296.0 25472.7    702    0.431   3      0.218    0.649
192.0  192.0   64.0   0.0    6144.0   1972.2   32000.0     4111.6   55296.0 25472.7    702    0.431   3      0.218    0.649
192.0  192.0   64.0   0.0    6144.0   2109.7   32000.0     4111.6   55296.0 25472.7    702    0.431   3      0.218    0.649

要明白上面各列的意义,先看JVM堆内存布局:

clipboard.png

能够看出:

堆内存 = 年轻代 + 年老代 + 永久代
年轻代 = Eden区 + 两个Survivor区(From和To)

如今来解释各列含义:

S0C、S1C、S0U、S1U:Survivor 0/1区容量(Capacity)和使用量(Used)
EC、EU:Eden区容量和使用量
OC、OU:年老代容量和使用量
PC、PU:永久代容量和使用量
YGC、YGT:年轻代GC次数和GC耗时
FGC、FGCT:Full GC次数和Full GC耗时
GCT:GC总耗时

hprof(Heap/CPU Profiling Tool)

hprof可以展示CPU使用率,统计堆内存使用状况。

语法格式以下:

java -agentlib:hprof[=options] ToBeProfiledClass
java -Xrunprof[:options] ToBeProfiledClass
javac -J-agentlib:hprof[=options] ToBeProfiledClass

完整的命令选项以下:

Option Name and Value  Description                    Default
---------------------  -----------                    -------
heap=dump|sites|all    heap profiling                 all
cpu=samples|times|old  CPU usage                      off
monitor=y|n            monitor contention             n
format=a|b             text(txt) or binary output     a
file=<file>            write data to file             java.hprof[.txt]
net=<host>:<port>      send data over a socket        off
depth=<size>           stack trace depth              4
interval=<ms>          sample interval in ms          10
cutoff=<value>         output cutoff point            0.0001
lineno=y|n             line number in traces?         y
thread=y|n             thread in traces?              n
doe=y|n                dump on exit?                  y
msa=y|n                Solaris micro state accounting n
force=y|n              force output to <file>         y
verbose=y|n            print messages about dumps     y

来几个官方指南上的实例。

CPU Usage Sampling Profiling(cpu=samples)的例子:

java -agentlib:hprof=cpu=samples,interval=20,depth=3 Hello

上面每隔20毫秒采样CPU消耗信息,堆栈深度为3,生成的profile文件名称是java.hprof.txt,在当前目录。

CPU Usage Times Profiling(cpu=times)的例子,它相对于CPU Usage Sampling Profile可以得到更加细粒度的CPU消耗信息,可以细到每一个方法调用的开始和结束,它的实现使用了字节码注入技术(BCI):

javac -J-agentlib:hprof=cpu=times Hello.java

Heap Allocation Profiling(heap=sites)的例子:

javac -J-agentlib:hprof=heap=sites Hello.java

Heap Dump(heap=dump)的例子,它比上面的Heap Allocation Profiling能生成更详细的Heap Dump信息:

javac -J-agentlib:hprof=heap=dump Hello.java

虽然在JVM启动参数中加入-Xrunprof:heap=sites参数能够生成CPU/Heap Profile文件,但对JVM性能影响很是大,不建议在线上服务器环境使用。

垃圾收集器

程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束以后也会消失,所以不须要对这三个区域进行垃圾回收。垃圾回收主要是针对 Java 堆和方法区进行。

判断对象是否死亡

引用计数算法

给对象添加一个引用计数器,每当有一个地方引用它,计数器值就加1;引用时效时,计算器值就减1;当计数器值为0的对象就是不可能再被使用的。

当两个对象相互引用时,此时引用计数器的值永远不为0,致使没法对它们进行垃圾回收。

public class ReferenceCountingGC {
        public Object instance = null;
    
        public static void testGC() {
            ReferenceCountingGC objA = new ReferenceCountingGC();
            ReferenceCountingGC objB = new ReferenceCountingGC();
            objA .instance = objB ;
            objB .instance = objA ;
            objA = null;
            objB = null;
            
            System.gc();
        }
    }

可达性分析算法

以GC Roots为起始点,从这些节点开始向下搜索,可以搜索到的对象都是存活的,不可达的对象则为不可用。

clipboard.png

在Java语言中,可做为GC Roots的对象包括下面几种:

  • 虚拟机栈中引用的对象
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中Native方法引用的对象

引用类型

不管是引用计数算法仍是可达性分析算法判断对象是否存活都与引用有关。在JDK1.2以后,Java对引用的概念进行了扩充,划分为强度不一样的四个的引用类型。

强引用

经过new来建立对象的引用类型,被强引用的对象永远不会被垃圾收集器回收。

Object obj = new Object();

软引用

经过SortReference类来实现,只有在内存不足的时候才会被回收。

Object obj = new Object();
    SoftReference<Object> sr = new SoftReference<Object>(obj);
    obj = null;

弱引用

经过WeakReference类来实现,只能存活到下一次垃圾收集发生以前。

Object obj = new Object();
    WeakReference<Object> wr = new WeakReference<Object>(obj);
    obj = null;

WeakHashMap 的 Entry 继承自 WeakReference,主要用来实现缓存。

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V>
Tomcat 中的 ConcurrentCache 就使用了 WeakHashMap 来实现缓存功能。ConcurrentCache 采起的是分代缓存,常用的对象放入 eden 中,而不经常使用的对象放入 longterm。eden 使用 ConcurrentHashMap 实现,longterm 使用 WeakHashMap,保证了不常使用的对象容易被回收。

public final class ConcurrentCache<K, V> {

    private final int size;

    private final Map<K, V> eden;

    private final Map<K, V> longterm;

    public ConcurrentCache(int size) {
        this.size = size;
        this.eden = new ConcurrentHashMap<>(size);
        this.longterm = new WeakHashMap<>(size);
    }

    public V get(K k) {
        V v = this.eden.get(k);
        if (v == null) {
            v = this.longterm.get(k);
            if (v != null)
                this.eden.put(k, v);
        }
        return v;
    }

    public void put(K k, V v) {
        if (this.eden.size() >= size) {
            this.longterm.putAll(this.eden);
            this.eden.clear();
        }
        this.eden.put(k, v);
    }
}

虚引用

也称为幽灵引用或者幻影引用,是最弱的一种引用关系。

经过PhantomReference类来实现,为一个对象设置虚引用关联的惟一目的就是能在这个对象被收集器回收时收到一个系统通知。

Object obj = new Object();
    PhantomReference<Object> wr = new PhantomReference<Object>(obj, null);
    obj = null;

垃圾收集算法

标记清除

算法分为“标记”跟“清除”两个阶段:首先标记出全部须要回收的对象,在标记完成以后统一回收全部被标记的对象。

不足:

  • 效率问题,标记跟清除两个过程的效率都不高
  • 空间问题,标记清除以后会产生大量不连续的内存碎片。

clipboard.png

复制算法

将内存分为大小相等的两块,每次只使用其中的一块,当这块内存用完了,就将还存活的对象负责到另外一块上面,而后再把一是要难过过得内存空间一次清理掉。

不足:

  • 代价过高,只使用一半内存。

clipboard.png

标记整理算法

首先标记出全部须要回收的对象,而后将全部存活的对象都向一端移动,最后清理掉端边界之外的内存。

clipboard.png

分代收集算法

根据对象的存活周期将内存划分为几块。通常将Java堆分为新生代跟老年代,这样就能够根据各个年代的特色采用最适当的收集算法。

  • 新生代:复制算法
  • 老年代:标记整理或标记清除算法。

垃圾收集器

若是说手机算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

clipboard.png

上图展现了7种不一样分代的收集器,若是两个收集器之间存在连线,就说明它们能够搭配使用。

知道目前为止尚未最好的收集器出现,更加没有万能的收集器,因此咱们只能选择对具体应用最合适的收集器。

Serial收集器

clipboard.png

最基本、最悠久的收集器,单线程收集器,复制算法

在它进行垃圾收集时,必须暂停其余全部的工做线程,直到它收集结束。

相比较与其余收集器,它具备:简单而高效的特色,对于限定CPU环境来讲,Serial收集器没有线程交互的开销。

依然是虚拟机运行在Client模式下的默认新生代收集器。

ParNew收集器

clipboard.png

Serial的多线程版本、并行,复制算法。

是许多运行在Server模式下的虚拟机中首选的新生代收集器,由于目前除了Serial收集器外,只有它能与CMS收集器配合使用。

默认开启的收集线程数与CPU的数量相同,在CPU很是多的环境下,可使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

Parallel Scavenge收集器

新生代、并行的多线程收集器,复制算法。

Parallel Scavenge收集器的目标是达到一个可控制的吞吐量:CPU用户运行用户代码的时间与CPU的执行时间,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。

停顿时间越短越适合须要与用户交互的程序,良好的响应速度能提高用户体验,而高吞吐量能够高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不须要太多交互的任务。

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。

Serial Old收集器

clipboard.png

Serial的老年代版本,单线程,标记-整理算法。

这个收集器的主要意义在于给Client模式下的虚拟机使用,若是在Server默认下,它还有两大用途:

  • 在JDK1.5以及以前版本中与Parallel Scavenge收集器搭配使用。
  • 做为CMS收集器的后备方案。

Parallel Old收集器

clipboard.png

parallel Scavenge的老年代版本,多线程,标记-整理算法,JDK1.6以后提供。

在注重吐吞量以及CPU资源敏感的场合,均可以优先考虑Parallel Scavenge加Parallel Old收集器。

CMS收集器

clipboard.png

CMS(Concurrent Mark Sweep),从名字来就能够看出,基于标记-清除算法。

并发收集、低停顿。

运算过程分为4个步骤:

  • 初始标记:标记GC Roots能直接关联获得的对象,速度很快
  • 并发标记:进行GC Roots Tracing过程,时间较长,不停顿
  • 从新标记:修正并发标记期间因用户程序继续运做而致使标记产生变更的那一部分对象的标记记录,停顿时间通常比初始标记稍长一些,但远比并发标记短。
  • 并发清理:耗时较长,不停顿。

整个过程耗时最长的并发标记和并发清除过程收集器线程均可以与用户安城一块儿工做。

CMS还远达不到完美的程度,还有如下3个缺点:

  • 对CPU资源敏感,并发阶段占用CPU资源而致使用户线程变慢;低停顿时间是以牺牲吞吐量为代价的,致使CPU利用率不高。
  • 没法处理浮动垃圾,因为CMS并发清理阶段用户线程还在运行着,伴随着程序运行天然还会有新的垃圾产生,这部分垃圾CMS没法在档次收集中处理掉它们,只有留待下一次GC时再清理。也是因为垃圾收集阶段用户还须要运行,故还须要预留足够的内存空间给用户线程使用,所以CMS收集器不能像其余收集器那样等到老年代几乎彻底被填满了在进行收集,须要预留一部分空间提供并发收集时的程序运做使用。
  • 标记-清除算法会致使大量的空间碎片,空间碎片过多时,将会给大对象分配带来很大麻烦,每每会出现老年代还有很大空间剩余却没法找到足够大的连续空间来分配当前对象,不得不提早触发一次Full GC。

G1收集器

一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是将来能够替代掉JDK1.5中发布的CMS收集器。

与其余收集器相比,G1具备如下特色:

  • 并发与并行:使用多个CPU来缩短Stop-The-World停顿的时间。
  • 分代收集:与其余收集器同样,分代概念在G1中依然得以保存,虽然G1能够不须要其余收集器配合就能独立管理GC堆,但它可以采用不一样的方式去处理新建立的对象和一存活了一段时间、熬过屡次GC的旧对象。
  • 空间整合:从总体来看是基于标记-整理算法实现的收集器,从局部(两个Region之间)来看是基于复制算法来实现的,意味着G1运做期间不会产生内存空间碎片。
  • 可预测停顿:可以让使用者明确指定一个长度为M毫秒的时间片断内,消耗在垃圾收集上的时间不得超过N毫秒。

G1将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,可是新生代和老年代再也不是物理隔离级别,它们都是一部分Region的集合。

clipboard.png

G1收集器之因此可以创建可预测的停顿时间模型,是由于它能够有计划低避免在整个Java堆中进行全区域的垃圾回收。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所得到的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据容许的收集时间,优先回收价值最大的Region。

虚拟机使用Remembered Set来避免全栈扫描,G1中每一个Region都有一个与之对应的Remembered Set,用来记录该Region对象的引用对象所在的Region。

clipboard.png

若是不计算Remembered Set的操做,G1收集器的运做大体可分为:

  • 初始标记
  • 并发标记
  • 最终标记:为了修正在并发标记期间因用户程序继续运做而致使标记产生变化的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段须要把Remembered Set Logs的数据合并到Remembered Set中,这阶段须要停顿线程,可是能够并行执行。
  • 筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所指望的GC停顿时间来制定回收计划。此阶段其实能够作到与用户程序一块儿并发执行,可是由于只回收一部分Region,时间是用户可控制的,并且停顿用户线程将大幅度提升收集效率。

回收策略

  • 新生代GC(Minor GC):发生在新生代的垃圾收集动做,由于Java对象大多都具有朝生息灭的特性,因此Minor GC很是频繁,通常回收速度也比较快。
  • 老年代GC(major GC/full GC):发生在老年代的GC,出现了Major GC常常会伴随一次的Minor GC(但非绝对,在paraller Scavenge收集器的手机策略里就有直接进行Major GC的策略选择过程)。Major GC的速度通常会比Minor GC慢10倍以上。
  • Full GC的触发条件

    • 调用System.gc():只是建议虚拟机执行Full GC,可是虚拟机不必定真正地执行,不建议使用这种方式,而是让虚拟机管理内存。
    • 老年代空间不足:老年代空间不足的最多见场景为前文所讲的大对象直接进入老年代,长期存活的对象进入老年代等。为了不以上缘由引发的 Full GC,应当尽可能不要建立过大的对象以及数组。除此以外,能够经过 -Xmn 虚拟机参数调大新生代的大小,让对象尽可能在新生代被回收掉,不进入老年代。还能够经过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
    • 空间分配担保失败:使用复制算法的 Minor GC 须要老年代的内存空间做担保,若是担保失败会执行一次 Full GC。具体内容请参考上面的第五小节。
    • JDK 1.7 及之前的永久代空间不足:在 JDK 1.7 及之前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的状况下也会执行 Full GC。若是通过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。为避免以上缘由引发的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
    • Concurrent Mode Failure:执行 CMS GC 的过程当中同时有对象要放入老年代,而此时老年代空间不足(有时候“空间不足”是指 CMS GC 当前的浮动垃圾过多致使暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。

内存分配策略

对象的内存分配规则并非百分百固定的,其细节取决于当前使用的是哪种垃圾收集器组合,还有虚拟机中与内存有关的参数设置。

  • 对象优先在eden分配
  • 大对象直接进入老年代
  • 长期存活对象进入老年代:Survivor区对象每经历一次Minor GC,年龄就增长1,当年龄达到MaxTenuringThreshold设置的值(默认15),就将会被晋升到老年代。
  • 动态对象年龄断定:若是Survivor空间中相同年龄全部对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就能够直接进入老年代。
  • 空间分配担保:在发生Minor GC以前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代全部对象总空间,若是这个条件成立,那么Minor GC就是安全的。若是不成立,则虚拟机会先查看HandlePromotionFailure设置值是否容许担保失败。若是容许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,若是大于,将会尝试进行一次Minor GC,尽管此次Minor GC是有风险的;若是小于,或者HandlePromotionFailure设置不容许冒险,那这时也要改成进行一次Full GC。

类加载机制

类加载的时机

虚拟机规范严格规定了有且只有下面5种状况必须当即对类进行初始化:

  • 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,若是累没有进行过初始化,则须要先触发其初始化。生成这4条指令的最多见的java代码场景:使用new关键字实例化对象,读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法。
  • 使用java.lang.reflect包的方法对类进行反射调用,若是类没有初始化则须要先触发其初始化。
  • 当虚拟机加载一个类的时候,父类尚未进行初始化,则需先触发父类初始化。
  • 但虚拟机启东市,用户须要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先执行这个主类。
  • 当使用JDK1.7的动态语言规则时,若是一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且这个方法所对应的类尚未进行初始化,则须要先触发其初始化。

如下状况不会初始化:

  • 经过子类引用父类静态变量,只有直接定义这个字段的类才会被初始化,所以经过其子类来引用父类中定义的静态字段只会初始化父类。
  • 经过数组定义来引用类SuperClass[] sca = new SuperClass[];
  • 引用常量,常量在编辑阶段会存入调用类的经常使用池中,本质上并无直接引用到定义常量的类。

类加载的过程

clipboard.png

加载

在加载阶段,虚拟机须要完成下面三件事:

  • 经过一个类的全限定名来获取定义此类的二进制字节流。
  • 将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构。
  • 在内存中生成一个表明这个类的java.lang.Class对象,做为方法去对这个类的各类数据的访问入口。

验证

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

从总体看,验证阶段大体上会完成4个阶段的检验动做:

  • 文件格式验证:验证字节流是否符合Class文件格式的规范,而且能被当前版本的虚拟机处理。(魔数、主、次版本号等)
  • 元数据验证:对字节码描述的信息进行语义分析,验证点:是否有父类、是否继承了不被容许继承的类、若是不是抽象类,是否实现了其父类或接口之中要求实现的全部方法。
  • 字节码验证:经过数据流和控制分析肯定程序的语义是否合法、符合逻辑。
  • 符号引用验证:发生在虚拟机将符号引用转化为直接饮用的时候,这个转化动做在解析阶段发生。符号引用能够看作对类自身之外(常量池中各类符号引用)的信息进行匹配性检验:可以经过字符串描述的全限定名找到对应的类、方法、字段以及访问性可否被当前类访问。

准备

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

解析

将常量池中的符号引用替换成直接饮用的过程。

  • 符号引用:以一组符号来描述所引用的目标,符号能够是任何形式的字面量,只要使用时能无歧义地定位到目标便可。
  • 直接引用:直接指向目标的执行、相对偏移量或能间接定位到目标的句柄。

初始化

开始执行类中定义的java程序代码,根据程序员经过程序制定的主观计划去初始化类变量和其余资源,或者说执行类构造器<clinit>()方法的过程。

  • <clinit>()方法和构造函数不一样,虚拟机保证子类的<clinit>()方法执行以前,父类的<clinit>()方法已经执行完毕。
  • <clinit>()方法并非必需的,若是累没有静态语句块,也没有对变量的赋值操做,那么编译器能够不为这个类生成<clinit>()方法。
  • 接口中不能使用静态语句块,但仍然有变量初始化的赋值操做,所以接口与类同样都会生成<clinit>()方法,可是与类不一样的是,执行借口的<clinit>()方法先执行父接口的,只有当父接口定义的变量被使用时,父接口才会初始化。
  • 虚拟机保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步。

类加载器

虚拟机设计团队把类加载阶段中“经过一个类的全限定名来获取描述此类的二进制字节流”这个动做放到java虚拟机外部去实现,以便让应用程序本身决定如何获取所须要的类。实现这个动做的代码模块称为“类加载器”。

对于任意一个类,都须要由加载它的加载器和这个类自己确立其在Java虚拟机中的惟一性,每个类加载器都拥有一个独立的类名称空间。两个类“相等”包括表明类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系断定等状况。

从Java虚拟机的角度来说,只存在两种不一样的类加载器:

  • 启动类加载器(Bootstrap ClassLoader):这个类加载器使用C++语言实现,是虚拟机自身的一部分。
  • 全部其余的类加载器:由Java语言实现独立于虚拟机外部,而且所有继承自抽象类java.lang.ClassLoader。

从Java开发人员的角度来看,类加载器划分为更细致一些:

  • 启动类加载器(Bootstrp ClassLoader):负责将存在<JAVA_HOME>lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,而且是虚拟机识别的(仅按照文件名识别)类库加载到虚拟机内存中。启动类加载器没法被Java程序直接饮用,用户在编写自定义类加载器时,若是须要把加载请求委派给引导类加载器,那直接使用null替代便可。
  • 扩展类加载器(Extemsion ClassLoader):由sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>libext目录中的,或者被java.ext.dirs系统变量所指定的路径中的全部类库,开发者能够直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$App-ClassLoader实现。因为这个类加载器是Classloader中的getSystemClassLoader()的返回值,因此通常也称为系统类加载器。负责加载用户路径(ClassPath)上所指定的类库,开发者能够直接使用这个类加载器。若是应用程序没有自定义过本身的类加载器,通常状况下这个就是程序中默认的类加载器。

双亲委派模型

应用程序都是由三种类加载器互相配合进行加载的,若是有必要还能够加入本身定义的类加载器,这些类加载器之间的关系通常以下:

clipboard.png

图中展现的类加载器之间的这种层次关系,称为类加载器的双亲委派模型,双亲委派模型除了顶层的启动类加载器外,其他的类加载器都应当有本身的父类加载器,这里加载器之间的父子关系通常不会以继承(Inheritance)的关系来实现,而是都是用组合(Composition)关系来服用父加载器的代码。

工做过程

若是一个类加载器收到了类加载的请求,它首先会把这个请求委派给父类加载器去完成,每个层次的类加载器都是如此,所以全部的加载请求最终都应该传送到顶层的启动类加载器中,只有父加载器反馈本身没法尝试完成这个加载请求时,子加载器才会尝试本身去加载。

好处

Java类随着它的类加载器一块儿具有了一种带有优先级的层次关系。

例如类java.lang.Object存放在rt.jar中,不管哪个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器来进行加载,所以Object类在程序的各类加载器环境中都是一个类。相反若是没有双亲委派模型,若是用户本身编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那么系统中将会出现多个不一样的Object类。

实现
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
相关文章
相关标签/搜索