JMM
JMM是java memory model的简称。一个java应用程序就是一个java进程,进程是操作系统资源分配的基本单位。也就是说对于一个应用程序,操作系统会分配一块内存区域供该进程使用。当然当内存区域容量不够的时候,操作系统会自动给该进程增加内存空间。一个进程可以产生多个线程,对于每个线程,操作系统只负责调度,当CPU不够的时候,选择某个线程获得时间片开始运行。
JMM描述的就是进程与线程之间内存使用的模型。线程是由进程创建,那么进程肯定会有一块内存空间是线程共享的,创建出的新线程也会有自己的内存空间,相当于从进程内存空间分配的一块区域独享,类似于分西瓜的过程。
我们知道jvm将内存总体分为方法区、堆区、栈区、本地方法区、程序计数器。这五块区域,只有本地方法区域、栈区、程序计数器是线程私有,方法区域、堆区是公有。然而在堆区HotSpot VM使用了一种TLAB(Thread local allocation buffer)技术实现了线程私有,旨在降低因同步机制带来的性能损耗。
公有内存区域自然需要保证安全性能,要保证安全性能就有必要使用一定的同步策略,常见的锁机制由于性能代价太高不合适,所以jvm更多的是使用CAS和失败重试技术完成同步。
java提出了volatile变量,该种变量有两种特性,一是保证了jvm在操作该变量时指令不会被重排序,二是保证了该变量对所有线程的可见性。要保证可见性,我们知道,线程首先会在自己的内存私有区域安放自己的变量,若是该变量为共有变量,我们便在公有内存(主内存)区域存储同样一份变量。一旦jvm发现该变量是volatile修饰的,每次更新和提取变量的时候都会直接从主内存提取或者修改。保证了变量的修改对每一个线程的可见性。
jvm的类加载机制
首先我们看看jvm有那些类加载器:
1)启动类加载器(Bootstrap ClassLoader):负责加载JAVA_HOME\lib目录中并且能被虚拟机识别的类库到JVM内存中,如果名称不符合的类库即使放在lib目录中也不会被加载。该类加载器无法被Java程序直接引用。
2)扩展类加载器(Extension ClassLoader):该加载器主要是负责加载JAVA_HOME\lib\ext\,该加载器可以被开发者直接使用。
3)应用程序类加载器(Application ClassLoader):该类加载器也称为系统类加载器,它负责加载用户类路径(Classpath)上所指定的类库,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
然后我们看看它们之间的关系:
可以看到jvm类加载存在层次结构,为什么要这样呢?
jvm的类加载需要保证两个原则,一是全盘负责,二是双亲委派。全盘负责指加载一个类的加载器同时需要加载该类引用的其它类,双亲委派指的是类加载器在加载一个类的时候不会先去加载,二是先让其父类进行加载,若父类无法加载则自己加载。每个类加载器都有自己加载类的缓存,当一个类需要被加载时,类加载器首先会在缓存中查找,一旦发现类已经被加载,则直接返回。
如果不采用具有优先级的层次结构,会出现什么问题呢?假如用户自己定义了一个java.lang.Object类,那么是不是类加载器也需要加载该类,我们知道jvm类库中已经存在该类,而且该类是所有类的顶级父类,具有重要的意义。一旦jvm将我们自定义的Object类加载,就会出现类冲突。为了防止类冲突,保护原始类,我们使用这种具有优先级的层次结构。- jvm之GC
jvm GC的对象主要为堆区,堆区存放的是对象实例,当堆内存不足时,会抛出Outofmemory错误。我们可以使用-Xmn(初始堆大小),-Xmx(最大堆大小)来显示分配堆内存。
jvm将堆区分为两部分,年轻代和年老代。HotSPot其实也将方法区划入堆区,但是GC一般不对该区进行。其中年轻代被划分成了8:1:1三个区,分别为Eden、S0、S1,年轻代实行的GC算法为复制算法。可以注意到S0与S1大小相等,S0与S1不论在何时一定有一个为空,当年轻代GC被触发后,复制算法将Eden区和S0中存活内存复制到S1中,可以想到,S1的容量可能不足以存储,因此此种情况发生时,存活内存直接存到年老代。年老代为一块较大的内存区域,应用的算法为标记-清除或者标记-整理算法。标记-清除算法主要原理为将内存区被标记为无用的对象实例直接清理掉,可以发现,如果使用标记-清除算法将会产生大量的内存碎片,jvm使用空闲链表去管理可用内存空间。空闲链表在一定程度上可以解决内存分配问题,但是当需要大块内存区域的时候,内存分配将不太容易,因此在使用标记-清除算法时候jvm允许开发员控制,比如在多少次完全GC后实行一次整理。标记-整理算法则是将内存区域未被标记为无用的对象实例往内存的一端移动,这样保证年老代的内存区不会出现内存碎片,便于使用指针碰撞算法实现内存分配。
根据回收策略的不同,jvm将GC分为以下几种收集器:
Serial收集器:单线程收集器,必须暂停其他所有的工作线程,默认client模式下新生代的收集器
ParNew收集器:Serial的多线程版本,一般在Servr模式下的新生代首选收集器,除Serial外,目前只有它能与CMS收集器配合工作,ParNew收集器在单cpu的情况下不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个cpu的环境中都一定能超越Serial收集器。
Parallel Scavenge收集器:新生代收集器,使用复制算法,是并行的多线程收集器,跟其他收集器不一样,CMS等收集器是关注尽可能的缩短垃圾收集时用户线程的停顿时间,而Parallel
Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量是cpu用于运行用户代码的时间与cpu总消耗时间的比值。
Serial Old收集器:单线程收集器,是Serial收集器老年代版本,使用“标记-整理”算法,主要用在client模式下。
Parallel Old收集器:Parallel Scavenge收集器的老年代版本,在注重吞吐量及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge收集器家Parallel Old收集器(高吞吐)。
CMS收集器:使用“标记-清除”算法,以获取最短回收停顿时间为目标(低延时),对于重视服务的相应速度,希望系统停顿时间最短的B/S系统的服务器端尤其有用。CMS收集器分4个步骤:1,初始标记 2,并发标记 3,重新标记 4,并发清除 耗时最长的并发标记和并发清除都可以与用户线程一起工作了。
还有三个缺点:1,对cpu资源敏感,默认启动的回收线程数是(cpu数量+3)/4,当cpu数较少的时候,会分掉大部分的cpu去执行收集器线程,影响用户,降低吞吐量。2,无法处理浮动垃圾,浮动垃圾即在并发清除阶段因为是并发执行,还会产生垃圾,这一部分垃圾即为浮动垃圾,要等下次收集。3,因为使用的是“标记-清除”算法,会产生碎片。
G1收集器(Garbage First):基于“标记-整理”算法,之前的垃圾收集器都是整个新生代或者老生代,而G1将整个java堆(包括新生代和老生代)划分为多个大小固定的独立区域,并跟踪这些区域的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(也是Garbage First的由来)。
- jvm性能控制
jvm性能控制需要根据GC来判断,因此我们需要在程序运行的时候观察到GC情况,如果minor GC次数过多说明年轻代大小设置偏小;如果Full GC次数过多,说明程序需要增大年老代的容量。
怎样能查看到进程运行时的GC情况呢?
jdk中自带有几种命令,jps、jstat、jstack、jmap、jhat;这些是命令行工具。如果要更直观的查看,可以使用JConsole。如需要更强大的工具的话,可以下载VisualVM工具。