JVM详解

为了让自己以后更好查阅,所以根据自己理解记录jvm。
可参考官网的描述:java虚拟机
也可以参考该链接:Java虚拟机

JVM内存结构

在这里插入图片描述
**多线程共享内存区域😗*方法区、堆。
**每一个线程独享内存😗*java栈、本地方法栈、程序计数器。

堆:
Java虚拟机具有一个在所有Java虚拟机线程之间共享的堆。堆是运行时数据区,从中分配所有类实例和数组的内存。
堆是在虚拟机启动时创建的。对象的堆存储由自动存储管理系统(称为垃圾收集器)回收;对象永远不会显式释放。Java虚拟机实现可以为程序员或用户提供对Java虚拟机堆栈初始大小的控制,并且在动态扩展或收缩Java虚拟机堆栈的情况下,可以控制最大和最小大小。堆的内存不必是连续的。
以下异常条件与Java虚拟机堆栈相关:
如果线程中的计算需要比允许的Java虚拟机更大的堆栈,则Java虚拟机将抛出StackOverflowError。
如果可以动态扩展Java虚拟机堆栈,并尝试进行扩展,但是可以提供足够的内存来实现扩展,或者如果没有足够的内存来为新线程创建初始Java虚拟机堆栈,则Java虚拟机机器抛出一个OutOfMemoryError。
堆:Java堆是程序员需要重点关注的一块区域,因为涉及到内存的分配(new关键字,反射等)与回收(回收算法,收集器等);
方法区:也叫永久区,用于存储已经被虚拟机加载的类信息,常量(“zdy”,"123"等),静态变量(static变量)等数据。(jdk1.8已经将方法区去掉了,将方法区移动到直接内存)

java 栈:线程私有,生命周期和线程,每个方法在执行的同时都会创建一个 栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等
信息。方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程;栈里面存放着各种基本数据类型和对象的引用;

运行时常量池:运行时常量池是方法区的一部分,用于存放编译期生成的各种字面(“zdy”,"123"等)和符号引用。
直接内存:不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域;
1)如果使用了NIO,这块区域会被频繁使用,在java堆内可以用directByteBuffer对象直接引用并操作;
2) 这块内存不受java堆大小限制,但受本机总内存的限制,可以通过MaxDirectMemorySize来设置(默认与堆内存最大值一样),所以也会出现OOM异常;

堆和栈的区别
1)堆和栈功能上的区别
以栈帧的方式存储方法调用的过程,并存储方法调用过程中基本数据类型的变
量(int、short、long、byte、float、double、boolean、char等)以及对象的引
用变量,其内存分配在栈上,变量出了作用域就会自动释放;
而堆内存用来存储Java中的对象(就是new出来的)。无论是成员变量,局部变量,还是类变量,
它们指向的对象都存储在堆内存中;
2)堆和栈在线程共享和线程私有区别
栈内存归属於单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。
堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。
3)空间大小
栈的内存要远远小于堆内存,栈的深度是有限制的,如果递归没有及时跳出,很可能发生StackOverFlowError问题。
你可以通过-Xss选项设置栈内存的大小(这个参数是设定单个线程的栈空间)。-Xms选项可以设置堆的开始时的大小,-Xmx选项可以设置堆的最大值

根据以下代码来具体说明jvm虚拟机如何运作的:

public class Math{
	public static final int initData = 666;
	public static User user = new User();
	
	public int compute(){ //一个方法对应一块栈帧
		int a = 1;
		int b = 2;
		int c = (a+b) * 10;
		return c;
	}
	public static void Math(String[] args){
		Math math = new Math();
		math.compute();
		System.out.println("test");
	}
}

在这里插入图片描述
可以根据如上图片的命令 javap -c xx.class, 通过JVM指令手册进行分析。
以下是分析图:
在这里插入图片描述
Java虚拟机栈描述的是Java方法执行的内存模型:每个方法被调用的时候都会创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机中从入栈到出栈的过程。
上述main()-栈帧中局部变量表存储的是堆中math对象的内存地址,同理方法区中存储了堆中user的内存地址。

操作数栈:是一块临时的内存区域,用来存放程序在运行中临时操作数。

动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用,一部分会在类加载阶段或第一次使用的时候转化为直接引用(如final、static域等),称为静态解析,另一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。

程序计数器:较小的内存空间,当前线程执行的字节码的行号指示器;各线程之间独立存储,互不影响;(简单意思就是CPU多线程切换,记录从哪一行继续执行)

本地方法栈::本地方法栈保存的是native方法的信息,当一个JVM创建的线程调用native方法后,JVM不再为其在虚拟机栈中创建栈帧,JVM只是简单地动态链接并直接调用native方法;(主要是因为之前某些方法用的是c语言,java语言与c语言进行跨源)

JVM内存分配与回收

主要详细讲解堆

在这里插入图片描述
对象会优先在Eden区分配:大多数情况下,对象在新生代中Eden区分配。当Eden区没有足够空间进行分割时,虚拟机将发起一次Minor GC。

Minor GC/Young GC:指发生新生代的垃圾手机动作,Minor GC非常频繁,回收速度一般也比较快。
Full GC/Major GC:一般回收老年代,年轻代,方法区的垃圾,Full GC的速度一般比Minor GC的慢10倍以上。

在这里插入图片描述
碎碎念模式开启:
GC roots还会引用其他的对象,在这一链条中都是存活的对象。不在这链条中的对象就是可回收对象。
凡是在这链条上能找到的对象就会移动到Survivor区中,垃圾对象就会一次性清理掉。当进行minor gc后,还没被销毁的对象就会移动到Survivor区中。对象会有个分代年龄,就是+1,
jvm调优的目的就是减少full gc。

如何判断对象可以被回收
堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是 要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。
引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为的对象就是不可能再被使用的。
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。所谓对象之间的相互引用问题,如下面代码所示:除了对象objA和objB相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为0,于是用计数算法无法通知GC回收器回收他们。

 public class ReferenceCountingGc {
   object instance = null;
   public static void main(String[] args) {
		ReferenceCountingGc objA = new ReferencecountingGc();
		ReferenceCountingGc objB = new ReferenceCountingGc();
		objA. instance = objB;
		objB. instance = objA;
		objA= null;0  
		objB = null;
		}
	}

长期存活的对象将进入老年代
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age) 计数器。
如果对象在Eden出生并经过第一-次 Minor GC后仍然能够存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor中每熬过- -次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁), 就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数-XX: MaxTenuring Threshold来设置。

对象动态年龄判断
当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了,例如Survivor区域里现在有一批对象, 年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年龄判断机制-般是在minor gc之后触发的。
Eden与Survivor区默认8:1:1
大量的对象被分配在eden区,eden区满了后会触发minor gc,可能会有99%以上的对象成为垃圾被回收掉,剩余存活的对象会被挪到为空的那块survivor区,下一 次eden区满了后又会触发minor gc,把eden区和survivor去垃圾对象回收, 把剩余存活的对象一次性挪动到另外一块为空的survivor区。因为新生代的对象都是朝生夕死的,存活时间很短,所以JVM默认的8:1:1的比例是很合适的, 让Eden区尽可能的大,Survivor区够用即可。JVM默认有这个参数-XX:+UseAdaptiveSizePolicy,会导致这个比例自动变化,如果不想要这个比例变化可以设置-XX:+UseAdaptiveSizePolicy。

JVM调优案例

根据业务场景以及内存进行计算
在这里插入图片描述
常规想法是这样的:
弊端:当Eden区执行了Minor gc后存活的对象会放入Survivor区,但是根据对象动态年龄判断中,如果存入的对象大小超过Survivor的50%就会直接进入Old(老年代),那样在三至四分钟左右就会执行一次Full GC,执行一次Full GC就会执行STW,STW就会暂停所有的线程,从而影响性能。
在这里插入图片描述
优化:让其几乎不发生Full GC。
将Eden区内存扩大,old区内存减小,将-Xmn(新生代内存大小)参数设置大一点。
在这里插入图片描述
这样做的优点:当Eden区放满时执行Minor GC, Minor gc后存活的60M对象会挪到S0区,根据对象动态年龄判断60M不大于S0区的50%,所以不会进入老年代;当Eden区再次放满时会执行Minor GC, 同时会将Eden,S0区垃圾一起直接销毁掉,这样就基本上不会 发生Full GC。

调优参数设置:
java -Xms3072M -Xmx3072M -Xmn2048 -Xss1M -XX:MetaspaceSize=256M --XX:MaxMetaspaceSize= 256M -jar microservice-eureka-server.jar

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章