JVM 学习大坑入门
JVM
JVM是运行在操作系统上的。
结构
类加载器
虚拟机自带的加载器
- 启动类加载器(BootStrap) C++ :java 打印出来为null -> 打印 .parent 会出空指针
- 扩展类加载器(Extension)JAVA -> 打印 .parent 出来为null
- 应用程序类加载器(AppClassLoader)->java也叫系统类加载器,加载当前应用的classpath的所有类 -> 打印 .parent 出来为Extension
用户自定义加载器
- 用户可以定制类的加载方式
双亲委派机制
- 加载类的顺序从顶层往下匹配,先去bootstrap找,找不到去extension找,再找不到去App找。
- 用以保证沙箱安全,即自己写的代码不会污染java底层代码
执行引擎
- 负责解释命令,提交操作系统
native方法栈
- 负责存放java之外的语言或者系统底层的方法。
PC寄存器
- 存在于cpu中,本质上是一个指针,指向下一条执行的代码的内存地址。
- 他是当前线程所执行的字节码的行号指示器。
- 如果执行的是native方法,计数器是空的
方法区
- 供各线程共享的运行时内存区域
- 存储了每一个类的结构信息,例如运行时的常量池,字段,方法数据,构造函数和普通方法的字节码内容。
- 在不同的虚拟机里实现是不一样的,最典型的就是永久代和元空间。
- Java7以前是永久代
- Java8变味了元空间
- 实例变量存在堆内存中,与方法区无关
栈
- 栈管运行,堆管存储
- 程序 = 算法+数据结构 / 框架+业务逻辑
- 栈的生命周期跟线程相同,线程结束,栈内存释放,因此栈本身不存在GC问题,且是私有的。
- 栈保存了8种基本类型的变量+对象的引用变量+实例方法。
- 栈帧 = java被扔进虚拟机的栈空间的方法
- 栈帧保存三种数据:
- 1.本地变量(输入参数输出参数以及方法内的变量)
- 2.栈操作(记录出栈入栈)
- 3.战阵数据:包括类文件,方法等等
- 栈的运行原理:当一个方法被调用,就换创建一个栈帧,将其压入栈,秉持着先入后出的原则,逐步执行,每个方法执行的时候都会创建一个栈帧用来存储局部变量表,操作数栈,动态链接,方法出口等信息,栈的大小和具体的JVM的实现有关,通常在256K~756K之间,与等于1Mb左右。
- 栈溢出的报错是 java.lang.StackOverflowError->SOF Error
栈,堆,方法区的交互和关系
- 实例对象放在堆里面
- Java方法区中会存放类的元数据(结构信息)的存储地址
- 栈中的reference存储的是对象的内存地址
堆
- java7和java8的堆,将永久存储区变成了元空间
- 堆内存逻辑上分为三部分:新生+养老+永久(java8:元空间)
- 物理上有新生代+养老代
1)简单版
- 新生区的GC指的是Y(young)GC(轻GC):Eden区基本全部清空,没被清空的移动到幸存者0区(也称作from区),简写(S0)
- 第二次GC如果s0并没有被清空,幸存者0区和幸存者1区(to区)就会进行交换
- 交换15次之后对象还没被GC收集,则进入到养老区
- 养老区如果满了,进行Full GC (FGC)(重GC)
- Full GC 多次后发现养老区空间无法腾出来,JVM就会报OOM异常即对内存溢出。
2)较为详细
- 幸存者0区:s0->from区
- 幸存者1区:s1->to区
- 幸存者两个区谁是from谁是to不确定, GC后,为空的区是to区
- 堆内存中内存空间占比,新生代1/3,老年代2/3
- 新生代内部伊甸园区空间为8/10,两个幸存者区都是是1/10
- YGC的过程:复制->清空->互换
- YGC要求伊甸园区全部清空,因此要将幸存下来的对象从伊甸园区和survivorFrom中复制到survivorTo,年龄+1(第一次是把伊甸园区->sfrom)
- 永久代(对应方法区),虽然JVM贵方将方法去描述为堆的一个逻辑分区,但是他还有一个别名叫Non-Heap非堆
- 方法区是JVM规范中应该拥有的一个分区,在HotSpot虚拟机中,用永久代来方法去的规范,所以可以理解成方法区是一个接口,永久代是HotSpor对JVM中方法区的一个实现(1.8之前,1.8之后变成了元空间,但是1.8的元空间使用的内存是本地内存区域)
- 用就去用来存放JDK所携带的CLass,Interface的元数据,并不会被GC。
堆参数调优
- 调节堆内存大小(-Xms:初始化大小 -Xmx:最大 -Xmn:新生代大小)
- java8将永久代取消,变成了元空间,元空间并不使用虚拟机内存,而是用本机物理内存,字符串池和类的静态变量放入java堆中,类的元数据放入native memory中。
- OOM有八种
- java默认使用物理内存的1/4
重要参数
- -Xms1024Mb:设置初始分配配大小,默认为物理内存的1/64
- -Xmx1024M:最大分配内存,默认为物理内存的1/4
- -XX:+PrintGCDetails:输出详细的GC日志
- 查看CPU信息和内存信息:(Runtime.getRuntime().xxxxx
- Object = new Object():左边默认4kb,右边8Kb
- 生产环境,jvm初始内存和最大内存设置一样,避免程序和GC争抢内存,同防止内存忽高忽低,产生停顿。
- 堆内存溢出报错: java.lang.OutOfMemoryError:Java heap space
GC日志分析
YGC日志
- [GC (Allocation Failure)
- [PSYoungGen: 1016K->504K(1024K)]年轻代的大小经过GC之后的变化
- 1040K->624K(1536K), 0.0007451 secs] 总共的堆大小经过GC之后
- [Times: user=0.00 sys=0.00, real=0.00 secs]
- [名称,GC前内存->GC后内存,(该区域总大小)]
FGC日志
- [Full GC (Ergonomics)
- [PSYoungGen: 1024K->267K(1024K)]
- [ParOldGen: 504K->399K(512K)] 1528K->666K(1536K),
- [Metaspace: 3285K->3285K(1056768K)], 0.0052950 secs]
- [Times: user=0.00 sys=0.00, real=0.00 secs]
GC
- GC是什么:
-
分代收集算法:
1.次数上频繁收集Young区
2.次数上较少收集Old区
3.基本不动perm区(JDK1.7) - FGC的速度一般要比YGC慢上10倍以上
1.引用计数法
- 对象被引用了指针+1,引用一次+1,取消引用就-1,为0的时候就清楚。
- 缺点:
- 1.每次对对象赋值时均要维护引用计数器,且计数器本身也有一定的消耗。
- 较难处理循环引用
2.复制算法
- 复制算法使用在年轻代,也就是说YGC使用的是复制算法
- 复制算法默认是15岁,可以通过-XX:MaxTenuringThreshold(JDK1.8之前)
- 复制算法将内存分为两块,ende+from 和 to
- 优点是不会产生内存碎片
- 缺点:
- 1.它浪费了一半的内存
- 2.如果对象存活率很高,那我们需要将所有对象都复制一遍,并将所有引用地址重置一边,复制这一工作所花费的时间,在对象存活率达到一定程度时就会很多,因此对于这种算法保持高效率的前提是对象的存活率要低。
- 原理:从根集合开始,通过便利从From中找到存活对象拷贝到To中,From和To交换。
3.标记清除
- 老年代一般是用标记清楚和下面的标记压缩算法。
- 算法分为标记和清楚两个阶段,先标记处要回收的对象,然后统一回收这些对象。
- 优点:减少了空间占用
- 缺点:
- 产生大量的内存碎片
- 更加耗时
4.标记压缩
- 全称:标记清楚压缩算法->标记整理算法
- 原理:先进行标记清楚,然后再次扫描,将对象向一段移动,产生连续的内存区域。
- 缺点:时间消耗高,需要考虑移动对象的成本
java9之后存在更好的垃圾回收算法
G1算法
JMM(java内存模型)
- volatile是java虚拟机提供的轻量级的同步机制
- JMM俗称java内存模型,有三个特点:
- 可见性:当一个线程对共享变量做了修改后其他线程可以立即感知到该共享变量的改变
- 原子性:成功就一起成功,不然就一起失败
- 有序性: Java 内存模型中的指令重排不会影响单线程的执行顺序,但是会影响多线程并发执行的正确性
- 本身是一种抽象的概念,并不真实存在,描述的是一组规范或规则。
- JMM规定所有变量都存储在主内存,但线程对变量的操作必须在工作内存(栈空间)中进行,因此需要将主内存变量拷贝到自己的工作内存,然后修改,然后再写回到主内存。这个时候其他线程就可以看见主内存的变量被改变,所以其他工作线程需要同步已经更改完的变量。
代码加载
- JVM语法规定 静态块>构造块>构造方法
- static修饰的只加载一次