先上图
3个部分中运行时数据区最主要
类装载器:加载class文件,(类模板是抽象的,对象是具体的)
1、虚拟机自带的加载器
2、启动(根)类加载器
3、扩展类加载器
4、应用程序加载器(有的地方叫系统类加载器)
双亲委派机制:是安全的。运行一个类之前先向上找 APP——>Ext——>Boot。类加载器接收到类加载请求会将请求向上委托给父类加载器去完成,一直向上委托,直到根加载器,根加载器会检查是否能加载当前这个类Boot,能加载就使用当前的加载器,如果都找不到就抛出异常通知子类加载器加载App。如果都找不到就报class not found。另:BootstrapClassLoader不继承ClassLoader,是由JVM内部实现,所以用java程序获取不到,得到的是null。
栈(虚拟机栈、线程栈):存储当前线程运行所需要的数据,指令,返回地址。八大基本类型、对象引用、实例的方法局部变量,方法的局部变量只在方法的运行作用域范围之内有效 。java虚拟机,只要线程开始运行,当执行到方法的时候,就会给这个方法在线程栈分配一块自己专属的内存区域,这一小块内存空间就叫栈帧内存空间,用来存放方法自己的局部变量。这个栈遵循先进后出(FILO),跟方法的嵌套、调用顺序相符合,有压栈弹栈。
程序计数器:是一个记录着当前线程所执行的字节码的行号指示器。在JVM中,通过程序计数器来记录某个线程的字节码执行位置。因此,程序计数器是具备线程隔离的特性,也就是说,每个线程工作时都有属于自己的独立计数器。程序计数器的值是由执行引擎来修改的。
方法区:是线程共享的,存储字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,所有定义的方法的信息都保存在此区域。如:静态变量static、常量final、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中和方法区无关。
方法区:1.8之前叫永久代,之后叫元空间,是操作系统的物理内存。存放常量、静态变量、类信息。静态的可以被所有线程共享的数据。
本地方法栈:native修饰的(凡是被native修饰的说明java的作用范围达不到了,需要调用底层C语言的库,进入本地方法栈,调用本地方法接口JNI,JNI是为了扩展java的使用融合不同的语言为java所用 ),运行本地方法时分配的内存空间。
元空间:逻辑上存在,物理上不存在,属于堆但是非堆,
栈堆方法区的交互关系:
堆:存放对象的实例
可达性:将“GC Roots”对象作为起点,从这些节点开始向下搜索引用的对象,能找到的对象标记为“非垃圾对象”其余未标记的对象都是垃圾对象。线程栈的本地变量、静态变量、本地方法栈的对象等等都可以作为GC Toots的根节点。没有指针指向的游离对象会被回收。垃圾回收主要是在伊甸园区和养老区。伊甸园满了进行一次轻GC,能活下来的到幸存者取,当整个年轻代都满了则进行一次重GC ,将伊甸园区和幸存者区都清一次,能活下来的去到养老区,当年轻代和养老区都满则OOM。
注意:
不可达对象并不是立即死亡的,要经历两次标记过程:第一次经过可达性分析发现没有与GC Roots相连接的引用链,第二次是在由虚拟机自动建立的finalizer队列中判断是否需要执行finalize()方法。当对象不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖则直接将其回收,若对象未执行过finalize方法,这个对象会被放置在F-Queue队列中,并在稍后由一个虚拟机自动建立的低优先级的Finalizer线程区执行触发finalizer( )方法,但不承诺等待其运行结束。执行结束后,GC会再次判断该对象是否可达,若不可达,进行回收,否则“复活”。每个对象只能执行一次finalize方法,运行代价高昂,不确定性大,且无法保证各个对象的调用顺序。可用try-finally或其他替代。垃圾回收器决定回收某对象时,就会运行该对象的finalize()方法; GC本来就是内存回收了,应用还需要在finalization做什么? ——大部分时候,什么都不用做(也就是不需要finalize方法)。只有在某些很特殊的情况下,比如你调用了一些native的方法,可以要在finaliztion里去调用C的释放函数。
GC Roots的对象有:
- 虚拟机栈(栈帧中的本地变量)中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
JAVA调优的时候是减少full gc。真正要解决的是STW(stop the world)影响使用影响性能,要减少stw的时间,full gc的stw时间比较长。JAVA虚拟机有stw机制的原因,gc的过程会寻找并标记非垃圾对象并stw,线程会卡住,假如没有stw,线程不卡住一直往下执行,此时在gc过程中线程结束,那么该线程所产生的对象则变为垃圾对象却不会被gc掉(因为非垃圾对象标记工作已完成),这是不合理的,不可能一直无限的扫描。每个垃圾收集器都会有stw。
案例1
一个订单系统(服务器4核8G,操作系统分配2~3G,剩下5~6G给虚拟机,堆分配3G老年代占2/3年轻代中伊甸园区和幸存者区8:1:1,1~2G分配给其他),jvm设置如下图,每隔四五分钟就发生一次full gc。
分析:每一次请求创建多个对象,每个对象根据属性类型之和等计算,大概1kb,多个对象大概几十kb,加上每秒并发请求及其他操作,按每秒300单来计算,每秒大概产生60MB的对象(具体根据业务情况)。1秒后都变为垃圾对象(线程结束,局部变量没有了,GC Root根没有了),minor gc 进行回收,但是,有可能在第13、14秒的时候有的线程执行慢,没有结束,执行到一半的时候此时伊甸园区触发minor gc ,发生stw,部分线程没有结束,内部有一批对象不一定是垃圾不能被回收,会移到幸存者区假设这些对象又比较大,超过了幸存区的一半,那么会直接挪到老年代,如此,每过13/14秒发生一次minor gc 没多久老年代就会放满,触发full gc。原因找到
调优:
年轻代调大了一点,让垃圾对象在年轻代就被回收,减少full gc
思考:能否对JVM优化,使其几乎不发生full gc ,很少发生。是可以的。
案例2
大内存的年轻代minor gc调优。单机高并发(如MQ、kafka)系统的java虚拟机优化。假设一条消息1KB,每秒几十万的并发消息就要几百兆(消息持久化到磁盘之后就变为垃圾,还在内存中还被引用着不会被回收),伊甸园区很快就会满,触发minor gc,假如开始几秒钟把伊甸园区发满,后面一两秒中产生的消息还没有来得及存到磁盘那就不是垃圾,会挪到幸存者区,而幸存者区放不下了,便移到老年代,那么每隔几秒就会有一批消息到老年代,进而导致频繁发生full gc。难道调大伊甸园区么?调大后的问题就是堆积的对象变多要遍历判断的对象变多,minor的时间加长,对客户端大量消息请求超时。
解决:并发收集,分片收集。此场景可以用G1收集器,频繁收集但是时间短停顿的时间大大减少了。
堆空间的配置
例子:活跃数据300M(借助jdk等相关工具统计出来)
总堆: 300M*4=1.2g
新生代-450M
者年代=750M
永久代元空间=300M (Fu'll GC 后永久代空间占用的1.2~1.5倍)
JVM中哪些内存区域会发生内存溢出:java.lang.StackOverflowError
栈溢出:一个线程启动默认会分配1M的栈空间。递归时有可能栈溢出
堆溢出:报OOM Error后会标出是哪个空间出现了OOM,如Java heap space堆
方法区(永久代/元空间)溢出:OOM:Metaspace
本机直接内存(堆外溢出)溢出: java. lang.outofMemoryError: Direct buffer menory
/**
* VM Args: - XX: MaxDirec tMemorysize= 100m
* 限制最大直按内存大办100m
*/
public static void main(String[] args) {
ByteBuffer bb = ByteBuffer .allocateDirect(128*1024*1024);
}
JVM在创建对象时采用了哪些并发安全机制
默认是本地线程分配缓冲(Thread Local Allocatlon Buffer,ILAB)
每个线程在Java堆中预先分配一小块私有内存。也就是本地线程分配缓中,这样每个线程都单独拥有一个Buffer,如果需要分配内存,就在自己的Buffer.上分配,这样就不存在竞争的情况,可以大大提升分配效率
---------------------------------------------------------------------------------------------------------------------------------
在本地可使用jvisualvm命令利用可视化图形界面查看本地所有Java进程,以进行调优,但在生产环境一般使用一些Java自带命令,如:
用jps查看运行的程序进程id,随后用各种jdk自带的命令优化
Jmap查看进程信息,实例个数及占用的内存大小
jmap -histo 进程id 查看历史生成的实例
jmap -histo: live 进程id 查看当前存活的实例,执行过程中可能会出发一次full gc
jmap heap 进程id 查看堆信息
Jstack加进程id查找死锁,找出占用CPU最高的线程堆栈信息
Jinfo查看正在运行的Java应用程序的扩展参数
jstat查看堆内存各部分使用量以及类加载数量。查看垃圾回收情况,特别是full gc 比较频繁那么就要进行调优了。先判断频繁发生full gc的原因,如果频繁full gc但有没有内存溢出那么就表示full gc实际上回收了很多对象,那么这些对象最好在young gc过程中就被回收掉,进而不进入老年代,对于这种情况就要考虑到这些存活时间不长的对象是不是比较大,导致年轻代放不下而直接进入了老年代,可以尝试调大年轻代的大小。
jstat -gc pid 评估程序内存使用及GC压力整体情况
此处待补充
近些年使用阿里的Arthas,官方文档 Arthas 用户文档 — Arthas 3.5.4 文档
下载arthas-boots.jar包到服务器 java -jar arthas-boots.jar 运行,会列出所有运行中的Java程序,输入想查看的程序序号进行查看。