JVM

JVM architecture

  • Main function: load & execute java application

  • Process: edit - javac myapp.java - java myapp (create a jvm instance)

  • 1WkqSg.png
  • 1WAGnA.png
  • components:

    • class loader: input .class files output bytecode for execution engine
    • Runtime data areas
    • execution engine: executes byte code by talking to OS (may use native method calls that will be translated to machine language)

1. Class Loader and its subsystem

  • 1WA2NV.png

Load

  • Load: Load bytecode into memory
    • can read from different sources: file system, socket
    • can have classNotFound
    • Three types of class loaders:
      • bootstrap rt.jar: load java internal classes
      • extension jre/lib/ext: load classes from additional application jar in jre/lib
      • application CLASSPATH, -cp: load classes from specified path
  • Link phase
    • verify bytecode compatibility with JVM
    • Prepare allocate memory and init to default value for class (static) variables
    • Resolve resolve symbolic references to other classes / constant pool to actual reference
      • classDefNotFound

Initialize

  • Initialize
    • Execute static code block “static initializer”
    • actual initialization of static variables

类初始化时机

  1. 主动引用
    虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生):
  • 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。
  • 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
  • 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;
  1. 被动引用
    以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:
  • 通过子类引用父类的静态字段,不会导致子类初始化。
1
System.out.println(SubClass.value);  // value 字段在 SuperClass 中定义Copy to clipboardErrorCopied
  • 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
1
SuperClass[] sca = new SuperClass[10];Copy to clipboardErrorCopied
  • 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
1
System.out.println(ConstClass.HELLOWORLD);

2. Runtime data areas

  • per thread means operations in thread local storage are generally safe.
  • per JVM means operations in shared data areas such as meatspace, Heap are not thread-safe.

Per-thread

1WmTkn.png
PC Register
  • PC Register: program counter register
    • pointer to next instruction per thread
Java Stacks
  • Java Stacks: stack frames “chains of stack frames” corresponded to current methods execution per thread
    • use -Xss to mention size of stacks we want to maintain
    • when run out of memory, can have StackOverflowError
Native method Stacks
  • Native method stacks: native method stacks if needed / used per thread.
    • a thread with its method may call a native method such as loading a dll and run something, then the native method stack will be created, and you will get a pointer

Per-JVM

1WZNA1.md.png
Metaspace / Method Area
  • Method Area “PermGen space 64MB”: metadata for class, available for reflection per JVM.

    • use -XX:MaxPermSize to adjust size if we need to store a lot more classes
    • Removed since Java 8
    • Now is called: Metaspace
      • a seperate memory portion in native operating system
      • no limit now, can grow infinitely, but can have a limit if tuned.
Heap
  • Heap: stores Object data such as arrays, objects… per JVM.

    • now has runtime constant pool and string pool
    • use -Xms for min size, -Xms for max size if need to tune.
Direct Memory
  • 直接内存: 在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。

3. Execution Engine

1WQdZF.png
  • Interperter: interpret & execute bytecode related native operations
    • done by interacting with Java Native Method Interface (JNI)
      • Platform independent (native) libraries: ex. in windows JRE /bin you will see .dll files .so on unix platform
  • JIT Compiler (Just-in-time): do not interpret instructions that will be executed again and agin, compile it on the fly and keep the bytecode to avoid wasteful interpretations.
  • Hotspot profiler: make statistics on hotspot to help JIT compiler.
  • Garbage Collector (GC): cleans up unused classes / objects in memory areas.

JVM GC

  • Intro
    • Memory leak: 内存管理不当导致的“不需要的内存没有被释放” can have Memory leak in Java
    • In C++/C, programmers responsible for manage memory, can easily lead to memory leaks if not handled properly
      • malloc() realloc() calloc() free() new and destructors
  • Basics
    • Live object = reachable (referenced by someone else)
    • dead object = unreachable (not referenced anywhere)
    • root node = main thread
    • Objects e.g. (new xxx) are allocated in the heap, static members, class definitions (metadata) are stored in Permgen / Metaspace
    • GC is carried out by a daemon thread "Garbage collector"
    • we cannot force gc to happen (System.gc()😉
    • failed new allocations in heap = java.lang.OutOfMemoryError
    • GC只处理java new出来的对象,但无法关闭其他资源,也无法处理java调用C或其他语言分配出的内存。
    • 垃圾回收分多级,0级为全部(Full)的垃圾回收,会回收OLD段中的垃圾;1级或以上为部分垃圾回收,只会回收Young中的垃圾。
    • System.gc并不保证GC执行,只是向JVM发送建议,并不是命令。
    • finalize被调用时代表gc准备回收该对象内存

Directed Graph & Reachability

  • .NET的垃圾回收采用引用计数,java的垃圾回收机制采取的是有向图的方式来实现,具体的说,java程序中的每个线程对象就可以看作是一个有向图的起点,有向边从栈中的引用者指向堆中的引用对象。在这个有向图中,如果一个对象和根节点之间是可达的,那么这个对象就是有效的,反之,这个对象就是可以被回收的。采取这样一种机制的优点是可以有效的避免循环引用。

General GC steps: Mark Sweep Compact

  • Mark
    • Starts from root node (main) walks the object graph, marks reachable object as live.
  • Sweep/Delete
    • clean unreachable objects and reclaim memory
  • Compacting
    • arrange things in order: move objects around to avoid fragmentation = make memory contiguous.

Java GC: Generational collectors

Heap division

  • 1WtC4S.png
  • Young Generation
    • Eden space: new object();
    • Survivor space 1 / 2: used to move back and force survivors that survive minor GC each turn
      • can help avoid compacting step
  • Old Generation
    • objects that survived at least “threshold” rounds of GC
    • if full, iincur Major GC

Minor vs Major GC

  • Minor:
    • only Young Generation
    • Responsive
  • Major:
    • run through entire heap
    • Long pause / latency
    • High throughput
    • Good for batch processing, jobs in database that that care about throughput > latency

Entire process

  1. new obj allocated in Eden
  2. Eden full = allocation fail for new obj
    1. Minor GC run, mark reachable objs
    2. move reachable obj to survivor 1
    3. Sweep unreachables
    4. survivors counters = “1” means 1 rounds
  3. Now Eden clear, allocate new objs… some in survivor 1 became unreachable… when Eden full,
    1. minor GC mark and move reachables to survivor 2 the empty one
    2. move reachables in S1 to S2, increment S1 survivor counter. By moving around this way we avoids compacting steps.
  4. Repeat: allocate in Eden, Eden full, mark and move all reachable (Eden or Sx) to the empty S, increment counters, sweep unreachables
  5. If counter hit threshold, promote them to Old Generation.
  6. If Old Generation is near full, Major GC runs across the entire heap MSC. very time consuming, can lead to pause.

GC types & Usages

Basic Serial Collector

  • runs in single thread, used for basic applications

Concurrent collector (CMSC)

  • A thread that performs GC concurrently for the app.
  • No waiting for the old generation.
  • small pause only when mark / remark. Otherwise no pause “concurrent”.
  • use when
    • more available memory
    • high number of CPUs
    • needs responsiveness

Parallel collector (PMSC)

  • use multiple CPU and multiple threads to do MSC (mark sweep compact).
  • only runs when heap is full / near full.
  • long pause when it runs.
  • use when
    • less available memory
    • less number of CPUs
    • needs high throughput & can withstand pauses

G1 collector (Garbage - first) 1.7+

  • Divide heap into small regions, each of them can be eden / survivor / old
  • Dynamically chose regions with most garbage to GC
    • More predictable GC pauses
    • Low pauses and fragmentation
    • Parallelism & concurrency together
    • Better heap utilization

Usage

1WBMZj.png

  • notice CMS is only in old generation.

Memory Leaks

  • 内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。

  • 建议将不用的对象引用设为null来避免临时的内存泄漏: 将他们标记为可清理对象。

  • 常发生于

    1. 使用生命周期较长的单位:单例模式类对象, 静态集合类形成的对象引用 暂时内存泄漏:只有相应对象/类被释放时才会gc
    1
    2
    3
    4
    5
    6
    7
    Static Vector v = new Vector(); 
    for (int i = 1; i<100; i++)
    {
    Object o = new Object();
    v.add(o);
    o = null;
    }
    1. 使用了各种资源连接忘了关:数据库连接,网络连接,IO连接等,显式调用close关闭后才能被GC回收

    2. **改变哈希值,**当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      public static void main(String[] args) {
      Set<Person> set = new HashSet<Person>();
      Person p1 = new Person("唐僧","pwd1",25);
      Person p2 = new Person("孙悟空","pwd2",26);
      Person p3 = new Person("猪八戒","pwd3",27);
      set.add(p1);
      set.add(p2);
      set.add(p3);
      System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:3 个元素!
      p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变
      set.remove(p3); //此时remove不掉,造成内存泄漏
      set.add(p3); //重新添加,居然添加成功
      System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:4 个元素!
      }

Java对象引用类别:强引用,软引用,弱引用,虚引用

  • 强引用就是平时最常用的引用,当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

  • 如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。

  • 只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存

  • 虚引用,这种引用不常用,主要用途是关联对象,实现对对象引用关系追踪。虚引用并不会决定对象的生命周期。也无法通过虚引用得到一个对象。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

  • 几种引用分别位于java.lang.ref.SoftReference; java.lang.ref.WeakReference; 和 java.lang.ref.PhantomReference;

finalize( )

  • do not use finalizer manually: finalize()

    • guranteed to be called only ONCE at the end of execution when GC is done

    • no gurantee of gc happens

    • do not use finalizer on anything important

    • suppose we try to resurect obj by recreating an object in finalize( ), this = new xxx( );

      • obj is recreated in memory at the end of gc = memory leak
  • finalize被调用时代表gc准备回收该对象内存

    • 对象不可达,但是调用finalize之后又变得可达的情况存在,在finalize函数中通过this指针让其他句柄执行本身即可,但是再下次回收时不会再调用finalize,因为只能调用一次。
    1
    2
    3
    4
    protected void finalize()
    {
    main.ref=this; // 恢复本对象,让本对象可达
    }
    • 垃圾回收器不能对用Java以外的代码编写的Class(比如JNI,C的new方法分配的内存)进行正确的回收,这时就需要覆盖默认finalize的方法来实现对这部分内存的正确释放和回收(比如C需要delete)。
    • finalize不能等同于C对象的destructor析构函数,C析构函数在在对象生命周期结束时会确定执行,而finalize函数的调用具有很大的不确定性。

    1、调用时间不确定——有资源浪费的风险

    如果把某些稀缺资源放到finalize()中释放,可能会导致该稀缺资源等上很久很久以后才被释放。造成资源的浪费!另外,某些类对象所携带的资源(比如某些JDBC的类)可能本身就很耗费内存,这些资源的延迟释放会造成很大的性能问题。

    2、可能不被调用——有资源泄漏的风险

    在某些情况下,finalize()压根儿不被调用。比如在JVM退出的当口,内存中那些对象的finalize函数可能就不会被调用了。

    因此一些清理工作如文件的关闭,连接的关闭等不要放到finalize函数中,要在程序中单独进行管理,一般finalize只做C/C++内存的回收。
    3、对象可能在finalize函数调用时复活——有诈尸的风险  
    诈尸的情况比较少见,不过俺还是稍微提一下。
      本来,只有当某个对象已经失效(没有引用),垃圾回收器才会调用该对象的finalize函数。但是,万一碰上某个变态的程序员,在finalize()函数内部再把对象自身的引用(也就是this)重新保存在某处,也就相当于把自己复活了(因为这个对象重新有了引用,不再处于失效状态)。这种做法是不是够变态啊

    为了防止发生这种诡异的事情,垃圾回收器只能在每次调用完finalize()之后再次去检查该对象是否还处于失效状态。这无形中又增加了JVM的开销。
      随便提一下。由于JDK的文档中规定了(具体见“这里”),JVM对于每一个类对象实例最多只会调用一次finalize()。所以,对于那些诈尸的实例,当它们真正死亡时,finalize()反而不会被调用了。这看起来是不是很奇怪?
    4、要记得自己做异常捕获
      刚才在介绍finalize()调用机制时提到,一旦有异常抛出到finalize函数外面,会被垃圾回收线程捕获并丢弃。也就是说,异常被忽略掉了(异常被忽略的危害,“这里”有提到)。为了防止这种事儿,凡是finalize()中有可能抛出异常的代码,你都得写上try catch语句,自己进行捕获。  
    5、要小心线程安全
      由于调用finalize()的是垃圾回收线程,和你自己代码的线程不是同一个线程;甚至不同对象的finalize()可能会被不同的垃圾回收线程调用(比如使用“并行收集器”的时候)。所以,当你在finalize()里面访问某些数据的时候,还得时刻留心线程安全的问题。

常量池总结

  • **class文件常量池 / constant pool:存静态常量,符号引用和字面量 ** 存在于.class文件中

  • 运行时常量池:类加载后,常量池中的数据会在运行时常量池中存放!这里所说的常量包括:基本类型包装类**(包装类不管理浮点型,整形只会管理-128到127)和String(也可以通过String.intern()方法可以强制将String放入常量池)**

  • 字符串常量池: HotSpot VM里,记录interned string的一个全局表叫做StringTable,它本质上就是个HashSet。注意它只存储对java.lang.String实例的引用,而不存储String对象的内容

  • 运行时常量池和字符串常量池都在堆中。

Other Notes

  1. Tune the heaps
1WspsU.png
  1. GC logging with graphical tool if we suspect gc is the problem for performance issues.
1
2
3
4
5
-verbose:gc

-XX:+PrintGCDetails

-Xloggc:gc.log
  1. jvisualvm: visual gc plugin
打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  • © 2016-2020 th2zz

请我喝杯咖啡吧~

支付宝
微信