深入理解JVM

   日期:2024-12-25    作者:sheji 移动:http://oml01z.riyuangf.com/mobile/quote/10968.html

特点

  • 线程私有。
  • 不会存在内存溢出。

多线程通过cpu的调度器分配,每个线程有不同的时间片,在一个线程的时间片用完后,程序计数器记住下一次轮到此线程执行的指令的内存地址,继续执行。

2.Java Virtual Machine Stacks (Java 虚拟机栈)
定义

  • 每个线程运行需要的内存空间,称为虚拟机栈。
  • 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存。
  • 每个线程只能有一个活动栈帧(线程正在执行的方法对应的栈帧,对应着每次方法调用时所占用的内存。

1.垃圾回收是否涉及栈内存
栈内存:一次次方法调用所产生的栈帧内存。栈帧内存:栈帧在每一次方法调用后都会弹出栈,也就是栈帧内存会被自动回收掉。所以不需要垃圾回收管理栈内存。
垃圾回收值会回收堆内存中的无用对象,栈内存不需要。

2.栈内存分配越大越好吗
栈内存可以通过运行虚拟机代码时,通过一个参数指定。-Xss size

方法内的局部变量是否线程安全
线程安全:局部变量对多个线程是共享的,还是属于某个线程私有的。
某个变量是否线程安全判断方法:1、是否是方法内的局部变量 2、是否逃离了方法的作用范围(有可能被别的线程访问到,不再线程安全)。
1.方法内的局部变量没有逃离方法的作用范围,那么它就是线程安全的。
2.局部变量引用了对象,并逃离了方法的作用范围,需要考虑线程安全。
2.局部变量引用的是基本类型变量,不考虑线程安全。

场景1:有两个线程同时调用这个方法m1,会不会造成x值得混乱呢。不会:x是方法内的局部变量,一个线程对应一个栈,线程内每一次方法调用都会产生一个新的栈帧,线程1的栈帧中有对应的x变量是线程1私有的,线程2的栈帧中有对应的x变量是线程2私有的,各自加5000,互不影响。

 

场景2:有两个线程同时调用这个方法m2,会不会造成x值得混乱呢。会:x是类变量,线程共享的,一个线程对应一个栈,线程内每一次方法调用都会产生一个新的栈帧,线程1和线程2的栈帧中是对同一个变量x操作。

 

场景3:针对下面程序Demo1,有两个线程同时调用这个方法m1,会不会造成sb值得混乱呢。不会:x是方法内的局部变量,一个线程对应一个栈,线程内每一次方法调用都会产生一个新的栈帧,线程1的栈帧中有对应的sb变量是线程1私有的,线程2的栈帧中有对应的sb变量是线程2私有的,互不影响。

针对方法m2, sb作为参数传入,其他的线程就有可能访问到它,sb不再是线程私有的,是线程共享的。如下示例

 

主线程中创建了一个StringBuilder对象sb,又在新线程里面把sb作为参数传给了m2方法,主线程和新线程都在修改sb,sb是主线程和新线程共享的,线程不安全。

场景4:m3方法中的sb对象是否线程安全呢?答:不安全。
sb虽然是方法内的局部变量,但是作为方法的返回值返回了,这样其他的线程有可能拿到sb的引用对象,并发的修改它。

 

2.2 栈内存溢出

  • 栈帧过多导致栈内存溢出
    两个类循环引用导致栈内存溢出
  • 栈帧过大导致栈内存溢出

2.3线程运行中断
案例1:cpu占用过多
定位

  • 用top定位哪个进程对cpu的占用过高
  • ps H -eo pid,tid,%cpu | grep pid (用ps命令进一步定位是哪个线程引起的cpu占用过高)输出的线程编号是10进制的
  • jstack pid 将此进程下jvm使用的线程和用户定义的线程都列出详情, 输出的线程id是十六进制的,可以将上一步得到的十进制线程id进行转换,对应后,可以根据线程id找到有问题的线程,进一步找到引起问题的代码行数

案列2:程序运行很长时间没有结果

  • jstack pid 输出的最后有 Found one Java-level deadlock:…
  • 定位到发生死锁的位置

3.本地方法栈
jvm调用本地方法时,给本地方法分配的内存。
本地方法:不是由java代码编写的方法,是有此C或者C++语言编写的代码。java方法不方便与底层操作系统打交道,而是通过调用本地方法与底层操作系统打交道。所有对象的父类Object类里面的方法声明就是用native声明的,是没有方法实现的,java通过native方法接口间接调用C或者C++的接口实现。

从下面说道的堆和方法区是线程共享的
4. Heap 堆
通过new关键字,创建对象都会使用堆内存
特点
1)它是线程共享的,堆中对象都需要考虑线程安全问题 2)有垃圾回收
4.2 堆内存溢出
OutOfMemoryError Java heap space
堆空间参数:-Xmx

4.3堆内存诊断
1.jps工具
查看当前系统中有哪些java进程
2.jmap工具
查看堆内存使用情况 jmap -head pid
3.jconsole工具
图形界面的,多功能检测工具,可以连续检测

案例:垃圾回收后,内存占用仍然很高

  1. 方法区

定义
组成
5.3 方法区内存溢出
1.8以前会导致永久代内存溢出
java.lang.OutOfMemoryError:PermGen space
-XX:MaxPermSize=8m
1.8以后会导致元空间内存溢出
java.lang.OutOfMemoryError:Metaspace
-XX:MaxMetaspace=8m

类加载器:用来加载类的二进制字节码
ClassWriter 用来生成类的二进制字节码
场景:动态产生class,并加载这些类的场景是很多的
Spring:用到字节码技术,都会用到Cglib,生成的代理类,是spring中aop的核心。
myBatis:用到字节码技术,都会用到Cglib。 产生map接口的实现类。

5.4 运行时常量池

  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
  • 运行时常量池,常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。

5.5 StringTable
特性

  • 常量池中的字符串仅是符号,第一次用到时才变成变量
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBuilder(1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池
    1)1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回。
    2)1.6将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则会把此对象复制一份,放入串池,会把串池中的对象返回。

5.6 StringTable的位置
1.6方法区中永久代中,常量池的一部分
1.7-8 堆中
永久代回收效率很低,只有fullGC才会回收,等到老年代空间不足时,才会触发FullGC。java应用程序中大量的字符串常量,会分配到StringTable中,如果StringTable回收效率不高,会占用大量的内存。
基于这样的缺点,StringTable被转移到堆中。MinorGC就可以触发,大大减轻对内存的压力。

5.7 StringTable 垃圾回收

当内存空间不足时,StringTable中没有被引用到的字符串常量仍然会被垃圾回收。

5.8 StringTable 性能调优

  • 调整-XX:StringTableSize=桶个数 hash查找会变快
  • 考虑将字符串对象是否入池

直接内存溢出
OutOfMemory: Direct buffer memory

6.3 直接内存分配和回收原理

  • 使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
  • ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferrenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存

直接内存禁用显示回收对直接内存的影响。

垃圾回收
1.如何判断对象可以回收
1.1引用计数法,循环引用导致垃圾回收
1.2可达性分析算法
确定一系列根对象,根对象:肯定不能被当成垃圾回收的对象。
垃圾回收前,对堆内存中的所有对象进行一遍扫描,确定每一个对象是不是被根对象做直接或者间接的引用,如果是,此对象就不能被垃圾回收,否则,就被作为垃圾将来被回收。

  • java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象。

  • 扫描堆中的对象,看是否能够沿着GC Root对象为七点的引用链找到该对象,找不到表示可以回收。

  • 哪些对象可以作为GC Root

  • Memory Analyzer: 快速的多功能的java堆分析工具.
    -先用jmap获取堆内存快照。jmap -dump:format=b,live,file=1.bin pid
    1)System Class(系统类):启动类加载器加载的类,是一些核心类,代码运行过程中都会用到的
    2)Native Stack jvm执行操作时候会调用操作系统的方法,操作系统在执行时引用的java对象也是可以作为根对象。
    3)Thread 活动线程中使用的对象不能被当成垃圾回收,线程运行时由一次次的方法调用组成,每次方法调用都会产生栈帧,栈帧内使用的对象(局部变量引用的对象,方法参数引用的对象)被作为根对象。
    4)Busy Monitor synchronized正在加锁的对象不能被回收,因为后面还要释放锁。
    1.3 五种引用
    1)强引用–只有所有GC Roots对象都不通过【强引用】引用该对象,该对象才能被垃圾回收。
    new 一个对象通过复制运算符赋值给了一个变量,此变量就强引用了创建的对象。只要沿着GC Root链能够找到此对象,就不会被垃圾回收。
    2)软引用(SoftReference) --仅有软引用引用该对象时,在垃圾回收后,内存仍不足会再次触发垃圾回收,回收软引用对象。可以配合引用队列来释放软引用自身。
    当没有强引用引用对象,只有软引用引用对象,当发生垃圾回收,并且内存不够时候,会将软引用引用的对象回收掉。
    应用:不重要的资源可以使用软引用
    3)弱引用(WeakReference)–
    当没有强引用引用对象,只有弱引用引用对象,当发生垃圾回收,不管内存是否充足时候,会将弱引用引用的对象回收掉。可以配合引用队列来释放弱引用自身。
    软弱引用配合引用队列进行工作:软弱引用自身也是一个对象,如果在创建软弱引用时给它们分配了队列,当软弱引用引用的对象被回收的时候,软弱引用就会进入对应的队列。
    软弱引用被回收时,需要通过引用队列找到它们,再进行处理。
    4)虚引用
    必须配合引用队列使用,创建ByteBuffer的实现类对象时,就会创建一个Cleaner的虚引用对象,ByteBuffer会分配一个直接内存,然后将直接内存地址给虚引用对象。当ByteBuffer被回收时,锁使用的直接内存不会被回收,让虚引用对象进入引用队列,虚引用所在的引用队列由一个叫ReferenceHandler的线程定时寻找是否有新入队的Cleaner,如果有就会调用Cleaner的虚方法。虚方法根据前面记录的直接内存地址,调用Unsafe.freeMemory将直接内存释放掉。
    虚引用引用的对象被垃圾回收时,虚引用对象本身就会被放入引用队列,从而间接的用一个线程来调用虚引用对象的方法,然后调用Unsafe.freeMemory将直接内存释放掉。
    5)终结器引用
    当对象重写了终结方法finalize,并且没有强引用引用它时,就可以被垃圾回收。当没有强引用引用对象时,虚拟机为我们创建对象对应的终结器引用,当对象没有被强引用引用时,终结器引用对象也加入一个引用队列,由优先级很低的一个线程finalizeHandler,查看队列中队列中是否有终结器引用,如果有就找到对应的对象并调用其finalize方法,调用完后,等真正发生垃圾回收,就可以将此对象占用的内存回收掉。效率很低,线程有限地低

  • 垃圾回收算法
    标记清除算法(Mark Sweep:速度较快,会造成内存碎片。
    标记整理:速度慢,没有内存碎片。
    复制:不会有内存碎片,需要占用双倍内存空间。

  • 分代垃圾回收
    堆内存将内存区域分成新生代(朝生夕死)和老年代(长时间存活的对象)。
    新生代又分成伊甸园、缓存区From和幸存区To。
    新生代垃圾回收:Minor GC
    老年代垃圾回收:Full GC
    对象首先分配在伊甸园区域
    1)新生代空间不足时,触发Minor GC,伊甸园和缓存区From存活的对象使用复制算法复制到缓存区To中,存活的对象年龄加一,并交换From和To。
    2)Minor GC会引发stop the world,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行
    3)当对象寿命超过阈值时,会晋升到老年代,最大寿命是15(4bit
    4)当老年代空间不足,会先触发Monir GC后,空间仍然不足,会触发Full GC (STW时间更长)
    3.1 相关VM参数
    堆初始大小:-Xms
    堆最大大小:-Xmx或-XX:MaxHeapSize
    新生代大小:-Xmn或-XX:NewSize=size+ -XX:MaxNewSize=size
    幸存区比例(动态:-XX:InitialSurvivorRatio=ratio 和-XX+UseAdptiveSizePolicy
    幸存区比例:-XX:SurvivorRatio=ratio
    晋升阈值:-XX:MaxTenuringDistribute=threshold
    晋升详情:-XX+PrintTenuringDistribution
    GC详情:-XX+PrintGCDetails -verbose:gc
    FullGC 前 MinorGC -XX+ScavengeBeforeFullGC

  • 卡表与RememberedSet

  • 在引用变更时通过post-write barrier + dirty card queue

  • concurent refinement threads更新Remembered Set

7)Remark
pre-wrire barrier + satb_mark_queue
当对象的引用发生改变时,jvm给其加入一个写屏障。经对象加入一个队列中,变成灰色,表示还没有处理完,等并发标记结束后,进入最终标记,从队列中取出再进行一次检查,再变成黑色。

8)JDK 8u20字符串去重
优点:节省大量内存
缺点:略微多占用了cpu时间,新生代回收时间略微增加
-xx:+UseStringDeduplication
String s1 = new String(“hello”);
String s2 = new String(“hello”);

  • 将所有新分配的字符串放入一个队列
  • 当新生代回收时,G1并发检查是否有字符串重复。
  • 如果它们值一样,让它们引用同一个char[]
  • 注意,与String.intern()不一样
    -String.intern()关注的是字符串对象
    -而字符串去重关注的是char[]
    -在jvm内部,使用了不同的字符串表

9)JDK 8u40并发标记类卸载
所有对象经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它它所加载的所有类
-xx:+classUnloadingWithConcurentMark默认启用
很多框架程序都使用了自定义的类加载器,很有用,jdk的启动、扩展、应用程序类加载器不会使用的
10)DK 8u60回收巨型对象

  • 一个对象大于region的一半时,称之为巨型对象。
  • G1不会对巨型对象进行拷贝
  • 回收时被优先考虑
  • G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉。

11)JDK 9并发标记起始时间的调整

  • 并发标必须在堆空间占满前完成,否则退化为FullGC
  • JDK 9之前需要使用 -xx:InitiatingHeapOccupancyPercent
  • jdk 9可以动态调整
    -xx:InitiatingHeapOccupancyPercent用来设置初始值
    进行数据采样并动态调整-xx:InitiatingHeapOccupancyPercent=percent
    总会添加一个安全的空档空间
    减少并发垃圾回收退化为fullGC

垃圾回收调优

类加载与字节码技术

  1. 多态原理小结
    当执行invokevirtual指令时
    (1).先通过栈帧中的对象引用找到对象
    (2). 分析对象头,找到对象的实际Class
    (3). Class结构中有vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
    (4).查表得到方法的具体地址
    (5).执行方法的字节码

  2. 字节码指令

6.运行期优化
6.1即时编译
分层编译
JVM将字节码执行状态分成了5个层次
0层:解释执行(Interpreter
字节码被加载到虚拟机后,靠解释器一个字节一个字节的解释执行,解释成一个个真正的机器码。当字节码被反复调用次数达到一定程度后,启用编译器(C1和C2编译器)对字节码进行编译执行
1层:使用C1即时编译器编译执行(不带profiling
2层:使用C1即时编译器编译执行(带基本profiling
3层:使用C1即时编译器编译执行(带完全的profiling
4层:使用C2即时编译器编译执行
profiling是指在运行过程中收集一些程序执行状态的数据,例如方法的调用次数,循环的回边次数等

即时编译器和和解释器的区别
解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
JIT是将一些字节码编译为机器码,并存入Code Cache,下次遇到相同的代码,直接执行,无需再编译
解释器是将字节码解释为针对所有平台都通用的机器码
JIT会根据平台类型,生成平台特定的机器码


 

特别提示:本信息由相关用户自行提供,真实性未证实,仅供参考。请谨慎采用,风险自负。


举报收藏 0评论 0
0相关评论
相关最新动态
推荐最新动态
点击排行
{
网站首页  |  关于我们  |  联系方式  |  使用协议  |  隐私政策  |  版权隐私  |  网站地图  |  排名推广  |  广告服务  |  积分换礼  |  网站留言  |  RSS订阅  |  违规举报  |  鄂ICP备2020018471号