JVM内存结构
1.堆:所有线程共享,主要存放对象实例。
2.栈:线程独享。每个方法在执行的时候都会创建一个栈帧,存储局部变量,操作数,动态链接,方法返回地址等。每个方法的调用和完毕对应的就是入栈和出栈。
3.元区间:JDK1.8之前叫方法区,所有线程共享,用于存放类信息,字符串常量和静态变量。
4.本地方法栈:线程私有,主要是为虚拟机提供调用Native方法的服务。
5.程序计数器:线程私有,指向当前正在执行的字节码的行号,通俗易懂的说,就是标记当前线程执行到哪里了。
JVM垃圾回收机制
通过上面的解析,相信大家对JVM有了比较清晰的认知。现在我们谈谈JVM的工作原理,从以下的代码分析。
public static void main(String[] args) {
//list存在栈上,new 出来的对象存在堆中
List list=new ArrayList();
for (int i=0;i<=Integer.MAX_VALUE;i++)
{
HashMap hashMap=new HashMap();
list.add(hashMap);
}
}
以上代码会出现一个非常常见的错误,堆内存溢出,通俗来说就是jvm运行的时候会给堆一个固定的大小,当不断的向list中添加元素,不断的创建hashmap对象,导致list集合过大,直到堆没有空间在执行代码,从而抛出异常。我们要弄懂其中的运行原理,必须逐个分析。
对象引用的级别
1.强引用:指的就是我们new出来的对象,即便是触发垃圾回收机制也不会回收。
2.软引用:意思就是当我们的内存不够的时候才会北回收掉的对象,jdk中给出了SoftReference这个类供开发者们编写相应的代码。
3.弱引用:无论我们的内存够不够用,只要触发了垃圾回收机制,就是被回收,典型的代表就是ThreadLocal线程局部变量,jdk提供了 WeakReference这个类。
4.虚引用:顾名思义,就是一个虚假的引用,当垃圾回收器执行的时候,就会被通知。因为jvm自动管理的内存是堆内存,而堆外内存是由unsafe类去管理的,所以当jvm申请到了堆外内存,就会在堆内存中创建一个虚引用,当垃圾回收器执行的时候,虚引用就会被通知到,那么想应得线程就会取清理堆外内存。
什么是垃圾?怎么回收?
判断一个对象是否是垃圾?jvm主要提供了以下几种思想。
1.引用计数法:就是jvm创建对象的时候,就会给对象添加引用一个计数器,当有对象引用它得时候,这个计数器就会加一,引用失效时就会减一,直到为0时,这个对象就会被GC。但是这不能解决循环依赖得问题,比如A对象有个属性依赖B对象,B对象有个属性依赖A对象,GC是无法识别得。
2.可达性分析:就是以一系列被称为GCRoot的对象作为起点向下搜索,如果有对象没有出现在搜索链上,则证明这个对象不可达,会被GC。
怎么回收?jvm主要提供了以下几种算法
1.标记清除算法:先标记需要清除的对象,GC时进行清除,缺点就是会产生内存碎片。
2.标记整理算法:和标记清除算法类似,只不过会把没有被标记的对象移到一边,然后再清除,就不会有内存碎片。但是因为要移动对象位置,所以会Stop the World,暂停用户线程。
3.复制算法:将内存按照容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完了,就将还活着的对象复制到另一块上,然后再把使用过的内存空间一次性清理掉。
4.分代回收算法
a.当对象被创建时,会被jvm放在Eden区,当Eden区满了时候,会进行一次miniorGC,将存活的对象放在so区。
b.当Eden区再次被填满是,会对Eden区和s0区进行一次回收,存活的对象被移到s1区,然后再将s1区的对象通过复制算法复制到s0区,以此类推。
c.当有一个对象经历过16次minorGC依然存活,就会被放到老年代。
d.当老年代满的时候,会进行一次majorGC,同时清理新生代和老年代。
垃圾回收器
新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:CMS、Serial Old、Parallel Old
整堆收集器: G1
1.Serial收集器:这是一个单线程的收集器,对于限制单CPU的环境下,它可以转心做垃圾回收,效率高,但是收集垃圾时必须Stop the world,就是暂停其他工作线程,直到它结束,比较适合Client模式下的虚拟机,采用的是复制算法。
2.ParNew收集器:Serial收集器的多线程版本,默认开启的收集线程数与CPU的数量相同,适合运行在Server模式下的在虚拟机,缺点也是会Stop the world采用的是复制算法。
3.Scavenge收集器:吐量优先收集器,可以通过一些配置达到GC的自适应回收策略,也是多线程的,采用的也是复制算法。
4.Serial Old收集器:Serial收集器的老年代版本,但是采用的是标记整理算法。
5.Parallel Old收集器:ParNew收集器的老年代版本,采用标记整理算法。
6.CMS收集器:一种以获取最短回收停顿时间为目标的收集器。初始标记阶段,会标记GCRoot能直接到达的对象,存在StopWorld但是很慢。并发标记阶段,会继续标记引用对象,不影响用户线程。重新标记阶段,会修正因为用户线程运行而导致变动的对象。并发清除阶段,直接清除标记的对象,采用的是标记清除算法,会产生内存碎片。
7.G1收集器:
并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿时间。部分收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让Java程序继续运行。
分代收集:G1能够独自管理整个Java堆,并且采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
空间整合:G1运作期间不会产生空间碎片,收集后能提供规整的可用内存。
可预测的停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型。能让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒。
JVM调优
为什么要进行JVM调优,我相信朋友们读完上面的文章就能很清晰的认识到这样的问题。对于不同的场景,我们要设置新生代老年代的大小,垃圾回收器等等一系列配置,才能让我们的java程序稳定,高效的运行,在这里,首要考虑的就是GC问题,我们应让GC的次数足够少,时间足够短,在这里提供几个策略供大家参考。。
1.一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,我们通常把最大、最小设置为相同的值。
2.年轻代和年老代将根据默认的比例(1:2)分配堆内存,可以通过调整二者之间的比率NewRadio来调整二者之间的大小,也可以针对回收代,比如年轻代,通过 Xmn 来设置其绝对大小。
3.线程堆栈的设置:每个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太了,一般256K就足用。理论上,在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统,可以通过-Xss进行设置。
4.设置垃圾回收器
-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行年老代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器