Java那点事儿
开局一问
java代码是怎么运行的一个main方法执行就输出了结果,究其原因,我们都知道是java虚拟机提供了运行环境JRE,我们在学习java第一节的时候就知道需要配置jdk的环境变量,然后javac编译,java执行。等等,那么到底是怎么的方式就把我们写的HelloWorld从哪个命令行的黑框中打印出来呢。那我呢就需要从Class文件的加载开始分析分析了。
字节码如何加载
我呢就基于HotSpot虚拟机,JDK版本1.8来进行分析,我们都知道虚拟机划分出如下区域,java方法栈,本地方法栈,堆,pc寄存器,方法区,这么多区域到底是怎么工作的,java方法栈呢就是每当Java方法执行时,会在当前线程的的java方法栈生成一个栈帧,存放局部变量和字节码操作数,当方法执行完毕之后则弹出栈帧。本地方法栈则是java调用本地方法的存放区域,堆数据空间呢则是程序运行时的数据存储区域,分为年轻代,年老代,元数据空间,这个地方呢也衍生了一波JVM调优的业务线,合理配置的垃圾回收器和JVM参数可以让整个虚拟就更加高效的运行,堆内存的垃圾回收机制我另开篇去介绍,方法区存放的是就是加载到虚拟就的class文件,各个区域均有个了解,都知道.java文件进过编译生成的是字节码文件.class,但是字节码文件是不可以直接运行,也就是说字节码文集是无法直接被机器所识别的。java作为一个高级语言。一次编写,到处运行的前提就是jvm虚拟机这个代码托管环境。我们所编写的java代码进过编译之后加载到jvm虚拟机,由虚拟机和物理机沟通来实现软件的功能。虚拟机把字节码翻译成机器码,这个过程呢细分有两种方式,一种是解释执行,就是流程化遇到了再翻译成机器码,第二种是(JIT)即时编译,就是会把这个方法里的所有字节码全部变成机器码,这两者各有利弊,第一种不需要编译过程,遇到就开始翻译,第二个则是第一次编译比较好使,往后执行则会高效很多,HotSpot则使用时混合编译的方式,先去解释执行,然后对于执行比较频繁的热点代码以方法为单位进行即时编译,java虚拟机提供的C1,C2编译器,C1也叫Client编译器,一般用于GUI程序,编译时间较短,性能不如C2,C2又叫做Server编译器,针对的是峰值性能程序,所以编译时间比较长,性能也比C1要强,1.7之后的HotSpot采用的是的混合编译也可以叫做分层编译的,就是先会使用C1去编译,C1中的热点代码再会用C2去编译,这个编译线程收守护线程,不会影响应用正常运行,编译线程数是更具CPU进行自动设置的C1:C2=1:2的数目设定,被即时编译后的代码在下一次调用时候回直接替换原来的调用块。
java分为八大基本数据类型和引用数据类型,引用数据类型范围类,接口,数组,泛型,基本数据类型呢虚拟机就先预定义好了,数组类型呢会有虚拟机直接生成,泛型在编译之后会被擦除,class文件被加载之后会进入方法区根据类型类和接口则会根据对应的字节流将其加载,一个类彻底加载到虚拟机中一共有三个步骤分别是加载阶段,链接阶段(主要干的一件事是解析),初始化阶段,首先加载阶段通过类加载器将方法区的字节流加载,类加载器共有三类一个是由C++提供为BootStrapClassLoader,有兴趣的会发现在程序中断点查看时该启动器得值null,该加载器会加载JRE的lib下最基础的类,或者由-Xbootclasspath指定的类,extensionClassLoader加载的是jre下的lib下ext中的jar,还有一个是我们程序运行的ApplicationClassLoader应用类加载器,类加载的过程是先去找父类加载器去加载,如果父类加载器可以加载则不会被子类加载器加载,这就是类加载的双亲委派,双亲委托可以避免一个字节码被多次加载入内存,当然也有打破双亲委派的比如JDBC驱动,SPI等,当然我们也可以使用自定义类加载器来加载我们自己的类,在编译的时候对类进行加密,在加载时候去解密。自定义类加载器增加一些特有的信息在类中。因为类加载器加载的类是惟一的,可以做高低版本的隔离。类加载之时回去校验这些字节码是否符合虚拟就约束规范,如果非法则会抛出,进过加载阶段之后就进入了解析阶段,进过加载时会对静态字段分配内存,构造与该类相关联的方法表,然后在初始化的时候会对其进行初始化,在类进行解析时会把方法区唯一标识的符号引用(类名+方法名+参数类型+返回值类型)解析为实际引用,如果说解析的时候遇到的对应的符号引用所指代得类没有加载会对其进行加载。熬过了链接阶段就是初始化,会对常量和终态字段进行初始化由虚拟机直接处理,其余的赋值操作和静态代码快的执行放在< clinit >,中加锁只执行一次,所以这个特性可以利用在单例安全发布上。这样这个类就彻底的加载到堆中了。
方法是如何执行
当方法区的字节码经过 加载,链接,初始化之后,字节码加载到内存中了,经过链接阶段之后符号引用转变成实际引用,当方法执行时,虚拟机搞了些什么鬼呢,首先得定位到正确的方法,定位的规则虚拟机会根据类名方法名和方法描述符去定位,应为Java 重载,重写机制的,这个找寻的规则呈阶梯来找寻,第一步在不考虑自动拆封箱去按照规则去找寻,如果第一步没有找到,那么会在允许自动拆封箱,不允许可变长参数的时候去找寻上诉俩种都没有找到之后则会允许自动拆封箱和可变长参数去定位,如果在此情况下发现了多个与之匹配的方法,那么则会跟据形式参数的类型去定位,优先子类执行。若还找不到那就是nosuchmethoderror,重载呢又叫做静态绑定,重写呢则是动态绑定是Java多态的具体体现,方法重载,是指在同一个类中方法名相同,参数类型不同,重写呢是出现在接口或者类的实现和继承时,是父类子类之间方法的覆盖规则,要求参数类型一致,方法名一致,返回值一致,但是java允许子类方法的返回值类型是父类返回值类型的子类,说的可能有点绕,jvm虚拟机为这个语义上的重写提供了桥接方法来实现。可以使用javap -v 来查看 。直接上代码了更加直观。
class Father{
public static void sayHello(){
System.out.println("Father hello");
}
public Number actionPrice(double price) {
return price * 0.8;
}
}
public class Son extends Father{
@Override
public Double actionPrice(double price) {
return 0.9 * price;
}
public Double createDayMoney(Double money,int day) {
return money*day;
}
public Double createDayMoney(Double money,int day,int ...num) {
Double result= money*day;
for (int i:num){
result=result*i;
}
return result;
}
public static void main(String args[]){
Father son=new Son();
Number price = son.actionPrice(40);
System.out.println(price);
}
}
附带反编译执行字节码的结果javap -v Son
C:\Users\wangzhifei\Desktop>javap -v Son
Classfile /C:/Users/wangzhifei/Desktop/Son.class
Last modified 2019-2-2; size 1007 bytes
MD5 checksum ab5f895d5a9fad334a60cab5a1044332
Compiled from "Son.java"
// 。。。。。省略多余的常量池信息
public java.lang.Number actionPrice(double);
descriptor: (D)Ljava/lang/Number;
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=3, locals=3, args_size=2
0: aload_0
1: dload_1
2: invokevirtual #13 // Method actionPrice:(D)Ljava/lang/Double;
5: areturn
LineNumberTable:
line 10: 0
}
SourceFile: "Son.java"
/*
public class OverrideTest {
public static void main(String args[]){
Father son=new Son();
Number price= son.actionPrice(40);
System.out.println(price);
}
}
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: new #2 // class Son
3: dup
4: invokespecial #3 // Method Son."<init>":()V
7: astore_1
8: aload_1
9: ldc2_w #4 // double 40.0d
12: invokevirtual #6 // Method Father.actionPrice:(D)Ljava/lang/Number;
15: astore_2
16: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
19: aload_2
20: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
23: return
LineNumberTable:
line 3: 0
line 4: 8
line 5: 16
line 6: 23
}
SourceFile: "OverrideTest.java"
*/
有的同学如果main方法写在其他类中的话是看不多 编译方法使用的ACC_BRIDGE, ACC_SYNTHETIC
的 只能看到 ACC_PUBLIC ACC_STATIC
,我认为是在其他类执行时会隐藏具体方法的方法桥接的过程。直接输出了结果// Method Father.actionPrice:(D)Ljava/lang/Number;
实际上方法桥接是编译器做的处理,我们再后来的泛型分析会着重分析。因为泛型在编译时编译器会检查类型是否匹配泛型类型,不正确会在编译时就会发现错误,不用在运行时才发现错误。泛型是在1.5引入的,为了向前兼容,所以会在编译时去掉泛型(泛型擦除),但是我们还是可以通过反射API来获取泛型的信息,在编译时可以通过泛型来保证类型的正确性,而不必等到运行时才发现类型不正确。由于java泛型的擦除特性,如果不生成桥接方法,那就没法向前兼容了。虚拟机识别方法的过程根据重写和重载就分为了动态绑定和静态绑定,动态顾名思义就是需要在运行时才能够确定调用类,型,静态则是编译时期就可以确认,由于子类父类之间同名方法的重载也是需要运行时确认,虚拟机把所有的非私有的实例方法默认全部使用动态绑定,这是时候呢就要说下虚拟机执行方法调用的几个指令,invokestatic 用来调用类方法,invokespecial 调用私有实例方法,构造器,super default方法,invokeinterface 调用接口方法,invokedynamic 调用动态方法。invokevirutal 调用非私有的实例方法,就是虚拟机使用动态绑定执行方法的调用指令。如果目标方法为终态方法,那么虚拟就会放弃动态绑定,直接定位到方法。
Invokevirutal
虚方法调用,所有的invokevirutal和invokeinterface的调用都归属于虚方法调用,虚方法调用需要根据调用者的具体实例来确认,这个过程叫做动态绑定。动态绑定是实现基础是方法表,jvm虚拟机为每个类会生成一个方法表,这个方法表中拥有其父类和自身的所有方法,方法表的数组初始为父类的方法在子类中均拥有,而且子类覆盖的方法和父类的方法拥有的方法表的索引一致,这是典型的空间换时间的方式,动态绑定和静态绑定的最主要的区别就是多了几步需要获取栈帧上的调用者的类型然后获取类型,在读取方法表,再读取方法表的索引值 再去读取方法。进而执行。静态绑定则是直接就可以定位到方法,省去了内存解引用的过程。任何方法调用都有固定开销比如新建和销毁栈帧等,针对上述的虚方法调用时因为动态绑定而导致的内存消耗,即时编译提供了二种方式来处理
-
内联缓存
顾名思义,就是一个缓存。它缓存虚方法调用时的调用者的动态类型以及目标方法,在第二次喷到同一个方法时候先去命中缓存,如果存在则只需读取缓存执行方法即可,否则就需要去执行内存解引用的操作,内联缓存呢有单态,多态,超多态的区分,java虚拟机呢默认采用的单态内联缓存,就是仅仅缓存常使用的调用者的动态类型以及目标方法。和大多数的缓存设计一样,缓存中的内容会被实时的更新,但是在最坏情况下如果频繁的有写缓存的操作时,不仅没有起到优化的作用,反而还增加了额外的写缓存的开销。
-
方法内联
方法内联是可以消除方法调用的固定开销的
invokedynamic
方法的调用指令会和符号引用绑定,在运行是会把符号引用解析为具体的目标方法,可以看到在虚拟机中调用方法时候,必须是需要提供目标方法的类型的,使用反射可以绕过这个但是反射的开销比较大,另一个呢就是java7引入的invokedynamic指令,实际上invokedynamic是抽象了一个调用点的概念,通过这个调用点可以链接到任意符合条件的方法上。要说invokedynamic就得先说说方法句柄MethodHandles,方法句柄和反射的method类有什么区别呢,我们先看下通过MethodHandle进行方法调用一般需要以下几步:
(1)创建MethodType对象,指定方法的签名;
(2)在MethodHandles.Lookup中查找类型为MethodType的MethodHandle;
(3)传入方法参数并调用MethodHandle.invoke或者MethodHandle.invokeExact方法
方法句柄可以指向静态方法,实例方法,构造器或者字段,指向字段的时候实际指向包含字段访问字节码的虚构方法,语义上等同getter/setter,并不是直接就是getter或者setter方法,方法句柄的创建依赖于MethodHandles.Lookup类完成,可以通过反射得到的method创建,也可以通过类、方法名以及方法句柄类型来查找,方法句柄类型 根据如下代码创建MethodType mt = MethodType.methodType(void.class);
通过MethodHandle.invokeExact方法调用时候会对参数进行强校验,如果自动匹配参数时候则需要调用MethodHandle.invoke。invoke方法调用的时候对调用MethodHandle.asType(MethodType实例)去生成一个适配器方法句柄类型对入参进行适配再去调用原方法句柄。,也就是修改了方法句柄类型,返回返回值的时候也会进行适配。使用bindTo
API可以再调用另一个方法句柄。
反射中的Method类位于java.lang.reflect包中,主要用于在程序运行状态中,动态地获取方法信息,可以根据class对象得到Method实例,然后通过调用 invoke方法来实现目标方法的调用。反射的调用和方法句柄的调用都是间接调用,他也有无法内联的问题,不过方法句柄执行时候如果即时编译器可以将该方法句柄识别为常量。如果可以的话就会被内联。
PS:2019-03-04增加。业务开发使用通用的导出方法,经过查询发现返回的数据字段是动态的,故而无法指定导出的标题,所以引入了一个注解动态列来,在导出类中使用了MethodHandles来间接执行方法,
Field IMPL_LOOKUP = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
IMPL_LOOKUP.setAccessible(true);
MethodHandles.Lookup lkp = (MethodHandles.Lookup) IMPL_LOOKUP.get(null);
MethodHandle methodHandle= lkp.findSpecial(t.getClass(), getMethodName, MethodType.methodType(String.class), t.getClass());
value = methodHandle.bindTo(t).invoke();
//IMPL_LOOKUP 是用来判断私有方法是否被信任的标识,用来控制访问权限的.默认是false,
// 默认情况下findSpecial方法中的最后一个参数Class<?> specialCaller 有一个校验 checkSpecialCaller,
// 如果当前的lookup的类和specialCaller不一致的话就会检测不通过,
// IMPL_LOOKUP.setAccessible(true);设置为true之后,(MethodHandles.Lookup) IMPL_LOOKUP.get(null)这是获取一个Lookup,
// 这种方式返回的allowedModes为-1 这样的话就可以绕过检查,从而执行执行传入specialCaller类中的方法,
当然也有风险,舍弃了强校验,很容易抛出NoSuchMethodError.
先用反射获取了所有的私有字段,然后执行get方法,配合lombok的注解,去执行的方法,这避免了NoSuchMethodError,当然由于MethodHandles访问范围由生命的lookup类的所在位置来决定访问权限,所以原来的创建方式就会提示如下的错误
**expected (DemoEntity,String)void but found (BaseEntity,String)void**
所以就使用了上述的创建方式。
Java8 Lambda表达式
lambda表达式就是用这个操作实现的。在javap去查看含有lambda的测试类中可以看到invokedynamic指令,谈及该指令,就得涉及关键点指令对应的启动方法,这个启动方法是返回了这个调用点CallSite,这个方法如下,必须有三个参数1是Lookup类的实例,一个是方法名称,一个是该调用点能够动态链接的MethodType实例,
public static CallSite bootstrap(MethodHandles.Lookup l, String name, MethodType callSiteType) throws Throwable {
MethodHandle mh = l.findVirtual(T.class, name, MethodType.methodType(void.class));
return new ConstantCallSite(mh.asType(callSiteType));
}
java不支持直接生成invokedynamic指令,需要借助ASM工具包来生成,就是重写字节码,在编译后的字节码文件中增加方法句柄调用的字节码。lambda表达式具体的实现是编译器利用invokedynamic指令生成函数式接口适配器,以集合操作的stream的多个map为例来说
int y=222;
Arrays.asLisy(1,2,3,4,5).stream().map(x->x*2).map(z->z*y);
上述的几个map中的lambda在运行是会转化成Function实例,这个过程就是invokedynamic指令实现的。在编译时,编译器会解语法糖,desugar的时候回生成一个temp方法来保存lambda的参数和捕获的变量。如果没有捕获变量的情况下 invokedynamic指令返回的是惟一的方法句柄,如果含有了捕获变量的时候,为了线程安全则会每一次读取新建这个方法句柄。在这个情况下还会生成一个额外的静态方法用来生成适配器实例把捕获的值赋给该实例的终态实例字段final。可以通过jvm参数-Djdk.internal.lambda.dumpProxyClasses=/PATH
导出这个适配器类,听到这里我么就可能想到如果调用此处超多,那么每次都新建这个适配器类,岂不是极大地消耗,实则不然,由于java8默认打开了逃逸分析,此处的适配器类被判断不会逃逸则会被优化掉,实行标量替换。从而减少新建适配器类的损耗,如果关闭逃逸分析则会性能下降。所以不管是否捕获变量,lambda基本可以和直接调用一致。
// i->i*2 对应的适配器类
final class LambdaTest$$Lambda$1 implements IntUnaryOperator {
private LambdaTest$$Lambda$1();
Code:
0: aload_0
1: invokespecial java/lang/Object."":()V
4: return
public int applyAsInt(int);
Code:
0: iload_1
1: invokestatic LambdaTest.lambda$0:(I)I
4: ireturn
}
// i->i*x 对应的适配器类
final class LambdaTest$$Lambda$2 implements IntUnaryOperator {
private final int arg$1;
private LambdaTest$$Lambda$2(int);
Code:
0: aload_0
1: invokespecial java/lang/Object."":()V
4: aload_0
5: iload_1
6: putfield arg$1:I
9: return
private static java.util.function.IntUnaryOperator get$Lambda(int);
Code:
0: new LambdaTest$$Lambda$2
3: dup
4: iload_0
5: invokespecial "":(I)V
8: areturn
public int applyAsInt(int);
Code:
0: aload_0
1: getfield arg$1:I
4: iload_1
5: invokestatic LambdaTest.lambda$1:(II)I
8: ireturn
}
还有一点内部类的this,指向的是内部类自身实例,而lambda的this指向的是表达式包裹类的的实例,这点区别很重要,相对于内部类来说,很大程序避免了内存泄漏
异常
提起异常我们就立马想到了try-cache-finally这个是用来捕获异常,throw用来抛出异常,throws用来声明异常,异常呢不管是Exception还是Error都是Throwable的子类,Error类型呢只要出现了就面临着线程终止或者是jvm死掉,Exception呢是可以子啊程序内部捕获处理的,Exception中一类特殊的运行时异常(RuntimeException)非检查异常,不用在方法声明时候被标注,其余的检查异常会在编译时被校验,在程序出现异常时候。程序会变的异常的卡顿,持续的比较多会导致虚拟机的崩溃,我们在编写代码时,就要深入的考虑所写的代码的可能抛出的异常,从异常信息的打印来看也能知道,e.printStackTrace();
输出的错误信息就可以知道会收集栈轨迹,从虚拟机角度来看构造异常的实例比较耗性能,构造时会逐一访问当前线程的栈帧记录下调试信息,类名,方法名,文件名 行号,等等。就捕获异常来看,我们知道程序容易抛出异常的代码处于try块中监测,多个cache块配合工作,异常粒度在多个cache块中逐一排开由小到大,最后finally块用于存放一定会执行的代码,那么为什么final块中代码无论异常与否都能被执行?这个事情是由编译器来实现的,现在的做法是这样的,编译器在编译Java代码时,会复制finally代码块的内容,然后分别放在try-catch代码块所有的正常执行路径及异常执行路径的出口中。工作中是有时候会遇到那么如果finally有return语句或者抛异常的操作,catch内throw的异常会被忽略是,因为catch里抛的异常会被finally捕获了,再执行完finally代码后重新抛出该异常。由于finally代码块有个return语句在重新抛出前就返回了,有异常抛出的话会抛出finally中的,这样调试则会误导,Java7引入了Supressed附加异常来保证原有的信息不被忽略,try-with-resources
语法糖就是使用了这个特性,观察字节码则会清楚的看到所有的异常信息。那么虚拟机到底是怎么来实现这个异常的捕获呢,我们使用javap编译输出之后可以看到跟随着一张异常表,from 指针和 to 指针标示了该异常处理器所监控的范围,这个代表的值为字节码的索引也就是Code中的左边的数字,target代表的异常字节码的索引,type 表示的全路径的异常类,这个捕获异常的过程在jvm角度来看呢,就是执行时如果有异常则会去异常表去遍历,如果匹配则执行对应的target代表的字节码即可,当然最坏情况就得遍历全部方法的字节码。正常流程会弹出当前栈帧,如果遍历完异常表依旧没有与之匹配也会弹出当前栈帧同时抛出异常。
public static void main(String[] args) {
try {
mayThrowException();
} catch (Exception e) {
e.printStackTrace();
}
}
//javap结果
public static void main(java.lang.String[]);
Code:
0: invokestatic mayThrowException:()V
3: goto 11
6: astore_1
7: aload_1
8: invokevirtual java.lang.Exception.printStackTrace
11: return
Exception table:
from to target type
0 3 6 Class java/lang/Exception
GC
垃圾回收机制是java语言无需手动释放内存,不像C++需要手动调用析构函数去手动释放内存,jvm虚拟机会在GC Root不可达的时候将其回收,谈到垃圾回收及得说下我们怎么知道我们的引用不在使用了,除了提到的GC Root可达性分析,最直接想到的就是引用计数法,某变量被赋值了A的引用,那么A引用数+1,如果该变量被赋值了B引用,那么A引用数-1。这样最简单,但是如果出现循环引用就无法处理了,这一块引用就无法被标记不在使用了。这样就导致了内存泄漏。 而可达性分析呢 就可以解决这种循环引用的问题,计算式A=B之前相互引用 但是从一个初始的root节点展开,都无法到达A或者B时候,A,B就不会被计算在引用可达的存活对象内,在之后的某次垃圾回收是就会被回收掉 ,虽然这样但是在多线程情况下如果说某引用在不同线程间的可见性没有保证,就容易导致GC可达分析的结果失去了偏差,可能正常,也可能指向了null就是指向了已经回收的对象,也可能实际不再使用了但是依旧持有引用表示存活着,这部分内存没有办法回收,第一种就可能导致程序崩溃。第二种拖的时间长了也会内存溢出而导致崩溃。alive Thread,Class static variable,Stack Local variable ,JNI Local variable ,JNI Global variable ,Monitor Used variable
这些均可以作为GC Root。
然而针对垃圾对象的回收呢一般来说呢就是标记清除,标记复制,标记清除压缩
- 标记清除
标记清除呢就是比较直观的理解了,内存中当引用不可达之后这一块内存就要被释放出来,,当然我们知道java中的对象在内存中的分布不是连续的,每一次将标记的对象清除之后,就会释放出来当前的空间,这些空间大小不均,如果很小的话不足以再次分配的话,这就造成了大量的内存碎片,CMS就是基于该算法
- 标记复制
将内存中分成俩块区域,俩块区域来回复制,一般是from区域和to区域,在垃圾回收发生器的时候,from区域所有存活的对象移动到to区,然后以此类推,to区再移回from区域,这样内存区域的利用率比较低,适用于朝生夕灭的对象。
- 标记压缩
基于处理内存碎片,会对内存进行压缩,其压力在于压缩算法的瓶颈。
我们知道对象的生命周期是不一致的,大部分的对象属于朝生夕灭,仅有一部分会长期存活,这个现象就造就了分代回收,jvm堆区域被划分为新生代,老年代,处于新生代的对象呢就是朝生夕灭,所以其垃圾回收算法那就是标记复制,也就是YGC(Minor GC),当YGC默认进行了15次之后,新生代的对象会晋升到老年代,处于老年代的对象呢当不被使用时候,会被回收。
JVM堆内存划分
- 新生代
新生代区域被划分为一个Eden区和两个幸存区S1和S2(一直为空,直到MinorGC发生的时候才会把对象填充到该内存区域,之后会完全复制到S1区域),默认比例为8:1:1,新建对象的时候会在Eden区开辟一块空间,当然不加任何同步的开辟空间这样会导致并发问题,所以当前线程会在该区域预先开辟一块内存叫做私有工作内存,用来避免并发冲突,这就是TLAB,当前访问工作内存时每占用一块,内存中的占用指针就增加,当TLAB被分别完了之后就会再申请一块,直到Eden区域满了之后就会进行YGC,此时呢会把Eden区域的存活对象晋升到幸存区,如此往复,直到一直存活的对象(YGC>15)晋升到老年代。那么具体幸存区是如何工作的呢,发生Minor GC时候 会把Eden区域和S1区域的存活对象直接复制到S2区域,GC完毕之后再把S2区域存活对象复制到S1区域。Minor GC的时候会扫描GC Root,如果老年代持有了新生代的引用的话,这样岂不是要全堆扫描,hotspot的解决方法是增加了卡表,来大致规划出老年代持有新生代引用的内存区域,在进行扫描的时候只去扫描脏卡区的对象即可,
- 老年代
存放的对象比如开启担保分配的直接在老年代分配的大对象,或者由新生代晋升而来的存活对象
说道垃圾回收就离不开STW,stop the world就是说所有的非垃圾回收线程均会停顿,等待垃圾回收线程回收不存活的引用,这也就是GC停顿,当然不是其他非垃圾回收线程在当前线程状态就停顿,这样做不稳定,所以jvm提供了一个安全点的概念,当准备进行GC时,jvm虚拟机收到了STW信号,他回通知非垃圾回收线程就近到达安全点,然后在通知垃圾回收线程去回收不活跃引用释放内存。
基于分代回收的各种垃圾回收器
新生代的回收器 Serial
Parallel Scavenge
Parallel New
, Serial
,Parallel New
是这两个垃圾回收器一个是单线程,一个是并发收集,Parallel Scavenge
是一个强调吞吐率的收集器,但是不可以和CMS配合使用,
老年代的回收期 Serial Old
CMS
Parallel Old
,Serial Old
,Parallel Old
垃圾回收器的回收算法是标记压缩,而CMS
则是标记清除,还可以并发收器,当内存碎片达到阈值时候会触发Old区域GC。
还有个G1回收器,这个G1垃圾回收器是把整个JVM区域划分为多个区域,每次回收的收会收器存在不活跃对象最多的区域。
常用的垃圾回收器配合为 Parallel New
+ CMS
如何诊断GC
要查看一个健康状态的GC,最主要我们要考虑GC频率和STW的时间。我在这里要介绍几个JDK提供的使用的命令
JPS
使用的最为频繁的一个命令,用于获取java进程的的进程id,比 Linux的查看进程ID命令 ps -ef | grep java |awk '{print $2}' 更加的方便。
去掉冗余的信息只显示进程ID
JSTAT
利用JVM内建的指令对Java应用程序的资源和性能进行实时的命令行的监控,包括了对Heap size和垃圾回收状况的监控
JMAP
查看当前堆中的对象占比情况。使用jmap -dump:format=b,file=dump.bin 8668
即可导出堆的dump快照,然后通过MAT工具进行分析。查看堆中占用对象的状态,查询存活状态的对象jmap -histo:live 8668显示也是直方图的形式。只不过的事执行该命令会执行一次fullGC
JSTACK
利用jstack可以定位死循环、线程阻塞、死锁等问题 命令:jstack.exe -l 8668,增对线程阻塞的情况,可以导出多次的线程dump来进行分析比对例如8668该进程CPU一直飚高,在linux上可以通过top -Hp 8668 来找到对应的该进程中占用资源或者等待资源释放的线程id,然后转换pid为16进制,然后在线程的dump中搜索,找到该线程栈信息,如果存在线程等待的情况,看是否有资源未释放或者平凡读取文件之类的,定位到代码即可。
JINFO
jinfo -flags pid 可以查看jvm的命令行参数,比如是否打印DC,初始化堆的大小,是否指针压缩等。。