自己对JVM的一点理解

JVM应该是运行在操作系统之上的,和软件并没有直接的交互
在我的理解中,JVM是这么组成的。
首先是一个类加载器,它的作用就是加载一个class文件。
举个例子,比如现在有一个Student的类。它是抽象的,但是如果我们把它new一下,它就变成一个具体的实例了。
它的对象实例化过程是这样的:
首先是一个Student.class文件,然后它进入到了类加载器中。类加载器通过加载并实例化把他变成了一个类的模板Student Class。这就是反射的class对象。如果想要用Student的反射对象,就需要用Student.class,合格时候会返回一个Class,也就是会变成这样:
ClassstudentClass = Studnet.class;
这个Class就是个模板反射对象,全局唯一。也就是说无论new上多少个Studnet都是这一个Class。
然后会进行实例化,实例出来几个Student实例。这些实例的名字是在栈里面的,但是他们具体的引用实际上是在堆里的。
这里其实还涉及到了一个机制,叫做双亲委派机制
双亲委派机制其实在我看来很好理解。首先要知道一点,就是我们是有好几个类加载器的,他们是有父子关系的。最底下的是应用加载器,它的父类是扩展类加载器,扩展类加载器的父类的根加载器。
如果说我们需要加载一个类,首先把这个请求让根加载器去完成,它辈分最大嘛。如果说根加载器说,我不知道这玩意,加载不了,那就给他的子类扩展类加载器去完成,以此类推。
举个例子,比如我们自己建一个java.lang包,在包底下弄一个String的类去加载,可以加载出来,但是加载出来的不是我们自己写出来的那个String。这是因为本来就是java.lang.String,这个在根加载器那里就加载出来了。我们自己写出来的java.lang.String需要到应用加载器才能加载出来,但事实上根本就轮不到他。
说完了类加载器,再往下走其实就是我们的运行时数据区。这里面分成了好几个部分,方法区,java栈,本地方法栈,堆以及程序计数器。
首先说程序计数器吧。程序计数器其实就是一个很小的内存空间,他的作用就是计数,然后帮助完成循环,跳转之类的基础功能。
然后是栈,栈有一个规则叫做先进后出,后进先出,其实也可以把他理解为一个桶或者说是以前上学时候交作业。最早交的作业会被压在最底下,也就最后一个被老师批改到。桶也是一个道理。
和他有点像的是队列。队列就是桶把底打通了,和管道一样,所以是先进先出的,就相当于排队,你先来排队所以先轮到你。
栈呢,有几个特点:
1、他的读取数据是比较快的,起码比堆要快,然后他的数据是可以共享的
2、他是负责程序的运行,因为里面一般放的都是方法,比如mian方法,然后main方法在调用一个test()方法压在他上头。
3、栈的生命周期是和线程同步的。线程创建的时候他创建,线程结束了他也就释放
4、所以说对于栈来说完全没有垃圾回收的问题。线程在跑你不能把人家东西当垃圾扔了吧,人家线程跑完了自己就扔了不用你帮忙。因为一般来说垃圾回收,也就是GC,基本都在堆内存。
5、方法如果自己调用自己,就是递归嘛,你不能写死循环。因为栈这个桶就这么大,你把他放满了后面的就没地方放了,就会报一个StackOverflowError异常,看到这个异常就说明,我们的栈满了
所以说我们程序正在执行的方法,一定是在栈的顶部。
栈的组成元素就是栈帧。每执行一个方法就会产生一个栈帧,保存到栈顶。如果说这个方法执行完了,那么就会自动把这个栈帧出栈。
说完了栈说一下方法区吧。
方法区是所有线程共享的。简单来说就是方法区里面保存了所有定义的方法的信息,这个区域是一个共享区间。
方法区里储存的东西其实很好记。一个static,静态变量,一个final,常量,一个Class,类信息,还有一个运行时常量池。
常量池分为三个,一个字符串常量池在堆里,还有两个分别为静态常量池和运行时常量池,都在方法区里。jdk8后有了元空间,元空间是方法区的一种落地实现,可以理解为方法区在元空间中。而元空间又是逻辑上存在于堆里,物理上存在于本地内存中的。
然后说说堆吧。
堆在JDK7之前都是分为新生区,养老区和永久区的,JDK7开始逐渐取消永久代,JDK8正式取消永久代,改为元空间。
新生区分为三个区,伊甸园区和两个幸存者区。
首先是伊甸园区,所有的类都是在伊甸园区被new出来的。不断地创建对象,伊甸园区总是会满的,这个时候再需要创建对象就需要进行GC垃圾回收,在这里有一个GC算法叫做复制算法。
复制算法是这样的。不是有两个幸存区嘛,空的那个被称为幸存区to区,另外一个被称为幸存区from区。然后清除伊甸园区里面不用的垃圾,把幸存下来的,也就是说以后还要用的,这些对象,以及之前from区里面的东西都复制转移到to区里面,这个时候伊甸园区和之前的from区都空了嘛,伊甸园区就可以继续new了,而之前的to区就变成了from区,之前的from区就变成了to区。
但是这个算法就有一个弊端,就是他会浪费你的内存空间,因为他始终有一个空的to区是在待命嘛。不过也有好处,就是他不会出现内存碎片。
如果说一个对象经过了一定数量的GC仍然存在,那么他就会进入到养老区。养老区很好理解,就是不怎么常用的,但是还在用的。因为基本上99%的对象都是临时对象,在新生区就被干掉了,所以能到养老区的不会很多 。
养老区也会有GC垃圾回收,一般称之前的GC是minor GC,也就是轻GC,养老区进行的一般都是重GC,也就是full GC。
一般用的GC算法是标记清除算法和标记压缩算法结合的方式。
什么是标记清除算法呢?标记清除算法就是对存活的对象进行标记,然后回收不需要的对象。举个例子,就像最近的健康码,一人一码,然后不是绿码的统统回收。但是这样就有个问题,确实和复制算法比较起来,不占用内存空间了,但是又多了内存碎片了,而且因为要扫描两次,耗时会比比较旧,效率很低。
这个时候就是标记压缩算法了。标记压缩算法就是在标记清除算法的基础上进行压缩,这样就没有内存碎片了。当然一般是先标记清除几次再进行压缩,这样效率会更高。
就内存效率而言,复制算法>标记清除算法>标记压缩算法
就内存整齐度而言,复制算法=标记压缩算法>标记清除算法
就内存利用率而言,标记清除算法=标记压缩算法>复制算法
还有一个是永久代,这个概念其实现在已经没有了。这是一个JDK7以前的概念,永久代其实就是方法区的一个概念实现,现在取而代之的是元空间。三个常量池,静态常量池和运行时常量池都在元空间中,字符串常量池在堆中。而且元空间在逻辑上是在堆中的,只不过物理上是在本地内存中。因为默认情况下,元空间的大小只受本地内存的限制。
最后是堆内存调优。
默认情况下,分配的总内存,是电脑内存的1/4,而初始化内存是1/64。
如果说需要进行调优,那么就可以用过输入vm参数来实现
-Xms设置初始化内存分配大小 -Xmx设置最大分配内存大小 -XX:+PrintGCDetails

猜你喜欢

转载自blog.csdn.net/NewBeeMu/article/details/107091770