1。JavaVirtualMachine人群中,一个叫java的年轻人正在向周围的人群细数着他取得的辉煌和辉煌。就在这时候,c老头和c++老头缓缓走了过来,看着被众人围在身边的java,c老头叹了口气对旁边的c++说道:“我还以为你能惹到我,继续走呀。”C++笑着回应:“一代又一代人才出世,这个世界以后永远都是90后,甚至00后!”注意到c和c++的Java赶紧从人群中冲了出来:“两位学长谦虚点,这个世界还是离不开两位学长的,我只是站在两位学长的肩膀上而已。”“你这小子解决了我们的很多问题,比如指针、多重继承、内存管理……当时很多程序员都对我们抱怨很深!c++好评。“还有Java虚拟机,真是个好主意!”C加在一边。......Java虚拟机一直是我们在学习Java的过程中反复提到的一个东西,那么JVM到底是什么呢?请看下图:简单的说,JVM的工作就是通过类加载系统将字节码文件加载到内存中,加载到内存中的数据在逻辑上形成了我们在图中看到的操作时间数据区(内存model),然后执行引擎对内存模型中的数据执行程序进行操作/调度。看到内存模型中的东西是不是很熟悉了呢?现在回想我的面试,我遇到的JVM面试题是不是都是内存模型里面的东西。例如:栈、堆、Eden、Survivor、GC等。2.举个小栗子publicclass例子{publicintadd(){inta=3;整数b=4;intc=a+b;返回c;}publicstaticvoidmain(String[]args){Examplee1=newExample();e1.add();//.......}//...}这个栗子是干什么的,不用多说了!今天我们要慢慢剥掉它。以前剥下来吃的太快了,是不会有太大感觉的。3.栈栈的全称是Java虚拟机栈,是线程私有的,与线程具有相同的生命周期。描述了Java方法执行的内存模型:每个方法在执行过程中都会创建一个栈帧(StackFrame)来存放局部变量表、操作数栈、动态链接、方法出口等信息。从调用到执行结束,每个方法都对应一个栈帧从虚拟机栈push到出栈的过程。上面怎么解释?然后你需要开始剥栗子了!道来了(大声喊)!栗子开始执行时,由于只有一个主线程,JVM只需要在栈区为主线程分配内存即可(也就是说,如果有多个线程,则有自然会有多个栈区,而且是每个线程私有的。)。好的!Main继续执行,会遇到main()方法。遇到之后!JVM会在栈区再画一个小块来存放main()方法执行过程的数据,而这个小区也将是栈帧。另一个add()方法出现在main()方法的执行过程中。同样,JVM会为add()分配一个栈帧,同时压入栈区。以后遇到的其他方法也是如此。当然,方法执行完成后,会弹出并释放内存。当线程中栈区的所有方法都返回时,程序才算执行完毕。那么栈帧是一个什么样的属性呢?嘿,我的刀呢?算了,撕了。当我们撕开栈区和栈帧时,一不小心,局部变量表、操作数栈、动态链接、方法出口……散落一地。拿起add()栈帧的局部变量表和操作数栈就可以看到这样一张图。在执行Chestnut中add()方法中的三行代码时,局部变量表和操作数栈的一个变化过程:首先,执行inta=3;在局部变量表中分配一个int区域,记为a;同时iconst命令将常量3压入操作数栈,然后istore命令弹出3赋值给变量表中的局部a。同样,intb=4;代码行是一样的。那么,intc=a+b;从右到左,先执行a+b,即iload命令从局部变量中取出a和b对应的值,然后将iadd之后的值压入操作数栈。剩下的就是intc=7的操作了。通过上面的栗子,就很容易理解了;局部变量表,顾名思义,存放的是各个方法中的局部变量(即编译器已知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(referencetype)和returnAddress类型(指向一条字节码指令的地址)),如图中a和b所示。操作数栈,即存放方法中各种操作数的临时空间,如栗子中的3和4。动态链接:Class文件的常量池中存在大量的符号引用。字节码中的方法调用指令使用对常量池的引用作为参数,在运行时将一些符号引用转换为直接引用。转换是动态链接。这个讲解会涉及到很多概念,比如常量池、符号引用等,想要理解这些概念,就需要了解class文件的结构。内容太多,这里就不一一详述了。方法退出:简单来说就是用来标记当前方法执行完成后,应该回到下一条指令的执行位置。比如上面的栗子,执行完add()之后,应该会回到e1.add(),继续执行main()后面的代码。4.堆对于大多数应用程序来说,这个区域是JVM管理的最大的一块内存。线程共享主要用于存储对象实例和数组。另外,堆区还涉及到JVM-GC(GarbageCollection)中一项非常重要的工作。从图中我们可以看出栈和堆的关系。对于新的对象,栈中的局部变量表只会存储堆中的地址引用,具体实例变量的空间分配在堆中。并且堆中的内存区域会被分成两部分,年轻一代和老一代。一般来说,新生代占内存的1/3,老年代占内存的2/3;新生代分为伊甸区(Edenarea)和两个幸存者区(survivorareas),分别占新生代空间的8/10、/1/10、1/10。也就是说如果堆内存区有600M,那么新生代200M,老年代400M,Eden区160M,S0区20M,S1区20M。这样划分区域的目的是什么?这个答案跟JVM的GC机制有关。首先,在程序开始的时候,所有的实例对象都会在Eden区产生。当Eden区满了,此时会触发minorgc,jvm会使用gcroots的查找方式,将非垃圾对象(复制算法)移动到S0区,对Eden区的其他对象进行处理当实例对象再次填满Eden区时,再次触发minorgc;但这一次,Eden区和S0区的所有非垃圾对象都会被移动到S1,Eden区和S0区会被清除;同样下一次minorgc的同时,就是把Eden区和S1区的非垃圾对象转移到S0中……当然,这个从左到右的切换过程并不是无穷无尽的。在重复minorgc的过程中,每个对象还有一个属性叫做generationalage。每次minorgc对象的分代年龄都会增加1,当达到15(默认)时,该对象就会被放入老年代,成为长期对象。另外还有一种情况,就是从Eden区复制内容到Survivor区时,复制的内容大小超过了S0或者S1区的一半,会直接放入Survivor区oldgeneration,所以oldgeneration需要这么大的面积,不然这些年轻人怎么会抗拒这样做呢~~。oldgeneration的空间虽然很多,但总会有满的时候。这时候麻烦的事情就出现了——fullgc。在fullgc中,jvm会先触发STW(Stop-The-World),即暂停所有其他java进程,回收整个内存模型中的内存资源,导致用户响应超时或系统无响应,非常important对于并发度比较高的系统(比如秒杀活动)影响程度极高。通过gc机制,我们可以想出一个简单有效的JVM优化方法,就是减少fullgc的次数,怎么减少呢?只需要调整老年代和年轻代的内存空间分配,让minorgc过程中尽可能的消除大部分垃圾对象。比如这种java-Xmx3072-Xms3072M-Xmn2048M-Xss1M-Xmx3072M:设置JVM最大可用内存为3072M。-Xms3072M:设置JVM初始内存为3072M。该值可以设置为与-Xmx相同,以避免每次垃圾收集完成时JVM都重新分配内存。-Xmn2048M:将新生代大小设置为2G。增加新生代后,老年代的大小会减小。但是这个值对系统性能影响很大,Sun官方推荐配置为整个堆的3/8。-Xss1M:设置每个线程的堆栈大小。JDK5.0之后每个线程的栈大小为1M,之前每个线程的栈大小为256K。调整更多应用程序线程所需的内存大小。在相同的物理内存下,减小这个值可以产生更多的线程。GCRoots:在上面的gc过程中,我们也提到了JVM是如何判断垃圾对象的。简单来说就是从gcroots的根开始(也就是局部变量表中的引用对象),一路找引用关系,能找到的对象都是非垃圾对象,会被搬到下一个应该是在小区里去。当区域被清空时,剩余的物体将被一起清理而无需担心。5.总结除了栈和堆之外,还有程序计数器、方法区(元空间)、局部方法栈,比较容易理解。程序计数器:用来记录下一个的位置当前指令执行完成后的指令,由执行引擎完成相应的修改操作。方法区(元空间):存储常量、静态变量、类信息等。本地方法栈:类似于java虚拟机栈,但它存放的是native方法执行时的局部变量等数据存放位置。因为native方法一般不会用java语言写的,常用的方法就是.dll文件中的方法(用C/C++写的)。例如,Thread类中的start()方法会在运行时调用start0()方法。查看源码时会看到privatenativevoidstart0();此方法是本地方法。本地方法的作用相当于一个“接口”,用来连接java和其他语言的接口。另外,对于新建的对象,无论是在栈的局部变量表中,还是在方法区的空间中,只存储对象在堆中的地址(引用),具体的空间分配在堆中。最后,最近有很多朋友找我要一份Linux学习路线图,所以我结合自己的经验,利用业余时间熬夜一个月,整理了一本电子书。无论你是面试还是自我提升,相信都会对你有所帮助!免费送给大家,只求大家给我点个赞!电子书|LinuxDevelopmentLearningRoadmap也希望有小伙伴可以和我一起把这本电子书做得更完美!获得?希望老铁们来个三连击,让更多人看到这篇文章。推荐阅读:干货|程序员和高级架构师免费发送工件的必备资源|支持搜索的资源网站
