我们都知道Java源代码是通过JVM-Java虚拟机识别并执行的。Java虚拟机不难理解,通过这篇文章你能快速get到:
1.Java类加载的过程及常见的3种类加载器
2.JVM体系结构
3.堆体系结构丶常见的两种JVM异常以及简单的参数调优
4.垃圾回收简单介绍
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
1.Java虚拟机没有与硬件直接交互,它是运行在操作系统之上的(wondows/linux),Java源文件并不能直接交给机器执行,儿视需要翻译成字节码文件,如下图:
字节码必须通过类加载器加载到JVM环境后才能被invoke。执行的方式有三种:第一,解释执行,第二,JIT编译执行,第三,JIT与解释混合执行。混合执行的模式优势是解释器在启动时先解释执行能省去编译的时间。
1)常见的3种类加载器:Bootstrap ClassLoader(最高级别),Platform ClassLoader(平台类加载器),Application Classloader(应用类加载器).,注意:Bootstrap 只是类加载器,别误以为是前端框架的那个Bootstrap
类加载器采用的是双亲委派模型,意思就是,低层次的类加载器不能覆盖高层次已经加载的累。如果底层的类加载器想要加载一个未知类,必须逐级依次向上询问(Bootstrap>Platfrom>Application),第一,我是否加载过此类,第二,如果没有加载此类,我是否可以加载此类,如果以上两点均为否,然后从Boorstaro依次向下加载,如果都加载不了,低层次的类加载器才能加载此类。
画如下图:
2.JVM的内存布局
JVM的体系结构包含有:本地方法栈,程序计数器,Java虚拟机栈,Heap堆区,元数据区,JIT编译产物
着重介绍Heap堆区。体系结构如下图:
我们先来看一个实例:Father father=new Son();这个相信大家都很长见到,但是我们要知道这个实例的“=”左边存放在Java栈里面,"="右边存放在Heap堆区里面。Java栈存放了一些对象引用,基本数据类型和常量,而堆区存放的是实例对象,因此堆区常见的异常时因为内存不足而产生OOM异常(OutofMemory)。Java栈常见的异常则是栈溢出问题(StackOverflowError),通常在递归调用时会产生此异常。下面在eclipse种演示StackOverflowError:
Java栈的就讲到这了,你们还有兴趣的自己可以去查一些文档。
3.Heap堆区
堆区是OOM异常最主要的地方,下面先仔细了解Heap堆区的体系结构:Eden区,S0(survivor0幸存者0区),S1(survivor1幸存者1区),以及老年代Old区,下面来了解堆区的工作原理以及JVM堆区参数调优。
1)堆区分为新生代和老年代,对象刚产生的时候就在新生代,随着时间的推移不被使用会进入老年代,但是一些刚产生的超大对象不会存在新生区,而是直接放入老年代。新生代=1个Eden区+2个Survivor区。绝大部分的对象在Eden产生,如果Eden满了,则会触发GC(垃圾回收机制),Eden区装满了,触发的是YGC-Young Carbage Collection。在实行垃圾回收时,如果在Eden区中的对象没有被引用,则直接回收,其他存活的对象则被移送到Survivor区,但是Survivor区分为S0和S1,到底存在哪里呢?答案是将存活的对象在SO和S1交换,例如有一个对象在S0,YGC会先把S0的那个对象复制到未使用的那块空间,然后讲当前使用的空间完全清除。那么问题来了,如果YGC移送的对象大于Survivor区工作空间怎么办?GC会直接将其移送到老年代。那么一个对象不想被销毁,一直在S0和S1互相交换来交换去怎么办,那就会很占内存了。这下JVM计数器的作用就来了,每个对象都有一个计数器,每次invoke一次YGC,计数器计数就会+1,当达到一定的阈值,对象就会从新生代晋升到老年代。
如下图:
Java虚拟机没有我们想的那么智能,如果堆内存满了没有及时处理则会出现OOM异常,这时候JVM内存调优就很重要了(当然我们现在肯定是用不到的)
JVM内存调优
在了解内存调优之前,我们要先了解-Xms和-Xmx。Xms是初试参数大小,默认是物理内存的“1/64”,Xmx是最大分配内存,默认为物理内存的“1/4”,一般来说,Xms和Xmx设置成一样的大小,因为服务器在开发的时候堆内存不断的扩容和回缩(产生新对象和回收新对象导致),会造成系统压力增大,所以Xms和Xms大小一样可以避免GC调整堆后带来的压力。我打开Eclipse,来查看最小和最大内存是否是“1/64”和“1/4”,代码如下:
package Heap;
public class test {
public static void main(String[] args) {
long maxMemory = Runtime.getRuntime().maxMemory();//返回JVM试图使用的最大内存
long totalMemory = Runtime.getRuntime().totalMemory();//返回JVM的内存总量
System.out.println("MAX_MEMORY = " + maxMemory + "(字节)、" + (maxMemory / (double)1024 / 1024) + "MB");
System.out.println("TOTAL_MEMORY = " + totalMemory + "(字节)、" + (totalMemory / (double)1024 / 1024) + "MB");
}
}
结果如下:
证实了最大内存为物理内存的“1/4”(默认),但是一旦内存不够用了,则会产生OOM异常。JVM简单调优也是通过修改Xms-Xmx参数来实现。
我把默认内存调整为1024M,输出的结果是:
根据打印出来的值,证明了新生代 PSYoungGen,老年代ParOldGen的存在,同时也证明了元空间(逻辑存在)的存在。元空间是非堆内存结构,只是逻辑存在(Java7版本叫永久带,Java8叫元空间)
演示OOM异常
为了演示OOM异常,我先把JVM内存调到8m,如下图:
我们用过Java的人都知道,如果我们不停的用+ 直接来拼接String,会产生大量的内存,代码如下:
String str = "www.zdxh.com" ;
while(true)
{
str += str + new Random().nextInt(88888888) + new Random().nextInt(999999999) ;
}
然后运行结果如图:
根据打印的结果显示,FullGC表示重量级的GC ,一般的GC都是发生在Eden区里。
4.垃圾回收
Java会对内存进行自动分配和管理,不同JVM实现的垃圾回收机制也不一样,垃圾回收的目的是清楚不再使用的对象,自动释放内存
1)GC是如何判断对象是否可以回收的?
GC引入了GC Roots,加入一个对象与GC Roots没有直接或者间接的引用关系,或者凉饿互相环岛状的循环引用(闭合引用)关系,则会被直接回收。类静态属性中引用的对象,常量引用的对象,Java栈中引用的对象,本地方法栈中引用的对象可以作为GC Roots!
2)常见的垃圾回收算法:
1.标记-清除算法:这算法会从每个GC Roots出发,先找出有引用关系的对象,标记起来,再把没有引用关系的对象标记清除,但是这种算法会带来大量的空间碎片问题,需要分配一个较大且连续空间容易触发FGC(全局范围的GC),为了解决这个问题,引入第二种算法
2.标记-整理算法:类似于计算机的磁盘清理。把存活的对象整理到连续的空间一端,再把原占有的空间全部清除,这样就不会产生空间碎片问题
3.Mark-Copy算法(推荐使用):垃圾回收时,把空间划分为两块,每次只激活其中的一块,把存活的对象存到未激活的空间,讲未激活标记为已激活,然后将之前激活的空间标记为未激活,再删除这个空间的内存。每次只使用Eden区和Survivor的其中一块,这样做的话可以减少空间的浪费
垃圾回收器(3种主流的垃圾回收):
1.Serial
2.CMS
3.G1
自己去Goole查看吧哈哈!