JVM虚拟机主要内容:
1、运行时内存区域(内存结构)
2、对象的创建,内存布局以及访问定位
3、垃圾回收
4、内存的分配策略
5、jvm性能监控工具:命令行工具和图形化界面工具
6、类加载机制
7、运行时栈帧结构
类的文件结构,类加载机制,字节码执行引擎,虚拟机编译以及运行时优化
jvm,jre以及jdk三者之间的关系?
内存溢出分析以及解决方案
如何创建一个占用大内存的对象?或者说如何让一个对象占用的内存增加?
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
如何设置虚拟机参数?Run As ->Run Configurations
这三个参数分别代表什么意思?
-XX:+HeapDumpOnOutOfMemoryError OOM之时导出堆镜像到文件
-Xms20m
-Xmx20m
1、-XX:+PrintGCDetails : 打印gc详情
2、System.gc(); 手动调用GC.
3、如何查看GC日志?
System.gc()应该只是用于提醒虚拟机来进行垃圾回收而已,并不会直接输出信息才是,而且这个方法也没有任何返回,是不可能直接被打印输出的。
-verbose:gc 开启gc日志
"Stop-The-World" Java中一种全局暂停的现象
全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互
多半由于GC引起
-Xms20m:堆最小为20M; -Xmx20m:堆最大为20M;
-Xmn15m:新生代内存设置为15M;
-XX:NewRatio新生代和年老代的比例; 4表示年轻代:年老代=1:4, 即年轻代占堆的1/5.
-XX:SurvivorRatio新生代内部Survivor和Eden区域的比例; 示例: 8表示两个Survivor:Eden=2:8, 即一个Survivor占1/10的新生代空间
将堆最大值与最小值设置为一样,可避免堆自动扩展。
-Xmx20m -Xms20m -Xmn15m -XX:+PrintGCDetails
常用的内存分析工具:
1、MemoryAnalyzer:专业用于分析dump文件的工具.查看内存快照。
2、jvisualvm jvm性能分析和调优工具
3、jconsole jvm可视化监控工具
如何查看内存快照文件?在idea中也可以查看内存快照。
里面常用的指标:todo:当前以自己包名开头的类。
如何设置虚拟机参数?
如何生成内存快照?
如何查看内存快照?
如何观测内存变化?
2014年 jdk1.8版本,
2017年 jdk1.9版本,
2018年 jdk10版本发布
.java文件---->javac编译器---->.class文件---->jvm虚拟机---->操作系统执行。
jdk1.8两大新特性:lambda表达式与函数式编程(响应式编程)。
lambda表达式:代码更简洁,但是可读性不好;对匿名内部类的简化操作,对for循环的简化操作。
java虚拟机有很多种类:dalvik.MicrosoftJVM(微软出的一款虚拟机),HotSpot,TaoBaoVM等
高性能的java虚拟机
HotSpot:应用最广泛。
jvm虚拟机内存管理(虚拟机内存结构,虚拟机的内存模型)
1、程序计数器:是当前线程所执行的字节码的行号指示器。
2、Java虚拟机栈:存放的是方法栈帧。
3、本地方法栈:为虚拟机执行native方法服务。两者区别。
4、方法区
5、java堆
在虚拟机运行时的数据内存可分为线程共享区和线程独占区。线程共享区代表每个线程都可以共享的内存区(多个线程共享);线程独占区,即每一个线程都独有的内存区(每个线程独占)。
程序计数器:
1、可以看做是当前线程执行的字节码的行号指示器,通过这个计数器的值来读取下一条(指的是字节码的行数,并不是java代码的行数)需要执行的字节码指令,比如一些循环跳转指令。由于java是2、通过线程轮流切换,然后分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器只会执行一条线程中的指令,所以为了线程切换之后能到正确的执行位置,每条线程都需要有一个独立的线程计数器,这是线程私有的,各个线程互相不影响(每个线程都有自己的程序计数器)。
虚拟机栈:
Java虚拟机栈同程序计数器一样,都是线程私有的,生命周期跟线程相同。
虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈幁,用来存储局部变量表,操作栈,动态链接,方法出口等信息。每个方法从调用直到执行完成的过程,都对应一个栈幁在虚拟机栈中从入栈到出栈的过程。
图表:线程----->栈帧----->局部变量表,操作栈,动态链接,方法出口等
局部变量表:
1、java程序员会把java内存分为堆内存和栈内存,这种划分方式只能说明大多数程序员最为关注的与对象分配关系最为密切的区域是这两块,实际的划分要复杂的多。其中的堆在后面再说,这里所说的栈就是java虚拟机栈,更准确的说应该是虚拟机栈中的局部变量表。
2、局部变量表是一组变量值存储空间,用以存储方法参数与方法内部定义的局部变量(基本数据类型,引用数据类型)。在Java程序被编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了方法所需要分配的最大局部变量表的容量。
栈内存溢出:
StackOverFlow栈内存溢出,发生在虚拟机栈。
Java虚拟机栈的容量大小也是有限的,当创建的栈帧超出了他的容量大小,将会报StackOverFlow异常,即栈溢出异常。
方法递归调用即可:StackOverFlowError。方法之间相互调用,方法不停的进栈。
java堆内存:
1、堆内存用来存放由new创建的对象实例和数组。(重点)
2、Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例 。
3、Java堆是垃圾收集器管理的主要区域。由于现在收集器基本采用分代回收算法,所以Java堆还可细分为:新生代和老年代。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(TLAB)。
4、Java堆可以处于物理上不连续的内存空间,只要逻辑上连续的即可。在实现上,既可以实现固定大小的,也可以是扩展的。
5、如果堆中没有内存完成实例分配,并且堆也无法完成扩展时,将会抛出OutOfMemoryError异常。
方法区:
1、方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据(重点)。 类的版本,字段,方法,接口,字符串常量,static变量,常量池等
2、相对而言,垃圾收集行为在这个区域比较少出现,但并非数据进了方法区就永久的存在了,这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
3、当方法区无法满足内存分配需要时,将抛出OutOfMemoryError异常。
运行时常量池:
1、属于方法区
2、是方法区的一部分,它用于存放编译期生成的各种字面量和符号引用。将在类加载后,进入方法区的运行时常量池中存放。
3、不是同一块内存区域,所以肯定不相同。字节码常量 和 运行时常量(运行时也会产生常量的)
直接内存:
堆外内存。使用Native函数库直接分配堆外内存。
对象的管理机制:
对象的创建流程:
1、给对象分配内存?如何进行内存分配?其实就是指针移动的过程。指针碰撞和空闲列表。
2、线程安全性问题:进行同步处理,其实就是加锁。TLAB:本地线程分配缓冲。
3、初始化对象:初始化默认值,将分配的内存初始化为零值,成员变量默认值。案例。
4、执行构造函数。(3,4的顺序一定要注意)
new 对象的时候,才会加载类?
对象的结构(内存布局):
对象在内存中存储的布局可分为三个部分:对象头、实例数据和对齐填充。
类型指针:虚拟机通过这个指针来确定这个对象是哪个类的实例。
如何获取对象的哈希值?
GC分代回收。
Hotpot VM要求对象起始地址必须是8字节的整数倍,对象头部分正好是8字节的倍数,所以当实例数据部分没有对齐时,需要通过对齐填充来对齐。
对象的访问定位:
如何找到对象的呢?
Java程序通过栈上的reference数据来操作堆上的具体对象。目前主流的访问方式由“使用句柄”和“直接指针”。虚拟机栈上的局部变量区。
Person p=new Person(); 引用通过何种方式访问堆上的对象?
垃圾回收:回收对象,释放内存资源。
在Java虚拟机中,存在自动内存管理和垃圾清扫机制。
如何判定一个对象为垃圾对象?
引用计数器+可达性分析方法。
如何回收?
垃圾回收策略+垃圾收集器
回收策略:标记-清除算法;复制算法;标记-整理算法;分代收集算法。
垃圾回收器:Serial,Parnew,Cms
何时回收?
堆内存:新生代(Eden,伊甸园;Survivor(səˈvaɪvə(r)),存活区;Tenured Gen)和老年代
复制算法:将还存活着的对象复制到另外一块内存上,然后再把已使用过的内存空间一次清理掉。
在对象存活率较高时,复制操作次数多,效率降低.内存浪费的解决方案。
标记-整理算法:针对老年代中的对象进行回收。让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法:复制算法(新生代)+标记整理算法(老年代);
垃圾收集器
Serial收集器(sɪəriəl):单线程的方式,串行的方式,一般在新生代来收集垃圾。Serial Old收集器采用标记/压缩的算法一般用来收集老年代。没有线程切换带来的性能消耗(gc线程启动,导致任务线程停止)。
ParNew收集器:多线程的,采用复制算法和Stop the World的方式。如果是多CPU多Core的物理环境,采用ParNew收集器更佳,提升了吞吐量。实际生产环境下,如果资源延迟的要求特别高的情况下,采用ParNew和CMS收集器组合(多个线程同时进行垃圾回收;降低垃圾回收的时间)。
Parallel收集器(ˈpærəlel):也是复制算法和多线程和Stop the World的方式。Parallel可以控制JVM的吞吐量的大小,是吞吐量优先的收集器,新生代垃圾收集器,达到可控制的吞吐量。
吞吐量=运行用户代码的时间与cpu消耗的总时间的比值(运行用户代码的时间+垃圾回收占用时间)。
-XX:MaxGCPauseMillis 垃圾收集器的最大停顿时间,单位毫秒;时间端,频率高。
-XX:GCTimeRatio吞吐量的大小(0,100)
ParNew收集器与Parallel收集器,应用场景不同。高并发还是用户体验?
CMS收集器是 Concurrent Mark Sweep 的缩写,意为并发标记清除。
G1收集器既可以收集新生代,也可以收集老年代。其实G1收集器在实现的时候将整个内存区间并不是划分成了严格的新生代和老年代。它将整个Java堆划分成为多个大小相等的独立区域,虽然还保留有新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了,他们都是一部分Region的集合。G1主要关注点在于:1. 并行与并发的性能、2. 能够独立的管理Java堆、3. 内存空间整合、4. 可预测的停顿。(多核cpu的优势;并行可以缩短等待时间,并发可以提高响应速度。)
内存分配策略
堆内存分配的基本原则
1、对象优先在 Eden 分配。如何验证?修改虚拟机参数,打印日志。案例
2、大对象直接进入老年代。如何验证?,为什么大对象直接进入到老年代中?执行gc频率非常高,如果大对象放到Eden区域,需要进行频繁的移动,这样就会导致gc效率下降。案例
3、长期存活的对象进入老年代
4、动态对象年龄判定
5、空间分配担保
逃逸分析与栈上分配
随着方法的出栈,对象被释放掉,不需要垃圾回收器进行回收。
逃逸分析:分析出对象的作用域,当前对象是否被其他线程引用;如果发生逃逸就不能在栈上分配,只能在堆上。如果没有发生逃逸,就可以在栈上分配,私有即可。
是否被其他线程引用。(线程共享区与线程独占区)
单例模式所有线程共享。
本质上:是在堆上分配内存还是在栈上分配内存?是线程独占还是线程共享?这是提高性能的一个方面。
Minor GC ,Full GC 触发条件
Minor GC触发条件:当Eden区满时,触发Minor GC。
Full GC触发条件:
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
那么对于 Minor GC 的触发条件:大多数情况下,直接在 Eden 区中进行分配。如果 Eden区域没有足够的空间,那么就会发起一次 Minor GC;对于 Full GC(Major GC)的触发条件:也是如果老年代没有足够空间的话,那么就会进行一次 Full GC。
虚拟机工具(原来老曹敲的都是这些命名)
jdk下的bin目录,工具对应的源码在tools.jar下面。
jps:java process status;java进程管理器
cmd---->jps:本地所有的jvm进程。
1)本地虚拟机唯一Id,有多少个虚拟机实例,就会存在多少个id。local virtual machine id;与当前系统的id是一致的。
jps -l:查看本地进程的包名+类名com.yunque.www.springbootdemo.jvm.jps.TestJPS
jps -m:查看主类中方法接受的参数(运行时传入主类的参数)
jsp -v:查看虚拟机的参数配置。
jstat详解
类装载,内存,垃圾收集,jit编译的信息
jstat详解 命令 依赖于jps命令。
jstat -gcutil 端口号 1000 10 每秒打印一次,打印十次。
jinfo:查看和调整虚拟机参数。
PrintGCDetails 是否开启了GC日志打印功能?
jmap:JVM Memory Map命令用于生成heap dump文件。观察运行中的jvm内存占用情况。
dump : 生成堆转储快照, 可以使用这个命令。
jmap -dump:format=b,file=d:\a.bin 8264(端口号) 随时可以生成内存快照。
内存快照,内存某一秒的状态。
jhat:JVM Heap Analysis Tool命令是与jmap搭配使用。
jhat d:\a.bin。
jstack:获取线程快照。
jconsole:内存监控,线程监控(wait等待状态),死锁(还是不太理解? 有空请教一下)。
jvisualvm:一定要看概述,描述了主要的进程信息。
为jvisualvm安装插件。
性能调优
知识(理论),工具(监控工具),数据,经验。
问题1:经常有用户反映长时间出现卡顿的现象。(偶尔,当时堆内存50G)
处理思路:
1、优化sql,建索引之类的(排除,如果某个特定的功能比较慢,可以去优化sql)。
2、监控CPU
3、监控内存:内存使用量正常,但是经常会发生Full GC,5-6s。论文可能出现大的对象,大对象存储在老年代中,大对象非常多,导致老年代内存不够用,直接出发Full GC。频繁创建大量的大对象,导致Full GC频繁触发。
经验总结:
部署多个web容器,每个web容器的堆内存指定为4G。
☆ 可以手动设置老年代和新生代的内存大小。
☆ 老年代内存比较大,就会导致GC回收时间特别长,响应比较慢(内存越大,GC耗时越长)。
☆ 把堆内存修改小,指定为4G。
☆ 由单体应用程序转化成微服务集群,通过Nginx进行负载均衡。
☆ 如何查看类编译之后的字节码文件?jd-gui可以,反编译工具。
☆ 分代收集算法:不同的年龄代,使用不同的垃圾收集算法。
☆ 堆内存分配的基本原则(内存分配策略,对象在内存的分配原则)
☆ 高并发解决方案:Nginx+微服务集群。考虑到硬件环境,服务器内存多大。
☆ JVM虚拟机的缺陷:内存,CPU.
问题2:不定期出现内存溢出,把堆内存增大,也无济于事。导出堆内存快照信息,没有任何异常信息。内存监控正常。
处理思路: NIO--->堆外内存,直接内存。不能触发GC,导致内存溢出。
问题3:使用消息队列进行削峰(数据不对等)。大量数据传递,jvm处理不了。
回顾:
原理+工具+案例
java虚拟机的运行时区域。
并发:ConCurrency
jvm工具:命令行工具+图形化工具(jconsole,VisualVM)
文档:做后台的人,一定要会写文档,接口文档,概要设计文档(领导特别喜欢看文档,文档结构和格式;想往高级资深走,必须要学会看文档)。
Class文件
类加载机制:类加载的时机,类加载的过程,类加载器三部分。
刨根问底,了解底层原理。运行在java虚拟机上的语言。
类加载机制概述
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。
懒加载策略
类的生命周期:加载、连接(验证,准备,解析)、初始化、使用、卸载。
什么时候要开始一个类的类加载?
以下7种情况下,必须立即对类进行初始化(而加载、验证、准备自然需要在此之前开始)。
new关键字,主启动类,父类,反射,调用一个类的静态方法的时候(当前类要被初始化);读取或设置一个类的静态字段的时候(不能被final修饰;如果被)。
如何确定一个类是否被初始化了?
类的初始化,static代码块首先被执行。
因为main方法,索引Son类首先可能被初始化;又因为存在继承关系,所以父类要首先被初始化。
parent初始化... son初始化..
类不会被初始化的情况:
1)通过子类引用父类的静态字段,子类不会被初始化,父类会被初始化。
2)调用类的常量(类不会被初始化,常量直接被替换了) 哪种调用方式?两种
3)通过数组定义来引用类(不会被初始化)。
class文件,二进制字节码文件,二进制字节流文件。全是一些jvm虚拟机指令。
1、加载:在内存中生成一个代表这个类的class对象, 作为这个类的各种数据的访问入口。Class对象存放在方法区中,不是在堆内存里面。(.class文件加载到内存里面)
与类加载器有关系。
2、验证:为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,不会危害虚拟机自身的安全。
3、准备:为类变量分配内存并设置类变量初始值(默认值)的阶段,这些内存都将在方法区进行分配。
方法区:class对象(数据访问入口),运行时常量池,类变量的存储区。
final修饰,常量池中,直接替换成常量(指定的值)。
4、解析:虚拟机将常量池中的符号引用替换为直接引用的过程。对符号引用进行解析的过程。
符号引用:全限定类名;直接引用:内存地址。
类与接口的解析,字段解析,类方法解析,接口方法解析。
5、初始化:类加载的最后一步,真正执行类中定义的java程序代码。初始化类变量和其他资源。
对类进行编译,不执行。clinit方法的执行。
编译器收集的顺序是由语句在源文件中出现的顺序决定的。
静态的定义是有顺序的,静态代码块只能访问定义在此之前的变量。
类加载器:“通过一个类的全限定名来获取描述此类的二进制字节流”,实现这个动作的代码模块称为“类加载器”。加载二进制字节流,在方法区形成一个类的Class对象。
四种类加载器:自定义类加载器
应用程序类加载器:加载用户类路径上所指定的类库。比如Spring类库。
获取一个对象的类型类。
运行时栈帧结构
方法调用链,一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表:用于存放方法参数和方法内部定义的局部变量。
jvm不会给局部变量赋初始值,只给全局变量赋初始值。
slot复用。
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用。
方法无论以何种方式退出,都需要返回到方法被调用的位置,程序才能继续执行。
Java方法调用主要分为解析调用和分派调用,分派调用包括:静态分派和动态分派。
静态类型 与 实际类型。
子类重写父类中的方法,方法调用:优先寻找当前类中是否含有被重写的方法,如有直接调用本类方法;若没有找到,则在父类中寻找,直到找到为止并调用。
动态类型语言支持
静态类型的语言在非运行阶段,变量的类型是可以确定的,也就是说变量是有类型的;
动态类型的语言在非运行阶段,变量的类型是无法确定的,也就是说变量是没有类型的,但是值是有类型的,也是就说在运行期间可以确定变量值的类型。
int a=10;
var a=10;