那些繞不過去的 Java 知識(shí)點(diǎn)(二)

性能測(cè)試
環(huán)境部署
$ python -m pip install --trusted-host pypi.python.org --trusted-host files.pythonhosted.org --trusted-host pypi.org locustio

$ locusts -V
  [2019-02-02 10:17:55,387] BenedictJin.local/INFO/stdout: Locust 0.9.0
  [2019-02-02 10:17:55,387] BenedictJin.local/INFO/stdout:

$ locusts -f put_one_point_with_variable_data_types.json --processes 4
  [2019-02-02 10:18:53,668] BenedictJin.local/INFO/locust.main: Starting web monitor at *:8089
  [2019-02-02 10:18:53,668] BenedictJin.local/INFO/locust.main: Starting Locust 0.9.0
測(cè)試限流
# 需要先打開服務(wù)端的限流
$ source .env
$ locusts -f testcases/limit/limit_tps_with_number.json --processes 4 -H ${base_url_from_env} -L DEBUG
$ locusts -f testcases/limit/limit_tps_with_string.json --processes 4 -H ${base_url_from_env} -L DEBUG
抓包并轉(zhuǎn)為 TestCase
使用 Charles 抓包工具

通過該方法纹坐,可以將現(xiàn)有的測(cè)試用例蛹含,輕松地轉(zhuǎn)為 http_runner 的表現(xiàn)形式决左。具體步驟如下:

a) 安裝 Charles Proxy

# Mac
# https://www.charlesproxy.com/download/

# Linux
$ cat <<EOF > /etc/yum.repos.d/Charles.repo
  [charlesproxy]
  name=Charles Proxy Repository
  baseurl=https://www.charlesproxy.com/packages/yum
  gpgkey=https://www.charlesproxy.com/packages/yum/PublicKey
  EOF

$ sudo yum install charles-proxy

b) 關(guān)閉 browsermob-proxy

$ pkill -1 -f browsermob-proxy

c) 在 Http Request 中增加 Proxy

request.setConfig(RequestConfig.custom().setProxy(new HttpHost("127.0.0.1", 8888)).build());
// 修改好之后颜启,重新打包編譯即可
使用 browsermobproxy 代理庫

也可以通過編寫 Python 腳本偷俭,來創(chuàng)建 Proxy,并使用 Json 庫將 Proxy 抓包內(nèi)容以 har 的形式保存為文件

import json
import os
import time

import psutil
from browsermobproxy import Server

# pkill -1 -f browsermob-proxy
for proc in psutil.process_iter():
    if proc.name() == "browsermob-proxy":
        proc.kill()

server = Server(path="/apps/browsermob-proxy-2.1.4/bin/browsermob-proxy",
                options={'port': 8880})
server.start()

time.sleep(1)
proxy = server.create_proxy(params={'port': 8881})
time.sleep(1)

with open('testcases.txt', "r+", encoding="utf8") as of:
    os.chdir("/code/yuzhouwan-test-cloud")
    for line in of.readlines():

        line = line.strip('\n')
        print(line)
        if '.' not in line:
            continue
        splits = line.split('.')
        clazz = splits[0]
        method = splits[1]
        proxy.new_har("com.yuzhouwan.yuzhouwan.client.%s,%s" % (clazz, method),
                      options={'captureHeaders': True, 'captureContent': True, 'captureBinaryContent': True})

        command = 'java -ea -Didea.test.cyclic.buffer.size=1048576 -javaagent:' \
                  '<...>" ' \
                  'com.intellij.rt.execution.junit.JUnitStarter -ideVersion5 -junit4 ' \
                  'com.yuzhouwan.yuzhouwan.client.%s,%s' % (clazz, method)
        os.system(command)

        json_obj = json.dumps(proxy.har)

        file_dir = '/Users/benedictjin/Documents/http_runner/%s/' % clazz
        file_name = method
        file_extension = '.har'
        if not os.path.exists(file_dir):
            os.makedirs(file_dir)

        fileObject = open(file_dir + file_name + file_extension, 'w')
        fileObject.write(json_obj)
        fileObject.close()

server.stop()
二次開發(fā)
解決 http_runner 中只能解析部分 parameters 的問題
# vim httprunner/parser.py
parsed_parameters_list = []
for parameter in parameters:
    parameter_name, parameter_content = list(parameter.items())[0]
    parameter_name_list = parameter_name.split("-")


# 改為
parsed_parameters_list = []
for parameter_name, parameter_content in parameters.items():
    parameter_name_list = parameter_name.split("-")
使得 har2case 支持 list 結(jié)構(gòu)的 JSON

詳見:Let _make_validate method supports the list structures JSON #18

使得 har2case 支持普通文本的返回類型
# vim har2case/core.py
mime_type = resp_content_dict.get("mimeType")
if mime_type and mime_type.startswith("application/json"):

    encoding = resp_content_dict.get("encoding")
    if encoding and encoding == "base64":
        content = base64.b64decode(text).decode('utf-8')
        try:
            resp_content_json = json.loads(content)
        except JSONDecodeError:
            logging.warning("response content can not be loaded as json.")
            return
    else:
        resp_content_json = json.loads(text)

# 改為
mime_type = resp_content_dict.get("mimeType")
if mime_type and mime_type.startswith("application/json"):

    encoding = resp_content_dict.get("encoding")
    if encoding and encoding == "base64":
        text = base64.b64decode(text).decode('utf-8')
    try:
        resp_content_json = json.loads(text)
    except JSONDecodeError:
        logging.warning("response content can not be loaded as json.")
        return
    print("resp_content_json", resp_content_json)
使二次開發(fā)的改動(dòng)生效
$ pip uninstall har2case -y
$ cd /usr/local/har2case/har2case
$ python setup.py install

$ har2case --log-level DEBUG test.har
可視化管理系統(tǒng)
部署

按照部署手冊(cè)即可部署成功缰盏。相關(guān)的涌萤,比如淹遵,MySQL 安裝RabbitMQ 安裝负溪,都能找到很多資料透揣。這里,主要記錄幾個(gè)可能踩到的坑

可視化管理頁面

配置環(huán)境笙以、增加測(cè)試用例淌实、組合測(cè)試套件等,效果如下圖所示:

運(yùn)行結(jié)果報(bào)告頁面猖腕,效果如下圖所示:

支持權(quán)限管理拆祈,超級(jí)管理員可以配置整個(gè)管理系統(tǒng),效果如下圖所示:

踩到的坑
MySQL 默認(rèn)編碼導(dǎo)致 Django 報(bào)錯(cuò) OperationalError
  • 描述
$ python manage.py migrate
# 報(bào)錯(cuò) django.db.utils.OperationalError: (1366, "Incorrect string value for column 'name' at row 1")
  • 解決

這個(gè)問題是因?yàn)樾掳惭b的 MySQL 默認(rèn)的編碼不是 UTF-8 導(dǎo)致的倘感,停掉 mysql 實(shí)例配置 my.cnf 再重啟即可放坏。如果停止不掉,可以 pkill mysql 強(qiáng)制停止老玛。之前創(chuàng)建的數(shù)據(jù)庫也需要?jiǎng)h掉重新創(chuàng)建

$ cd /usr/local/mysql/support-files
$ sudo ./mysql.server stop
$ vim my.cnf
  [client]
  default-character-set=utf8

  [mysql]
  default-character-set=utf8

  [mysqld]
  collation-server=utf8_unicode_ci
  init-connect='SET NAMES utf8'
  character-set-server=utf8

# 如果 my.cnf 沒有生效淤年,可能需要拷貝一份到 /private/etc 目錄下
$ cd /private/etc
$ sudo cp /usr/local/mysql/support-files/my.cnf .

$ sudo ./mysql.server restart
$ mysql -u root -p
mysql> show variables like '%char%';
+--------------------------+-----------------------------------------------------------+
| Variable_name            | Value                                                     |
+--------------------------+-----------------------------------------------------------+
| character_set_client     | utf8                                                      |
| character_set_connection | utf8                                                      |
| character_set_database   | utf8                                                      |
| character_set_filesystem | binary                                                    |
| character_set_results    | utf8                                                      |
| character_set_server     | utf8                                                      |
| character_set_system     | utf8                                                      |
| character_sets_dir       | /usr/local/mysql-5.7.25-macos10.14-x86_64/share/charsets/ |
+--------------------------+-----------------------------------------------------------+
8 rows in set (0.00 sec)

mysql> drop database HttpRunner;
Query OK, 27 rows affected (0.05 sec)

mysql> create database HttpRunner;
Query OK, 1 row affected (0.00 sec)

# 增加 `TEST_CHARSET` 連接配置
$ vim HttpRunnerManager/settings.py
  DATABASES = {
      'default': {
          # ...
          'TEST_CHARSET': 'utf-8'
      }
  }

$ python manage.py migrate
資料
Doc
Github
Blog

JVM 相關(guān)

基本概念

堆內(nèi) vs. 堆外

堆內(nèi)內(nèi)存

一般情況下,Java 中分配的非空對(duì)象都是由 Java 虛擬機(jī)的垃圾收集器管理的蜡豹,也稱為堆內(nèi)內(nèi)存(on-heap memory)

堆外內(nèi)存

堆外內(nèi)存(off-heap memory)意味著把內(nèi)存對(duì)象分配在 Java 虛擬機(jī)的堆以外的內(nèi)存麸粮,這些內(nèi)存直接受操作系統(tǒng)管理(而不是虛擬機(jī))

堆外內(nèi)存的優(yōu)勢(shì)
  • 對(duì)于大內(nèi)存有良好的伸縮性
  • 對(duì)垃圾回收停頓的改善可以明顯感覺到
  • 在進(jìn)程間可以共享,減少虛擬機(jī)間的復(fù)制
堆外內(nèi)存的劣勢(shì)
  • 數(shù)據(jù)結(jié)構(gòu)變得不那么直觀镜廉,發(fā)生內(nèi)存溢出的時(shí)候弄诲,排查定位會(huì)很麻煩
  • 如果數(shù)據(jù)結(jié)構(gòu)比較復(fù)雜,就要對(duì)它進(jìn)行串行化(serialization)娇唯,而串行化本身也會(huì)影響性能
  • 可以使用更大的內(nèi)存的同時(shí)齐遵,需要擔(dān)心虛擬內(nèi)存(即硬盤)的速度對(duì)應(yīng)用的影響
示例
/*
-XX:MaxDirectMemorySize=64M 可以控制堆外內(nèi)存大小,默認(rèn)在 VM 靜態(tài)變量 directMemory 為 64M

maxDirectMemory: 67108864
isDirect: true
 */
public static void main(String[] args) throws InterruptedException {
    System.out.println("maxDirectMemory: " + VM.maxDirectMemory());
    ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024 * 64);
    Thread.sleep(200);
    boolean isDirect = buffer.isDirect();
    System.out.println("isDirect: " + isDirect);
    if (isDirect) ((DirectBuffer) buffer).cleaner().clean();
    else buffer.clear();
    Thread.sleep(200);
    System.exit(0);
}
參考

指針壓縮

定義

64 位環(huán)境下, 寄存器是 64 位的,對(duì)應(yīng)指針也就成 64 位了想许,也就是 8 字節(jié)伶授。我們知道 4 字節(jié)可以表示 4G,實(shí)際中基本不會(huì)有需要加載這么多對(duì)象的情況伸刃。因此 8 字節(jié)就顯得浪費(fèi)了谎砾,narrowKlass 只使用 4 個(gè)字節(jié),預(yù)分配給 _metadata 的 8 字節(jié)中的另外 4 字節(jié)就可以用做它用了捧颅【巴迹看似 4 個(gè)字節(jié)無關(guān)緊要,但是堆中存在上千萬到億個(gè)對(duì)象時(shí)碉哑,省下的內(nèi)存就是幾百兆啊

流程

基于以下事實(shí)

  • CPU 使用的虛擬地址是 64 位的挚币,訪問內(nèi)存時(shí)亮蒋,必須使用 64 位的指針訪問內(nèi)存對(duì)象
  • Java 對(duì)象是分配于具體的某個(gè)內(nèi)存位置的,對(duì)其訪問必須使用 64 位地址
  • 對(duì) Java 對(duì)象內(nèi)的引用字段進(jìn)行訪問時(shí), 必須經(jīng)過虛擬機(jī)這一層, 操作某個(gè)對(duì)象引用不管是 getfield 還是 putfield妆毕,都是由虛擬機(jī)來執(zhí)行慎玖。或者簡單來說笛粘,要改變 Java 對(duì)象某個(gè)引用字段, 必須經(jīng)過虛擬機(jī)的參與

細(xì)心的你從上面一定可以看出一點(diǎn)線索趁怔,由于存一個(gè)對(duì)象引用和取一個(gè)對(duì)象引用必須經(jīng)過虛擬機(jī),所以完全可以在虛擬機(jī)這一層做些手腳薪前。對(duì)于外部來說润努,putfield 提供的對(duì)象地址是 64 位的,經(jīng)過虛擬機(jī)的轉(zhuǎn)換示括,映射到 32 位铺浇,然后存入對(duì)象;getfield 指定目標(biāo)對(duì)象的 64 位地址和其內(nèi)部引用字段的偏移垛膝,取 32 位的數(shù)據(jù)鳍侣,然后反映射到 64 位內(nèi)存地址。對(duì)于外部來說吼拥,只看見 64 位的對(duì)象放進(jìn)去倚聚,拿出來,內(nèi)部的轉(zhuǎn)換是透明的(本質(zhì)上凿可,就是按字節(jié)尋址秉沼,變成了按字尋址

原理
描述

CompressedOops 的原理是,解釋器在解釋字節(jié)碼時(shí)矿酵,植入壓縮指令(不影響正常和 JVM 優(yōu)化后的指令順序)
 具體邏輯是,當(dāng)對(duì)象被讀取時(shí)矗积,解壓全肮,存入 heap 時(shí),壓縮

壓縮指令偽碼
! int R8; oop[] R9; // R9 is 64 bits
! oop R10 = R9[R8]; // R10 is 32 bits
! load compressed ptr from wide base ptr:
movl R10, [R9 + R8<<3 + 16]
! klassOop R11 = R10._klass; // R11 is 32 bits
! void* const R12 = GetHeapBase();
! load compressed klass ptr from compressed base ptr:
movl R11, [R12 + R10<<3 + 8]
零基壓縮優(yōu)化(Zero Based Compressd Oops)

零基壓縮是針對(duì)壓解壓動(dòng)作的進(jìn)一步優(yōu)化棘捣。它通過改變正常指針的隨機(jī)地址分配特性辜腺,強(qiáng)制從零開始做分配(需要 OS 支持),進(jìn)一步提高了壓解壓效率

要啟用零基壓縮乍恐,你分配給 JVM 的內(nèi)存大小必須控制在 4G 以上评疗,32G 以下
 如果小于 4G,那么 JVM 會(huì)使用低虛擬地址空間(low virutal address space茵烈,64 位下模擬 32 位)百匆,這樣就不需要做壓解壓動(dòng)作了
 而對(duì)于大于 32G,將采用默認(rèn)的隨機(jī)地址分配特性呜投,進(jìn)行壓解壓

適用場(chǎng)景

CompressedOops加匈,可以讓跑在 64 位平臺(tái)下的 JVM存璃,不需要因?yàn)楦鼘挼膶ぶ罚冻?Heap 容量損失的代價(jià)雕拼。
 不過纵东,它的實(shí)現(xiàn)方式是在機(jī)器碼中植入壓縮與解壓指令,可能會(huì)給 JVM 增加額外的開銷

參數(shù)控制

-XX:+UseCompressedOops 開啟(jdk1.6.0_14+
 -XX:-UseCompressedOops 關(guān)閉

零基壓縮的邊界
$ JAVA_HOME=`/usr/libexec/java_home -v 1.8` java -Xmx32766m -XX:+PrintFlagsFinal 2> /dev/null | grep UseCompressedOops
  bool UseCompressedOops   := true

$ JAVA_HOME=`/usr/libexec/java_home -v 1.8` java -Xmx32767m -XX:+PrintFlagsFinal 2> /dev/null | grep UseCompressedOops
  bool UseCompressedOops   = false

# 如果已經(jīng) JVM 進(jìn)程已經(jīng)啟動(dòng)了啥寇,可以通過 jinfo 進(jìn)行查詢
$ jinfo -flag UseCompressedOops 18979
  -XX:-UseCompressedOops
壓縮 class 信息中的指針

從 JDK6_u23 開始 UseCompressedOops 被默認(rèn)打開了偎球。因此既能享受 64bit 帶來的好處,又避免了 64bit 帶來的性能損耗辑甜。當(dāng)然衰絮,如果你有機(jī)會(huì)使用超過 32G 的堆內(nèi)存,記得把這個(gè)選項(xiàng)關(guān)了

到了 Java8栈戳,永久代被干掉了岂傲,有了 “meta space” 的概念,存儲(chǔ) JVM 中的元數(shù)據(jù)子檀,包括 Byte code镊掖,class 等信息。Java8 在 UseCompressedOops 之外褂痰,額外增加了一個(gè)新選項(xiàng)叫做 UseCompressedClassPointer亩进。這個(gè)選項(xiàng)打開后,class 信息中的指針也用 32bit 的 Compressed 版本缩歪。而這些指針指向的空間被稱作 “Compressed Class Space”归薛。默認(rèn)大小是 1G,但可以通過 “CompressedClassSpaceSize” 調(diào)整

如果你的 Java 程序引用了太多的包匪蝙,有可能會(huì)造成這個(gè)空間不夠用主籍,于是會(huì)看到

java.lang.OutOfMemoryError: Compressed class space

這時(shí),一般調(diào)大 CompreseedClassSpaceSize 就可以了

常用 Collector

CMS

定義

CMS逛球,全稱 Concurrent Mark Sweep千元,是一款并發(fā)的、使用標(biāo)記-清除算法的垃圾回收器

內(nèi)存碎片

CMS 本身是不會(huì)移動(dòng)內(nèi)存的颤绕,長時(shí)間運(yùn)行后幸海,會(huì)產(chǎn)生很多內(nèi)存碎片,導(dǎo)致沒有一段足夠大的連續(xù)區(qū)域可以存放大對(duì)象奥务,導(dǎo)致 promotion failed物独、concurrent mode failure 等異常,從而觸發(fā) Full GC

啟用 -XX:+UseCMSCompactAtFullCollection 參數(shù)之后氯葬,會(huì)在 Full GC 的時(shí)候挡篓,對(duì)年老代的內(nèi)存進(jìn)行壓縮。再配合 -XX:CMSFullGCsBeforeCompaction=0 參數(shù)可以控制多少次 FGC 后對(duì)老年代做壓縮操作帚称。默認(rèn)值為 0瞻凤,代表每次都?jí)嚎s憨攒。該參數(shù)開啟后,會(huì)把對(duì)象移動(dòng)到內(nèi)存的最左邊阀参,可能會(huì)影響性能肝集,但是可以消除碎片

浮動(dòng)垃圾

由于 CMS 并發(fā)清理階段用戶線程還在運(yùn)行著,伴隨程序運(yùn)行自然就還會(huì)有新的垃圾不斷產(chǎn)生蛛壳,這一部分垃圾出現(xiàn)在標(biāo)記過程之后杏瞻,CMS 無法在當(dāng)次收集中處理掉它們,只好留待下一次 GC 時(shí)再清理掉衙荐。這些無法被 GC 掉捞挥,留到下一次 GC 的垃圾,稱之為浮動(dòng)垃圾

G1GC

C4

Shenandoah

ZGC

常用參數(shù)

默認(rèn)配置

-Xss 堆棧大小
Platform Default(KB)
Windows IA32 64
Linux IA32 128
Windows x86_64 128
Linux x86_64 256
Windows IA64 320
Linux IA64 1024
Solaris Sparc 512

Tips: 實(shí)際案例 OpenTSDB 大查詢導(dǎo)致 java.long.StackOverflowError 后砌函,調(diào)整 -Xss32m 得以解決

日志方面

常用的配置項(xiàng)
Flag Comment
-verbose:gc The -verbose:gc option enables logging of garbage collection (GC) information.
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDetails and -XX:+PrintGCTimeStamps are used to print detailed information about garbage collection.(這里由于 -verbose:gc 相當(dāng)于 -XX:+PrintGCDetails 的別名,避免冗余溜族,應(yīng)該去掉 -verbose:gc
-XX:-PrintTenuringDistribution Print tenuring age information.
-XX:-UseGCLogFileRotation Enabled GC log rotation, requires -Xloggc.
-XX:NumberOfGCLogFiles=3 Set the number of files to use when rotating logs, must be >= 1. The rotated log files will use the following naming scheme, <filename>.0, <filename>.1, ..., <filename>.n-1.
-XX:GCLogFileSize=8K The size of the log file at which point the log will be rotated, must be >= 8K.
實(shí)際效果
描述

Apache Druid 為例讹俊,在 Tranquility 組件啟動(dòng)時(shí),加上 -XX:+PrintTenuringDistribution 參數(shù)后的效果如下:

$ nohup ./bin/tranquility -J-XX:+HeapDumpOnOutOfMemoryError -J-XX:HeapDumpPath=/data02/druid/ -J-verbose:gc -J-XX:+PrintGCDetails -J-XX:+PrintGCDateStamps -J-XX:+PrintGCDetails -J-XX:+PrintTenuringDistribution -J-Xloggc:/data02/druid/gc.log -Ddruid.extensions.directory=/home/druid/software/druid/extensions -Ddruid.extensions.loadList='["druid-avro-extensions"]' kafka -configFile conf/service/hbase_metrics_kafka2_avro.json > /home/druid/logs/tranquility/hbase_metrics_kafka2_avro.log 2>&1 &

# before
2017-03-23T14:38:23.582+0800: 90.288: [GC (Allocation Failure) [PSYoungGen: 9934821K->5622K(10469376K)] 12602986K->7093554K(28291584K), 0.6114898 secs] [Times: user=13.74 sys=0.06, real=0.61 secs] 
2017-03-23T14:38:24.801+0800: 91.507: [GC (Allocation Failure) [PSYoungGen: 10071976K->3022K(10469376K)] 17159909K->8861529K(28291584K), 0.1748974 secs] [Times: user=3.91 sys=0.03, real=0.17 secs] 
2017-03-23T14:38:26.178+0800: 92.884: [GC (Allocation Failure) [PSYoungGen: 10303201K->3733K(10469376K)] 19161707K->9746931K(28291584K), 0.1744318 secs] [Times: user=3.89 sys=0.02, real=0.17 secs] 
2017-03-23T14:38:27.277+0800: 93.983: [GC (Allocation Failure) [PSYoungGen: 10091092K->902K(10469888K)] 19834290K->10628837K(28292096K), 0.1108355 secs] [Times: user=2.48 sys=0.01, real=0.12 secs] 
2017-03-23T14:38:28.586+0800: 95.292: [GC (Allocation Failure) [PSYoungGen: 10089131K->1078K(10472448K)] 20717066K->11513094K(28294656K), 0.1054663 secs] [Times: user=2.34 sys=0.01, real=0.11 secs] 
2017-03-23T14:38:29.840+0800: 96.546: [GC (Allocation Failure) [PSYoungGen: 10400114K->1501K(10461696K)] 21912131K->13281552K(28283904K), 0.1337201 secs] [Times: user=2.99 sys=0.01, real=0.13 secs] 
2017-03-23T14:38:30.865+0800: 97.571: [GC (Allocation Failure) [PSYoungGen: 10133038K->1099K(10472448K)] 23413089K->15049201K(28294656K), 0.1312707 secs] [Times: user=2.94 sys=0.02, real=0.13 secs] 
2017-03-23T14:38:30.996+0800: 97.702: [Full GC (Ergonomics) [PSYoungGen: 1099K->0K(10472448K)] [ParOldGen: 15048102K->1783009K(17664000K)] 15049201K->1783009K(28136448K), [Metaspace: 54341K->54341K(1095680K)], 0.3020992 secs] [Times: user=3.90 sys=0.00, real=0.31 secs]

# after
2017-03-23T14:41:03.576+0800: 31.699: [GC (Allocation Failure) 
Desired survivor size 23592960 bytes, new threshold 1 (max 15)
[PSYoungGen: 9975727K->6112K(10454016K)] 13540045K->6222293K(27026432K), 0.2530233 secs] [Times: user=5.67 sys=0.03, real=0.25 secs] 
2017-03-23T14:41:05.031+0800: 33.154: [GC (Allocation Failure) 
Desired survivor size 23592960 bytes, new threshold 1 (max 15)
[PSYoungGen: 10241863K->11872K(10457088K)] 16458045K->8885401K(27029504K), 0.2811251 secs] [Times: user=6.27 sys=0.05, real=0.29 secs] 
2017-03-23T14:41:06.022+0800: 34.145: [GC (Allocation Failure) 
Desired survivor size 25165824 bytes, new threshold 1 (max 15)
[PSYoungGen: 10378567K->15664K(10458624K)] 19252097K->10661383K(27031040K), 0.2552984 secs] [Times: user=5.70 sys=0.05, real=0.25 secs] 
2017-03-23T14:41:06.893+0800: 35.016: [GC (Allocation Failure) 
Desired survivor size 24117248 bytes, new threshold 1 (max 15)
[PSYoungGen: 10054634K->10669K(10460160K)] 20700353K->12431545K(27032576K), 0.3152384 secs] [Times: user=6.89 sys=0.19, real=0.31 secs] 
2017-03-23T14:41:07.208+0800: 35.331: [Full GC (Ergonomics) [PSYoungGen: 10669K->0K(10460160K)] [ParOldGen: 12420876K->1803376K(14484992K)] 12431545K->1803376K(24945152K), [Metaspace: 54185K->54185K(1095680K)], 0.2905749 secs] [Times: user=4.77 sys=0.00, real=0.29 secs] 
參考

內(nèi)存方面

-XX:+AlwaysPreTouch
描述

Pre-touch the Java heap during JVM initialization. Every page of the heap is thus demand-zeroed during initialization rather than incrementally during application execution.

參考

啟動(dòng)方面

-ea
# Usage:
  -enableassertions[:<packageName>"..." | :<className>]
  -ea[:<packageName>"..." | :<className>]

  該參數(shù)用來設(shè)置 jvm 是否啟動(dòng)斷言機(jī)制(從 JDK 1.4 開始支持)仍劈,默認(rèn) JVM 是關(guān)閉斷言機(jī)制的,增加 `-ea` 參數(shù)可打開斷言機(jī)制
  不指定 packageName 和 className 時(shí)運(yùn)行所有包和類中的斷言
  如果希望只運(yùn)行某些包或類中的斷言寡壮,可將包名或類名加到 `-ea` 之后
  比如想要啟動(dòng)包 `com.yuzhouwan.common` 下的斷言機(jī)制贩疙,可用命令 `java -ea:com.yuzhouwan.common...<Main Class>`

實(shí)戰(zhàn)技巧

查看 JVM 參數(shù)的默認(rèn)值

# 查看 JDK8 中是否默認(rèn)打開了 “對(duì)象頭壓縮” 開關(guān)
# 實(shí)際上,JDK6u23 版本之后况既,Hotspot 都已經(jīng)打開了 -XX:+UseCompressedOops 功能(OOP这溅,Ordinary Object Pointer)
$ java -XX:+PrintFlagsFinal -version | grep UseCompressedOops
       bool UseCompressedOops                        := true
            {lp64_product}
  java version "1.8.0_111"
  Java(TM) SE Runtime Environment (build 1.8.0_111-b14)
  Java HotSpot(TM) 64-Bit Server VM (build 25.111-b14, mixed mode)

查看 Java 進(jìn)程的 JVM 參數(shù)

$ jcmd 666 VM.flags
  666:
  -XX:CICompilerCount=15 -XX:ConcGCThreads=6 -XX:G1HeapRegionSize=33554432 -XX:InitialHeapSize=197904039936 -XX:MarkStackSize=4194304 -XX:MaxGCPauseMillis=200 -XX:MaxHeapSize=197904039936 -XX:MaxNewSize=118715580416 -XX:MinHeapDeltaBytes=33554432 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseFastUnorderedTimeStamps -XX:+UseG1GC

堆外內(nèi)存分析

google-perftools
安裝
# 先安裝 g++
$ yum -y install gcc gcc-c++

# 安裝 libunwind
$ wget http://download.savannah.gnu.org/releases/libunwind/libunwind-0.99.tar.gz
$ tar -xzvf libunwind-0.99.tar.gz
$ cd libunwind-0.99
$ ./configure --prefix=/data0/java/deploy/google-perftools/local/libunwind
$ make && make install

# 安裝 gperftools
$ wget https://github.com/gperftools/gperftools/releases/download/gperftools-2.5/gperftools-2.5.tar.gz
$ tar -xzvf gperftools-2.5.tar.gz
$ cd gperftools-2.5
$ ./configure --prefix=/data0/java/deploy/google-perftools/local/gperftools-2.5/
$ make && make install

# 使配置生效
$ vim /etc/ld.so.conf.d/usr_local_lib.conf
  /data0/java/deploy/google-perftools/local/libunwind/lib

# 執(zhí)行 ldconfig 命令,使libunwind生效棒仍。 需要 sudo 權(quán)限
$ /sbin/ldconfig

# 創(chuàng)建 tmp 目錄
$ mkdir -p /data0/java/deploy/google-perftools/local/tmp

# 加入環(huán)境變量
# 不要加在 .bashrc 里面芍躏,放在 jvm 啟動(dòng)腳本里面即可
  export LD_PRELOAD=/data0/java/deploy/google-perftools/local/gperftools-2.5/lib/libtcmalloc.so
  export HEAPPROFILE=/data0/java/deploy/google-perftools/local/tmp/gzip

# 啟動(dòng) jvm 進(jìn)程,就會(huì)在 /data0/java/deploy/google-perftools/local/ 目錄下生成 heap 文件
$ bin/hitsdb restart
分析
# 分析函數(shù)調(diào)用
$ /data0/java/deploy/google-perftools/local/gperftools-2.5/bin/pprof --text /usr/local/jdk1.8.0_181/bin/java /data0/java/deploy/google-perftools/local/tmp/gzip.0001.heap

  Using local file /usr/local/jdk1.8.0_181/bin/java.
  Using local file /data0/java/deploy/google-perftools/local/tmp/gzip.0001.heap.
  Total: 0.0 MB
       0.0  87.9%  87.9%      0.0 100.0% __FRAME_END__
       0.0   9.6%  97.5%      0.0   9.6% _nl_intern_locale_data
       0.0   1.2%  98.7%      0.0   1.2% __gconv_lookup_cache
       0.0   0.6%  99.3%      0.0   0.6% new_composite_name
       0.0   0.3%  99.6%      0.0  10.0% _nl_load_locale_from_archive
       0.0   0.2%  99.8%      0.0   0.2% __GI___strdup
       0.0   0.1%  99.9%      0.0   1.3% __wcsmbs_load_conv
       0.0   0.1% 100.0%      0.0   0.1% __bindtextdomain
       0.0   0.0% 100.0%      0.0  10.7% __GI_setlocale
       0.0   0.0% 100.0%      0.0   1.3% __btowc
       0.0   0.0% 100.0%      0.0   1.2% __gconv_find_transform
       0.0   0.0% 100.0%      0.0 100.0% __libc_start_main
       0.0   0.0% 100.0%      0.0   0.0% __textdomain
       0.0   0.0% 100.0%      0.0  10.0% _nl_find_locale

常用工具

JMC 監(jiān)控報(bào)警工具(Java Mission Control)

JMC 工具是 JDK 里面自帶的降狠,只需要運(yùn)行 jmc 命令即可

GCViewer

安裝

在 GCViewer 的下載頁面,找到當(dāng)前最新版本 gcviewer-1.35.jar 進(jìn)行下載

使用

# 文件 yuzhouwan01.gc 是由增加了 `verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/home/fm/logs/worker-gc.log` 參數(shù)的 JVM 進(jìn)程生成的 GC 日志
$ java -jar gcviewer-1.35.jar yuzhouwan01.gc

MAT 內(nèi)存分析工具(Memory Analyzer Tool)

創(chuàng)建 dump 文件

# 如果指定 live 參數(shù)的話庇楞,將會(huì)在 dump 之前榜配,強(qiáng)制進(jìn)行一次 Full GC
$ jmap -dump:[live,]format=b,file=<file_name>.hprof <pid>

火焰圖

簡介

火焰圖是一個(gè)二維圖片,火焰圖的 X 軸代表采樣總量吕晌,而 Y 軸代表棧深度蛋褥。每個(gè)框就代表了一個(gè)棧里的函數(shù),其寬度代表了所占用的 CPU 總時(shí)間睛驳。因此烙心,比較寬的框就表示膜廊,該函數(shù)運(yùn)行時(shí)間較慢或被調(diào)用次數(shù)較多,從而占用的 CPU 時(shí)間多淫茵。通過火焰圖爪瓜,相關(guān)設(shè)計(jì)或分析人員就可以輕松觀察到各個(gè)應(yīng)用占用 CPU 的情況

Linux 平臺(tái)上,對(duì)于多數(shù) C/C++ 編寫的應(yīng)用匙瘪,可以通過 perf 來方便的采樣铆铆,還可以進(jìn)一步生成火焰圖來更直觀地觀察。Java 是沒法直接用 perf 的丹喻。雖然有一個(gè) perf-map-agent薄货,但是并不方便,嘗試過程中還弄出了 kernel panic碍论,所以這玩意是不敢在線上用了谅猾。不過 JDK 自己其實(shí)已經(jīng)帶了一個(gè)采樣工具 FlightRecorder,算是 JMC 的一部分

以往獲得火焰圖所需要的復(fù)雜步驟

首先鳍悠,應(yīng)用啟動(dòng)的時(shí)候税娜,要給 java 加上參數(shù)

-XX:+UnlockCommercialFeatures -XX:+FlightRecorder -XX:FlightRecorderOptions=loglevel=info

因?yàn)?JVM 默認(rèn)在 safepoint 的地方才可以返回棧,所以最好加上下面兩個(gè)參數(shù)贼涩,讓 JVM 在非 safepoint 的時(shí)候也提供原數(shù)據(jù)

-XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints

然后在準(zhǔn)備開始采樣的時(shí)候巧涧,使用下面命令,指定輸出的文件路徑遥倦,和采樣時(shí)間

sudo -u <java_user> -i jcmd <pid> JFR.start filename=/tmp/app.jfr duration=60s

之后可以用 JFR.check 來檢查采樣是不是已經(jīng)完成了(詳見:Java Platform, Standard Edition Java Flight Recorder Runtime Guide

sudo -u <java_user> -i jcmd <pid> JFR.check

確認(rèn)完成后谤绳,就可以把 jfr 文件傳回本地,用 jmc 來分析了袒哥。如果想要生成火焰圖缩筛,還有這么個(gè)工具:jfr-flame-graph。具體用法可以看文檔堡称。大致上瞎抛,代碼拖回來后就編譯好了。另外還需要 FlameGraph

cd jfr-flame-graph
install-mc-jars.sh
mvn clean install -U

工具準(zhǔn)備好以后却紧,執(zhí)行下面命令桐臊,就能生成一個(gè)漂亮的火焰圖了

path/to/jfr-flame-graph/run.sh -f app.jfr -o app.txt
cat app.txt | path/to/FlameGraph/flamegraph.pl >app.svg

使用新版 JVM Profile 功能之后一鍵搞定

<div class="note success">這里我們以 2018.3 版本為例,但只要是高于該版本的 IDEA 也都是支持的</div>
 打開下載 Intellij Idea 下載頁面晓殊,找到 Coming in 2018.3断凶,然后下載 EAP 版本的 Intellij Idea

打開項(xiàng)目后,使用快捷鍵 ???/ 打開 Maintenance 面板巫俺,選擇 Experimantal features认烁,勾選 linux.native.menuidea.profiler.enabled

使用 Run xxx with Async Profiler 執(zhí)行任意程序

即可獲得火焰圖、方法調(diào)用鏈、方法列表

<center>(對(duì) <a target="_blank">Intellij IDEA</a>? 的截圖)</center>

火焰圖却嗡,主要用于分析 CPU 性能消耗舶沛。可以交互式地分析 JVM 進(jìn)程中所有線程的 CPU 消耗火焰圖窗价,也可以選擇某一個(gè)線程來分析如庭;
方法調(diào)用鏈,可以找到在某個(gè)線程中舌镶,消耗 CPU 最多的方法
方法列表柱彻,可以看到每個(gè)方法的調(diào)用次數(shù),展開后還可以看到詳細(xì)的調(diào)用棧

Tips: Spark 任務(wù)生成火焰圖

序列化 ID 排查工具

使用

serialver [ options ] [ classnames ]

  options
    The command-line options. See Options.

  classnames
    The classes for which the serialVersionUID is to be returned.

參考

Arthas 診斷工具

如果你想要對(duì)線上運(yùn)行的 JVM 進(jìn)程進(jìn)行邏輯或性能分析餐胀,但是此時(shí)如果進(jìn)程本身重啟很耗時(shí)哟楷,或者進(jìn)程內(nèi)存很大無法進(jìn)行 Dump 操作,亦或是不想有任何的代碼侵入否灾,就統(tǒng)計(jì)出各個(gè)方法的調(diào)用次數(shù)和耗時(shí)卖擅,那么,Arthas 絕對(duì)是不二之選

常見問題

編碼相關(guān)

SimpleDateFormat 多線程安全問題

分析

不安全的主要原因是墨技,SimpleDateFormat 繼承的 DateTime 類惩阶,本身就是不安全的。而根本原因是 DateTime 的類屬性中 Calendar 實(shí)例扣汪,并沒有使用同步代碼塊進(jìn)行多線程安全處理断楷。如果在執(zhí)行 format(...) 方法的同時(shí),有其他線程調(diào)用 setCalendar(Calendar newCalendar) 方法崭别,則會(huì)出現(xiàn)混亂冬筒。處理的方法有很多,除了在每次需要使用重新初始化 SimpleDateFormat 實(shí)例茅主,另外還可使用 ThreadLocal 對(duì)其進(jìn)行緩存舞痰,再或者使用 Joda-time 替換 Jdk 原生的 SimpleDateFormat 和 Java 8 里面的 DateTimeFormatter(注意別觸發(fā) JDK-8031085,該問題在 JDK9 才修復(fù))诀姚,亦可

Tips: Full code is here and here.

參考

java.lang.IllegalThreadStateException

報(bào)錯(cuò)是因?yàn)?Thread 不可以使用 start() 啟動(dòng)多次响牛,可以將邏輯放在 Runnable 對(duì)象中,每次啟動(dòng)的時(shí)候赫段,再通過 new Thread(runnable).start() 初始化一個(gè) Thread 對(duì)象來啟動(dòng)即可

取消數(shù)值的科學(xué)計(jì)數(shù)法

// 數(shù)值過大后呀打,double、long 的 toString 可能會(huì)出現(xiàn)科學(xué)計(jì)數(shù)法
// 可以通過 NumberFormat 來解決
NumberFormat nf = NumberFormat.getInstance();
nf.setGroupingUsed(false);
nf.setMaximumFractionDigits(0);
nf.setMaximumIntegerDigits(64);

assertEquals("1234567890123456789", nf.format(1234567890123456789L));
assertEquals(1234567890123456789L, Long.valueOf(nf.format(1234567890123456789L)).longValue());
// 或者轉(zhuǎn)換 double 的字符串為 long
assertEquals(0L, Long.valueOf(nf.format(0.0)).longValue());

nf.setMaximumFractionDigits(64);
assertEquals("0.12345678901234568", nf.format(0.1234567890_12345678D));
assertEquals(0.12345678901234568D, Double.valueOf(nf.format(0.1234567890_12345678D)), 64);

File#toURL 過期

使用 file.toURI().toURL() 替代

Jersey 的 @Produces 默認(rèn)不設(shè)置 UTF-8 存在中文亂碼

方案

實(shí)現(xiàn) ContainerResponseFilter 接口糯笙,給 @Produces 注解增加 charset=UTF-8 屬性

實(shí)現(xiàn)
編碼 MediaTypeFilter
import com.sun.jersey.core.util.Priority;
import com.sun.jersey.spi.container.*;

import javax.ws.rs.Priorities;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.ext.Provider;
import java.lang.annotation.Annotation;

@Provider
@Priority(Priorities.HEADER_DECORATOR)
public class MediaTypeFilter implements ResourceFilter, ContainerResponseFilter {

    @Override
    public ContainerResponse filter(ContainerRequest request, ContainerResponse response) {
        Annotation[] annotations = response.getAnnotations();
        for (int i = 0; i < annotations.length; i++) {
            if (!(annotations[i] instanceof Produces)) {
                continue;
            }
            Produces produces = (Produces) annotations[i];
            String[] producesValues = produces.value();
            for (int j = 0; j < producesValues.length; j++) {
                if (!MediaType.APPLICATION_JSON.equals(producesValues[j]) && !MediaType.TEXT_PLAIN.equals(producesValues[j])) {
                    continue;
                }
                producesValues[j] += ";charset=UTF-8";
            }
            annotations[i] = produces;
        }
        response.setAnnotations(annotations);
        return response;
    }

    @Override
    public ContainerRequestFilter getRequestFilter() {
        return null;
    }

    @Override
    public ContainerResponseFilter getResponseFilter() {
        return this;
    }
}
在 BrokerResource 中使用
import com.google.common.collect.ImmutableMap;
import com.google.inject.Inject;
import com.sun.jersey.spi.container.ResourceFilters;
import org.apache.druid.client.BrokerServerView;
import org.apache.druid.server.http.security.StateResourceFilter;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Path("/druid/broker/v1")
@ResourceFilters({StateResourceFilter.class, MediaTypeFilter.class})
public class BrokerResource
{
  private final BrokerServerView brokerServerView;

  @Inject
  public BrokerResource(BrokerServerView brokerServerView)
  {
    this.brokerServerView = brokerServerView;
  }

  @GET
  @Path("/loadstatus")
  @Produces(MediaType.APPLICATION_JSON)
  public Response getLoadStatus()
  {
    return Response.ok(ImmutableMap.of("inventoryInitialized", brokerServerView.isInitialized())).build();
  }
}
參考

IDE 相關(guān)

[Intellij Idea] Code Check 中設(shè)置行長度為 120贬丛,卻仍然有一條虛線豎在 80

依次選擇菜單 Settings - Editor - Code Style - Java,這時(shí)候可以將默認(rèn)的 Scheme 設(shè)置成想要的方案炬丸,也可以在 Wrapping and Braces 中修改 Hard wrap at120,以達(dá)到想要的效果

版本相關(guān)

Unsupported major.minor version 52.0

此類報(bào)錯(cuò),是因?yàn)橛玫桶姹?JDK 去運(yùn)行高版本的 Java 程序了

JDK 版本 Major Version Number
Java SE 11 55
Java SE 10 54
Java SE 9 53
Java SE 8 52
Java SE 7 51
Java SE 6.0 50
Java SE 5.0 49
JDK 1.4 48
JDK 1.3 47
JDK 1.2 46
JDK 1.1 45

Maven 相關(guān)

需要正確地在 Maven 的 checkstyle 插件的 Regexp 正則中使用 XML 關(guān)鍵字

如果不對(duì)下面 5 個(gè)符號(hào)加 \ 反斜杠轉(zhuǎn)義的話稠炬,就需要使用 HTML 來表示

原始符號(hào) 含義 HTML
\lt 小于 &lt;
\gt 大于 &gt;
& &amp;
' 單引號(hào) &apos;
" 雙引號(hào) &quot;

LogBack 相關(guān)

日志文件過大

描述

多個(gè) JVM 進(jìn)程將日志寫入同一個(gè)日志文件中焕阿,導(dǎo)致按照文件大小切割的策略失效,xxx.log.1 會(huì)持續(xù)寫下去首启,以至于磁盤被撐爆

解決

在日志路徑上增加 ${JVM_PREFIX} 變量暮屡,并在不同的 JVM 啟動(dòng)的時(shí)候,通過 -DJVM_PREFIX="process001" 的方式傳入毅桃,使得不同的 JVM 進(jìn)程將日志寫入各自的日志文件中

參考

配置相關(guān)

ConfigFactory.load() 只能加載固定目錄下的配置文件

對(duì)于 Java 開發(fā)而言褒纲,要實(shí)現(xiàn)加載配置文件的功能,一般都會(huì)選用 typesafe 下 config 框架钥飞,它具備純 Java 源碼和無任何外部依賴的優(yōu)點(diǎn)莺掠。而調(diào)用 ConfigFactory.load() 方法只能加載 src/main/resources 下配置文件的問題。針對(duì)該問題读宙,只需調(diào)用 ConfigFactory.parseFile(new File("yuzhouwan.conf")) 方法彻秆,即可指定其他任意位置的配置文件

JVM 相關(guān)

Too small initial heap

-Xmx1024 -Xms512 應(yīng)改為 -Xmx1024M -Xms512M

如何計(jì)算 Java 對(duì)象實(shí)際占用內(nèi)存

G1GC 報(bào)錯(cuò) To-space Exhausted

描述
2018-08-12T00:52:36.255+0800: 17308164.871: [GC pause (G1 Evacuation Pause) (young) (to-space exhausted), 2.3349764 secs]
   [Parallel Time: 331.1 ms, GC Workers: 23]
      [GC Worker Start (ms): Min: 17308164872.2, Avg: 17308164872.3, Max: 17308164872.3, Diff: 0.2]
      [Ext Root Scanning (ms): Min: 0.7, Avg: 1.0, Max: 3.1, Diff: 2.4, Sum: 23.1]
      [Update RS (ms): Min: 56.3, Avg: 58.3, Max: 59.2, Diff: 2.9, Sum: 1341.8]
         [Processed Buffers: Min: 75, Avg: 95.6, Max: 133, Diff: 58, Sum: 2199]
      [Scan RS (ms): Min: 0.8, Avg: 1.4, Max: 1.5, Diff: 0.7, Sum: 33.0]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
      [Object Copy (ms): Min: 269.3, Avg: 269.5, Max: 269.8, Diff: 0.6, Sum: 6197.7]
      [Termination (ms): Min: 0.0, Avg: 0.4, Max: 0.6, Diff: 0.5, Sum: 9.3]
         [Termination Attempts: Min: 1, Avg: 290.0, Max: 325, Diff: 324, Sum: 6669]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 3.0]
      [GC Worker Total (ms): Min: 330.7, Avg: 330.8, Max: 331.0, Diff: 0.3, Sum: 7608.0]
      [GC Worker End (ms): Min: 17308165202.9, Avg: 17308165203.0, Max: 17308165203.1, Diff: 0.2]
   [Code Root Fixup: 0.2 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 1.1 ms]
   [Other: 2002.7 ms]
      [Evacuation Failure: 779.0 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 1217.9 ms]
      [Ref Enq: 1.6 ms]
      [Redirty Cards: 0.8 ms]
      [Humongous Register: 0.2 ms]
      [Humongous Reclaim: 0.9 ms]
      [Free CSet: 1.5 ms]
   [Eden: 8520.0M(8520.0M)->0.0B(3536.0M) Survivors: 368.0M->440.0M Heap: 14.8G(16.0G)->10.4G(16.0G)]
 [Times: user=25.22 sys=0.29, real=2.33 secs] 
原因

在日志中看到類似 Evacuation Failure结耀、To-space Exhausted 或者 To-space Overflow 這樣的輸出(取決于不同版本的 JVM留夜,輸出略有不同)。這是 G1GC 收集器在將某個(gè)需要垃圾回收的分區(qū)進(jìn)行回收時(shí)饼记,無法找到一個(gè)能將其中存活對(duì)象拷貝過去的空閑分區(qū)香伴。這種情況被稱為 Evacuation Failure,常常會(huì)引發(fā) Full GC

解決
  1. 增加 -XX:G1ReservePercent 選項(xiàng)的值(并相應(yīng)增加總的堆大芯咴颉)即纲,為目標(biāo)空間增加預(yù)留內(nèi)存量
  2. -XX:InitiatingHeapOccupancyPercent 參數(shù)調(diào)低(默認(rèn)值是45),可以使 G1GC 收集器更早開始 Mixed GC博肋;但另一方面低斋,會(huì)增加 GC 發(fā)生頻率
  3. 提高 -XX:ConcGCThreads 的值,在 Mixed GC 階段投入更多的并發(fā)線程匪凡,爭取提高每次暫停的效率膊畴。但是此參數(shù)會(huì)占用一定的有效工作線程資源
參考
  • 本文作者: Benedict Jin
  • 本文鏈接: https://yuzhouwan.com/posts/190413/
  • 版權(quán)聲明: 本博客所有文章除特別聲明外,均采用 BY-NC-ND 許可協(xié)議病游。轉(zhuǎn)載請(qǐng)注明出處唇跨!
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末稠通,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子买猖,更是在濱河造成了極大的恐慌改橘,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,546評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件玉控,死亡現(xiàn)場(chǎng)離奇詭異飞主,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)高诺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門碌识,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人虱而,你說我怎么就攤上這事筏餐∨胫玻” “怎么了许饿?”我有些...
    開封第一講書人閱讀 164,911評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長者蠕。 經(jīng)常有香客問我诅迷,道長佩番,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,737評(píng)論 1 294
  • 正文 為了忘掉前任罢杉,我火速辦了婚禮趟畏,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘滩租。我一直安慰自己赋秀,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,753評(píng)論 6 392
  • 文/花漫 我一把揭開白布律想。 她就那樣靜靜地躺著猎莲,像睡著了一般。 火紅的嫁衣襯著肌膚如雪技即。 梳的紋絲不亂的頭發(fā)上著洼,一...
    開封第一講書人閱讀 51,598評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音而叼,去河邊找鬼身笤。 笑死,一個(gè)胖子當(dāng)著我的面吹牛葵陵,可吹牛的內(nèi)容都是我干的液荸。 我是一名探鬼主播,決...
    沈念sama閱讀 40,338評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼脱篙,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼娇钱!你這毒婦竟也來了伤柄?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,249評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤文搂,失蹤者是張志新(化名)和其女友劉穎响迂,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體细疚,經(jīng)...
    沈念sama閱讀 45,696評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,888評(píng)論 3 336
  • 正文 我和宋清朗相戀三年川梅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了疯兼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,013評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡贫途,死狀恐怖吧彪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情丢早,我是刑警寧澤姨裸,帶...
    沈念sama閱讀 35,731評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站怨酝,受9級(jí)特大地震影響傀缩,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜农猬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,348評(píng)論 3 330
  • 文/蒙蒙 一赡艰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧斤葱,春花似錦慷垮、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至衩茸,卻和暖如春芹血,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背递瑰。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評(píng)論 1 270
  • 我被黑心中介騙來泰國打工祟牲, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人抖部。 一個(gè)月前我還...
    沈念sama閱讀 48,203評(píng)論 3 370
  • 正文 我出身青樓说贝,卻偏偏與公主長得像,于是被迫代替她去往敵國和親慎颗。 傳聞我的和親對(duì)象是個(gè)殘疾皇子乡恕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,960評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容