JVM
类加载结束,class文件(因为有主方法)里面携带着成员方法,成员属性会进入方法区,然后new 出来的对象放在堆内存中,然后地址值放在栈中.所以我们要进行垃圾回收.但是如果是在静态属性和静态代码块,会先加载,地址值放在方法区中.
JVM内存模型
栈内存:方法 运行时候才会进入内存,里面储存局部变量
堆内存:new 出来的数据都会进行堆内存
方法区:字节码文件加载时候进入的内存
本地方法区:管理操作系统的相关资源,比如查询本地时间
寄存器:管理cpu的
如何效验对象是否被回收
可以重写object类中的finalize方法,当垃圾回收器开始执行的时候,会执行该方法.
怎么通知垃圾回收器清理垃圾
调用system.gc
1. 如何判断对象是否存活
什么对象会被当作垃圾?
当一个对象没有被引用的对象是一个垃圾。
提示:计数器
JDK 1.2时,引用计数算法
对每个对象创建一个计数器,如果有一个引用去指向它,计数器+1,当一个引用不再指向它,这个计数器-1,最后判断这个计数器是否大于0,如果大于0,说明有引用指向它,如果不大于0,表示没有引用指向它。
引用计数算法失效的原因:循环引用问题
public class Demo01 {
Demo01 instance = null;
public static void main(String[] args) {
Demo01 a = new Demo01();
Demo01 b = new Demo01();
a.instance = b;
b.instance = a;
a = null;
b = null;
}
}
根据引用计数算法,在a=null和b=null时,彼此之间他们的计数器都是大于0的,a对象有b去指向它,同时b对象有a去指向它,但是当引用一旦为null,其对象无法调用,本质上说他们应当是垃圾,所以引用计数算法存在缺陷。
可达性算法:
选取一个节点作为GC ROOTS,其他的引用和对象去指向这个GC ROOTS,如果这些对象能够到达GC ROOTS顶点,那么说明这个对象不是垃圾,反之是一个垃圾,指向GC ROOTS形成的链称为GC链。
4种对象:
- 虚拟机栈(栈帧中本地变量表所指向的对象)
- 方法区中类静态属性引用的对象
- 方法区的常量
- tive方法中的引用的对象
应用场景
尽量不要去创建很大的对象(存太多数据):因为GC回收算法从来不对大的对象堆进行内存压缩,在堆中大的内存块会浪费太多cpu时间
不要频繁去new 生命周期很短的对象:对导致内存不连续,有许多碎片
4种引用:
不同的引用类型,主要体现的是对象不同的可达性状态和对垃圾收集的影响。
- 强引用 常见的普通对象引用。只要超过作用域或显式赋值为null,就可以被垃圾回收。
- 软引用(soft reference)相对弱化一些,可以让对象豁免一些垃圾收集。在系统发生内存溢出异常之前,会清理软引用指向的对象。SoftReference类实现软引用,通常是实现对内存敏感的缓存。
- 弱引用(weak reference) 无论当前内存是否足够,都会回收只被弱引用关联的对象。WeakReference类实现弱引用。
- 虚引用,幻象引用(Phantom Reference)最弱,无法通过其获得对象实例。唯一目的是跟踪对象被垃圾回收器回收的活动,关联对象被收集器回收之前会收到一个系统通知。仅仅提供了对象被finalize之后做某些事情的方法。
finalize 对象的自我拯救
设计的目的是保证对象在垃圾收集前完成特定资源的回收。JDK9已被标记为deprecated
对象的三种状态:
- 可达
- 可恢复状态
- 不可达
当满足两个条件(它就有可能会调用finalize方法):
- 该方法被重写
- finalize有且只能被执行一次
满足条件后会被放到F-Queue队列,由低优先级的Finalizer线程去执行,不能保证执行成功。
finalize()能做的,try-finally做的更好,更及时,所以不鼓励使用这种方法拯救对象。
2. java虚拟机运行时数据区
多线程运行时JVM内存图
按照jvm规范,java虚拟机所管理的内存包括以下几个运行时数据区域:
程序计数器
program counter register,可以看做当前线程所执行的字节码的行号指示器。通过改变这个计数器的值来选取下一条需要执行的字节码指令。每条线程都需要一个独立的程序计数器。如果线程执行的是java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是native方法,计数器值为空(undefined)。
java虚拟机栈
同程序计数器一样,也是线程私有的,生命周期和线程相同。每个方法执行都会创建一个栈帧。每个方法从调用到执行完毕的过程,对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
本地方法栈
和栈作用类似,为native方法服务。
java 堆
该内存区域的唯一目的就是存放对象实例,规范中:所有对象实例都在这里分配内存。但JIT编译器的发展和逃逸分析技术的成熟,使其不再绝对。
java堆是垃圾收集器管理的主要区域。很多时候也被称作“GC堆”。从内存回收的角度,根据分代收集算法,java堆可以细分为:年轻代和年老代,比例1:2。再详细的有Eden空间、From Survivor空间(s0)、To Survivor(s1)空间。比例为8:1:1。
方法区
在jvm规范中规定:方法区所属于堆,而常量池属于方法区,所以常量池在堆空间;在jvm实现中:并没有遵守这套规范。方法区有个别名:non-heap。
Hotspot的方法区也被称为永久代(Permanent Generation),因为设计团队使用永久代来实现方法区而已。1.7把永久代的常量池移出。1.8把永久代移出,增加了元数据区。
和堆一样,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
常量池中14种常量的类型,基本数据类型对应的有Integer、Double、Float、Long
JVM大小
JVM大小是一个区间,初始大小(totalMemory)和最大使用大小(maxMemory)
实际开发中通常调节成一样的,避免不必要的资源浪费,通常调节为内存的3/4
3. 内存分配与回收策略
- 大的方向,是在堆上分配内存,对象主要分配在新生代的Eden区,少数情况直接分配年老代(触发分配担保原则)
- minorGC:年轻代GC,非常频繁,回收速度比较快
- majorGC/Full GC:年老代GC,经常伴随至少一次minorGC,速度比minorGC慢10倍以上。
- 对象优先在Eden区分配
- 大对象直接进入年老代(因此写程序时避免写“短命大对象”,提前触发垃圾收集),典型很长的字符串或数组
- 长期存活的对象将进入年老代(如池对象)
- 年老代的特点:对象存活率高,没有额外空间进行分配担保
垃圾收集算法:
年轻代采用复制清除算法。
年老代采用标记-清除和标记整理(标记压缩)算法。
标记-清除算法(Mark-Sweep)
分为标记和清除两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
不足:一个是效率问题,两个过程效率都不高;一个是空间问题,会产生大量不连续的内存碎片,导致分配较大对象时,无法找到足够连续内存而提前触发另一次垃圾收集动作。
复制-清除算法(copying)
对象优先在Eden区分配,每个对象定义一个对象年龄(age)计数器。当eden区满了,此时触发minorGC-> 将eden区中存活的对象一次性复制到s0区,年龄设为1,再清理掉eden区内存,当s0区满了,触发minorGC,此时会将s0区存活的对象一次性复制到s1区,再清理掉s0区内存, s0->s1 。每熬过一次minorGC,年龄加1,默认到15岁就会晋升到年老代。
年轻代特点:对象98%朝生夕死。特殊场景下,survivor空间不够,即年轻代销毁垃圾的速度赶不上生产垃圾的速度,那么会触发分配担保原则,将对象移动到年老代。
只需要付出少量存活对象的复制成本即可完成收集。
优点:速度快;没有内存碎片
标记整理算法(Mark-Compact)
标记过程同标记-清除算法,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后清理掉边界以外的内存。
优点:内存连续
缺点:自身对象特征就决定,速度慢
垃圾收集器
stop the world
CMS 并行收集器 几乎不会停顿
4. 类加载器
类加载结束,class文件里面携带着成员方法,成员属性会进入方法区,然后new 出来的对象放在堆内存中,然后地址值放在栈中.所以我们要进行垃圾回收.但是如果是在静态属性和静态代码块,会先加载,地址值放在方法区中.
4.1 类加载器的介绍
ClassLoader负责类装载过程中的加载阶段 ,虽然只用于实现类的加载动作,但是,对于任意一个类,都需要由加载它的类加载器和这个类本身一同确定其在虚拟机中的唯一性。
4.2 方法
ClassLoader的重要方法:
public Class loadClass(String name) throws ClassNotFoundException
装入并返回一个Class -> 类加载器的父类双亲委派机制protected final Class defineClass(byte[] b,int off,int len)
定义一个类,不公开调用protected final Class findClass(String name)
适用于寻找这个名称对应的Class实例,这个方法不保证父类双亲委派机制
4.3 分类
-
启动类加载器(Bootstrap Classloader)
使用C++实现,是虚拟机自身的一部分,负责将存放在
<JAVA_HOME>\lib
目录中,或者被-Xbootclasspath
参数所指定的路径中的,且是虚拟机识别的(按照文件名识别,rt.jar
)类库加载到虚拟机内存。无法被程序直接引用。 -
扩展类加载器(Extension Classloader)
负责加载
<JAVA_HOME>\lib
目录中的,或者被java.ext.dirs
系统变量指定的路径中的所有类库,开发者可以直接使用扩展类加载器 -
应用程序类加载器(Application Classloader)
这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,一般也称为系统类加载器,负责加载用户类路径(ClassPath)上指定的类库。程序默认类加载器。
5. 双亲委派模型(Parents Delegation Model)
5.1. 概念
双亲委派模型中除了启动类加载器之外其余都需要有自己的父类加载器
父子类关系是逻辑上的父子类关系,并不是继承上的父子类关系
当一个类收到了类加载请求时: 自己不会首先加载,而是委派给父加载器进行加载,每个层次的加载器都是这样。
所以最终每个加载请求都会经过启动类加载器。只有当父类加载返回不能加载时子加载器才会进行加载。
双亲委派的好处 : 由于每个类加载都会经过最顶层的启动类加载器,比如 java.lang.Object
这样的类在各个类加载器下都是同一个类(只有当两个类是由同一个类加载器加载的才有意义,这两个类才相等。)
如果没有双亲委派模型,由各个类加载器自行加载的话。当用户自己编写了一个 java.lang.Object
类,那样系统中就会出现多个Object
,这样 Java 程序中最基本的行为都无法保证,程序会变的非常混乱。
5.2 打破父类双亲委派机制
- jdk1.0出现的类加载器,父类双亲委派机制是jdk1.2 出现的,向前兼容
- 热部署:热部署的每一个类都会形成一个类加载器,众多类加载器形成一个网状结构,从而破坏了父类双亲委派机制
- 根类加载器加载了rt.jar下的内容, 而rt.jar下的内容一旦加载,对应本次操作结束,但rt.jar下的内容会使用到某些第三方的jar包,此时就需要反向委托线程类上下文类加载器来反向加载这些内容,如jdbc驱动的问题。
6. JVM调优
6.1说一下 jvm 调优的工具?
JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。
- jconsole:用于对 JVM 中的内存、线程和类等进行监控;
- jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。
6.2 常用的 jvm 调优的参数都有哪些?
- -Xms2g:初始化推大小为 2g;
- -Xmx2g:堆最大内存为 2g;
- -XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
- -XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
- –XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
- -XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
- -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
- -XX:+PrintGC:开启打印 gc 信息;
- -XX:+PrintGCDetails:打印 gc 详细信息。
tomcat调优
1.tomcat中jvm调优
tomcat中jvm优化:存在于bin/catalina.sh文件中进行配置
export JAVA_OPTS="-server -Xms1600M -Xmx1600M -Xss512k -XX:+AggressiveOpts -XX:+UseBiasedLocking -XX:PermSize=128M -XX:MaxPermSize=256M -XX:+DisableExplicitGC -XX:MaxTenuringThreshold=31 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -Djava.awt.headless=true"
- -server: 必须配置在第一位,变成企业级服务器,默认client
- Xms/-Xmx: 堆内存中初始化大小和最大大小,配置成一样的
- -Xss512k 配置栈内存大小,通常不超过2M
- -XX:+AgressiveOpts: 将JDK中最新最稳定的功能注入到当前环境,提升效率
- -XX:+UseBiasedLocking 优化底层线程锁,对高并发访问很重要,对长短不一的请求,排队,会自动进行优化
- -XX:MaxTenuringThreshold 调整默认年龄,新生代存活的次数
- -XX:+DisableExplicitGC:禁止显式调用GC 也就是system.gc()方法
- -Djava.awt.headless=true:默认放在配置参数的最后一行,有些图片显示在开发环境好用,生产环境不好用,用这个配置解决
- -XX:PermSize XX:MaxPermSize 配置方法区大小,放jar包
2.tomcat启动方式
- BIO 性能最差的一种,开发不要用这种方式启动
- NIO tomcat7默认启动方式为bio,tomcat8默认启动方式为nio
- apr 配置较为繁琐
- maxThreads=“600” tomcat支持的最大线程数
- minSpareThreads=“100” 最小备用线程数
- maxSpareThreads=“500” 最大备用线程数,活跃状态但拿不到线程数的100
- acceptCount=“700” 线程达到maxThreads后,后续将请求(不是活跃的)放置到一个等待队列中,acceptCount就是队列大小,当这个队列也满了,直接refuse connection,默认是100