Java常见问题总结五

1、垃圾回收方式

SerialGC(串行垃圾回收):为单线程环境设计且使用一个线程进行垃圾回收,会暂停所有的用户线程。
ParalleGC(并行垃圾回收):对过GC线程并行工作,此时用户线程是停止的。
ConcMarkSweep(CMS):用户线程和GC线程同时执行,不需要停顿用户线程。
G1垃圾回收器:将内存分割成不同的区域然后并发的进行垃圾回收。 

2、查看当前默认的垃圾回收器

java -XX:+PrintCommandLineFlags -version

3、7大垃圾回收器

在新生代主要使用:Serial copying、Parnew、Parallel Scavenge。相对应的同时会在老年代激活:Serial MSC、CMS、Parallel Compacting;以及还有一个G1垃圾回收器。

/*************************新生代*********************/
serial收集器(串行收集器):
    -- 是一个单线程的收集器,在进行垃圾回收时,暂停其他所有用户线程,有可能会产生较长的停顿状态(stop-the-world)
    -- 通过设置-XX:+UseSerialGC进行开启,会使用Serial(Young 区)+Serial old(old区)的收集器组合。
    -- 即新老代都会使用串行收集器,新生代使用复制算法,老年代使用标记-整理算法。

ParNew(并行收集器):
    -- 使用多线程进行垃圾回收,也会暂停其他所有用户线程。
    -- 通过设置-XX:+UseParNewGC启用收集器,会使用ParNew(Young区)+Serial old(old区)的收集器组合。
    -- 新生代使用复制算法,老年代采用标记-整理算法。不过现在已被移除。

Parallel New(并行收集器):
    -- 是一个吞吐量有限的收集器。串行收集器在新生代和老年代的并行化。
    -- 关注的重点是吞吐量和自适应调节策略(会根据当前系统的运行情况来提供最适应的停顿时间 -XX:+MaxGCPauseMillis)
    -- 通过设置-XX:+UseParallelGC或-XX:+UseParallelOldGC(相互激活)来使用
    -- 参数-XX:+ParallelGCThreads=N N代表启动多少个GC线程。

/*************************老年代*********************/
Parallel old
    -- 是ParallelScavenge的老年代版本
    -- 通过设置-XX:+UseParallelGC或-XX:+UseParallelOldGC(相互激活)来使用

CMS(并发标记清除)
    -- 是一种以获取最短回收停顿时间为目标的收集器。
    -- 通过设置-XX:+UseConcMarkSweepGC会自动打开-XX:+UseParNewGC。
    -- 开启后使用ParNew(Young区)+CMS(Old区)+Serial old的收集器组合。
    -- Serial old将作为CMS出错的后备收集器。CMS必须要在老年代堆内存用完之前完成回收,要不然就出发后备收集器造成较大的停顿。
    -- 一共有4步:
        1.初始标记:标记GC roots能关联到的对象,会暂停所有工作。
        2.并发标记:和用户线程一起工作,主要标记过程,标记全部对象。
        3.重新标记:暂停所有工作线程,用于确认是否又有新的标记。
        4.并发清除:清理不可达对象,和用户线程一起,不需要暂停工作线程
        5.优点是并发执行CPU资源压力大,缺点是会导致大量的碎片。

Serial old
    -- 是Serial的老年版本

以上的收集器都有这些共同点:1、新生代和老年代都是各自独立且连续的内存块。2、老年代收集必须扫描整个老年代。3、都是尽可能的少而快速的执行GC为原则。

4、G1垃圾回收器

是一种服务端的垃圾收集器,在实现高吞吐量的同时,也实现尽可能满足垃圾收集暂停时间。像CMS一样能与应用程序并发执行。

G1的优点在于:1、充分利用多核多CPU的硬件优势,尽量缩短STW(系统暂停时间)。2、整体采用标记-整理算法不会产生内存碎片。3、不再区分老年代和新生代把内存划分为多个独立的子区域。

堆有新生代(1/3)和老年代(2/3),新生代包含 伊甸园和幸存者区两个(8:1:1),当分代年龄到达一定次数就会从新生代到老年代。JVM调优的目的是减少STW(stopedWord),大对象(数组、字符串)会直接进入老年代。

回收步骤:

  1. Eden区的数据移动到Survivor区,如果Survivor区内存不够,Eden区数据部分移动到old区。
  2. Survivor区移动到新的Survivor区,部分数据回到old区。
  3. 最后Eden区收拾干净,GC结束。

G1相关的参数:

-XX:+UseG1GC 使用G1垃圾回收器
-XX:G1HeapRegionSize=n  设置G1区域的大小,值是2的幂从1MB--32MB
-XX:MaxGCPauseMillis=n  设置GC的停顿时间,JVM尽可能停顿时间少于这个时间。
-XX:ConcGCThreads=n 并发使用GC的线程数
-XX:InitialingHeapOccupancyPercent=n  设置堆占用多少就出发GC
-XX:G1ReservePrecent=n  设置空闲时间的预留内存百分比

Springboot相关JVM调优

java -server [jvm相关参数]  -jar 包名

5. Java内存对象布局

一个对象实例包含对象头、实例数据、对齐填充;对象头包含对象标记(MarkWord) 、类型指针、长度(只有数组对象有,普通对象中没有);MarkWord包含了hashcode、GC标记、GC次数、同步锁标记、偏向锁持有者。在64位系统中MarkWord占8字节、类元指针占8字节,总共16字节。

 这就是为什么GC最大年龄是15,因为二进制1111就是15,age只分配了四位。类型指针是指向类元数据的指针(也就是指向方法区的 xxx的klass);实例数据是存放类属性field的信息,包括父类信息;对齐填充虚拟机要求对象起始地址必须是8字节的整倍数,不是必须存在的,仅仅为了对齐。一个对象的大小 = MarkWord(8字节) + 类型指针(4字节) + 实例数据(根据实际情况,空对象没有) + 对其填充(根据实际情况)。

通过JOL查看对象的内存分布情况,首先先导入maven信息。

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

 通过JOL就可以查看当前虚拟机信息以及对象的内存布局。

Object o = new Object();
System.out.println(VM.current().details());  // 获取当前虚拟机信息
System.out.println(VM.current().objectAlignment());   //  获取对象的对齐数
System.out.println(ClassLayout.parseInstance(o).toPrintable()); //  获取一个对象的布局

对象的内存布局:这里的前两个header是MrakWord,第三个是类型指针,第四个是填充对齐。这里默认是开启了指针压缩导致类型指针由8字节压缩成4字节。SIZE代表字节大小,OFFSET代表偏移量也就是这个字段所占的byte数,TYPE代表class中定义的类型,VALUE代表内存中的值,DESCRIPTION代表类型的描述。value值需要从右下角到左上角倒着看,每个字节按照顺序,字节之间按照逆序

 6. 锁升级的过程

在JDK1.6之前只有无锁和重量级锁两种模式,由于重锁是在内核态处理的,所以从无锁到重锁之间切换时非常消耗资源的,为了减少获得和释放锁的资源消耗,添加了两种锁的方式偏向锁和轻量级锁;在JDK15后偏向锁被废弃了。锁升级的过程如下。

无锁 --> 偏向锁 --> 轻量级锁(CAS) --> 重量级锁(synchronized)

※ 无锁

在无锁的状态下打印对象的内存布局,可以发现最后两位是01,正是无锁的状态,但是现在还没有看见hashCode的值?

 至于为什么没有看见hashCode的值是因为没有调用hashCode方法,只有手动调用后才会显示。

 ※ 偏向锁

是单线程竞争;一段同步代码一直被同一个线程访问,由于只有一个线程,那么该线程在后续访问中会自动获得锁。换言之,偏向锁会偏向第一个访问锁的线程,如果锁后续没有被其他线程访问,则持有偏向锁的线程永远不需要触发同步,也即偏向锁在资源没有竞争下消除了同步语句,都不用CAS,可直接提高性能。

可以通过命令在系统中看到,在JDK1.8中默认是开启偏向锁的,延迟4s(也就是程序执行4s后才会开启偏向锁,在JDK11中没有延迟)。

 有两种触发偏向锁的方式,一种是直接在程序执行的时候直接sleep大于4秒的时间;第二种就是通过给定Java虚拟机参数。

-XX:BiasedLockingStartupDelay=0  将延迟设置为0s
-XX:-UseBiasedLocking   关闭偏向锁

偏向锁的流程:偏向锁会将第一个访问锁的线程Id记录在自己的MarkWord中,第二次,会先判断当前线程Id和MarkWord保存的是否一致,如果一致则直接进入同步代码块无需加锁;如果不一致则需要抢占,如果抢占成功则修改MarkWord的中记录的线程Id,此时不会升级锁;如果失败则会在等待到 全局安全点(不会执行任何代码)、同时检查持有的偏向锁是否还在执行判断,如果持有偏向锁的线程正在执行同步代码块,则升级完CAS后,该线程还会占用CAS;如果持有偏向锁的线程已退出同步代码块,则剩下的线程进行CAS竞争;之后就会撤销偏向锁。

public void func2(){
    Object o = new Object();
    new Thread(() -> {
        synchronized (o){
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }).start();
}

设置完启动参数以后我们再次打印对象的内存布局会发现,MarkWord已经加上了线程Id。

 ※ 轻量级锁

是多线程竞争;但是任意时刻最多一个线程进行竞争,即不是很激烈的情况。可以通过关闭偏向锁的命令来验证轻量级锁。

Object o = new Object();
new Thread(() -> {
    synchronized (o){
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}).start();

当轻量级锁自旋一定次数以后就会升级成重量级锁。自旋次数在Java6之前自旋默认10次,或者自旋线程数超过CPU核数的一半,可以通过 -XX: PreBlockSpin = 10来修改;在Java6之后采用自适应自旋锁,其原理是如果本次自旋成功了,那么下次就会增大自旋的次数,反之会减少自选次数, 减少CPU的空转。

※ 重量级锁

Object o = new Object();
new Thread(() -> {
    synchronized (o){
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}).start();
new Thread(() -> {
    synchronized (o){
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}).start();

 升级成重量级锁之后 hashCode和GC年龄都去哪里了??

当一个对象计算过hashCode就无法进入偏向锁的状态了,会跳过偏向锁阶段直接进入轻量级锁;如果正处于偏向锁,并在此时调用了hashCode,就会撤销偏向锁,升级成为重量级锁(如果可以的话那么前后两次的hashCode就不能保持唯一);如果处于轻量锁状态,hashCOde会在当前锁的栈帧中存放,释放锁会将信息重新写回对象头内;如果处于重量级锁,对象头指向了重量级锁的位置 ObjetMonitor对象中会存储hashCode。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值