1、类加载机制
类加载的全过程:当类被引用(new或调用main方法)时,会进行类加载过程。
IO流读取的字节码文件,然后根据规则校验类的字节码文件是否正确(这一步在C、C++)实现,将类的静态变量赋予初始值(int、boolean等),然后将类的信息初始化到方法区中,同时会生成一个java.lang.class的对象到堆中,作为开发者获取类信息的切入点。
双亲委派机制:
类加载的过程中会先从顶层的类加载器(引导类加载器)中加载,没有找到类后才会从拓展类加载器中加载,再者才会从应用类加载器中加载。简单点说就是,先从父级开始找,没有找到才开始从儿子开始找。
好处:1、沙箱安全机制:能保证java的基础类不能被随意串改,从而保证安全性。
2、避免类的重复加载:当父类加载完后发现已存在该类,则没必要再从子类中查找。
自定义类加载器:可以通过继承ClassLoader类,实现其findClass来实现自定义类的加载,打破双亲委派机制。
Tomcat打破双亲委派机制
tomcat每一个webapp都拥有自己的webappClassLoader,在加载自己目录下的class文件时相互隔离,但是加载tomcat共用类库时也有shareClassLoader、CommonClassLoader,从而保证不同的war包下面class的相互隔离机制。
2、JVM整体结构
JDK整体结构:jre其实已经包含了java的基础类库以及java虚拟机,jdk包含jre和其他的一些工具包,在开发过程中jre已经完全足够。
JVM虚拟机:
- 方法区(元空间):存放一些常量、静态变量、类的元信息
- 堆:存放对象的信息
- 栈、本地方法栈以及程序计数器是和线程绑定的,当线程被创建时,会分配对应的内存空间。
- 栈:栈又可以分为很多个栈帧(一个方法对应一个栈帧,递归轮询很多次时会导致栈溢出),每一个栈帧里面包含:局部变量表、操作数栈、动态链接、方法出口。从字节码文件运行顺序分析,当运行到初始化时,先先将局部变量名加载局部变量表,然后将数值加载到操作数栈(如果需要运算,则加载到CPU内核寄存器中运算后再压回操作数栈),运算完后压入对应的局部变量表,直到方法运行完,则从方法出口返回结果。注意:由于JVM逃逸分析,有时会将不会逃逸的对象压入栈中,这个JVM默认是开启逃逸分析的,可通过配置来开启关闭(-XX:+DoEscapeAnalysis)。
- 本地方法栈:主要加载java中C、C++实现的方法接口(即native修饰的方法)。
- 程序计数器:用来标记方法运行到具体位置的。
堆的结构划分(年轻代【eden、s0、s1】、老年代):
eden区:对象初次来到堆中,默认会放入Eden区,当对象很大(eden区剩余空间)时,会直接加载到老年代;或者可以设置大对象阈值(-XX:PretenureSizeThreshold只在Serial 和ParNew收集器下才生效),当超过阈值时,也会直接进入老年代。当eden区放满时,会触发Minor GC。
s0、s1: s0、s1一定有一个为空,当触发Minor GC时,未被回收的对象头信息的分代年龄会+1,同时加载到一个空的s区。当分代年龄大于15(不同的收集器会有不同)时,对象会加载到老年代,可以设置-XX:MaxTenuringThreshold。
老年代:当老年代空间占满时,会触发full GC。
3、JVM优化相关:
- 指针压缩:java1.6之后支持,默认是开启指针压缩的XX:+UseCompressedOops,当堆内存大于32G时,指针压缩会失效,会强制使用64位(即8B)来寻址,所以一般小于32G为好。当堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间。
- 逃逸分析:有时会将不会逃逸(内有被外界引用)的对象压入栈中,开启逃逸分析可以减少Minor GC的次数。jdk1.7之后默认是开启的。-XX:+DoEscapeAnalysis
- 标量替换:当JVM逃逸分析出该对象不被外界引用,会不创建该对象,而是直接加载该对象的成员变量来替代,这样就不会需要一大块连续的内存空间来存储对象。-XX:+EliminateAllocations。注:标量为一个分解的量(int、long),聚合量为可分解的量(对象)