JVM结构
类加载子系统:在JVM启动时或者类在运行时将需要的class文件加载到jvm中
运行时数据区:在JVM运行时操作所分配的内存区域(堆、方法区、虚拟机栈、程序计数器及本地方法栈)
执行引擎:负责执行class文件中的字节码指令
本地方法接口:主要是调用C或C++实现本地方法及返回结果
JVM内存区域(运行时数据区)
1)、程序计数器:可以看做当前线程所执行的字节码的行号指示器;字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器来完成。(线程私有;内存空间小;无内存溢出)
2)、java虚拟机栈:描述java方法执行的线程内存模型,每一个方法被执行时,java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态链接及方法出口等信息,每一个方法从调用直至结束的过程,就对应一个栈帧从虚拟机中入栈到出栈的过程。(线程私有;生命周期同线程;StackOverFlowError:线程请求的栈深度大于虚拟机所允许的深度;OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展无法申请到足够内存时)
3)、本地方法栈:与java虚拟机栈发挥作用类似,但是是为虚拟机使用到的本地(Native)方法服务的。(线程私有;也会有StackOverFlowError和OutOfMemoryError异常)
4)、堆:java堆是java虚拟机所管理的内存中最大的一块,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。也是垃圾收集器管理的主要区域。(受JIT技术逃逸分析技术逐渐成熟影响,栈上分配、标量替换优化技术导致了所有对象都在堆上分配变得不那么“绝对了”)(线程共享;主流虚拟机都是按照可扩展实现的,通过-Xms和-Xmx控制;堆中没有内存完成实例分配,并且堆也无法扩展时,将会抛出OutOfMemoryError异常)
5)、方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。(线程共享;当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常;jdk1.8以前方法区也被称为永久代,jdk1.8以后被称为元空间,采用直接内存)
运行时常量池:是方法区的一部分,用于存放类的各种常量信息。(包含字符串常量池,字符串常量池jdk1.7以前在方法区,jdk1.7以后在堆中)
JIT:Just In Time Compiler的简称,即时编译器。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器就是JIT。
逃逸分析(Escape Analysis):并不是一种直接优化代码的手段,而是为其它优化手段提供依据的分析技术(通过此技术可以判定哪些代码可以被优化,如何被优化)。
逃逸分析的基本行为就是分析对象动态作用域:
方法逃逸:当一个对象在方法中被定义后,它可能被外部方法所引用则称为方法逃逸。例如用该对象作为调用参数传递到其它方法中。
线程逃逸:当一个对象在方法中被定义后如果能外部线程访问则为线程逃逸。比如将该对象赋值给类变量或可以在其它线程中访问该对象。
如果能证明一个对象不会逃逸到方法或线程之外,或者逃逸程度比较低(只逃逸出方法而不会逃逸出线程),则可能为这个对象实例采取不同程度的优化,如栈上分配、标量替换、同步消除;
栈上分配(Stack Allocation):如果确定一个对象不会逃逸出方法之外,那让这个对象在栈上分配内存,对象所占用的内存空间就可以随栈帧出栈而销毁;(HotSpot还未实现)
标量替换(Scalar Replacement):如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散的话,那么程序真正执行时将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。将对象拆分后,除了可以让对象的成员变量在栈上(栈上存储的数据,有很大概率会被虚拟机分配至物理机器的高速寄存器中存储)分配和读写外,还可以为后续进一步优化手段创建条件。
标量(Scalar),是指一个数据已经无法分解成更小的数据来表示了,例如Java虚拟机中的原始数据类型(int、long及reference类型等)都不能进一步分解。相反的,如果一个数据可以继续分解,那它就称作聚合量(Aggregate),Java中的对象就是最典型的聚合量。
同步消除(Synchronization Elimination):如果逃逸分析能确定一个变量不会逃逸出线程,无法被其它线程访问,这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以消除掉(锁消除)。例如:Vector和StringBuffer这样的类,它们中的很多方法都是有锁的,当某个对象确定是线程安全的情况下,JIT编译器会在编译这段代码时进行锁销除来提升效率。
类加载过程
装载(加载):指的是通过类的全限定名将.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,作为访问方法区这些数据结构的入口
链接:
验证:主要确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全(文件格式验证、元数据验证、字节码验证、符号引用验证)
准备:为类变量分配内存,并将其初始化为默认值
解析:把符号引用转换为直接引用
初始化:对静态变量及静态代码块进行初始化
使用
卸载
类加载器分类
启动类加载器(引导类加载器,BootstrapClassLoader):加载lib目录下的核心类库,例如rt.jar、charsets.jar等
扩展类加载器(ExtClassLoader):加载lib目录下的ext扩展目录中的jar类包
应用程序类加载器(AppClassLoader):加载classpath路径下的类包(即自己写的那些类)
自定义类加载器(CustomClassLoader):加载用户自定义目录下的类包
双亲委派模型(Pattern Delegation Model):要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器。这里父子关系通常是子类通过组合关系而不是继承关系来复用父加载器的代码。(一言概之,双亲委派模型,其实就是一种类加载器的层次关系)
双亲委派机制
若一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。每个层次类加载器都是如此。因此所有的加载请求都会传送到Bootstrap类加载器(启动类加载器)中,只有父类加载器反馈自己无法加载这个请求(它的搜索范围没有找到所需的类)时,子加载器才会尝试自己去加载。
双亲委派机制存在原因:
1)、沙箱安全机制:防止核心API库被随意篡改(例如自己写java.lang.String类不会被加载);
2)、避免类的重复加载:父类加载器已加载,子类加载器就没有必要再加载一次了,保证被加载类的唯一性。
自定义类加载器:继承ClassLoader类重写findClass方法(重写的最后使用defineClass方法将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组)
打破双亲委派机制:
1)、重写loadClass方法;
2)、线程上下文类加载器。
列举一些你知道的打破双亲委派机制的例子。为什么要打破:
Tomcat,应用的类加载器优先自行加载应用目录下的class,并不是先委派给父加载器,加载不了才委派给父加载器。打破的目的是为了完成应用间的类隔离。
JNDI通过引入线程上下文类加载器,可以在Thread.setContextClassLoader方法设置,默认是应用程序类加载器,来加载SPI的代码。有了线程上下文类加载器,就可以完成父类加载器请求子类加载器完成类加载的行为。打破的原因,是为了JNDI服务的类加载器是启动器类加载,为了完成高级类加载器请求子类加载器(即上文中的线程上下文加载器)加载类。
OSGi,实现模块化热部署,为每个模块都自定义了类加载器,需要更换模块时,模块与类加载器一起更换。其类加载的过程中,有平级的类加载器加载行为。打破的原因是为了实现模块热替换。
JDK 9,Extension ClassLoader被Platform ClassLoader取代,当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。打破的原因,是为了添加模块化的特性。
loadClass方法,该方法的大体逻辑如下:
1)、 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
2)、 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加 载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。
3)、 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的 findClass方法来完成类加载。
// 检查当前类加载器是否已经加载了该类
Class c = findLoadedClass(name);
if (c == null) {
if (parent != null) {
//如果当前加载器父加载器不为空则委托父加载器加载该类
c = parent.loadClass(name, false);
} else {
//如果当前加载器父加载器为空则委托引导类加载器加载该类
c = findBootstrapClassOrNull(name);
}
}
ClassLoader.loaderClass(String name)和Class.forName(String name)区别:
Class.forName得到的class是已经初始化完成的
ClassLoader.loadClass得到的class是还没有链接的
内存分配方式
类加载完成后,接着会在Java堆中划分一块内存分配给对象。根据Java堆中是否规整有两种内存的分配方式 :
指针碰撞(Bump the pointer):若Java堆中的内存是规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存也就是把指针向空闲空间那边移动一段与内存大小相等的距离。例如:Serial、ParNew等收集器。
空闲列表(Free List):若Java堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,就没有办法简单的进行指针碰撞了。虚拟机必须维护一张列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。例如:CMS这种基于Mark-Sweep算法的收集器。
选择哪种分配方式是由 Java 堆是否规整来决定的,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
JVM 对象的访问定位有哪两种?HostSpot版本中用哪种?
句柄和直接指针两种方式,
句柄: 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;
直接指针: 如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象的地址。
HotSpot 中使用直接指针,使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
对象的创建流程
1)、类加载检查:先执行相应的类加载过程。如果没有,则进行类加载;
2)、分配内存:根据内存是否规整,有分两种配方式:指针碰撞和空闲列表,Java 堆空间规整的话采用指针碰撞,不规整采用空闲列表;
3)、初始化:JVM需要将分配到的内存空间都初始化为零值(如int值为0,boolean值为false,对象为null等等);
4)、设置对象头:虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中;
5)、执行方法:最后调用构造方法。
jvm垃圾回收
判断对象是否失活:
1)、引用计数算法:在创建对象时添加一个引用计数器,每当有一个地方引用它时,计数器值加一,当引用失效时,计数器值减一,任何时候计数器值为零的对象就是不可能再被使用的。
缺点:无法解决对象之间循环引用的问题
2)、可达性分析算法:通过一系列称为“GC Roots”的根对象作为起始节点,根据引用关系向下搜索,搜索走过的路径称为引用链,若一个对象到GC Root没有引用链时,则证明此对象不可用
注:不可达对象不一定就是可回收对象,从不可达对象到可回收对象必须至少经历两次标记过程,如果两次标记没有逃脱成为可回收对象则基本就真的成为可回收对象了。
可以作为“GC Roots”根节点的对象:
1)、虚拟机栈栈帧中的本地变量
2)、方法区中的静态变量
3)、本地方法栈中的变量
4)、方法区中的常量引用
finalize()方法最终判定对象是否存活
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一 个对象死亡,至少要经历再次标记过程。 标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。
1)、第一次标记并进行一次筛选。 筛选的条件是此对象是否有必要执行finalize()方法。 当对象没有覆盖finalize方法,对象将直接被回收。
2)、第二次标记 如果这个对象覆盖了finalize方法,finalize方法是对象脱逃死亡命运的最后一次机会,如果对象要在finalize()中成功拯救自己,只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,
那在第 二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。
注意:一个对象的finalize()方法只会被执行一次,也就是说通过调用finalize方法自我救命的机会就一次。
java常见引用类型:
1)、强引用:new出来的普通对象,不会被垃圾回收(即使空间不足,也不会随意回收强引用,宁愿抛出OutOfMemoryError)
2)、软引用:将对象用SoftReference软引用类型包裹,正常情况不会被回收,但GC做完后发现释放不出内存空间存放新的对象,则会把这些软引用对象回收掉。可用来实现内存敏感的高速缓存还有浏览器的后腿按钮
3)、弱引用:将对象用WeakReference软引用类型的对象包裹,弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
4)、虚引用:和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。
如何判断一个类是无用的类:方法区主要回收无用的类,无用类需要满足以下三个条件
1)、该类所有实例都已经被回收,也就是java堆中不存在该类的任何实例;
2)、加载该类的ClassLoader已经被回收;
3)、该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射 访问该类的方法。
垃圾收集算法:
1)、标记-清除算法:分为“标记”和“清除”两个阶段,标记存活的对象,统一回收未被标记的对象(一般选该种);也可反过来,标记所需回收的对象,在标记完成后统一回收标记的对象。
缺点:1、空间问题(标记清除后会产生大量不连续的碎片);2、效率问题(若需要标记的对象太多,效率不高)。
2)、复制算法:将可用内存分为大小相同的两块,每次使用其中一块,当该块内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。
缺点:该方法将可用内存缩减为原来一半,大幅减少了空间的可用度。
3)、标记-整理算法:分为“标记”和“整理”两个阶段,标记阶段与“标记-清除”算法一样,而“整理”阶段是将存活的对象向一端移动,然后直接清理掉端边界以外的内存。
分代收集理论:根据对象存活周期的不同将内存划分为几块,一般把java堆划分为新生代和老年代,这样就可以根据各个年代的特点选择合适的垃圾收集算法。比如新生代中,每次收集都有大量的对象(98%)死去,所以可以选择复制算法,只需付出少量的复制成本就可以完成每次垃圾收集,而老年代对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,就必须采用“标记-清除”或“标记-整理”算法进行垃圾收集。
建立基础:分代假说
1)、弱分代假说:绝大多数对象都是朝生夕灭的
2)、强分代假说:熬过越多次垃圾收集过程的对象就越难消亡
3)、跨代引用假说:跨代引用相对于同代引用只占极少数
垃圾收集器:
1)、Serial收集器(-XX:+UseSerialGC)
简单高效,由于没有线程交互的开销,自然可以获得很高的单线程收集效率。
进行垃圾收集工作时必须暂停其他所有线程的工作(STW,Stop The World)
工作在新生代,采用复制算法
串行;单线程
2)、Serial Old收集器(-XX:+UseSerialOldGC)
简单高效,由于没有线程交互的开销,自然可以获得很高的单线程收集效率。
进行垃圾收集工作时必须暂停其他所有线程的工作(STW,Stop The World)
工作在老年代,采用标记-整理算法
串行;单线程
3)、ParNew收集器(-XX:+UseParNewGC)
其实就是Serial收集器的多线程版本,除了多线程外,其余的行为和特点跟Serial收集器一样。(Server模式下的首选新生代垃圾收集器,除Serial收集器外唯一可以跟CMS收集器一起使用的收集器)
进行垃圾收集工作时必须暂停其他所有线程的工作(STW,Stop The World)
工作在新生代,采用复制算法
并行;多线程
4)、Parallel Scavenge收集器(-XX:+UseParallelGC)
类似于ParNew收集器,但更关注吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))
工作在新生代,采用复制算法
并行;多线程
5)、Parallel Old收集器(-XX:+UseParallelOldGC)
Parallel Scavenge收集器的老年代版本,但更关注吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))
工作在老年代,采用标记-整理算法
并行;多线程
6)、CMS收集器(-XX:+UseConcMarkSweepGC)
是一种以获取最短回收停顿时间为目标的收集器。尤其重视服务响应速度,希望停顿时间最短,以给用户带来较好的体验。它是hotSpot虚拟机第一款真正意义上的并发收集器,第一次实现了让用户线程和垃圾回收
线程(基本上)同时工作。
工作在老年代,采用标记-清除算法
并发;多线程
垃圾收集过程:
初始标记:暂停所有的其他线程(STW),标记gc roots能直接关联到的对象,速度很快;
并发标记:从gc roots直接关联的对象开始遍历整个对象图的过程,可以与用户线程并发运行,不需要停顿用户线程,此过程耗时较长;
重新标记:为了修正并发标记期间因为用户程序继续运行而导致标记产生变化的那一部分对象的标记记录,需要暂停所有的其他线程(STW);
并发清除:开启用户线程,同时垃圾收集线程对未标记的对象进行清除。
优点:并发收集、低停顿
缺点:1)、对CPU资源敏感(会和服务抢资源);2)、无法处理浮动垃圾;(在并发清除时,用户线程新产生的垃圾,称为浮动垃圾)3)、采用标记-清除算法,存在大量的空间碎片。
7)、G1收集器(-XX:+UseG1GC)
是一款面向服务端应用的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。
G1将java堆划分为多个大小相等的独立区域(Region),JVM最多可以有2048个Region,一般Region大小等于堆大小除以2048(推荐默认计算方式);也可用参数-XX:G1HeapRegionSize手动指定大小。
G1保留了新生代老年代的概念,但不再是物理隔阂了,它们都是(可不连续)Region的集合;默认年轻代堆内存占5%,可以通过“-XX:G1NewSizePercent”设置新生代初始占比,在系统运行中,JVM会不停的给年轻代增加更多 的Region,
但是最多新生代的占比不会超过60%,可以通过“-XX:G1MaxNewSizePercent”调整。年轻代中的Eden和 Survivor对应的region也跟之前一样,默认8:1:1。
G1垃圾收集器对于对象什么时候会转移到老年代跟之前讲过的原则一样,唯一不同的是对大对象的处理,G1有专门分配 大对象的Region叫Humongous区,而不是让大对象直接进入老年代的Region中。在G1中,大对象的判定规则就是
一 个大对象超过了一个Region大小的50%。
运作过程:
初始标记:暂停所有的其他线程,并记录下gc roots直接能引用的对象,速度很快 ;
并发标记:从gc roots直接关联的对象开始遍历整个对象图的过程,可以与用户线程并发运行,不需要停顿用户线程,此过程耗时较长;
最终标记:为了修正并发标记期间因为用户程序继续运行而导致标记产生变化的那一部分对象的标记记录,需要暂停所有的其他线程(STW);
筛选回收:筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期 望的GC停顿时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划,回收算法主要用的是复制算法,将一个region中的存活对象复制到另一个region中,这种不会像CMS那样 回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片。
具备特点:
并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短StopThe-World停顿时间。部分其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式 让java程序继续执行。
分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
空间整合:与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部 上来看是基于“复制”算法实现的。
可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了 追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段(通过参数"- XX:MaxGCPauseMillis"指定)内完成垃圾收集。
什么场景适合使用G1:
1)、50%以上的堆被存活对象占用
2)、 对象分配和晋升的速度变化非常大
3)、 垃圾回收时间特别长,超过1秒
4)、8GB以上的堆内存(建议值)
5)、停顿时间是500ms以内
8)、ZGC收集器(-XX:+UseZGC)
CMS收集器和G1收集器的区别:
CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用;
G1收集器收集范围是老年代和新生代,不需要结合其他收集器使用;
CMS收集器以最小的停顿时间为目标的收集器;
G1收集器可预测垃圾回收的停顿时间
CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。
对象内存分配和回收策略
java堆分为新生代和老年代(新生代:老年代=1:2)
新生代又分为Eden区和Survivor区,Survivor区又分为From区和To区(Eden:From:To=8:1:1)
在JVM垃圾回收机制中,将应用程序可用的堆空间分为年轻代和老年代,又将年轻代分为Eden区、Survivor区的From和To区,新建对象总是在Eden区中被创建,当Eden区空间已满,就触发一次Minor GC将还被使用的对象复制到From区,这样整个Eden区都是未被使用的空间,可供继续创建对象,当Eden区再次用完,再触发一次Minor GC,将Eden区和From区还在被使用的对象复制到To区。因此,经过多次Minor GC,某些对象会在From区和To区多次复制,如果年龄超过某个阈值,该对象还未被释放,则将该对象复制到老年代,若该空间也已分配完,那么就触发Full GC
Minor GC/Young GC:指发生新生代的垃圾收集动作,非常频繁,但一般回收速度比较快
Major GC/Full GC:一般会回收老年代、年轻代、方法区的垃圾
大对象直接进入老年代(-XX:PretenureSizeThreshold参数可控制大对象的大小,这个参数只在 Serial 和ParNew两个收集器下有效)
长期存活的对象将进入老年代(默认15,-XX:MaxTenuringThreshold可设置)
对象动态年龄判断(当Survivor)
老年代空间分配担保机制(发生在Minor GC前)
为什么要分为Eden和Survivor:
如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。
Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
为什么要设置两个Survivor区:
设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)
主要的JVM参数
1)堆栈配置相关
-Xmx: 最大堆大小。
-Xms: 设置初始堆大小。
-Xmn: 设置年轻代大小。
-Xss: 每个线程的堆栈大小。
-XX:MaxPermSize: 设置持久代大小。
-XX:NewRatio=4: 设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。
-XX:SurvivorRatio=4: 设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:MaxTenuringThreshold=0: 设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。
2)垃圾收集器相关
-XX:+UseParallelGC
-XX:ParallelGCThreads=20
-XX:+UseConcMarkSweepGC
-XX:CMSFullGCsBeforeCompaction=5
-XX:+UseCMSCompactAtFullCollection:
-XX:+UseParallelGC: 选择垃圾收集器为并行收集器。
-XX:ParallelGCThreads=20: 配置并行收集器的线程数
-XX:+UseConcMarkSweepGC: 设置年老代为并发收集。
-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection: 打开对年老代的压缩。可能会影响性能,但是可以消除碎片
3)辅助信息相关
-XX:+PrintGC:打印 gc 信息
-XX:+PrintGCDetails:打印 gc 详细信息
常用命令符
jps:输出JVM进程信息
-q:不输出类名、jar名和传入main方法参数
-m:输出传入main方法的参数
-l:输出main类或jar的全限名
-v:输出传入jvm的参数
jmap:
jmap -heap pid:查看堆内存配置情况及使用情况
jmap -histo pid:统计对象的创建数量
jmap -dump:format=b,file=heapDump pid:生成dump文件与jhat配合使用
jstack:查看jvm中当前所有线程的运行情况和线程当前状态
jstat:可以用来监视VM内存内的各种堆和非堆的大小及其内存使用量,以及加载类的数量。(例:jstat -gc pid)
jinfo:观察进程运行环境参数,包括Java System属性和JVM命令行参数(例:jinfo -flags pid)
怎么打出线程栈信息:
输入jps,获得进程号。
top -Hp pid 获取本进程中所有线程的CPU耗时性能
jstack pid命令查看当前java进程的堆栈状态
或者 jstack -l > /tmp/output.txt 把堆栈信息打到一个txt文件。
可以使用fastthread 堆栈定位,fastthread.io/