线程私有: 程序计数器、虚拟机栈、本地方法栈
线程共享: 堆、方法区、直接内存(非运行时数据区的一部分)
JDK 1.8同JDK 1.7比,最大的差别就是:元数据区取代了永久代。元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。
①类加载(判断类是否被加载、解析、初始化过) --> ②分配内存(分配内存的方式) --> ③初始化零值 --> ④设置对象头 --> ⑤执行init方法
局部变量
基本数据类型:变量名和变量值存储在 方法栈 中。
引用数据类型:变量值存储在 方法栈 中(存储的是堆中对象的内存地址),所指向的对象是存储在堆内存中(如new出来的对象)。
全局变量
基本数据类型:变量名和变量值存储在 堆 内存中。
引用数据类型:变量名存储的是所引用对象的内存地址,变量名和变量值存储在 堆 内存中。
Java虚拟机:执行Java程序。程序开始执行时它才运行,程序结束时它就停止。你在同一台机器上运行三个程序,就会有三个运行中的Java虚拟机。Java虚拟机总是开始于一个main()方法,这个方法必须是公有、返回void、只接受一个字符串数组。在程序执行时,你必须给Java虚拟机指明这个包或main()方法的类名。 Main()方法是程序的起点,它被执行的线程初始化为程序的初始线程。程序中其他的线程都由它来启动。Java中的线程分为两种:守护线程(daemon)和普通线程(non-daemon)。守护线程是Java虚拟机自己使用的线程,比如负责垃圾收集GC的线程就是一个守护线程。当然,你也可以把自己的程序用setDeamon设置为守护线程。包含Main()方法的初始线程不是守护线程。只要Java虚拟机中还有普通的线程在执行,Java虚拟机就不会停止。如果有足够的权限,你可以调用exit()方法终止程序。
优点: CMS最主要的优点在名字上已经体现出来——并发收集、低停顿。
缺点: CMS同样有三个明显的缺点。
①分配内存;②确保被引用对象的内存不被错误的回收;③回收不在被引用的对象的内存空间。
1、新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。当Eden区没有足够的空间时,就会触发Young GC来清理新生代。
2、老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。
3、混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
① eden区满的时候,会触发Minor GC。
② 新创建的对象大小大于Eden区所剩下的空间大小时,会触发Minor GC。
一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去自定义类加载器去控制字节流的获取方式(重写一个类加载器的loadclass()方法)。数组类型不通过类加载器创建,它由Java虚拟机直接创建。
所有的类都由类加载器加载,加载的作用就是将.class文件加载到内存。
注意: 接口加载过程与类加载过程稍有不同。当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,当真正用到父接口的时候才会初始化。
JVM类加载器分为两类:JVM自带的类加载器和自定义类加载器。
① 隔离加载类;② 修改类加载的方式;③ 扩展加载源;④ 防止源码泄漏。
继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器。
每一个类都有一个对应它的类加载器。系统中的ClassLoader在协同工作的时候会默认使用双亲委派模型。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派给父类加载器的loadClass()处理,因此所有的请求最终都应该传送到顶层的启动类加载器BootstrapClassLoader中。当父类加载器无法处理时,才由自己来处理。当父类加载器为null时,会使用启动类加载器BootstrapClassLoader作为父类加载器。
①首先判断该类是否已经被加载;
②该类未被加载,同时父类不为空,交给父类加载;
③如果父类为空,交给启动类加载器Bootstrap ClassLoader加载;
④如果类还是无法被加载到,则触发findclass,抛出ClassNotFoundException(findclass这个方法当前只有一个语句,就是抛出ClassNotFoundException),如果想自己实现类加载器的话,可以继承ClassLoader后重写findclass方法,加载对应的类)。
双亲委派模型 保证了Java程序的稳定运行,可以避免类的重复加载(JVM区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类★★★),也保证了Java的核心API不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object类的话,那么程序运行的时候,系统就会出现多个不同的Object类。
在JVM中表示两个Class对象是否为同一个类存在两个必要条件:
换句话说,在JVM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。
自定义加载器的话,需要继承ClassLoader。如果我们 不想打破双亲委派模型 ,就重写ClassLoader类中的findclass()方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果 想打破双亲委派模型 则需要重写loadClass()方法。
想要实现热部署可以分以下三个步骤:
Tomcat破坏了双亲委派原则,提供隔离的机制,为每个web容器单独提供一个WebAppClassLoader加载器。每一个WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给CommonClassLoader加载,这和双亲委派刚好相反。
JConsole、VisualVM、Java Mission Control(前3个为JDK自带)、MAT(Java堆内存分析工具)、GChisto(GC日志分析工具)、GCViewer(GC日志分析工具)、JProfiler(性能分析工具)、arthas(阿里开源诊断工具)
top
列出系统各个进程的资源占用情况。top -Hp 进程id
列出对应进程里面的线程占用资源情况。printf "%x\n" PID
把线程id转换为16进制。jstack PID
打印出进程的所有线程信息,从打印出来的线程信息中找到上一步转换为16进制的线程id对应的线程信息。分析:内存飚高如果是发生在java进程上,一般是因为创建了大量对象所导致,持续飚高说明垃圾回收跟不上对象创建的速度,或者内存泄露导致对象无法回收。
jstat -gc PID 1000
查看GC次数、时间等信息,每隔一秒打印一次。jmap -histo PID | head -20
查看堆内存占用空间最大的前20个对象类型,可初步查看是哪个对象占用了内存。jmap -dump:live,format=b,file=/home/myheapdump.hprof PID dump
堆内存信息到文件。优化Minor GC频繁问题:通常情况下,由于新生代空间较小,Eden区很快被填满,就会导致频繁Minor GC,因此可以通过 增大新生代空间-Xmn来降低 Minor GC的频率 。
Full GC的排查思路大概如下:
1. 清楚从程序角度,有哪些原因导致full GC?
· 大对象:系统一次性加载了过多数据到内存中(比如SQL查询未做分页),导致大对象进入了老年代。
· 内存泄漏:频繁创建了大量对象,但是无法被回收(比如IO对象使用完后未调用close方法释放资源),先引发full GC,最后导致OOM.
· 程序频繁生成一些长生命周期的对象,当这些对象的存活年龄超过分代年龄时便会进入老年代,最后引发full GC。
· 程序BUG
· 代码中显式调用了gc方法,包括自己的代码甚至框架中的代码。
· JVM参数设置问题:包括总内存大小、新生代和老年代的大小、Eden区和S区的大小、元空间大小、垃圾回收算法等等。
2. 清楚排查问题时能使用哪些工具?
JDK 的自带工具,包括jmap、jstat等常用命令:
# 查看堆内存各区域的使用率以及GC情况
jstat -gcutil -h20 pid 1000
# 查看堆内存中的存活对象,并按空间排序
jmap -histo pid | head -n20
# dump堆内存文件
jmap -dump:format=b,file=heap pid
可视化的堆内存分析工具:JVisualVM、MAT等
3. 排查指南
· 查看监控,以了解出现问题的时间点以及当前full GC的频率(可对比正常情况看频率是否正常)
· 了解该时间点之前有没有程序上线、基础组件升级等情况。
· 了解JVM的参数设置,包括:堆空间各个区域的大小设置,新生代和老年代分别采用了哪些垃圾收集器,然后分析JVM参数设置是否合理。
· 再对步骤1中列出的可能原因做排除法,其中元空间被打满、内存泄漏、代码显式调用gc方法比较容易排查。
· 针对大对象或者长生命周期对象导致的full GC,可通过 jmap -histo 命令并结合dump堆内存文件作进一步分析,需要先定位到可疑对象。
· 通过可疑对象定位到具体代码再次分析,这时候要结合GC原理和JVM参数设置,弄清楚可疑对象是否满足了进入到老年代的条件才能下结论。
内存泄漏是内在病源,外在病症表现可能有:(内存泄漏可能导致内存溢出,排查步骤和内存泄漏类似)
严重内存泄漏往往伴随频繁的Full GC,所以分析排查内存泄漏问题首先还得从查看Full GC入手。主要有以下操作步骤:
jps
查看运行的 Java 进程 IDtop -p [pid]
查看进程使用 CPU 和 MEM 的情况top -Hp [pid]
查看进程下的所有线程占 CPU 和 MEM 的情况printf "%x\n" [pid]
,输出的值就是线程栈信息中的 nid。jstack 29452 > 29452.txt
,可以多抓几次做个对比。jstat -gcutil [pid] 5000 10
每隔 5 秒输出 GC 信息,输出 10 次,查看 YGC 和 Full GC 次数。通常会出现 YGC 不增加或增加缓慢,而 Full GC 增加很快。或使用 jstat -gccause [pid] 5000 ,同样是输出 GC 摘要信息。或使用 jmap -heap [pid] 查看堆的摘要信息,关注老年代内存使用是否达到阀值,若达到阀值就会执行 Full GC。jmap -histo:live [pid]
输出每个类的对象数量,内存大小(字节单位)及全限定类名。jhat -port 8000 29471.dump
,浏览器访问 jhat 服务,端口是 8000。或使用第三方式具分析的,如 JProfiler 也是个图形化工具,GCViewer 工具。Eclipse 或以使用 MAT 工具查看。或使用在线分析平台 GCEasy。