对JVM的理解
1、为什么需要了解JVM?
有一个段子:在餐厅吃饭,吃完后把餐盘端走的是C++从业者,吃完就走的是Java从业者。
大部分IT从业者天天面对的是CRUD,有人觉得JVM对实际工作没有什么用处,但我认为事实并不是如此。
不知道大家有没有接触过C++,在上大学的时候本人学过一点,C++可以手动给对象分配内存,创建对象之后还需要开发人员手动回收掉,这是C++的痛点。哪怕有经验的C++从业者,在排查起内存泄露这个问题起来,也会觉得痛不欲生。
Java很好的改进了这一点,通过JVM来自动管理垃圾的回收,彻底解放开发人员对于内存的回收。
当我们new 一个对象的时候,我们无需考虑别的,只需要用他就行了。如果你用过C++,再来用Java,一定会好奇JVM是如何做到这点的。
既然JVM帮我们做好了垃圾回收,那么为什么我们还要了解JVM呢?因为了解JVM的垃圾回收机制,可以让你写出更好的代码,写出的代码就可以让JVM尽量高效的回收垃圾,这样可以提高系统的性能。
2、JVM内存模型
JVM主要包含程序计数器、Java栈、本地方法栈、Java堆和方法区5个部分。这里主要讲讲java栈和堆。
大部分Java从业者可能感觉堆栈是老生常谈:
“对于栈,每创建一个线程,JVM都会为他分配一个栈,每执行一个方法,又会为这个栈分配一个栈帧。每创建一个对象,就会为这个对象在堆中分配内存,栈中存放这个对象的引用。”等等诸如此类。
上文提到过,为什么JAVA创建/销毁一个对象的时候,开发人员不需要显式地申请/归还内存呢? 因为JVM有自己的垃圾回收器,有自己的一套垃圾回收算法,当我们定义一个对象的时候:
publiv void init() {
Map<String, Object> map= new HashMap<String, Object>();
}
map是分配在Java栈上的,实际的HashMap对象分配在堆上,当init方法执行完毕后,栈帧消亡,下次GC时JVM会从GcRoot(GcRoot包括什么这里暂时不展开讲述,他包括战帧中的引用)中找出所有处于强引用链的对象,此时HashMap对象是找不到引用链的,就会被回收掉。JVM大概就是这样完成了垃圾回收。
基于上面的讲述,你在看到某些开源框架的源码在处理某些变量的时候会把某些引用置空,类似这样:
publiv void init() {
Map<String, Object> map= new HashMap<String, Object>();
...
map = null;
...耗时操作...
}
这样你就可以理解为什么他会这么做了,因为上面的对象在下面用不到,所以提前置空会让强引用链断掉,这样如果说JVM在执行耗时操作的时候触发了GC,就可以把那些没用的对象给回收掉,可以提高JVM内存的使用率。
3、拓展
Jvm并不想上文表述的那么简单,但是简单的这么理解也是可以的。实际上JVM远比讲述的要复杂许多,举个例子,很多人都以为JVM在分配对象的时候只能在堆上分配,其实未必。
试想,如果JVM每次给对象分配内存的时候都只是简单的在栈上分配,那么多个线程在申请内存的时候会不会有冲突呢(比如A,B两个线程同时new Object,那就要同时申请内存,如果不加锁,就会申请到同一块内存)?有冲突就要加锁,如果真的是这样,效率不知道要低到那里去。
针对上述问题,JVM也提供了很好的解决方案,JVM中的对象绝大多数都是朝生夕死的,还没到老年代就被GC掉了,所以JVM在给对象分配内存的时候会首先尝试在栈上分配,但是这并不是所有对象都能在栈上分配的,针对对象会做一个逃逸分析:
publiv void init() {
Map<String, Object> map = new HashMap<>();
Map<String, Object> map1 = new HashMap<>();
new Thread(() -> {
map.put("a","1");
}).start();
new Thread(() -> {
map.put("b","2");
}).start();
}
比如上述代码,对于map这个对象,有3个线程同时操作(主线程和两个子线程),对于map就没办法分配在栈上,因为如果你把map分配在栈上,其他线程是没办法操作的。但是对于map1这个对象,JVM会尝试把它先分配在栈上,因为map1对象只对主线程可见。
栈空间是很小的,并不是所有像上述的map1都能分配在栈上,如果map1稍微大一点,如下:
publiv void init() {
Map<String, Object> map = new HashMap<>();
Map<String, Object> map1 = new HashMap<>(100);
new Thread(() -> {
map.put("a","1");
}).start();
new Thread(() -> {
map.put("b","2");
}).start();
}
可能map1就放不到栈里面去了,这时候TLAB就发挥了作用,TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。
如果开启了JVM 的TLAB参数,JVM就会在每个线程初始化的时候在eden区申请一块指定大小的内存,这块内存只给当前线程使用,这样就会避免申请内存时的冲突问题。
这样看起来,对一个对象通过逃逸分析之后,如果不会逃逸,大概率分配在栈上或者TLAB上,这些对象在申请内存和销毁的时候效率就会很高。只有少数比较大的对象,或者会逃逸的对象,才会直接在堆上分配内存(TLAB其实也是堆的一部分)
这段时间有点忙,很久没发文,希望还是能坚持下来,有不对的地方还请大家多多指正,私信或者评论都可以。